Compare commits
2000 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac122a1db0 | ||
|
|
5cff01eef3 | ||
|
|
69a6256e54 | ||
|
|
bee0726c14 | ||
|
|
e3926863b1 | ||
|
|
1fdf5a4f07 | ||
|
|
fa009e729e | ||
|
|
48ab18d9e1 | ||
|
|
1584bb8dce | ||
|
|
72a9e89d3b | ||
|
|
92979ff7c8 | ||
|
|
f731ddb810 | ||
|
|
42f34c181f | ||
|
|
7ee4c8709e | ||
|
|
b85a94f269 | ||
|
|
2c99ab6457 | ||
|
|
074aa24b26 | ||
|
|
d277f2f7c3 | ||
|
|
8302916602 | ||
|
|
750509b5e8 | ||
|
|
6147ed790b | ||
|
|
e730af2ae5 | ||
|
|
8662f6e527 | ||
|
|
3103fc9864 | ||
|
|
637678db20 | ||
|
|
e97407a286 | ||
|
|
e494abb779 | ||
|
|
44093a42fa | ||
|
|
8e1481ae78 | ||
|
|
9c59e7498f | ||
|
|
0a202dd506 | ||
|
|
7eb4a3f961 | ||
|
|
1ce5603379 | ||
|
|
97b86b02ad | ||
|
|
f2da1635f2 | ||
|
|
f0ed5c3433 | ||
|
|
aca5925e57 | ||
|
|
b8d78174a5 | ||
|
|
edf2a43122 | ||
|
|
21de993546 | ||
|
|
49bc24b66e | ||
|
|
771d627c5a | ||
|
|
98967de31b | ||
|
|
c87c07dbd5 | ||
|
|
2478d20e76 | ||
|
|
cc3428eb3b | ||
|
|
6001bd4940 | ||
|
|
f8709f4091 | ||
|
|
3cff881b5b | ||
|
|
b79e997a14 | ||
|
|
ed2c34143c | ||
|
|
639b17ef6b | ||
|
|
7834411ef3 | ||
|
|
d8ea83a44c | ||
|
|
6b9818b748 | ||
|
|
b4d5b228ae | ||
|
|
29b4824ee2 | ||
|
|
e3a8b669b2 | ||
|
|
80e5c8a987 | ||
|
|
e0e4886e63 | ||
|
|
c0947f4192 | ||
|
|
7706b047ce | ||
|
|
a44c6ff27c | ||
|
|
f4fdd51ce9 | ||
|
|
ae6c7dd673 | ||
|
|
0cbc773126 | ||
|
|
45bd3473fa | ||
|
|
02175844da | ||
|
|
fd60f7ee70 | ||
|
|
9eb4c3ab23 | ||
|
|
72d1aa7d97 | ||
|
|
57628ead80 | ||
|
|
9733c2328b | ||
|
|
70663cecc3 | ||
|
|
7c77942a92 | ||
|
|
04cf18e149 | ||
|
|
1825edda7e | ||
|
|
045f91c411 | ||
|
|
96d24f548c | ||
|
|
c7f03ad64e | ||
|
|
1232989d7d | ||
|
|
8f66a7997f | ||
|
|
f32dd80c24 | ||
|
|
a06ba343de | ||
|
|
bba55d4d5a | ||
|
|
87111bd889 | ||
|
|
3661ffd3ab | ||
|
|
d8f111a5e3 | ||
|
|
ae5565ce68 | ||
|
|
e4c370a7d9 | ||
|
|
891005bcd3 | ||
|
|
d3a4a7a0fa | ||
|
|
10211d1a93 | ||
|
|
7f019a932b | ||
|
|
fae909de2f | ||
|
|
d8455ef6e5 | ||
|
|
934c994783 | ||
|
|
d0961d596d | ||
|
|
382df24764 | ||
|
|
bfcfa42125 | ||
|
|
2333886c34 | ||
|
|
0cdad3c886 | ||
|
|
eee23c543b | ||
|
|
f0a8812f5e | ||
|
|
a8d603f753 | ||
|
|
22acaa1d2c | ||
|
|
fe791ccee9 | ||
|
|
414557eee0 | ||
|
|
97d2741360 | ||
|
|
b95e5f1eae | ||
|
|
43b200dc91 | ||
|
|
29014699bb | ||
|
|
5576672957 | ||
|
|
5002606861 | ||
|
|
ba0fb343ff | ||
|
|
17e5ae6bc2 | ||
|
|
7a0186efc8 | ||
|
|
de64af4a68 | ||
|
|
4a852ac8a8 | ||
|
|
6784bfb98c | ||
|
|
c8f246d344 | ||
|
|
8b3d31a936 | ||
|
|
5e88d6445b | ||
|
|
fd7dff88df | ||
|
|
8cfee1f483 | ||
|
|
cf4d8e6125 | ||
|
|
c0e8a41d2a | ||
|
|
a02c27b1af | ||
|
|
712e1bac0d | ||
|
|
513ea46cbe | ||
|
|
b1919b6f95 | ||
|
|
43561d209b | ||
|
|
16dcbc5412 | ||
|
|
c8dd2d5cad | ||
|
|
4b37777066 | ||
|
|
95ecd85a12 | ||
|
|
5c475e3c15 | ||
|
|
f705ee6863 | ||
|
|
1f67c18989 | ||
|
|
de6d451c5b | ||
|
|
580296d6f3 | ||
|
|
a9e28fbce3 | ||
|
|
311779cb20 | ||
|
|
d2f8a89e87 | ||
|
|
84c95bf322 | ||
|
|
f75c801955 | ||
|
|
faa2f54371 | ||
|
|
4249ac193a | ||
|
|
c709274a28 | ||
|
|
c8f05e79db | ||
|
|
4d2887e99f | ||
|
|
29256a5154 | ||
|
|
82d42e4094 | ||
|
|
53850fb627 | ||
|
|
34b4c8ce46 | ||
|
|
e944841054 | ||
|
|
f6a5ff5552 | ||
|
|
01763b59d4 | ||
|
|
044173b2a1 | ||
|
|
99e7a88dbd | ||
|
|
01cd9fbb0e | ||
|
|
aaed1dc3d5 | ||
|
|
c8dce94c03 | ||
|
|
06496d07b3 | ||
|
|
a97f98c9cc | ||
|
|
8d0406f74f | ||
|
|
c64d14701d | ||
|
|
00332ae444 | ||
|
|
e8deb3d8fe | ||
|
|
8b234c99cf | ||
|
|
1f986d9c45 | ||
|
|
bacb8fb3cd | ||
|
|
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 | ||
|
|
be95396a57 | ||
|
|
59cbed429f | ||
|
|
d49df7aebb | ||
|
|
0aa9faad2e | ||
|
|
1337def888 | ||
|
|
4b100c558b | ||
|
|
1425a71ece | ||
|
|
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 | ||
|
|
3a377300e1 | ||
|
|
33a07e3a86 | ||
|
|
212cafc1d7 | ||
|
|
2643b3cbcc | ||
|
|
d445229b6d | ||
|
|
dab5c451b0 | ||
|
|
7bdf06131a | ||
|
|
854648d5af | ||
|
|
c5f7b97359 | ||
|
|
dd8a727ad6 | ||
|
|
6c627fe422 | ||
|
|
ee980e1caf | ||
|
|
22bfaf6527 | ||
|
|
48ab48cc30 | ||
|
|
a0b14d4127 | ||
|
|
03f9fe1a70 | ||
|
|
8915b8d796 | ||
|
|
c77ffeeec0 | ||
|
|
4acf5660b2 | ||
|
|
2d9f0a668c | ||
|
|
9e6cb246cc | ||
|
|
14544ca63d | ||
|
|
26b347c04c | ||
|
|
36f75d1811 | ||
|
|
27fc787294 | ||
|
|
d23286d390 | ||
|
|
7c3ccc76c3 | ||
|
|
892dc5d4f3 | ||
|
|
e278692749 | ||
|
|
8d77dd2246 | ||
|
|
14ede2a585 | ||
|
|
5b525622f1 | ||
|
|
a24b11905c | ||
|
|
5d70858341 | ||
|
|
3daa006741 | ||
|
|
0bcc0c2101 | ||
|
|
b8850c808c | ||
|
|
f4f2c01ac1 | ||
|
|
7072e82dff | ||
|
|
53dc36c4cf | ||
|
|
5aadc3af00 | ||
|
|
8c28a698ed | ||
|
|
5ed6d8b202 | ||
|
|
b73dc7bf5e | ||
|
|
71d0f4ab63 | ||
|
|
d479dcde81 | ||
|
|
ae536017d5 | ||
|
|
67ddfce279 | ||
|
|
b1f39b34d7 | ||
|
|
6cf958ccce | ||
|
|
eaed3677d3 | ||
|
|
b9c88da54d | ||
|
|
104ae77f7a | ||
|
|
bfcb2ce61b | ||
|
|
63ba5fed09 | ||
|
|
98a8464933 | ||
|
|
7e3e6726e0 | ||
|
|
09567b2bb2 | ||
|
|
f3bd116184 | ||
|
|
7509737563 | ||
|
|
cfb815d879 | ||
|
|
44241fb967 | ||
|
|
c4b45129bd | ||
|
|
70741008ca | ||
|
|
6c2d2cae2a | ||
|
|
28f13d3311 | ||
|
|
4e31aaa8fb | ||
|
|
ba99f0c2cc | ||
|
|
e0a96b4937 | ||
|
|
82c055f527 | ||
|
|
f94008192c | ||
|
|
3895d5279e | ||
|
|
3d85ecc525 | ||
|
|
7da00796e5 | ||
|
|
6086419cb6 | ||
|
|
5bc1f2f2c0 | ||
|
|
32a83b211e | ||
|
|
bead7b3a7f | ||
|
|
815d6d6572 | ||
|
|
95ce812992 | ||
|
|
9a36f4748c | ||
|
|
50b7849a35 | ||
|
|
6f1245b27c | ||
|
|
cc87ed3899 | ||
|
|
1d9037fefe | ||
|
|
03016e2d16 | ||
|
|
3d41617f4e | ||
|
|
35151ffdd1 | ||
|
|
4527d41a7a | ||
|
|
553cba12f3 | ||
|
|
116e068ac3 | ||
|
|
1010dd2d28 | ||
|
|
7354242906 | ||
|
|
e7d0b158e9 | ||
|
|
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 | ||
|
|
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 | ||
|
|
281319d2da | ||
|
|
5cb203685c | ||
|
|
01fa37900b | ||
|
|
edbe744e17 | ||
|
|
2a32a1a4a8 | ||
|
|
404bdb21e6 | ||
|
|
b260c9a512 | ||
|
|
4b941adb6a | ||
|
|
bd752550a8 | ||
|
|
b8b71bb961 | ||
|
|
5aaf7a4092 | ||
|
|
030e02ffb8 | ||
|
|
d962aa03f4 | ||
|
|
9e4a2aae43 | ||
|
|
ee6eb685e7 | ||
|
|
09a38a32ce | ||
|
|
d13b19d43d | ||
|
|
e730dca1ad | ||
|
|
8da30640bb | ||
|
|
6f4eb88e07 | ||
|
|
d9592b9dab | ||
|
|
b87ada72aa | ||
|
|
83363ba1f0 | ||
|
|
23ebe7f718 | ||
|
|
e04264cfa3 | ||
|
|
8d29e5037f | ||
|
|
6926ed45b0 | ||
|
|
736b85b8bb | ||
|
|
9e3361bc31 | ||
|
|
6e10381020 | ||
|
|
a1d37d379c | ||
|
|
07d87db7a2 | ||
|
|
4e556673d2 | ||
|
|
f421304fc1 | ||
|
|
c9271b1686 | ||
|
|
12eb6863da | ||
|
|
4834874091 | ||
|
|
8759ebf200 | ||
|
|
d4715aebef | ||
|
|
0fe2ade7bb | ||
|
|
0c71565535 | ||
|
|
6a637091a2 | ||
|
|
31eba60012 | ||
|
|
51e58e9078 | ||
|
|
4a1e76730a | ||
|
|
5599bb028b | ||
|
|
552c6da0cc | ||
|
|
cc6817a891 | ||
|
|
fb48d1b485 | ||
|
|
1c336dad6b | ||
|
|
a4940d46cd | ||
|
|
499b2f44c1 | ||
|
|
2b200c9281 | ||
|
|
36a900c98f | ||
|
|
5236b03f66 | ||
|
|
8be35e3621 | ||
|
|
509f00fe89 | ||
|
|
a98b87f148 | ||
|
|
ae9b2b3b72 | ||
|
|
02e1ec0ae3 | ||
|
|
daefb0f120 | ||
|
|
ff0604e3b6 | ||
|
|
20e41e22fa | ||
|
|
a0e3bdd594 | ||
|
|
6580aaf3ad | ||
|
|
0b46701b60 | ||
|
|
0bb4effede | ||
|
|
b07082a52d | ||
|
|
04f267f5a7 | ||
|
|
03ccce2804 | ||
|
|
e894bd9f24 | ||
|
|
10e6988273 | ||
|
|
905b61e5d8 | ||
|
|
ee69d393ae | ||
|
|
cab39973ae | ||
|
|
d93f5d07bb | ||
|
|
ba00ffe1ae | ||
|
|
6afaf5eaf5 | ||
|
|
d30459cc34 | ||
|
|
e92fbb7b1b | ||
|
|
42d464b532 | ||
|
|
c2e9e5c63a | ||
|
|
bc36726925 | ||
|
|
7abbff8c31 | ||
|
|
6236f4bcf4 | ||
|
|
3c3e80f77f | ||
|
|
4aae2fb289 | ||
|
|
66ff07752f | ||
|
|
5cf92f2742 | ||
|
|
6d3fddc474 | ||
|
|
66d4ad6174 | ||
|
|
2a366a1607 | ||
|
|
d87a0995b4 | ||
|
|
9a73a41e04 | ||
|
|
ba041b36bc | ||
|
|
f5f9de69b4 | ||
|
|
71e56c62e8 | ||
|
|
0f496619fd | ||
|
|
5fdd6a441a | ||
|
|
00f287bb63 | ||
|
|
785268efa6 | ||
|
|
2c976d9394 | ||
|
|
1e32582642 | ||
|
|
6f8f6d07f5 | ||
|
|
3958111e76 | ||
|
|
86fcc4af74 | ||
|
|
2fd26756df | ||
|
|
478f4b74d8 | ||
|
|
73d0d2a1bb | ||
|
|
546db08ec4 | ||
|
|
0dd41a8670 | ||
|
|
82c0c89f46 | ||
|
|
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 | ||
|
|
3c5efa0662 | ||
|
|
9b739bcbbf | ||
|
|
db89076e48 | ||
|
|
19b341ef18 | ||
|
|
be3713b1a3 | ||
|
|
99c4415cfb | ||
|
|
7b311f2ccf | ||
|
|
4aeabfe0a7 | ||
|
|
431ed02194 | ||
|
|
07f587ed83 | ||
|
|
0408341d82 | ||
|
|
5b3c9432f3 | ||
|
|
4a197e63f9 | ||
|
|
0876a12fe9 | ||
|
|
c43c7ecc03 | ||
|
|
4a6dee3044 | ||
|
|
019acdd840 | ||
|
|
1c98512720 | ||
|
|
23a09ad546 | ||
|
|
0836e8fe7c | ||
|
|
90196af8f8 | ||
|
|
566fe05772 | ||
|
|
18772c6292 | ||
|
|
6278bddc9b | ||
|
|
f74bf71735 | ||
|
|
efe9ed68b2 | ||
|
|
7c1e75865d | ||
|
|
a0aee41f1a | ||
|
|
2049dd75f4 | ||
|
|
0864c35ba9 | ||
|
|
92c9f66671 | ||
|
|
815784e809 | ||
|
|
2795d00d1e | ||
|
|
86dd0b4963 | ||
|
|
77a4f4819f | ||
|
|
b63d603482 | ||
|
|
e569b4e613 | ||
|
|
8a70997546 | ||
|
|
80d0a0f882 | ||
|
|
70b3997874 | ||
|
|
e8e4311068 | ||
|
|
c58b93ff51 | ||
|
|
7d8ebfe91b | ||
|
|
810381eab2 | ||
|
|
61dc6cf2de | ||
|
|
0205ebad2a | ||
|
|
09a94133ac | ||
|
|
1eb3c3b219 | ||
|
|
457845bb51 | ||
|
|
0c11b46585 | ||
|
|
c35100d9e9 | ||
|
|
847031cb04 | ||
|
|
f8d87bb452 | ||
|
|
f60b3505e0 | ||
|
|
addefbc511 | ||
|
|
c4314b25a3 | ||
|
|
921bb86127 | ||
|
|
b3a7fb9c3e | ||
|
|
c143c81a7e | ||
|
|
dd389ba0f8 | ||
|
|
46b1649ab8 | ||
|
|
89710412e4 | ||
|
|
931973b632 | ||
|
|
60aaa838e3 | ||
|
|
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 | ||
|
|
ae9fdd0255 | ||
|
|
b3874ee6fd | ||
|
|
62af4891f3 | ||
|
|
2176e0c0ad | ||
|
|
cac105b0d5 | ||
|
|
cd7c42cc23 | ||
|
|
a3fb847773 | ||
|
|
5c2f4f9e4b | ||
|
|
0a511d5b87 | ||
|
|
efe1aad5db | ||
|
|
eed4c53df0 | ||
|
|
9c08a6314b | ||
|
|
a6b2d2c722 | ||
|
|
3c6b5300e5 | ||
|
|
f084c30b20 | ||
|
|
206004fc1f | ||
|
|
d9641cbff8 | ||
|
|
13b272052a | ||
|
|
c79e0d26d8 | ||
|
|
ec4a4c2cfc | ||
|
|
9a9491bff9 | ||
|
|
5b5155819f | ||
|
|
1b941c6b29 | ||
|
|
9b9665d2e9 | ||
|
|
4cceb46641 | ||
|
|
19cf83cce6 | ||
|
|
bb60d399fc | ||
|
|
1a9f1dd0ae | ||
|
|
586c465aaa | ||
|
|
50ceb974d9 | ||
|
|
27cf40d392 | ||
|
|
bbb6005634 | ||
|
|
8dbd996558 | ||
|
|
8605345499 | ||
|
|
3671ddbd4b | ||
|
|
5bc1ceacb2 | ||
|
|
47b9fa3651 | ||
|
|
6062b87771 | ||
|
|
213152aa43 | ||
|
|
ea8047344f | ||
|
|
a7bc167d53 | ||
|
|
18e78ee2c2 | ||
|
|
754236e35b | ||
|
|
2645d62991 | ||
|
|
e55d9416dc | ||
|
|
24d35eec54 | ||
|
|
ee053f50b4 | ||
|
|
3593c9ed3e | ||
|
|
93f548696d | ||
|
|
cecb952add | ||
|
|
596571bb38 | ||
|
|
85a6fb75b8 | ||
|
|
7dea42433b | ||
|
|
ec5e4af6b7 | ||
|
|
0048754fe8 | ||
|
|
5c0bd0f79c | ||
|
|
669cdffe08 | ||
|
|
3cd553301b | ||
|
|
db7ef4f253 | ||
|
|
a09704567c | ||
|
|
21fe577a2e | ||
|
|
9f258f5c9c | ||
|
|
9cd088feb0 | ||
|
|
89e3828138 | ||
|
|
731c89dc27 | ||
|
|
3d920cab4d | ||
|
|
470b8c1fb8 | ||
|
|
dbf988fd5a | ||
|
|
0031743ad4 | ||
|
|
0f2c0ab65d | ||
|
|
53244b794f | ||
|
|
416122d61d | ||
|
|
d3c625e791 | ||
|
|
ca2c41783c | ||
|
|
e2a6446585 | ||
|
|
839790b5ab | ||
|
|
58b9946936 | ||
|
|
a19ba22eaf | ||
|
|
117715aa22 | ||
|
|
891a5a85ee | ||
|
|
166debfabb | ||
|
|
7258a09fe5 | ||
|
|
058a436187 | ||
|
|
1950802c55 | ||
|
|
eb52a03372 | ||
|
|
f8aa428be3 | ||
|
|
ec0893f136 | ||
|
|
92b99ea963 | ||
|
|
02cd52bb65 | ||
|
|
af1ec2c87b | ||
|
|
41006c3a33 | ||
|
|
116a6d500d | ||
|
|
87d0ac807f | ||
|
|
fc943172eb | ||
|
|
9daa5a2fbd | ||
|
|
b7b2746a61 | ||
|
|
d66a4fbfc8 | ||
|
|
683a172ad8 | ||
|
|
6e12358f5a | ||
|
|
8bcf16dc90 | ||
|
|
65c0a2a1f5 | ||
|
|
115236eb9c | ||
|
|
08de942abe | ||
|
|
e9dff83290 | ||
|
|
3bc6c7584d | ||
|
|
22a2bf1584 | ||
|
|
79ece5f72c | ||
|
|
5da6fe1373 | ||
|
|
48c10d0b95 | ||
|
|
9bb56b1457 | ||
|
|
83420fd828 | ||
|
|
52f4b9506f | ||
|
|
b501e9b20b | ||
|
|
1f7ae5319a | ||
|
|
68c201239d | ||
|
|
6e4e43f612 | ||
|
|
81c3708f39 | ||
|
|
f4d2bbde34 | ||
|
|
d14b42a42c | ||
|
|
0e9c32344c | ||
|
|
30c4ea06af | ||
|
|
8211264993 | ||
|
|
67cf5b49e1 | ||
|
|
8e7ba18e05 | ||
|
|
8359e1063e | ||
|
|
ca078e54b9 | ||
|
|
f7e930c5a2 | ||
|
|
479d95e1c8 | ||
|
|
2b0ff08eef | ||
|
|
67a487db15 | ||
|
|
2488cb3458 | ||
|
|
157e6336fa | ||
|
|
d808a1f406 | ||
|
|
2bb4d8cd63 | ||
|
|
a8164e1631 | ||
|
|
a31d286945 | ||
|
|
12eeef4cf0 | ||
|
|
ce8e6dc36e | ||
|
|
7a32e544a7 | ||
|
|
e16e9d7a0e | ||
|
|
821f908dbc | ||
|
|
e007e6f897 | ||
|
|
94f496fd65 | ||
|
|
d2ce35d2e6 | ||
|
|
2eeebb32dc | ||
|
|
f6d636d82f | ||
|
|
0cd397623e | ||
|
|
5978b6c9ee | ||
|
|
9e132811bc | ||
|
|
3a3b5c1f92 | ||
|
|
26be01ff82 | ||
|
|
8f6dd92374 | ||
|
|
d50b71a887 | ||
|
|
3bc9cbc767 | ||
|
|
b6f6b4fd8a | ||
|
|
a66bada8a3 | ||
|
|
a804f7de19 | ||
|
|
72a61a9966 | ||
|
|
b08bb658ea | ||
|
|
7b28bf608b | ||
|
|
b57747fdf1 | ||
|
|
0735271b10 | ||
|
|
770cd0f9f5 | ||
|
|
32b6266dd9 | ||
|
|
2a8412a2bf | ||
|
|
0c4d289002 | ||
|
|
cee01fec25 | ||
|
|
f00686f3f2 | ||
|
|
bd33f7726e | ||
|
|
22ab526b0c | ||
|
|
af269d198d | ||
|
|
995ef6356e | ||
|
|
aa3bf77c28 | ||
|
|
15667c1259 | ||
|
|
c7b6b565da | ||
|
|
3214ab52c6 | ||
|
|
e3062ff613 | ||
|
|
036b63efe7 | ||
|
|
8d3e1d60d0 | ||
|
|
59876452f4 | ||
|
|
04972ad87f | ||
|
|
c7e69f4e26 | ||
|
|
7a59b6d0d9 | ||
|
|
d227ad97a4 | ||
|
|
b93a474dae | ||
|
|
a5fe075bf3 | ||
|
|
17e5c3d2f5 | ||
|
|
27bfc539f7 | ||
|
|
821fded09d | ||
|
|
ec4a2aa873 | ||
|
|
d6b2d54f3f | ||
|
|
97ae67bb9a | ||
|
|
765514a33f | ||
|
|
e2cdcc96c4 | ||
|
|
0738b2a73f | ||
|
|
98db79910e | ||
|
|
0b21a05aac | ||
|
|
4b71db54aa | ||
|
|
a6bc890f36 | ||
|
|
76903c39e1 | ||
|
|
cf9ed1c631 | ||
|
|
50fc1389b0 | ||
|
|
c70cb2868b | ||
|
|
12fa571aa2 | ||
|
|
4a3018760f | ||
|
|
d005d06cf8 | ||
|
|
a87e3f9ee9 | ||
|
|
52b9a3f3a0 | ||
|
|
c01a7e41d0 | ||
|
|
fe301bb91a | ||
|
|
a42953e3be | ||
|
|
1899255a69 | ||
|
|
908a1009d2 | ||
|
|
fb9c68fc32 | ||
|
|
d54ec0eb05 | ||
|
|
a386948fd1 | ||
|
|
007b812ede | ||
|
|
0ddb0cec03 | ||
|
|
e687f83fbf | ||
|
|
458c9de70f | ||
|
|
87a652d038 | ||
|
|
d889df4c89 | ||
|
|
a2e72d26aa | ||
|
|
a4fdc874e7 | ||
|
|
dfbe382d60 | ||
|
|
0d56ebb1bf | ||
|
|
9e66da174e | ||
|
|
55fcb00168 | ||
|
|
68aa534e1d | ||
|
|
7fd94a401b | ||
|
|
2b9cec50ce | ||
|
|
d1a80cf082 | ||
|
|
fb445aa510 | ||
|
|
4b904934ef | ||
|
|
d6295a00e6 | ||
|
|
3b01673829 | ||
|
|
a5e83a807f | ||
|
|
ddd766ce58 | ||
|
|
a6d2fd36fb | ||
|
|
9156d6bdba | ||
|
|
d18a3ffeff | ||
|
|
e933eaa2b0 | ||
|
|
5393653ddc | ||
|
|
1f3274d3f5 | ||
|
|
39eaa76b8a | ||
|
|
e5396713ce | ||
|
|
79943de808 | ||
|
|
e05f329602 | ||
|
|
eed0e8ebea | ||
|
|
731eb4fcbe | ||
|
|
44a63e4b6d | ||
|
|
7651e5e48b | ||
|
|
2449636d32 | ||
|
|
f9990ca8eb | ||
|
|
c3eed981c0 | ||
|
|
bbb54d4a08 | ||
|
|
4566c585db | ||
|
|
a946338a18 | ||
|
|
0a60a44478 | ||
|
|
cef0ad6707 | ||
|
|
7176f0837a | ||
|
|
6b1f2b2d9d | ||
|
|
38a1a9b320 | ||
|
|
402e2c384f | ||
|
|
9d5faa096c | ||
|
|
97d0dc20f1 | ||
|
|
8d99ff07b6 | ||
|
|
04fa540a8c | ||
|
|
eb41867e04 | ||
|
|
eee5d7d9e8 | ||
|
|
e983f9ed35 | ||
|
|
8b16ef641b | ||
|
|
e87d616b7a | ||
|
|
2220f325fc | ||
|
|
b53ed47ccb | ||
|
|
39df2743fe | ||
|
|
3f729aaf03 | ||
|
|
b7324621e4 | ||
|
|
e8c782c8e1 | ||
|
|
9136505565 | ||
|
|
f406d728cc | ||
|
|
d649ca47c6 | ||
|
|
e8111527b4 | ||
|
|
2af66d7efc | ||
|
|
27706f37f6 | ||
|
|
3de17b2fa6 | ||
|
|
22ecb5de95 | ||
|
|
992b8b3cb5 | ||
|
|
bebc16d5a6 | ||
|
|
ddb719f866 | ||
|
|
0bd1bf2605 | ||
|
|
fd32ba4035 | ||
|
|
22f723b920 | ||
|
|
1248bd0413 | ||
|
|
c150eec2b6 | ||
|
|
c7248c2d47 | ||
|
|
e71e68e298 | ||
|
|
6969557693 | ||
|
|
f6be5ad839 | ||
|
|
cebe3664fd | ||
|
|
cdab465c90 | ||
|
|
144384655c | ||
|
|
0e213d6dab | ||
|
|
21294a4e4a | ||
|
|
3ba4d44d9e | ||
|
|
1f86ef5a37 | ||
|
|
fac60da333 | ||
|
|
5a5a37dfee | ||
|
|
0d487bc14f | ||
|
|
a52b4eb5ed | ||
|
|
f1b7f5f52f | ||
|
|
5ef58652bf | ||
|
|
e26a9e75c6 | ||
|
|
b0035ff4a7 | ||
|
|
94b6f9b2fe | ||
|
|
cad1482b3f | ||
|
|
ea7aafb3e6 | ||
|
|
42b15ad4a5 | ||
|
|
d3d613cca9 | ||
|
|
86893d999a | ||
|
|
4fd17b0bf5 | ||
|
|
76d2206058 | ||
|
|
51e8b608dc | ||
|
|
a68330fb8f | ||
|
|
2449ad5c69 | ||
|
|
064c812df3 | ||
|
|
48d5ec9e66 | ||
|
|
914419fd1e | ||
|
|
f005fc8ca0 | ||
|
|
43b7960de2 | ||
|
|
2ed1e58032 | ||
|
|
c63b212700 | ||
|
|
e9df78c0e7 | ||
|
|
b0daf81185 | ||
|
|
cee4fdcbb0 | ||
|
|
df3cdfccb0 | ||
|
|
894042cd0e | ||
|
|
8123287952 | ||
|
|
bc677705d8 | ||
|
|
5dd8ea8aab | ||
|
|
41172be796 | ||
|
|
ad1b4a9a86 | ||
|
|
e0e3ec02b3 | ||
|
|
a6cc392473 | ||
|
|
36f48b8656 | ||
|
|
3d883ca37d | ||
|
|
34ed81ca64 | ||
|
|
a9e0880572 | ||
|
|
9500e1c3c4 | ||
|
|
d81aa9cbbc | ||
|
|
21d4b25c2d | ||
|
|
0080783a11 | ||
|
|
2c3f44a3f8 | ||
|
|
3ddf414097 | ||
|
|
59fb63f1f7 | ||
|
|
fa1b514440 | ||
|
|
1579c58876 | ||
|
|
153d044331 | ||
|
|
f2496f7054 | ||
|
|
99022f4f3d | ||
|
|
60a5e4f261 | ||
|
|
661586d3b6 | ||
|
|
abc26cf906 | ||
|
|
12351bada7 | ||
|
|
a6816d53d7 | ||
|
|
3b0709f5f2 | ||
|
|
d7af7e2917 | ||
|
|
6516e62d33 | ||
|
|
6b832edd2f | ||
|
|
eebace1652 | ||
|
|
6ff6e05408 | ||
|
|
aaf569ca8c | ||
|
|
31eef6222e | ||
|
|
9963afa558 | ||
|
|
5b2e2fcf9d | ||
|
|
cc746e59a1 | ||
|
|
2cdb1c519d | ||
|
|
426074ded9 | ||
|
|
772a096615 | ||
|
|
e113e011cb | ||
|
|
22266484bd | ||
|
|
559c011420 | ||
|
|
411c0633a3 | ||
|
|
488f023bdf | ||
|
|
22878f4ef8 | ||
|
|
e732a39fea | ||
|
|
62b4bf7af4 | ||
|
|
47a525ddb4 | ||
|
|
f4360725e0 | ||
|
|
b86607cd41 | ||
|
|
bf57de85c3 | ||
|
|
2dd6118ff4 | ||
|
|
816a53a7b1 | ||
|
|
ced93b0525 | ||
|
|
524ff9a4a6 | ||
|
|
f15032f905 | ||
|
|
d7d31a19e5 | ||
|
|
df2a7ddca4 | ||
|
|
ba9c71ffa4 | ||
|
|
21b6c6569c | ||
|
|
92aba9565a | ||
|
|
6ea0aebb0b | ||
|
|
b5cdcb75b4 | ||
|
|
bd9aae40b8 | ||
|
|
33f931c0a4 | ||
|
|
ede8279c17 | ||
|
|
268b84a2b6 | ||
|
|
0a67145d80 | ||
|
|
2e55bc470c | ||
|
|
cf0d038978 | ||
|
|
92e7db1082 | ||
|
|
c45c47f935 | ||
|
|
341e27f9a3 | ||
|
|
ab167175c9 | ||
|
|
3c2933338f | ||
|
|
829784fa50 | ||
|
|
3c45f8dc91 | ||
|
|
f8ebf7c6ad | ||
|
|
510c364607 | ||
|
|
a3d6fcccb7 | ||
|
|
42c8082edd | ||
|
|
1a7edf7f0e | ||
|
|
4760deaf9c | ||
|
|
0f7b9d02a0 | ||
|
|
adc86c7945 | ||
|
|
12969eda07 | ||
|
|
e07952455f | ||
|
|
4494230854 | ||
|
|
e8dd21c0c3 | ||
|
|
36ef1b2fd6 | ||
|
|
c3d2bd8ed1 | ||
|
|
da2b4be539 | ||
|
|
b5e11f85d5 | ||
|
|
9e1b2f8912 | ||
|
|
1e5f4b0267 | ||
|
|
4fedd03074 | ||
|
|
f6feaeea85 | ||
|
|
c8743c0ab7 | ||
|
|
3d80ed95ca | ||
|
|
0a28bfa9c2 | ||
|
|
6d771f77e6 | ||
|
|
717ca1bb18 | ||
|
|
4f3c48cb4f | ||
|
|
b1b02dc8e5 | ||
|
|
a060ff52ad | ||
|
|
42d73fe25d | ||
|
|
b5946344dc | ||
|
|
dd46e45aba | ||
|
|
61ee4549e1 | ||
|
|
9767f6244f | ||
|
|
0038d74b86 | ||
|
|
6b2163c61f | ||
|
|
56f976c6b5 | ||
|
|
3ee0bfe1ea | ||
|
|
cd9f003da1 | ||
|
|
c452524e3e | ||
|
|
13f98ddbd6 | ||
|
|
9a5c7c10de | ||
|
|
41998565db | ||
|
|
3c64a8eb18 | ||
|
|
962ba0b358 | ||
|
|
16780f91a3 | ||
|
|
5e5a06b0ff | ||
|
|
859e0f20b8 | ||
|
|
9a15d5ce4e | ||
|
|
e4bba28579 | ||
|
|
f3efddd849 | ||
|
|
39190f97d4 | ||
|
|
3b037c5011 | ||
|
|
79387d5396 | ||
|
|
62e747b74a | ||
|
|
9643aed8f8 | ||
|
|
f4fb9e3ab4 | ||
|
|
30487e6108 | ||
|
|
fd2d285af5 | ||
|
|
87bbf59d87 | ||
|
|
37e954626d | ||
|
|
829c7d8be6 | ||
|
|
3274885803 | ||
|
|
c6153ea67d | ||
|
|
191bffedcb | ||
|
|
9ddda81372 | ||
|
|
ddb3c4e3ce | ||
|
|
c87d27630b | ||
|
|
c1d0bb830e | ||
|
|
93dde4c985 | ||
|
|
0eb1cbce43 | ||
|
|
a935c8bb35 | ||
|
|
03eea8ce15 | ||
|
|
76b1adebc4 | ||
|
|
3be8f685bd | ||
|
|
4a392395ab | ||
|
|
fd9755e4a0 | ||
|
|
34151b03ef | ||
|
|
f63205f86c | ||
|
|
5e5867528d | ||
|
|
05623b0e13 | ||
|
|
12602da16c | ||
|
|
2b6dee9949 | ||
|
|
11fa305508 | ||
|
|
b532a3e784 | ||
|
|
f37f5b0ae2 | ||
|
|
c779573204 | ||
|
|
897debb106 | ||
|
|
0b43716c56 | ||
|
|
4ad1c8a238 | ||
|
|
9578ce0820 | ||
|
|
c5d8a1b3ad | ||
|
|
99049db807 | ||
|
|
70de42ccea | ||
|
|
fb74f39793 | ||
|
|
ef130b23ef | ||
|
|
2549dc7d20 | ||
|
|
85a03e6249 | ||
|
|
0903f28b0c | ||
|
|
c663907e37 | ||
|
|
830be27eb2 | ||
|
|
041f4e4bb5 | ||
|
|
d3fa87fd94 | ||
|
|
4dffb5d593 | ||
|
|
b114672e03 | ||
|
|
3c3713553e | ||
|
|
fd164862f3 | ||
|
|
ac8804ca6a | ||
|
|
429e13bf4d | ||
|
|
5d9578d231 | ||
|
|
f4e0ce2ad4 | ||
|
|
aff6785e0b | ||
|
|
2656fae9c9 | ||
|
|
3ed10e304d | ||
|
|
7d17ef0da1 | ||
|
|
0202cf07d5 | ||
|
|
ad9c35e44b | ||
|
|
65286803a5 | ||
|
|
16bd58667c | ||
|
|
939d556c7e | ||
|
|
823d5459af | ||
|
|
10bd619712 | ||
|
|
9f5b0c9ddd | ||
|
|
87eadb4825 | ||
|
|
5b91e4214c | ||
|
|
fd5c120d36 | ||
|
|
3075764402 | ||
|
|
bdad599f36 | ||
|
|
29ab428979 | ||
|
|
4e92b06baa | ||
|
|
faf1209eba | ||
|
|
4dee009d51 | ||
|
|
9ad54bb86c | ||
|
|
2710d72e07 | ||
|
|
c3a1401960 | ||
|
|
585cc0d991 | ||
|
|
15ecb5b1d4 | ||
|
|
00a2ac7f2f | ||
|
|
4d34b5a3ee | ||
|
|
32dcedd703 | ||
|
|
42d48e4bfb | ||
|
|
6b12e9902c | ||
|
|
2801b929e7 | ||
|
|
16db68aa8e | ||
|
|
ae3a525008 | ||
|
|
44cead1865 | ||
|
|
24b3068203 | ||
|
|
52db726c9b | ||
|
|
8f4184b887 | ||
|
|
de2b8fbd88 | ||
|
|
c2c8fbec3c | ||
|
|
7de3a7039a | ||
|
|
3c11361502 | ||
|
|
a148bb5aeb | ||
|
|
ad632de6da | ||
|
|
bde8911dab | ||
|
|
3c894d83a2 | ||
|
|
d4e1d1e2f7 | ||
|
|
c01aacbcee | ||
|
|
939cb12670 | ||
|
|
91736ef29d | ||
|
|
6e303f7cf4 | ||
|
|
9440d18b25 | ||
|
|
890ba0f818 | ||
|
|
5a9270de85 | ||
|
|
95868c071b | ||
|
|
e427f20158 | ||
|
|
664a582576 | ||
|
|
df9ceb0274 | ||
|
|
118c4e8119 | ||
|
|
9ea803f89a | ||
|
|
0d43aba286 | ||
|
|
b6b30edf17 | ||
|
|
3784bd7027 | ||
|
|
806fdd721d | ||
|
|
915687f4f4 | ||
|
|
07aa30fccc | ||
|
|
e39ab82142 | ||
|
|
f0d5ad122a | ||
|
|
dd4db738fd | ||
|
|
50b1e3372d | ||
|
|
84f6e2e1bf | ||
|
|
59e4e0fba4 | ||
|
|
171496c2ca | ||
|
|
b6e8659371 | ||
|
|
1553ff1211 | ||
|
|
979a039847 | ||
|
|
8aa3617a18 | ||
|
|
5e5235f5d1 | ||
|
|
f53f1e64c6 | ||
|
|
cbcd2e14ce | ||
|
|
b9227b1570 | ||
|
|
e058140bac | ||
|
|
9caf45fd81 | ||
|
|
dfa71443ca | ||
|
|
c7511c7aa9 | ||
|
|
9503c34d5b | ||
|
|
19be1f85da | ||
|
|
4e44c26beb | ||
|
|
9d1ef85af8 | ||
|
|
a41d8d6101 | ||
|
|
46f2a204be | ||
|
|
bbc5ba7e2a | ||
|
|
f8e5521b50 | ||
|
|
0ae2b7f338 | ||
|
|
c2ac84c3d3 | ||
|
|
fd29dc5133 | ||
|
|
aab45dff44 | ||
|
|
6fee2b8b10 | ||
|
|
ad56608b4d | ||
|
|
bc63166f48 | ||
|
|
d2743e1b1e | ||
|
|
376253eb49 | ||
|
|
4997c3b5a9 | ||
|
|
9b5adfeb2c | ||
|
|
70af864d2d | ||
|
|
067167cc39 | ||
|
|
95ee037a44 | ||
|
|
3ecf3a359d | ||
|
|
8a4853948a | ||
|
|
31de8ffc3d | ||
|
|
602d04e236 | ||
|
|
a44d1fbd37 | ||
|
|
e4bb21f25c | ||
|
|
87d447f7b5 | ||
|
|
e2e1e23ab5 | ||
|
|
145410eb93 | ||
|
|
46a6afcc19 | ||
|
|
2d189c4b4b | ||
|
|
f2e4923277 | ||
|
|
222254896c | ||
|
|
7b812dee75 | ||
|
|
b8f153e4eb | ||
|
|
445affd609 | ||
|
|
20b1f013d0 | ||
|
|
7e97bb7bc2 | ||
|
|
30abab5925 | ||
|
|
af39d3e520 | ||
|
|
5d7ffd8d00 | ||
|
|
c2b986649c | ||
|
|
4d958d4e32 | ||
|
|
de4a96e471 | ||
|
|
89902ae637 | ||
|
|
dc9a9f979b | ||
|
|
82933e22e4 | ||
|
|
d2321d684d | ||
|
|
fe664bb7ab | ||
|
|
a34eabc594 | ||
|
|
85082d9608 | ||
|
|
ca1a2a2b26 | ||
|
|
e5c9c34dde | ||
|
|
75047edf8a | ||
|
|
6ef14f8e36 | ||
|
|
d0111cef75 | ||
|
|
d8b4218e8f | ||
|
|
f1ae9a088c | ||
|
|
572ed554fd | ||
|
|
9e95607fb5 | ||
|
|
75b5d25c46 | ||
|
|
97fe1902e6 | ||
|
|
2c01be58d3 | ||
|
|
748a863866 | ||
|
|
e168f20d9e | ||
|
|
a32e551fd0 | ||
|
|
6f1bfae957 | ||
|
|
7f63e753d4 | ||
|
|
d457a0e0bc | ||
|
|
f2528d3573 | ||
|
|
456371447f | ||
|
|
e842148c20 | ||
|
|
078f9a1c53 | ||
|
|
26077d3a6b | ||
|
|
4d40a637ed | ||
|
|
325ce9f2f4 | ||
|
|
7f1fe746ca | ||
|
|
49babe8f78 | ||
|
|
02b9f0911d | ||
|
|
701d22643e | ||
|
|
f8ec9abfad | ||
|
|
2b4f5f5879 | ||
|
|
fe36d33e0d | ||
|
|
08f7394af0 | ||
|
|
5a61000daa | ||
|
|
8d1fd75c6c | ||
|
|
e6b46ebccf | ||
|
|
621ca13906 | ||
|
|
22e64af6b5 | ||
|
|
ed0acd6c23 | ||
|
|
7050a1f8f8 | ||
|
|
06982a1b68 | ||
|
|
f7ff3d1021 | ||
|
|
8593a7206b | ||
|
|
a7af3c1bd4 | ||
|
|
55218a0629 | ||
|
|
9cf23d920b | ||
|
|
ecbb9b705f | ||
|
|
36c34ff40e | ||
|
|
61311f9b03 | ||
|
|
5c03e5453a | ||
|
|
1207e1a848 | ||
|
|
d8c156258b | ||
|
|
972c2d64a8 | ||
|
|
9e44617199 | ||
|
|
8aa4fcf448 | ||
|
|
39e62cd800 | ||
|
|
d8c5a42777 | ||
|
|
0a19924b36 | ||
|
|
665fd72480 | ||
|
|
76b073c366 | ||
|
|
fa357479ef | ||
|
|
19a9c24485 | ||
|
|
05ecca1f4d | ||
|
|
58847298be | ||
|
|
a9f8cecaec | ||
|
|
441b4d2797 | ||
|
|
1808bc3027 | ||
|
|
d4db1a51d2 | ||
|
|
f06ac47557 | ||
|
|
d8fb8ce606 | ||
|
|
26b7816552 | ||
|
|
8576f9c97f | ||
|
|
c4eecca6b6 | ||
|
|
02f1788261 | ||
|
|
4a908d970a | ||
|
|
0278ee2515 | ||
|
|
eabde97d17 | ||
|
|
eb88e06ab1 | ||
|
|
11ba0ed696 | ||
|
|
73f477941d | ||
|
|
2de4b7e4c4 | ||
|
|
9552ce6210 | ||
|
|
5f300b8aea | ||
|
|
688bf25be9 | ||
|
|
14f567c031 | ||
|
|
1ab304bfbc | ||
|
|
2e2a7cee79 | ||
|
|
a3c0ed8113 | ||
|
|
642a3673af | ||
|
|
971653f117 | ||
|
|
ff8fb087e0 | ||
|
|
8be3cce9a9 | ||
|
|
c7b2ec7502 | ||
|
|
f2da376df1 | ||
|
|
90035ded39 | ||
|
|
8c593fe3f6 | ||
|
|
96a4b73bb4 | ||
|
|
83fdaaf409 | ||
|
|
f17dd82d8e | ||
|
|
81929b9ec1 | ||
|
|
b8dfdfa31d | ||
|
|
4021be531e | ||
|
|
093097cf31 | ||
|
|
f985c02ce7 | ||
|
|
602eacf0ed | ||
|
|
bcca781896 | ||
|
|
b6a8e6ba81 | ||
|
|
4768c46151 | ||
|
|
fef46c71a0 | ||
|
|
acfbc8ef04 | ||
|
|
e72cd14d9d | ||
|
|
eb5b512c34 | ||
|
|
fecd317275 | ||
|
|
d848dcf56c | ||
|
|
df849b4ca8 | ||
|
|
0b3e627de6 | ||
|
|
4fc1fed44a | ||
|
|
bf2ffe8dfa | ||
|
|
e73e3b7726 | ||
|
|
e519c9c5f3 | ||
|
|
76aa2e9354 | ||
|
|
1bd64e97cc | ||
|
|
67e13d7249 | ||
|
|
60de0ffb3a | ||
|
|
d4fa00ab96 | ||
|
|
25aebd030a | ||
|
|
7ce5b8f5fb | ||
|
|
b703384f6b | ||
|
|
af298b86d7 | ||
|
|
cfdb687db3 | ||
|
|
6f9feb00ee | ||
|
|
7214c58b4b | ||
|
|
47aeefe395 | ||
|
|
a9708513aa | ||
|
|
26c1db4091 | ||
|
|
37e07338ca | ||
|
|
c4879aef3d | ||
|
|
ac9b41fdae | ||
|
|
137d998e46 | ||
|
|
b0eda6166d | ||
|
|
d0c48ad350 | ||
|
|
f20c5b66b9 | ||
|
|
536c27233c | ||
|
|
92a8a98000 | ||
|
|
40a4631eba | ||
|
|
cd71e87d0e | ||
|
|
8612bb1dea | ||
|
|
f1a0dabf15 | ||
|
|
17e256544c | ||
|
|
d968c55e48 | ||
|
|
a401c51364 | ||
|
|
a68eb721d5 | ||
|
|
cd98b90c35 | ||
|
|
b56f3fb3a8 | ||
|
|
4e7e66ac9b | ||
|
|
039fdc0384 | ||
|
|
cbd19c4b48 | ||
|
|
e166ba5c24 | ||
|
|
8c45a5ee84 | ||
|
|
81513888e3 | ||
|
|
b1ed3eff49 | ||
|
|
f8d9425c7f | ||
|
|
c7ea960718 | ||
|
|
ca4c09ceca | ||
|
|
16176d759a | ||
|
|
8b11764b08 | ||
|
|
a9dabbdd20 | ||
|
|
bc4126f526 | ||
|
|
294244b99f | ||
|
|
0a9a8e418b | ||
|
|
b04840ffa3 | ||
|
|
2fe812084a | ||
|
|
c4b640f073 | ||
|
|
51a5209218 | ||
|
|
988cda9b76 | ||
|
|
1c0797d990 | ||
|
|
90d9b5e39a | ||
|
|
6f2f97ec06 | ||
|
|
9905c2a3da | ||
|
|
008d19732a | ||
|
|
93f8614070 | ||
|
|
cbf5cfa4d3 | ||
|
|
c365c3388e | ||
|
|
77496b30fd | ||
|
|
52568da4a8 | ||
|
|
588efab2c7 | ||
|
|
1afd444238 | ||
|
|
9bd335f7d3 | ||
|
|
12cb8dfcaf | ||
|
|
7f3a70bf6b | ||
|
|
967dc4e4dd | ||
|
|
d6e30e48d9 | ||
|
|
055101d1fd | ||
|
|
d56d64985e | ||
|
|
3e2915d531 | ||
|
|
65caaaf7a8 | ||
|
|
58f3332dd6 | ||
|
|
4c09464838 | ||
|
|
97aafacd40 | ||
|
|
41d4ba9721 | ||
|
|
ade30470cc | ||
|
|
4242dc0f94 | ||
|
|
9f4054e7bf | ||
|
|
0012b8cd6e | ||
|
|
c41aaca444 | ||
|
|
290de33bbd | ||
|
|
9be2572a82 | ||
|
|
5aba0c509c | ||
|
|
6225159434 | ||
|
|
0cac32d0d5 | ||
|
|
75d57e4335 | ||
|
|
9e95e3162a | ||
|
|
5dc31e9fde | ||
|
|
ba678cb92b | ||
|
|
5e5e567181 | ||
|
|
e8bb7ccda7 | ||
|
|
b139bfabf0 | ||
|
|
a6eaba7e18 | ||
|
|
e9a5137889 | ||
|
|
690b7a7d05 | ||
|
|
5c8292d804 | ||
|
|
c88266f76c | ||
|
|
d8da2a8c50 | ||
|
|
4d09f6345e | ||
|
|
f8221b9b5d | ||
|
|
8b1be9d35a | ||
|
|
a1110c22b0 | ||
|
|
405e83b79c | ||
|
|
e3ab7e86ce | ||
|
|
1dc683e872 | ||
|
|
3fb3f07644 | ||
|
|
7f937aeaae | ||
|
|
ff93a16548 | ||
|
|
bec91ae52d | ||
|
|
bc8a624dbd | ||
|
|
ca9f8dad5c | ||
|
|
496ca64008 | ||
|
|
2383e134de | ||
|
|
f4442972bc | ||
|
|
38d1bd613f | ||
|
|
3315807a37 | ||
|
|
8dd84bb0ec | ||
|
|
e9eaff7f7e | ||
|
|
0469cad3bc | ||
|
|
d289dd523a | ||
|
|
1cb6adb3dc | ||
|
|
e93d0ab0f2 | ||
|
|
aa4b3d81ba | ||
|
|
61537d985d | ||
|
|
c999886fe0 | ||
|
|
0e6e391caf | ||
|
|
26573ce489 | ||
|
|
c2869c8c67 | ||
|
|
792a836121 | ||
|
|
72ef927b32 | ||
|
|
bb67901e92 | ||
|
|
f87d4ea150 | ||
|
|
7ec376774f | ||
|
|
21ed4bd420 | ||
|
|
93b8c39ad7 | ||
|
|
6ea771781f | ||
|
|
7fc4343dac | ||
|
|
b12bd3849c | ||
|
|
78d9cbe659 | ||
|
|
3e3800bc59 | ||
|
|
9adbff6e16 | ||
|
|
f7a2e3f874 | ||
|
|
5b78ce0bff | ||
|
|
2f2ac86513 | ||
|
|
cbb6432803 | ||
|
|
fbb1cd60d6 | ||
|
|
51d3f3829c | ||
|
|
53c8fc7751 | ||
|
|
22fa2825f8 | ||
|
|
752c17df3c | ||
|
|
66542292c3 | ||
|
|
4745597c0a | ||
|
|
1e7d101063 | ||
|
|
4dc9ee9363 | ||
|
|
07f9a34036 | ||
|
|
97b55a0da6 | ||
|
|
14afc8d998 | ||
|
|
cb8bcd7edf | ||
|
|
eb607a6b41 | ||
|
|
e83e15b9fc | ||
|
|
32430968b8 | ||
|
|
f3e9856107 | ||
|
|
d9ec870598 | ||
|
|
a182e526d7 | ||
|
|
76db17c7f8 | ||
|
|
a8d338a997 | ||
|
|
9589f343df | ||
|
|
71c602be95 | ||
|
|
d48c936770 | ||
|
|
1d7777fb0b | ||
|
|
ffc095a3e5 | ||
|
|
8db932afd9 | ||
|
|
527c994d43 | ||
|
|
45c2cfd92e | ||
|
|
8bb5eecf0d | ||
|
|
5dc4cf7206 | ||
|
|
e73d66d65a | ||
|
|
40b2050e71 | ||
|
|
0cc279b109 | ||
|
|
800faf96d4 | ||
|
|
86e13b6ee1 | ||
|
|
131d2dae3c | ||
|
|
de3cd9fe72 | ||
|
|
b8389e81a1 | ||
|
|
3e4ea1662c | ||
|
|
bc63b2cd3f | ||
|
|
2c3a11012f | ||
|
|
3701246fb1 | ||
|
|
95311cb225 | ||
|
|
0e4b47c13d | ||
|
|
79b4136403 | ||
|
|
91425aea62 | ||
|
|
6d61491a5b | ||
|
|
efbb251635 | ||
|
|
70471b54f6 | ||
|
|
f0205c8eba | ||
|
|
18b66c7835 | ||
|
|
edb77c24ad | ||
|
|
4e01e70ef5 | ||
|
|
0e16c0cb24 | ||
|
|
7e777c5460 | ||
|
|
07402c7a90 | ||
|
|
1c19fa9e38 | ||
|
|
8c799dbf5c | ||
|
|
65f26ae443 | ||
|
|
b690e71ecb | ||
|
|
4ba4ef3c7d | ||
|
|
704a73888a | ||
|
|
d2bf1112ad | ||
|
|
33e3da1f12 | ||
|
|
1a5fa290a3 | ||
|
|
0314610a95 | ||
|
|
98f6da3222 | ||
|
|
19c660c965 | ||
|
|
6ed23c7abe | ||
|
|
4fc50d5019 | ||
|
|
5a87326518 | ||
|
|
584c500247 | ||
|
|
bd790a2cd4 | ||
|
|
7e27275eae | ||
|
|
89ca98e84f | ||
|
|
342bb62635 | ||
|
|
7da0bf5a2e | ||
|
|
ce874d5c62 | ||
|
|
a1c90ceb52 | ||
|
|
ecda9bd34e | ||
|
|
a952009d4a | ||
|
|
6f2e1345b2 | ||
|
|
25d47ac7d0 | ||
|
|
a2be700a87 | ||
|
|
e396d48488 | ||
|
|
0c57379dfe | ||
|
|
b885100dfe | ||
|
|
3d0d201208 | ||
|
|
2e4d1d51e5 | ||
|
|
7b93c831de | ||
|
|
a18f6045a3 | ||
|
|
9f08900064 | ||
|
|
e0b88ce42a | ||
|
|
1ff2ec760b | ||
|
|
216214625a | ||
|
|
740b763e78 | ||
|
|
cd9b9a8ab8 | ||
|
|
faf1600721 | ||
|
|
80e1fcd672 | ||
|
|
f75384ecdd | ||
|
|
bfb9a7b855 | ||
|
|
78314d9529 | ||
|
|
019cce0203 | ||
|
|
2911861db8 | ||
|
|
0f679ac99c | ||
|
|
33bfddeba9 | ||
|
|
0486f5a294 | ||
|
|
b94c06f81c | ||
|
|
d7170c0264 | ||
|
|
60405fcfbc | ||
|
|
bff8dbee30 | ||
|
|
c0cc37787a | ||
|
|
32c828670a | ||
|
|
d4812c09a4 | ||
|
|
600c8117a3 | ||
|
|
1a156b1c75 | ||
|
|
f5d656c87d | ||
|
|
f22a7d29dd | ||
|
|
a7bde44ea9 | ||
|
|
2783a1da1b | ||
|
|
5c504ca9f4 | ||
|
|
fe44dd08cc | ||
|
|
9077f683ae | ||
|
|
9413c3e100 | ||
|
|
8c2563e64a | ||
|
|
ad1faee2ef | ||
|
|
7d8a279a12 | ||
|
|
382498e01d | ||
|
|
820598cdb8 | ||
|
|
42f33a2dca | ||
|
|
ca0765ac00 | ||
|
|
49aee6f291 | ||
|
|
5e20b74dcc | ||
|
|
fa87ebd9a7 | ||
|
|
b0b1505777 | ||
|
|
005fa14254 | ||
|
|
0d85c2e88a | ||
|
|
61da8de828 | ||
|
|
61ee956043 | ||
|
|
7b60b69968 | ||
|
|
efd081a2c5 | ||
|
|
c9134d1eeb | ||
|
|
36de48302d | ||
|
|
f74481cb53 | ||
|
|
4c17839831 | ||
|
|
ea7e44e122 | ||
|
|
bc02161d56 | ||
|
|
e8d5c92cb3 | ||
|
|
c6c35115e1 | ||
|
|
029a597a31 | ||
|
|
6619b9b98b | ||
|
|
225c3e3a20 | ||
|
|
596316536e | ||
|
|
ed123750d9 | ||
|
|
bede95cd05 | ||
|
|
693a226a41 | ||
|
|
7ec2793c9a | ||
|
|
521e92796b | ||
|
|
2a50beb9ee | ||
|
|
e310072782 | ||
|
|
622c449a86 | ||
|
|
b4aa41cac1 | ||
|
|
a1f7f7069f | ||
|
|
f74d8cb470 | ||
|
|
6b7c4d6330 | ||
|
|
f5d997bbbb | ||
|
|
fea911c3d7 | ||
|
|
5222c277e6 | ||
|
|
5835a1da5c | ||
|
|
2120d76250 | ||
|
|
b02cb2b833 | ||
|
|
79f132c23b | ||
|
|
4b92288f7b | ||
|
|
b9c667cdcc | ||
|
|
c4227b17e5 | ||
|
|
243b65961f | ||
|
|
a6d20b0865 | ||
|
|
7538169251 | ||
|
|
31e300e4e8 | ||
|
|
599bf78f20 | ||
|
|
f53fdb8d7a | ||
|
|
3b4bfeab22 | ||
|
|
8c1f828c1f | ||
|
|
f896755a31 | ||
|
|
e406f8be81 | ||
|
|
84107d2b65 | ||
|
|
d890984c70 | ||
|
|
72e4c84f8a | ||
|
|
acd41d985c | ||
|
|
1312657aca | ||
|
|
a502bb9529 | ||
|
|
5947dee9f9 | ||
|
|
e68d59098f | ||
|
|
06b79287e2 | ||
|
|
61efd60681 | ||
|
|
e906d27606 | ||
|
|
0968dd85aa | ||
|
|
d3654b2ee4 | ||
|
|
ef943a588d | ||
|
|
8c6ebc665d | ||
|
|
2d2df3fc2c | ||
|
|
958ddcb49c | ||
|
|
5e9e988a96 | ||
|
|
34f3409f9b | ||
|
|
49c548494e | ||
|
|
7ba41fbb7d | ||
|
|
75240a028a | ||
|
|
3335c82350 | ||
|
|
e16e72cbbd | ||
|
|
0b6f7962a4 | ||
|
|
ea3413be9b | ||
|
|
10055f578b | ||
|
|
cddd000848 | ||
|
|
cdb400d32b | ||
|
|
8e1f792cd1 | ||
|
|
f0299e07f9 | ||
|
|
b3be556837 |
58
.github/workflows/publish-to-pypi.yml
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
name: Publish to PyPI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- draft-v4
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'ltdrdata' || github.repository_owner == 'Comfy-Org' }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install build twine
|
||||
|
||||
- name: Get current version
|
||||
id: current_version
|
||||
run: |
|
||||
CURRENT_VERSION=$(grep -oP 'version = "\K[^"]+' pyproject.toml)
|
||||
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Current version: $CURRENT_VERSION"
|
||||
|
||||
- name: Build package
|
||||
run: python -m build
|
||||
|
||||
- name: Create GitHub Release
|
||||
id: create_release
|
||||
uses: softprops/action-gh-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
files: dist/*
|
||||
tag_name: v${{ steps.current_version.outputs.version }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
skip-existing: true
|
||||
verbose: true
|
||||
10
.github/workflows/publish.yml
vendored
@ -3,19 +3,23 @@ on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 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' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@main
|
||||
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 }}
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||
|
||||
23
.github/workflows/ruff.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
name: Python Linting
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
ruff:
|
||||
name: Run Ruff
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.x
|
||||
|
||||
- name: Install Ruff
|
||||
run: pip install ruff
|
||||
|
||||
- name: Run Ruff
|
||||
run: ruff check .
|
||||
7
.gitignore
vendored
@ -1,6 +1,8 @@
|
||||
__pycache__/
|
||||
.idea/
|
||||
.vscode/
|
||||
.history/
|
||||
*.code-workspace
|
||||
.tmp
|
||||
.cache
|
||||
config.ini
|
||||
@ -12,4 +14,7 @@ matrix_auth
|
||||
channels.list
|
||||
comfyworkflows_sharekey
|
||||
github-stats-cache.json
|
||||
pip_overrides.json
|
||||
pip_overrides.json
|
||||
*.json
|
||||
check2.sh
|
||||
/venv/
|
||||
295
README.md
@ -2,15 +2,14 @@
|
||||
|
||||
**ComfyUI-Manager** is an extension designed to enhance the usability of [ComfyUI](https://github.com/comfyanonymous/ComfyUI). It offers management functions to **install, remove, disable, and enable** various custom nodes of ComfyUI. Furthermore, this extension provides a hub feature and convenience functions to access a wide range of information within ComfyUI.
|
||||
|
||||

|
||||

|
||||
|
||||
## NOTICE
|
||||
* V2.48.1: Security policy has been changed. Downloads of models in the list are allowed under the 'normal' security level.
|
||||
* V2.47: Security policy has been changed. The former 'normal' is now 'normal-', and 'normal' no longer allows high-risk features, even if your ComfyUI is local.
|
||||
* V2.37 Show a ✅ mark to accounts that have been active on GitHub for more than six months.
|
||||
* V2.33 Security policy is applied.
|
||||
* V2.21 [cm-cli](docs/en/cm-cli.md) tool is added.
|
||||
* V2.18 to V2.18.3 is not functioning due to a severe bug. Users on these versions are advised to promptly update to V2.18.4. Please navigate to the `ComfyUI/custom_nodes/ComfyUI-Manager` directory and execute `git pull` to update.
|
||||
* V3.38: **Security patch** - Manager data migrated to protected path. See [Migration Guide](docs/en/v3.38-userdata-security-migration.md).
|
||||
* V3.16: Support for `uv` has been added. Set `use_uv` in `config.ini`.
|
||||
* V3.10: `double-click feature` is removed
|
||||
* This feature has been moved to https://github.com/ltdrdata/comfyui-connection-helper
|
||||
* V3.3.2: Overhauled. Officially supports [https://registry.comfy.org/](https://registry.comfy.org/).
|
||||
* You can see whole nodes info on [ComfyUI Nodes Info](https://ltdrdata.github.io/) page.
|
||||
|
||||
## Installation
|
||||
@ -19,8 +18,8 @@
|
||||
|
||||
To install ComfyUI-Manager in addition to an existing installation of ComfyUI, you can follow the following steps:
|
||||
|
||||
1. goto `ComfyUI/custom_nodes` dir in terminal(cmd)
|
||||
2. `git clone https://github.com/ltdrdata/ComfyUI-Manager.git`
|
||||
1. Go to `ComfyUI/custom_nodes` dir in terminal (cmd)
|
||||
2. `git clone https://github.com/ltdrdata/ComfyUI-Manager comfyui-manager`
|
||||
3. Restart ComfyUI
|
||||
|
||||
|
||||
@ -30,9 +29,10 @@ To install ComfyUI-Manager in addition to an existing installation of ComfyUI, y
|
||||
- standalone version
|
||||
- select option: use windows default console window
|
||||
2. Download [scripts/install-manager-for-portable-version.bat](https://github.com/ltdrdata/ComfyUI-Manager/raw/main/scripts/install-manager-for-portable-version.bat) into installed `"ComfyUI_windows_portable"` directory
|
||||
3. double click `install-manager-for-portable-version.bat` batch file
|
||||
- Don't click. Right-click the link and choose 'Save As...'
|
||||
3. Double-click `install-manager-for-portable-version.bat` batch file
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
### Installation[method3] (Installation through comfy-cli: install ComfyUI and ComfyUI-Manager at once.)
|
||||
@ -48,35 +48,35 @@ pip install comfy-cli
|
||||
comfy install
|
||||
```
|
||||
|
||||
Linux/OSX:
|
||||
Linux/macOS:
|
||||
```commandline
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
pip install comfy-cli
|
||||
comfy install
|
||||
```
|
||||
* See also: https://github.com/Comfy-Org/comfy-cli
|
||||
|
||||
|
||||
### Installation[method4] (Installation for linux+venv: ComfyUI + ComfyUI-Manager)
|
||||
### Installation[method4] (Installation for Linux+venv: ComfyUI + ComfyUI-Manager)
|
||||
|
||||
To install ComfyUI with ComfyUI-Manager on Linux using a venv environment, you can follow these steps:
|
||||
* **prerequisite: python-is-python3, python3-venv, git**
|
||||
|
||||
1. Download [scripts/install-comfyui-venv-linux.sh](https://github.com/ltdrdata/ComfyUI-Manager/raw/main/scripts/install-comfyui-venv-linux.sh) into empty install directory
|
||||
- Don't click. Right-click the link and choose 'Save As...'
|
||||
- ComfyUI will be installed in the subdirectory of the specified directory, and the directory will contain the generated executable script.
|
||||
2. `chmod +x install-comfyui-venv-linux.sh`
|
||||
3. `./install-comfyui-venv-linux.sh`
|
||||
|
||||
### Installation Precautions
|
||||
* **DO**: `ComfyUI-Manager` files must be accurately located in the path `ComfyUI/custom_nodes/ComfyUI-Manager`
|
||||
* **DO**: `ComfyUI-Manager` files must be accurately located in the path `ComfyUI/custom_nodes/comfyui-manager`
|
||||
* Installing in a compressed file format is not recommended.
|
||||
* **DON'T**: Decompress directly into the `ComfyUI/custom_nodes` location, resulting in the Manager contents like `__init__.py` being placed directly in that directory.
|
||||
* You have to remove all ComfyUI-Manager files from `ComfyUI/custom_nodes`
|
||||
* **DON'T**: In a form where decompression occurs in a path such as `ComfyUI/custom_nodes/ComfyUI-Manager/ComfyUI-Manager`.
|
||||
* You have to move `ComfyUI/custom_nodes/ComfyUI-Manager/ComfyUI-Manager` to `ComfyUI/custom_nodes/ComfyUI-Manager`
|
||||
* **DON'T**: In a form where decompression occurs in a path such as `ComfyUI/custom_nodes/ComfyUI-Manager-main`.
|
||||
* In such cases, `ComfyUI-Manager` may operate, but it won't be recognized within `ComfyUI-Manager`, and updates cannot be performed. It also poses the risk of duplicate installations.
|
||||
* You have to rename `ComfyUI/custom_nodes/ComfyUI-Manager-main` to `ComfyUI/custom_nodes/ComfyUI-Manager`
|
||||
* In such cases, `ComfyUI-Manager` may operate, but it won't be recognized within `ComfyUI-Manager`, and updates cannot be performed. It also poses the risk of duplicate installations. Remove it and install properly via `git clone` method.
|
||||
|
||||
|
||||
You can execute ComfyUI by running either `./run_gpu.sh` or `./run_cpu.sh` depending on your system configuration.
|
||||
@ -87,42 +87,17 @@ This repository provides Colab notebooks that allow you to install and use Comfy
|
||||
* Support for basic installation of ComfyUI-Manager
|
||||
* Support for automatically installing dependencies of custom nodes upon restarting Colab notebooks.
|
||||
|
||||
## Changes
|
||||
* **2.38** `Install Custom Nodes` menu is changed to `Custom Nodes Manager`.
|
||||
* **2.21** [cm-cli](docs/en/cm-cli.md) tool is added.
|
||||
* **2.4** Copy the connections of the nearest node by double-clicking.
|
||||
* **2.2.3** Support Components System
|
||||
* **0.29** Add `Update all` feature
|
||||
* **0.25** support db channel
|
||||
* You can directly modify the db channel settings in the `config.ini` file.
|
||||
* If you want to maintain a new DB channel, please modify the `channels.list` and submit a PR.
|
||||
* **0.23** support multiple selection
|
||||
* **0.18.1** `skip update check` feature added.
|
||||
* A feature that allows quickly opening windows in environments where update checks take a long time.
|
||||
* **0.17.1** Bug fix for the issue where enable/disable of the web extension was not working. Compatibility patch for StableSwarmUI.
|
||||
* Requires latest version of ComfyUI (Revision: 1240)
|
||||
* **0.17** Support preview method setting feature.
|
||||
* **0.14** Support robust update.
|
||||
* **0.13** Support additional 'pip' section for install spec.
|
||||
* **0.12** Better installation support for Windows.
|
||||
* **0.9** Support keyword search in installer menu.
|
||||
* **V0.7.1** Bug fix for the issue where updates were not being applied on Windows.
|
||||
* **For those who have been using versions 0.6, please perform a manual git pull in the custom_nodes/ComfyUI-Manager directory.**
|
||||
* **V0.7** To address the issue of a slow list refresh, separate the fetch update and update check processes.
|
||||
* **V0.6** Support extension installation for missing nodes.
|
||||
* **V0.5** Removed external git program dependencies.
|
||||
|
||||
|
||||
## How To Use
|
||||
|
||||
1. Click "Manager" button on main menu
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
2. If you click on 'Install Custom Nodes' or 'Install Models', an installer dialog will open.
|
||||
|
||||

|
||||

|
||||
|
||||
* There are three DB modes: `DB: Channel (1day cache)`, `DB: Local`, and `DB: Channel (remote)`.
|
||||
* `Channel (1day cache)` utilizes Channel cache information with a validity period of one day to quickly display the list.
|
||||
@ -138,9 +113,9 @@ This repository provides Colab notebooks that allow you to install and use Comfy
|
||||
|
||||
3. Click 'Install' or 'Try Install' button.
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
* Installed: This item is already installed.
|
||||
* Install: Clicking this button will install the item.
|
||||
@ -150,42 +125,66 @@ This repository provides Colab notebooks that allow you to install and use Comfy
|
||||
* Channel settings have a broad impact, affecting not only the node list but also all functions like "Update all."
|
||||
* Conflicted Nodes with a yellow background show a list of nodes conflicting with other extensions in the respective extension. This issue needs to be addressed by the developer, and users should be aware that due to these conflicts, some nodes may not function correctly and may need to be installed accordingly.
|
||||
|
||||
4. If you set the `Badge:` item in the menu as `Badge: Nickname`, `Badge: Nickname (hide built-in)`, `Badge: #ID Nickname`, `Badge: #ID Nickname (hide built-in)` the information badge will be displayed on the node.
|
||||
* When selecting (hide built-in), it hides the 🦊 icon, which signifies built-in nodes.
|
||||
* Nodes without any indication on the badge are custom nodes that Manager cannot recognize.
|
||||
* `Badge: Nickname` displays the nickname of custom nodes, while `Badge: #ID Nickname` also includes the internal ID of the node.
|
||||
|
||||

|
||||
|
||||
|
||||
5. Share
|
||||
 
|
||||
4. Share
|
||||
 
|
||||
|
||||
* You can share the workflow by clicking the Share button at the bottom of the main menu or selecting Share Output from the Context Menu of the Image node.
|
||||
* Currently, it supports sharing via [https://comfyworkflows.com/](https://comfyworkflows.com/),
|
||||
[https://openart.ai](https://openart.ai/workflows/dev), [https://youml.com](https://youml.com)
|
||||
as well as through the Matrix channel.
|
||||
|
||||

|
||||

|
||||
|
||||
* Through the Share settings in the Manager menu, you can configure the behavior of the Share button in the Main menu or Share Ouput button on Context Menu.
|
||||
* Through the Share settings in the Manager menu, you can configure the behavior of the Share button in the Main menu or Share Output button on Context Menu.
|
||||
* `None`: hide from Main menu
|
||||
* `All`: Show a dialog where the user can select a title for sharing.
|
||||
|
||||
|
||||
## Paths
|
||||
Starting from V3.38, Manager uses a protected system path for enhanced security.
|
||||
|
||||
* <USER_DIRECTORY>
|
||||
* If executed without any options, the path defaults to ComfyUI/user.
|
||||
* It can be set using --user-directory <USER_DIRECTORY>.
|
||||
|
||||
| ComfyUI Version | Manager Path |
|
||||
|-----------------|--------------|
|
||||
| v0.3.76+ (with System User API) | `<USER_DIRECTORY>/__manager/` |
|
||||
| Older versions | `<USER_DIRECTORY>/default/ComfyUI-Manager/` |
|
||||
|
||||
* Basic config files: `config.ini`
|
||||
* Configurable channel lists: `channels.list`
|
||||
* Configurable pip overrides: `pip_overrides.json`
|
||||
* Configurable pip blacklist: `pip_blacklist.list`
|
||||
* Configurable pip auto fix: `pip_auto_fix.list`
|
||||
* Saved snapshot files: `snapshots/`
|
||||
* Startup script files: `startup-scripts/`
|
||||
* Component files: `components/`
|
||||
|
||||
> **Note**: See [Migration Guide](docs/en/v3.38-userdata-security-migration.md) for upgrade details.
|
||||
|
||||
|
||||
## `extra_model_paths.yaml` Configuration
|
||||
The following settings are applied based on the section marked as `is_default`.
|
||||
|
||||
* `custom_nodes`: Path for installing custom nodes
|
||||
* Importing does not need to adhere to the path set as `is_default`, but this is the path where custom nodes are installed by the `ComfyUI Nodes Manager`.
|
||||
* `download_model_base`: Path for downloading models
|
||||
|
||||
|
||||
## Snapshot-Manager
|
||||
* When you press `Save snapshot` or use `Update All` on `Manager Menu`, the current installation status snapshot is saved.
|
||||
* Snapshot file dir: `ComfyUI-Manager/snapshots`
|
||||
* Snapshot file dir: `<USER_DIRECTORY>/default/ComfyUI-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 `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>/default/ComfyUI-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).
|
||||
|
||||
@ -199,48 +198,18 @@ This repository provides Colab notebooks that allow you to install and use Comfy
|
||||
|
||||
## Custom node support guide
|
||||
|
||||
* Currently, the system operates by cloning the git repository and sequentially installing the dependencies listed in requirements.txt using pip, followed by invoking the install.py script. In the future, we plan to discuss and determine the specifications for supporting custom nodes.
|
||||
* **NOTICE:**
|
||||
- You should no longer assume that the GitHub repository name will match the subdirectory name under `custom_nodes`. The name of the subdirectory under `custom_nodes` will now use the normalized name from the `name` field in `pyproject.toml`.
|
||||
- Avoid relying on directory names for imports whenever possible.
|
||||
|
||||
* Please submit a pull request to update either the custom-node-list.json or model-list.json file.
|
||||
* https://docs.comfy.org/registry/overview
|
||||
* https://github.com/Comfy-Org/rfcs
|
||||
|
||||
* The scanner currently provides a detection function for missing nodes, which is capable of detecting nodes described by the following two patterns.
|
||||
* Or you can provide manually `node_list.json` file.
|
||||
|
||||
```
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"ExecutionSwitch": ExecutionSwitch,
|
||||
"ExecutionBlocker": ExecutionBlocker,
|
||||
...
|
||||
}
|
||||
|
||||
NODE_CLASS_MAPPINGS.update({
|
||||
"UniFormer-SemSegPreprocessor": Uniformer_SemSegPreprocessor,
|
||||
"SemSegPreprocessor": Uniformer_SemSegPreprocessor,
|
||||
})
|
||||
```
|
||||
|
||||
* When you write a docstring in the header of the .py file for the Node as follows, it will be used for managing the database in the Manager.
|
||||
* Currently, only the `nickname` is being used, but other parts will also be utilized in the future.
|
||||
* The `nickname` will be the name displayed on the badge of the node.
|
||||
* If there is no `nickname`, it will be truncated to 20 characters from the arbitrarily written title and used.
|
||||
```
|
||||
"""
|
||||
@author: Dr.Lt.Data
|
||||
@title: Impact Pack
|
||||
@nickname: Impact Pack
|
||||
@description: This extension offers various detector nodes and detailer nodes that allow you to configure a workflow that automatically enhances facial details. And provide iterative upscaler.
|
||||
"""
|
||||
```
|
||||
|
||||
|
||||
* **Special purpose files** (optional)
|
||||
**Special purpose files** (optional)
|
||||
* `pyproject.toml` - Spec file for comfyregistry.
|
||||
* `node_list.json` - When your custom nodes pattern of NODE_CLASS_MAPPINGS is not conventional, it is used to manually provide a list of nodes for reference. ([example](https://github.com/melMass/comfy_mtb/raw/main/node_list.json))
|
||||
* `requirements.txt` - When installing, this pip requirements will be installed automatically
|
||||
* `install.py` - When installing, it is automatically called
|
||||
* `uninstall.py` - When uninstalling, it is automatically called
|
||||
* `disable.py` - When disabled, it is automatically called
|
||||
* When installing a custom node setup `.js` file, it is recommended to write this script for disabling.
|
||||
* `enable.py` - When enabled, it is automatically called
|
||||
* **All scripts are executed from the root path of the corresponding custom node.**
|
||||
|
||||
|
||||
@ -259,12 +228,12 @@ NODE_CLASS_MAPPINGS.update({
|
||||
}
|
||||
```
|
||||
* `<current timestamp>` Ensure that the timestamp is always unique.
|
||||
* "components" should have the same structure as the content of the file stored in ComfyUI-Manager/components.
|
||||
* "components" should have the same structure as the content of the file stored in `<USER_DIRECTORY>/default/ComfyUI-Manager/components`.
|
||||
* `<component name>`: The name should be in the format `<prefix>::<node name>`.
|
||||
* `<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 ComfyUI-Manager/components.
|
||||
* `<packname>`: If the packname is not empty, the category becomes packname/workflow, and it is saved in the <packname>.pack file in `<USER_DIRECTORY>/default/ComfyUI-Manager/components`.
|
||||
* `<category>`: If there is neither a category nor a packname, it is saved in the components category.
|
||||
```
|
||||
"version":"1.0",
|
||||
@ -279,23 +248,49 @@ NODE_CLASS_MAPPINGS.update({
|
||||
* 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
|
||||
|
||||

|
||||

|
||||
|
||||
* When you click on the ```Install Missing Custom Nodes``` button in the menu, it displays a list of extension nodes that contain nodes not currently present in the workflow.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
# Config
|
||||
* You can modify the `config.ini` file to apply the settings for ComfyUI-Manager.
|
||||
* The path to the `config.ini` used by ComfyUI-Manager is displayed in the startup log messages.
|
||||
* See also: [https://github.com/ltdrdata/ComfyUI-Manager#paths]
|
||||
* Configuration options:
|
||||
```
|
||||
[default]
|
||||
git_exe = <Manually specify the path to the git executable. If left empty, the default git executable path will be used.>
|
||||
use_uv = <Use uv instead of pip for dependency installation.>
|
||||
default_cache_as_channel_url = <Determines whether to retrieve the DB designated as channel_url at startup>
|
||||
bypass_ssl = <Set to True if SSL errors occur to disable SSL.>
|
||||
file_logging = <Configure whether to create a log file used by ComfyUI-Manager.>
|
||||
windows_selector_event_loop_policy = <If an event loop error occurs on Windows, set this to True.>
|
||||
model_download_by_agent = <When downloading models, use an agent instead of torchvision_download_url.>
|
||||
downgrade_blacklist = <Set a list of packages to prevent downgrades. List them separated by commas.>
|
||||
security_level = <Set the security level => strong|normal|normal-|weak>
|
||||
always_lazy_install = <Whether to perform dependency installation on restart even in environments other than Windows.>
|
||||
network_mode = <Set the network mode => public|private|offline>
|
||||
```
|
||||
|
||||
* network_mode:
|
||||
- public: An environment that uses a typical public network.
|
||||
- private: An environment that uses a closed network, where a private node DB is configured via `channel_url`. (Uses cache if available)
|
||||
- offline: An environment that does not use any external connections when using an offline network. (Uses cache if available)
|
||||
|
||||
|
||||
## Additional Feature
|
||||
* 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.
|
||||
@ -316,10 +311,41 @@ NODE_CLASS_MAPPINGS.update({
|
||||
* Custom pip mapping
|
||||
* When you create the `pip_overrides.json` file, it changes the installation of specific pip packages to installations defined by the user.
|
||||
* Please refer to the `pip_overrides.json.template` file.
|
||||
|
||||
|
||||
* Prevent the installation of specific pip packages
|
||||
* List the package names one per line in the `pip_blacklist.list` file.
|
||||
|
||||
* Automatically Restoring pip Installation
|
||||
* If you list pip spec requirements in `pip_auto_fix.list`, similar to `requirements.txt`, it will automatically restore the specified versions when starting ComfyUI or when versions get mismatched during various custom node installations.
|
||||
* `--index-url` can be used.
|
||||
|
||||
* Use `aria2` as downloader
|
||||
* [howto](docs/en/use_aria2.md)
|
||||
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The following features can be configured using environment variables:
|
||||
|
||||
* **COMFYUI_PATH**: The installation path of ComfyUI
|
||||
* **GITHUB_ENDPOINT**: Reverse proxy configuration for environments with limited access to GitHub
|
||||
* **HF_ENDPOINT**: Reverse proxy configuration for environments with limited access to Hugging Face
|
||||
|
||||
|
||||
### Example 1:
|
||||
Redirecting `https://github.com/ltdrdata/ComfyUI-Impact-Pack` to `https://mirror.ghproxy.com/https://github.com/ltdrdata/ComfyUI-Impact-Pack`
|
||||
|
||||
```
|
||||
GITHUB_ENDPOINT=https://mirror.ghproxy.com/https://github.com
|
||||
```
|
||||
|
||||
#### Example 2:
|
||||
Changing `https://huggingface.co/path/to/somewhere` to `https://some-hf-mirror.com/path/to/somewhere`
|
||||
|
||||
```
|
||||
HF_ENDPOINT=https://some-hf-mirror.com
|
||||
```
|
||||
|
||||
## Scanner
|
||||
When you run the `scan.sh` script:
|
||||
|
||||
@ -330,23 +356,20 @@ 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 ComfyUI-Manager/config.ini file that is generated.
|
||||
* If your `git.exe` is installed in a specific location other than system git, please install ComfyUI-Manager and run ComfyUI. Then, specify the path including the file name in `git_exe = ` in the `<USER_DIRECTORY>/default/ComfyUI-Manager/config.ini` file that is generated.
|
||||
* If updating ComfyUI-Manager itself fails, please go to the **ComfyUI-Manager** directory and execute the command `git update-ref refs/remotes/origin/main a361cc1 && git fetch --all && git pull`.
|
||||
* Alternatively, download the update-fix.py script from [update-fix.py](https://github.com/ltdrdata/ComfyUI-Manager/raw/main/scripts/update-fix.py) and place it in the ComfyUI-Manager directory. Then, run it using your Python command.
|
||||
For the portable version, use `..\..\..\python_embeded\python.exe update-fix.py`.
|
||||
* For cases where nodes like `PreviewTextNode` from `ComfyUI_Custom_Nodes_AlekPet` are only supported as front-end nodes, we currently do not provide missing nodes for them.
|
||||
* Currently, `vid2vid` is not being updated, causing compatibility issues.
|
||||
* 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`
|
||||
@ -363,10 +386,10 @@ When you run the `scan.sh` script:
|
||||
* `high` level risky features
|
||||
* `Install via git url`, `pip install`
|
||||
* Installation of custom nodes registered not in the `default channel`.
|
||||
* Display terminal log
|
||||
* Fix custom nodes
|
||||
|
||||
* `middle` level risky features
|
||||
* Uninstall/Update/Fix custom nodes
|
||||
* Uninstall/Update
|
||||
* Installation of custom nodes registered in the `default channel`.
|
||||
* Restore/Remove Snapshot
|
||||
* Restart
|
||||
@ -375,42 +398,6 @@ When you run the `scan.sh` script:
|
||||
* Update ComfyUI
|
||||
|
||||
|
||||
## TODO: Unconventional form of custom node list
|
||||
|
||||
* https://github.com/diontimmer/Sample-Diffusion-ComfyUI-Extension
|
||||
* https://github.com/senshilabs/NINJA-plugin
|
||||
* https://github.com/MockbaTheBorg/Nodes
|
||||
* https://github.com/StartHua/Comfyui_GPT_Story
|
||||
* https://github.com/NielsGercama/comfyui_customsampling
|
||||
* https://github.com/wrightdaniel2017/ComfyUI-VideoLipSync
|
||||
* https://github.com/bxdsjs/ComfyUI-Image-preprocessing
|
||||
* https://github.com/SMUELDigital/ComfyUI-ONSET
|
||||
* https://github.com/SimithWang/comfyui-renameImages
|
||||
* https://github.com/icefairy64/comfyui-model-tilt
|
||||
* https://github.com/andrewharp/ComfyUI-EasyNodes
|
||||
* https://github.com/SimithWang/comfyui-renameImages
|
||||
* https://github.com/Tcheko243/ComfyUI-Photographer-Alpha7-Nodes
|
||||
* https://github.com/Limbicnation/ComfyUINodeToolbox
|
||||
* https://github.com/chenpipi0807/pip_longsize
|
||||
* https://github.com/APZmedia/ComfyUI-APZmedia-srtTools
|
||||
|
||||
## Roadmap
|
||||
|
||||
- [x] System displaying information about failed custom nodes import.
|
||||
- [x] Guide for missing nodes in ComfyUI vanilla nodes.
|
||||
- [x] Collision checking system for nodes with the same ID across extensions.
|
||||
- [x] Template sharing system. (-> Component system based on Group Nodes)
|
||||
- [x] 3rd party API system.
|
||||
- [ ] Auto migration for custom nodes with changed structures.
|
||||
- [ ] Version control feature for nodes.
|
||||
- [ ] List of currently used custom nodes.
|
||||
- [x] Download support multiple model download.
|
||||
- [x] Model download via url.
|
||||
- [x] List sorting (custom nodes).
|
||||
- [x] List sorting (model).
|
||||
- [ ] Provides description of node.
|
||||
|
||||
|
||||
# Disclaimer
|
||||
|
||||
* This extension simply provides the convenience of installing custom nodes and does not guarantee their proper functioning.
|
||||
|
||||
16
__init__.py
@ -1,12 +1,22 @@
|
||||
"""
|
||||
This file is the entry point for the ComfyUI-Manager package, handling CLI-only mode and initial setup.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
cli_mode_flag = os.path.join(os.path.dirname(__file__), '.enable-cli-only-mode')
|
||||
|
||||
if not os.path.exists(cli_mode_flag):
|
||||
from .glob import manager_server
|
||||
WEB_DIRECTORY = "js"
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "glob"))
|
||||
import manager_server # noqa: F401
|
||||
import share_3rdparty # noqa: F401
|
||||
import cm_global
|
||||
|
||||
if not cm_global.disable_front and not 'DISABLE_COMFYUI_MANAGER_FRONT' in os.environ:
|
||||
WEB_DIRECTORY = "js"
|
||||
else:
|
||||
print(f"\n[ComfyUI-Manager] !! cli-only-mode is enabled !!\n")
|
||||
print("\n[ComfyUI-Manager] !! cli-only-mode is enabled !!\n")
|
||||
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
__all__ = ['NODE_CLASS_MAPPINGS']
|
||||
|
||||
5
check.sh
@ -9,6 +9,7 @@ files=(
|
||||
"alter-list.json"
|
||||
"extension-node-map.json"
|
||||
"github-stats.json"
|
||||
"extras.json"
|
||||
"node_db/new/custom-node-list.json"
|
||||
"node_db/new/model-list.json"
|
||||
"node_db/new/extension-node-map.json"
|
||||
@ -36,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
|
||||
|
||||
29831
custom-node-list.json
41
docs/README.md
Normal file
@ -0,0 +1,41 @@
|
||||
# ComfyUI-Manager: Documentation
|
||||
|
||||
This directory contains documentation for the ComfyUI-Manager, providing guides and tutorials for users in multiple languages.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
The documentation is organized into language-specific directories:
|
||||
|
||||
- **en/**: English documentation
|
||||
- **ko/**: Korean documentation
|
||||
|
||||
## Core Documentation Files
|
||||
|
||||
### Command-Line Interface
|
||||
|
||||
- **cm-cli.md**: Documentation for the ComfyUI-Manager Command Line Interface (CLI), which allows using manager functionality without the UI.
|
||||
|
||||
### Advanced Features
|
||||
|
||||
- **use_aria2.md**: Guide for using the aria2 download accelerator with ComfyUI-Manager for faster model downloads.
|
||||
|
||||
## Documentation Standards
|
||||
|
||||
The documentation follows these standards:
|
||||
|
||||
1. **Markdown Format**: All documentation is written in Markdown for easy rendering on GitHub and other platforms
|
||||
2. **Language-specific Directories**: Content is separated by language to facilitate localization
|
||||
3. **Feature-focused Documentation**: Each major feature has its own documentation file
|
||||
4. **Updated with Releases**: Documentation is kept in sync with software releases
|
||||
|
||||
## Contributing to Documentation
|
||||
|
||||
When contributing new documentation:
|
||||
|
||||
1. Place files in the appropriate language directory
|
||||
2. Use clear, concise language appropriate for the target audience
|
||||
3. Include examples where helpful
|
||||
4. Consider adding screenshots or diagrams for complex features
|
||||
5. Maintain consistent formatting with existing documentation
|
||||
|
||||
This documentation directory will continue to grow to support the expanding feature set of ComfyUI-Manager.
|
||||
@ -121,8 +121,9 @@ ComfyUI-Loopchain
|
||||
* If no file exists at the snapshot path, it is implicitly assumed to be in ComfyUI-Manager/snapshots.
|
||||
* `--pip-non-url`: Restore for pip packages registered on PyPI.
|
||||
* `--pip-non-local-url`: Restore for pip packages registered at web URLs.
|
||||
* `--pip-local-url`: Restore for pip packages specified by local paths.
|
||||
|
||||
* `--pip-local-url`: Restore for pip packages specified by local paths.
|
||||
* `--user-directory`: Set the user directory.
|
||||
* `--restore-to`: The path where the restored custom nodes will be installed. (When this option is applied, only the custom nodes installed in the target path are recognized as installed.)
|
||||
|
||||
### 5. CLI Only Mode
|
||||
|
||||
@ -138,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.
|
||||
|
||||
230
docs/en/v3.38-userdata-security-migration.md
Normal file
@ -0,0 +1,230 @@
|
||||
# ComfyUI-Manager V3.38: Userdata Security Migration Guide
|
||||
|
||||
## Introduction
|
||||
|
||||
ComfyUI-Manager V3.38 introduces a **security patch** that migrates Manager's configuration and data to a protected system path. This change leverages ComfyUI's new System User Protection API (PR #10966) to provide enhanced security isolation.
|
||||
|
||||
This guide explains what happens during the migration and how to handle various situations.
|
||||
|
||||
---
|
||||
|
||||
## What Changed
|
||||
|
||||
### Finding Your Paths
|
||||
|
||||
When ComfyUI starts, it displays the full paths in the terminal:
|
||||
|
||||
```
|
||||
** User directory: /path/to/ComfyUI/user
|
||||
** ComfyUI-Manager config path: /path/to/ComfyUI/user/__manager/config.ini
|
||||
```
|
||||
|
||||
Look for these lines in your startup log to find the exact location on your system. In this guide, paths are shown relative to the `user` directory.
|
||||
|
||||
### Path Migration
|
||||
|
||||
| Data | Legacy Path | New Path |
|
||||
|------|-------------|----------|
|
||||
| Configuration | `user/default/ComfyUI-Manager/` | `user/__manager/` |
|
||||
| Snapshots | `user/default/ComfyUI-Manager/snapshots/` | `user/__manager/snapshots/` |
|
||||
|
||||
### Why This Change
|
||||
|
||||
In older ComfyUI versions, the `default/` directory was **unprotected** and accessible via web APIs. If you ran ComfyUI with `--listen 0.0.0.0` or similar options to allow external connections, this data **may have been tampered with** by malicious actors.
|
||||
|
||||
**Note:** If you only used ComfyUI locally (without `--listen` or with `--listen 127.0.0.1`), your data was not exposed to this vulnerability.
|
||||
|
||||
The new `__manager` path uses ComfyUI's protected system directory, which:
|
||||
- **Cannot be accessed** from outside (protected by ComfyUI)
|
||||
- Isolates system settings from user data
|
||||
- Enables stricter security for remote access
|
||||
|
||||
**This is why only `config.ini` is automatically migrated** - other files (snapshots) may have been compromised and should be manually verified before copying.
|
||||
|
||||
---
|
||||
|
||||
## Automatic Migration
|
||||
|
||||
When you start ComfyUI with the new System User Protection API, Manager automatically handles the migration:
|
||||
|
||||
### Step 1: Configuration Migration
|
||||
|
||||
Only `config.ini` is migrated automatically.
|
||||
|
||||
**Important**: Snapshots are **NOT** automatically migrated. You must copy them manually if needed.
|
||||
|
||||
### Step 2: Security Level Check
|
||||
|
||||
During migration, if your security level is below `normal` (i.e., `weak` or `normal-`), it will be automatically raised to `normal`. This is a safety measure because the security level setting itself may have been tampered with in the old version.
|
||||
|
||||
```
|
||||
======================================================================
|
||||
[ComfyUI-Manager] WARNING: Security level adjusted
|
||||
- Previous: 'weak' → New: 'normal'
|
||||
- Raised to prevent unauthorized remote access.
|
||||
======================================================================
|
||||
```
|
||||
|
||||
If you need a lower security level, you can manually edit the config after migration.
|
||||
|
||||
### Step 3: Legacy Backup
|
||||
|
||||
Your entire legacy directory is moved to a backup location:
|
||||
```
|
||||
user/__manager/.legacy-manager-backup/
|
||||
```
|
||||
|
||||
This backup is preserved until you manually delete it.
|
||||
|
||||
---
|
||||
|
||||
## Persistent Backup Notification
|
||||
|
||||
As long as the backup exists, Manager will remind you on **every startup**:
|
||||
|
||||
```
|
||||
----------------------------------------------------------------------
|
||||
[ComfyUI-Manager] NOTICE: Legacy backup exists
|
||||
- Your old Manager data was backed up to:
|
||||
/path/to/ComfyUI/user/__manager/.legacy-manager-backup
|
||||
- Please verify and remove it when no longer needed.
|
||||
----------------------------------------------------------------------
|
||||
```
|
||||
|
||||
**To stop this notification**: Delete the `.legacy-manager-backup` folder inside `user/__manager/` after confirming you don't need any data from it.
|
||||
|
||||
---
|
||||
|
||||
## Recovering Old Data
|
||||
|
||||
### Snapshots
|
||||
|
||||
If you need your old snapshots, copy the contents of `.legacy-manager-backup/snapshots/` to `user/__manager/snapshots/`.
|
||||
|
||||
---
|
||||
|
||||
## Outdated ComfyUI Warning
|
||||
|
||||
If you're running an older version of ComfyUI without the System User Protection API, Manager will:
|
||||
|
||||
1. **Force security level to `strong`** - All installations are blocked
|
||||
2. **Display warning message**:
|
||||
|
||||
```
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
[ComfyUI-Manager] ERROR: ComfyUI version is outdated!
|
||||
- Most operations are blocked for security.
|
||||
- ComfyUI update is still allowed.
|
||||
- Please update ComfyUI to use Manager normally.
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
```
|
||||
|
||||
**Solution**: Update ComfyUI to v0.3.76 or later.
|
||||
|
||||
---
|
||||
|
||||
## Security Levels
|
||||
|
||||
| Level | What's Allowed |
|
||||
|-------|----------------|
|
||||
| `strong` | ComfyUI update only. All other installations blocked. |
|
||||
| `normal` | Install/update/remove registered custom nodes and models. |
|
||||
| `normal-` | Above + Install via Git URL or pip (localhost only). |
|
||||
| `weak` | All operations allowed, including from remote connections. |
|
||||
|
||||
**Notes:**
|
||||
- `strong` is forced on outdated ComfyUI versions.
|
||||
- `normal` is the default and recommended for most users.
|
||||
- `normal-` is for developers who need to install unregistered nodes locally.
|
||||
- `weak` should only be used in isolated development environments.
|
||||
|
||||
### Changing Security Level
|
||||
|
||||
Edit `user/__manager/config.ini`:
|
||||
```ini
|
||||
[default]
|
||||
security_level = normal
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Messages
|
||||
|
||||
### "comfyui_outdated" (HTTP 403)
|
||||
|
||||
This error appears when:
|
||||
- Your ComfyUI doesn't have the System User Protection API
|
||||
- All installations are blocked until you update ComfyUI
|
||||
|
||||
**Solution**: Update ComfyUI to the latest version.
|
||||
|
||||
### "security_level" (HTTP 403)
|
||||
|
||||
This error appears when:
|
||||
- Your security level blocks the requested operation
|
||||
- For example, `strong` level blocks all installations
|
||||
|
||||
**Solution**: Lower your security level in config.ini if appropriate for your use case.
|
||||
|
||||
---
|
||||
|
||||
## Security Warning: Suspicious Path
|
||||
|
||||
If you see this error on an **older** ComfyUI:
|
||||
|
||||
```
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
[ComfyUI-Manager] ERROR: Suspicious path detected!
|
||||
- '__manager' exists with low security level: 'weak'
|
||||
- Please verify manually:
|
||||
/path/to/ComfyUI/user/__manager/config.ini
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
```
|
||||
|
||||
On older ComfyUI versions, the `__manager` directory is not normally created. If this directory exists, it may have been created externally. For safety, manually verify the contents of this directory before updating ComfyUI.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### All my installations are blocked
|
||||
|
||||
**Check 1**: Is your ComfyUI updated?
|
||||
- Old ComfyUI forces `security_level = strong`
|
||||
- Update ComfyUI to resolve
|
||||
|
||||
**Check 2**: What's your security level?
|
||||
- Check `user/__manager/config.ini`
|
||||
- `security_level = strong` blocks all installations
|
||||
|
||||
### My snapshots are missing
|
||||
|
||||
Snapshots are not automatically migrated. You need to manually copy the `snapshots` folder from inside `.legacy-manager-backup` to the `user/__manager/` directory.
|
||||
|
||||
### I keep seeing the backup notification
|
||||
|
||||
Delete the `.legacy-manager-backup` folder inside `user/__manager/` after confirming you don't need any data from it.
|
||||
|
||||
### Snapshot restore is blocked
|
||||
|
||||
On old ComfyUI (without System User API), snapshot restore is blocked because security is forced to `strong`. Update ComfyUI to enable snapshot restore.
|
||||
|
||||
---
|
||||
|
||||
## File Structure Reference
|
||||
|
||||
```
|
||||
user/
|
||||
└── __manager/
|
||||
├── config.ini # Manager configuration
|
||||
├── channels.list # Custom node channels
|
||||
├── snapshots/ # Environment snapshots
|
||||
└── .legacy-manager-backup/ # Backup of old Manager data (temporary)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- **ComfyUI**: v0.3.76 or later (with System User Protection API)
|
||||
- **ComfyUI-Manager**: V3.38 or later
|
||||
@ -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]]`
|
||||
|
||||
@ -123,7 +123,8 @@ ComfyUI-Loopchain
|
||||
* `--pip-non-url`: PyPI 에 등록된 pip 패키지들에 대해서 복구를 수행
|
||||
* `--pip-non-local-url`: web URL에 등록된 pip 패키지들에 대해서 복구를 수행
|
||||
* `--pip-local-url`: local 경로를 지정하고 있는 pip 패키지들에 대해서 복구를 수행
|
||||
|
||||
* `--user-directory`: 사용자 디렉토리 설정
|
||||
* `--restore-to`: 복구될 커스텀 노드가 설치될 경로. (이 옵션을 적용할 경우 오직 대상 경로에 설치된 custom nodes만 설치된 것으로 인식함.)
|
||||
|
||||
### 5. CLI only mode
|
||||
|
||||
@ -132,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. 의존성 설치
|
||||
@ -140,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해서, 아무런 사전 실행이 적용되지 않도록 합니다.
|
||||
|
||||
42646
extension-node-map.json
26
extras.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"favorites": [
|
||||
"comfyui_ipadapter_plus",
|
||||
"comfyui-animatediff-evolved",
|
||||
"comfyui_controlnet_aux",
|
||||
"comfyui-impact-pack",
|
||||
"comfyui-impact-subpack",
|
||||
"comfyui-custom-scripts",
|
||||
"comfyui-layerdiffuse",
|
||||
"comfyui-liveportraitkj",
|
||||
"aigodlike-comfyui-translation",
|
||||
"comfyui-reactor",
|
||||
"comfyui_instantid",
|
||||
"sd-dynamic-thresholding",
|
||||
"pr-was-node-suite-comfyui-47064894",
|
||||
"comfyui-advancedliveportrait",
|
||||
"comfyui_layerstyle",
|
||||
"efficiency-nodes-comfyui",
|
||||
"comfyui-crystools",
|
||||
"comfyui-advanced-controlnet",
|
||||
"comfyui-videohelpersuite",
|
||||
"comfyui-kjnodes",
|
||||
"comfy-mtb",
|
||||
"comfyui_essentials"
|
||||
]
|
||||
}
|
||||
143
git_helper.py
@ -2,10 +2,9 @@ import subprocess
|
||||
import sys
|
||||
import os
|
||||
import traceback
|
||||
import time
|
||||
|
||||
import git
|
||||
import configparser
|
||||
import re
|
||||
import json
|
||||
import yaml
|
||||
import requests
|
||||
@ -13,6 +12,14 @@ from tqdm.auto import tqdm
|
||||
from git.remote import RemoteProgress
|
||||
|
||||
|
||||
comfy_path = os.environ.get('COMFYUI_PATH')
|
||||
git_exe_path = os.environ.get('GIT_EXE_PATH')
|
||||
|
||||
if comfy_path is None:
|
||||
print("\nWARN: The `COMFYUI_PATH` environment variable is not set. Assuming `custom_nodes/ComfyUI-Manager/../../` as the ComfyUI path.", file=sys.stderr)
|
||||
comfy_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
|
||||
|
||||
def download_url(url, dest_folder, filename=None):
|
||||
# Ensure the destination folder exists
|
||||
if not os.path.exists(dest_folder):
|
||||
@ -36,12 +43,11 @@ def download_url(url, dest_folder, filename=None):
|
||||
print(f"Failed to download file from {url}")
|
||||
|
||||
|
||||
config_path = os.path.join(os.path.dirname(__file__), "config.ini")
|
||||
nodelist_path = os.path.join(os.path.dirname(__file__), "custom-node-list.json")
|
||||
working_directory = os.getcwd()
|
||||
|
||||
if os.path.basename(working_directory) != 'custom_nodes':
|
||||
print(f"WARN: This script should be executed in custom_nodes dir")
|
||||
print("WARN: This script should be executed in custom_nodes dir")
|
||||
print(f"DBG: INFO {working_directory}")
|
||||
print(f"DBG: INFO {sys.argv}")
|
||||
# exit(-1)
|
||||
@ -59,9 +65,11 @@ class GitProgress(RemoteProgress):
|
||||
self.pbar.refresh()
|
||||
|
||||
|
||||
def gitclone(custom_nodes_path, url, target_hash=None):
|
||||
def gitclone(custom_nodes_path, url, target_hash=None, repo_path=None):
|
||||
repo_name = os.path.splitext(os.path.basename(url))[0]
|
||||
repo_path = os.path.join(custom_nodes_path, repo_name)
|
||||
|
||||
if repo_path is None:
|
||||
repo_path = os.path.join(custom_nodes_path, repo_name)
|
||||
|
||||
# Clone the repository from the remote URL
|
||||
repo = git.Repo.clone_from(url, repo_path, recursive=True, progress=GitProgress())
|
||||
@ -94,7 +102,12 @@ def gitcheck(path, do_fetch=False):
|
||||
|
||||
# Get the current commit hash and the commit hash of the remote branch
|
||||
commit_hash = repo.head.commit.hexsha
|
||||
remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha
|
||||
|
||||
if f'{remote_name}/{branch_name}' in repo.refs:
|
||||
remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha
|
||||
else:
|
||||
print("CUSTOM NODE CHECK: True") # non default branch is treated as updatable
|
||||
return
|
||||
|
||||
# Compare the commit hashes to determine if the local repository is behind the remote repository
|
||||
if commit_hash != remote_commit_hash:
|
||||
@ -112,12 +125,60 @@ def gitcheck(path, do_fetch=False):
|
||||
print("CUSTOM NODE CHECK: Error")
|
||||
|
||||
|
||||
def get_remote_name(repo):
|
||||
available_remotes = [remote.name for remote in repo.remotes]
|
||||
if 'origin' in available_remotes:
|
||||
return 'origin'
|
||||
elif 'upstream' in available_remotes:
|
||||
return 'upstream'
|
||||
elif len(available_remotes) > 0:
|
||||
return available_remotes[0]
|
||||
|
||||
if not available_remotes:
|
||||
print(f"[ComfyUI-Manager] No remotes are configured for this repository: {repo.working_dir}")
|
||||
else:
|
||||
print(f"[ComfyUI-Manager] Available remotes in '{repo.working_dir}': ")
|
||||
for remote in available_remotes:
|
||||
print(f"- {remote}")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def switch_to_default_branch(repo):
|
||||
show_result = repo.git.remote("show", "origin")
|
||||
matches = re.search(r"\s*HEAD branch:\s*(.*)", show_result)
|
||||
if matches:
|
||||
default_branch = matches.group(1)
|
||||
remote_name = get_remote_name(repo)
|
||||
|
||||
try:
|
||||
if remote_name is None:
|
||||
return False
|
||||
|
||||
default_branch = repo.git.symbolic_ref(f'refs/remotes/{remote_name}/HEAD').replace(f'refs/remotes/{remote_name}/', '')
|
||||
repo.git.checkout(default_branch)
|
||||
return True
|
||||
except:
|
||||
# try checkout master
|
||||
# try checkout main if failed
|
||||
try:
|
||||
repo.git.checkout(repo.heads.master)
|
||||
return True
|
||||
except:
|
||||
try:
|
||||
if remote_name is not None:
|
||||
repo.git.checkout('-b', 'master', f'{remote_name}/master')
|
||||
return True
|
||||
except:
|
||||
try:
|
||||
repo.git.checkout(repo.heads.main)
|
||||
return True
|
||||
except:
|
||||
try:
|
||||
if remote_name is not None:
|
||||
repo.git.checkout('-b', 'main', f'{remote_name}/main')
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
print("[ComfyUI Manager] Failed to switch to the default branch")
|
||||
return False
|
||||
|
||||
|
||||
def gitpull(path):
|
||||
@ -128,6 +189,7 @@ def gitpull(path):
|
||||
# Pull the latest changes from the remote repository
|
||||
repo = git.Repo(path)
|
||||
if repo.is_dirty():
|
||||
print(f"STASH: '{path}' is dirty.")
|
||||
repo.git.stash()
|
||||
|
||||
commit_hash = repo.head.commit.hexsha
|
||||
@ -141,15 +203,31 @@ def gitpull(path):
|
||||
remote_name = current_branch.tracking_branch().remote_name
|
||||
remote = repo.remote(name=remote_name)
|
||||
|
||||
if f'{remote_name}/{branch_name}' not in repo.refs:
|
||||
switch_to_default_branch(repo)
|
||||
current_branch = repo.active_branch
|
||||
branch_name = current_branch.name
|
||||
|
||||
remote.fetch()
|
||||
remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha
|
||||
if f'{remote_name}/{branch_name}' in repo.refs:
|
||||
remote_commit_hash = repo.refs[f'{remote_name}/{branch_name}'].object.hexsha
|
||||
else:
|
||||
print("CUSTOM NODE PULL: Fail") # update fail
|
||||
return
|
||||
|
||||
if commit_hash == remote_commit_hash:
|
||||
print("CUSTOM NODE PULL: None") # there is no update
|
||||
repo.close()
|
||||
return
|
||||
|
||||
remote.pull()
|
||||
try:
|
||||
repo.git.pull('--ff-only')
|
||||
except git.GitCommandError:
|
||||
backup_name = f'backup_{time.strftime("%Y%m%d_%H%M%S")}'
|
||||
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
|
||||
@ -166,9 +244,7 @@ def gitpull(path):
|
||||
|
||||
|
||||
def checkout_comfyui_hash(target_hash):
|
||||
repo_path = os.path.abspath(os.path.join(working_directory, '..')) # ComfyUI dir
|
||||
|
||||
repo = git.Repo(repo_path)
|
||||
repo = git.Repo(comfy_path)
|
||||
commit_hash = repo.head.commit.hexsha
|
||||
|
||||
if commit_hash != target_hash:
|
||||
@ -250,6 +326,9 @@ def checkout_custom_node_hash(git_custom_node_infos):
|
||||
|
||||
# clone missing
|
||||
for k, v in git_custom_node_infos.items():
|
||||
if 'ComfyUI-Manager' in k:
|
||||
continue
|
||||
|
||||
if not v['disabled']:
|
||||
repo_name = k.split('/')[-1]
|
||||
if repo_name.endswith('.git'):
|
||||
@ -258,7 +337,7 @@ def checkout_custom_node_hash(git_custom_node_infos):
|
||||
path = os.path.join(working_directory, repo_name)
|
||||
if not os.path.exists(path):
|
||||
print(f"CLONE: {path}")
|
||||
gitclone(working_directory, k, v['hash'])
|
||||
gitclone(working_directory, k, target_hash=v['hash'])
|
||||
|
||||
|
||||
def invalidate_custom_node_file(file_custom_node_infos):
|
||||
@ -308,19 +387,18 @@ def invalidate_custom_node_file(file_custom_node_infos):
|
||||
download_url(url, working_directory)
|
||||
|
||||
|
||||
def apply_snapshot(target):
|
||||
def apply_snapshot(path):
|
||||
try:
|
||||
path = os.path.join(os.path.dirname(__file__), 'snapshots', f"{target}")
|
||||
if os.path.exists(path):
|
||||
if not target.endswith('.json') and not target.endswith('.yaml'):
|
||||
if not path.endswith('.json') and not path.endswith('.yaml'):
|
||||
print(f"Snapshot file not found: `{path}`")
|
||||
print("APPLY SNAPSHOT: False")
|
||||
return None
|
||||
|
||||
with open(path, 'r', encoding="UTF-8") as snapshot_file:
|
||||
if target.endswith('.json'):
|
||||
if path.endswith('.json'):
|
||||
info = json.load(snapshot_file)
|
||||
elif target.endswith('.yaml'):
|
||||
elif path.endswith('.yaml'):
|
||||
info = yaml.load(snapshot_file, Loader=yaml.SafeLoader)
|
||||
info = info['custom_nodes']
|
||||
else:
|
||||
@ -332,12 +410,13 @@ def apply_snapshot(target):
|
||||
git_custom_node_infos = info['git_custom_nodes']
|
||||
file_custom_node_infos = info['file_custom_nodes']
|
||||
|
||||
checkout_comfyui_hash(comfyui_hash)
|
||||
if comfyui_hash:
|
||||
checkout_comfyui_hash(comfyui_hash)
|
||||
checkout_custom_node_hash(git_custom_node_infos)
|
||||
invalidate_custom_node_file(file_custom_node_infos)
|
||||
|
||||
print("APPLY SNAPSHOT: True")
|
||||
if 'pips' in info:
|
||||
if 'pips' in info and info['pips']:
|
||||
return info['pips']
|
||||
else:
|
||||
return None
|
||||
@ -414,10 +493,8 @@ def restore_pip_snapshot(pips, options):
|
||||
|
||||
|
||||
def setup_environment():
|
||||
config = configparser.ConfigParser()
|
||||
config.read(config_path)
|
||||
if 'default' in config and 'git_exe' in config['default'] and config['default']['git_exe'] != '':
|
||||
git.Git().update_environment(GIT_PYTHON_GIT_EXECUTABLE=config['default']['git_exe'])
|
||||
if git_exe_path is not None:
|
||||
git.Git().update_environment(GIT_PYTHON_GIT_EXECUTABLE=git_exe_path)
|
||||
|
||||
|
||||
setup_environment()
|
||||
@ -425,7 +502,11 @@ setup_environment()
|
||||
|
||||
try:
|
||||
if sys.argv[1] == "--clone":
|
||||
gitclone(sys.argv[2], sys.argv[3])
|
||||
repo_path = None
|
||||
if len(sys.argv) > 4:
|
||||
repo_path = sys.argv[4]
|
||||
|
||||
gitclone(sys.argv[2], sys.argv[3], repo_path=repo_path)
|
||||
elif sys.argv[1] == "--check":
|
||||
gitcheck(sys.argv[2], False)
|
||||
elif sys.argv[1] == "--fetch":
|
||||
@ -446,5 +527,5 @@ try:
|
||||
except Exception as e:
|
||||
print(e)
|
||||
sys.exit(-1)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
19700
github-stats.json
53
glob/README.md
Normal file
@ -0,0 +1,53 @@
|
||||
# ComfyUI-Manager: Core Backend (glob)
|
||||
|
||||
This directory contains the Python backend modules that power ComfyUI-Manager, handling the core functionality of node management, downloading, security, and server operations.
|
||||
|
||||
## Core Modules
|
||||
|
||||
- **manager_core.py**: The central implementation of management functions, handling configuration, installation, updates, and node management.
|
||||
- **manager_server.py**: Implements server functionality and API endpoints for the web interface to interact with the backend.
|
||||
- **manager_downloader.py**: Handles downloading operations for models, extensions, and other resources.
|
||||
- **manager_util.py**: Provides utility functions used throughout the system.
|
||||
|
||||
## Specialized Modules
|
||||
|
||||
- **cm_global.py**: Maintains global variables and state management across the system.
|
||||
- **cnr_utils.py**: Helper utilities for interacting with the custom node registry (CNR).
|
||||
- **git_utils.py**: Git-specific utilities for repository operations.
|
||||
- **node_package.py**: Handles the packaging and installation of node extensions.
|
||||
- **security_check.py**: Implements the multi-level security system for installation safety.
|
||||
- **share_3rdparty.py**: Manages integration with third-party sharing platforms.
|
||||
|
||||
## Architecture
|
||||
|
||||
The backend follows a modular design pattern with clear separation of concerns:
|
||||
|
||||
1. **Core Layer**: Manager modules provide the primary API and business logic
|
||||
2. **Utility Layer**: Helper modules provide specialized functionality
|
||||
3. **Integration Layer**: Modules that connect to external systems
|
||||
|
||||
## Security Model
|
||||
|
||||
The system implements a comprehensive security framework with multiple levels:
|
||||
|
||||
- **Block**: Highest security - blocks most remote operations
|
||||
- **High**: Allows only specific trusted operations
|
||||
- **Middle**: Standard security for most users
|
||||
- **Normal-**: More permissive for advanced users
|
||||
- **Weak**: Lowest security for development environments
|
||||
|
||||
## Implementation Details
|
||||
|
||||
- The backend is designed to work seamlessly with ComfyUI
|
||||
- Asynchronous task queuing is implemented for background operations
|
||||
- The system supports multiple installation modes
|
||||
- Error handling and risk assessment are integrated throughout the codebase
|
||||
|
||||
## API Integration
|
||||
|
||||
The backend exposes a REST API via `manager_server.py` that enables:
|
||||
- Custom node management (install, update, disable, remove)
|
||||
- Model downloading and organization
|
||||
- System configuration
|
||||
- Snapshot management
|
||||
- Workflow component handling
|
||||
@ -110,3 +110,8 @@ def add_on_revision_detected(k, f):
|
||||
traceback.print_exc()
|
||||
else:
|
||||
variables['cm.on_revision_detected_handler'].append((k, f))
|
||||
|
||||
|
||||
error_dict = {}
|
||||
|
||||
disable_front = False
|
||||
253
glob/cnr_utils.py
Normal file
@ -0,0 +1,253 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
import manager_core
|
||||
import manager_util
|
||||
import requests
|
||||
import toml
|
||||
|
||||
base_url = "https://api.comfy.org"
|
||||
|
||||
|
||||
lock = asyncio.Lock()
|
||||
|
||||
is_cache_loading = False
|
||||
|
||||
async def get_cnr_data(cache_mode=True, dont_wait=True):
|
||||
try:
|
||||
return await _get_cnr_data(cache_mode, dont_wait)
|
||||
except asyncio.TimeoutError:
|
||||
print("A timeout occurred during the fetch process from ComfyRegistry.")
|
||||
return await _get_cnr_data(cache_mode=True, dont_wait=True) # timeout fallback
|
||||
|
||||
async def _get_cnr_data(cache_mode=True, dont_wait=True):
|
||||
global is_cache_loading
|
||||
|
||||
uri = f'{base_url}/nodes'
|
||||
|
||||
async def fetch_all():
|
||||
remained = True
|
||||
page = 1
|
||||
|
||||
full_nodes = {}
|
||||
|
||||
|
||||
# Determine form factor based on environment and platform
|
||||
is_desktop = bool(os.environ.get('__COMFYUI_DESKTOP_VERSION__'))
|
||||
system = platform.system().lower()
|
||||
is_windows = system == 'windows'
|
||||
is_mac = system == 'darwin'
|
||||
is_linux = system == 'linux'
|
||||
|
||||
# Get ComfyUI version tag
|
||||
if is_desktop:
|
||||
# extract version from pyproject.toml instead of git tag
|
||||
comfyui_ver = manager_core.get_current_comfyui_ver() or 'unknown'
|
||||
else:
|
||||
comfyui_ver = manager_core.get_comfyui_tag() or 'unknown'
|
||||
|
||||
if is_desktop:
|
||||
if is_windows:
|
||||
form_factor = 'desktop-win'
|
||||
elif is_mac:
|
||||
form_factor = 'desktop-mac'
|
||||
else:
|
||||
form_factor = 'other'
|
||||
else:
|
||||
if is_windows:
|
||||
form_factor = 'git-windows'
|
||||
elif is_mac:
|
||||
form_factor = 'git-mac'
|
||||
elif is_linux:
|
||||
form_factor = 'git-linux'
|
||||
else:
|
||||
form_factor = 'other'
|
||||
|
||||
while remained:
|
||||
# Add comfyui_version and form_factor to the API request
|
||||
sub_uri = f'{base_url}/nodes?page={page}&limit=30&comfyui_version={comfyui_ver}&form_factor={form_factor}'
|
||||
sub_json_obj = await asyncio.wait_for(manager_util.get_data_with_cache(sub_uri, cache_mode=False, silent=True, dont_cache=True), timeout=30)
|
||||
remained = page < sub_json_obj['totalPages']
|
||||
|
||||
for x in sub_json_obj['nodes']:
|
||||
full_nodes[x['id']] = x
|
||||
|
||||
if page % 5 == 0:
|
||||
print(f"FETCH ComfyRegistry Data: {page}/{sub_json_obj['totalPages']}")
|
||||
|
||||
page += 1
|
||||
time.sleep(0.5)
|
||||
|
||||
print("FETCH ComfyRegistry Data [DONE]")
|
||||
|
||||
for v in full_nodes.values():
|
||||
if 'latest_version' not in v:
|
||||
v['latest_version'] = dict(version='nightly')
|
||||
|
||||
return {'nodes': list(full_nodes.values())}
|
||||
|
||||
if cache_mode:
|
||||
is_cache_loading = True
|
||||
cache_state = manager_util.get_cache_state(uri)
|
||||
|
||||
if dont_wait:
|
||||
if cache_state == 'not-cached':
|
||||
return {}
|
||||
else:
|
||||
print("[ComfyUI-Manager] The ComfyRegistry cache update is still in progress, so an outdated cache is being used.")
|
||||
with open(manager_util.get_cache_path(uri), 'r', encoding="UTF-8", errors="ignore") as json_file:
|
||||
return json.load(json_file)['nodes']
|
||||
|
||||
if cache_state == 'cached':
|
||||
with open(manager_util.get_cache_path(uri), 'r', encoding="UTF-8", errors="ignore") as json_file:
|
||||
return json.load(json_file)['nodes']
|
||||
|
||||
try:
|
||||
json_obj = await fetch_all()
|
||||
manager_util.save_to_cache(uri, json_obj)
|
||||
return json_obj['nodes']
|
||||
except:
|
||||
res = {}
|
||||
print("Cannot connect to comfyregistry.")
|
||||
finally:
|
||||
if cache_mode:
|
||||
is_cache_loading = False
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@dataclass
|
||||
class NodeVersion:
|
||||
changelog: str
|
||||
dependencies: List[str]
|
||||
deprecated: bool
|
||||
id: str
|
||||
version: str
|
||||
download_url: str
|
||||
|
||||
|
||||
def map_node_version(api_node_version):
|
||||
"""
|
||||
Maps node version data from API response to NodeVersion dataclass.
|
||||
|
||||
Args:
|
||||
api_data (dict): The 'node_version' part of the API response.
|
||||
|
||||
Returns:
|
||||
NodeVersion: An instance of NodeVersion dataclass populated with data from the API.
|
||||
"""
|
||||
return NodeVersion(
|
||||
changelog=api_node_version.get(
|
||||
"changelog", ""
|
||||
), # Provide a default value if 'changelog' is missing
|
||||
dependencies=api_node_version.get(
|
||||
"dependencies", []
|
||||
), # Provide a default empty list if 'dependencies' is missing
|
||||
deprecated=api_node_version.get(
|
||||
"deprecated", False
|
||||
), # Assume False if 'deprecated' is not specified
|
||||
id=api_node_version[
|
||||
"id"
|
||||
], # 'id' should be mandatory; raise KeyError if missing
|
||||
version=api_node_version[
|
||||
"version"
|
||||
], # 'version' should be mandatory; raise KeyError if missing
|
||||
download_url=api_node_version.get(
|
||||
"downloadUrl", ""
|
||||
), # Provide a default value if 'downloadUrl' is missing
|
||||
)
|
||||
|
||||
|
||||
def install_node(node_id, version=None):
|
||||
"""
|
||||
Retrieves the node version for installation.
|
||||
|
||||
Args:
|
||||
node_id (str): The unique identifier of the node.
|
||||
version (str, optional): Specific version of the node to retrieve. If omitted, the latest version is returned.
|
||||
|
||||
Returns:
|
||||
NodeVersion: Node version data or error message.
|
||||
"""
|
||||
if version is None:
|
||||
url = f"{base_url}/nodes/{node_id}/install"
|
||||
else:
|
||||
url = f"{base_url}/nodes/{node_id}/install?version={version}"
|
||||
|
||||
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())
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def all_versions_of_node(node_id):
|
||||
url = f"{base_url}/nodes/{node_id}/versions?statuses=NodeVersionStatusActive&statuses=NodeVersionStatusPending"
|
||||
|
||||
response = requests.get(url, verify=not manager_util.bypass_ssl)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def read_cnr_info(fullpath):
|
||||
try:
|
||||
toml_path = os.path.join(fullpath, 'pyproject.toml')
|
||||
tracking_path = os.path.join(fullpath, '.tracking')
|
||||
|
||||
if not os.path.exists(toml_path) or not os.path.exists(tracking_path):
|
||||
return None # not valid CNR node pack
|
||||
|
||||
with open(toml_path, "r", encoding="utf-8") as f:
|
||||
data = toml.load(f)
|
||||
|
||||
project = data.get('project', {})
|
||||
name = project.get('name').strip().lower()
|
||||
|
||||
# normalize version
|
||||
# for example: 2.5 -> 2.5.0
|
||||
version = str(manager_util.StrictVersion(project.get('version')))
|
||||
|
||||
urls = project.get('urls', {})
|
||||
repository = urls.get('Repository')
|
||||
|
||||
if name and version: # repository is optional
|
||||
return {
|
||||
"id": name,
|
||||
"version": version,
|
||||
"url": repository
|
||||
}
|
||||
|
||||
return None
|
||||
except Exception:
|
||||
return None # not valid CNR node pack
|
||||
|
||||
|
||||
def generate_cnr_id(fullpath, cnr_id):
|
||||
cnr_id_path = os.path.join(fullpath, '.git', '.cnr-id')
|
||||
try:
|
||||
if not os.path.exists(cnr_id_path):
|
||||
with open(cnr_id_path, "w") as f:
|
||||
return f.write(cnr_id)
|
||||
except:
|
||||
print(f"[ComfyUI Manager] unable to create file: {cnr_id_path}")
|
||||
|
||||
|
||||
def read_cnr_id(fullpath):
|
||||
cnr_id_path = os.path.join(fullpath, '.git', '.cnr-id')
|
||||
try:
|
||||
if os.path.exists(cnr_id_path):
|
||||
with open(cnr_id_path) as f:
|
||||
return f.read().strip()
|
||||
except:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
87
glob/git_utils.py
Normal file
@ -0,0 +1,87 @@
|
||||
import os
|
||||
import configparser
|
||||
|
||||
|
||||
GITHUB_ENDPOINT = os.getenv('GITHUB_ENDPOINT')
|
||||
|
||||
|
||||
def is_git_repo(path: str) -> bool:
|
||||
""" Check if the path is a git repository. """
|
||||
# NOTE: Checking it through `git.Repo` must be avoided.
|
||||
# It locks the file, causing issues on Windows.
|
||||
return os.path.exists(os.path.join(path, '.git'))
|
||||
|
||||
|
||||
def get_commit_hash(fullpath):
|
||||
git_head = os.path.join(fullpath, '.git', 'HEAD')
|
||||
if os.path.exists(git_head):
|
||||
with open(git_head) as f:
|
||||
line = f.readline()
|
||||
|
||||
if line.startswith("ref: "):
|
||||
ref = os.path.join(fullpath, '.git', line[5:].strip())
|
||||
if os.path.exists(ref):
|
||||
with open(ref) as f2:
|
||||
return f2.readline().strip()
|
||||
else:
|
||||
return "unknown"
|
||||
else:
|
||||
return line
|
||||
|
||||
return "unknown"
|
||||
|
||||
|
||||
def git_url(fullpath):
|
||||
"""
|
||||
resolve version of unclassified custom node based on remote url in .git/config
|
||||
"""
|
||||
git_config_path = os.path.join(fullpath, '.git', 'config')
|
||||
|
||||
if not os.path.exists(git_config_path):
|
||||
return None
|
||||
|
||||
# Set `strict=False` to allow duplicate `vscode-merge-base` sections, addressing <https://github.com/ltdrdata/ComfyUI-Manager/issues/1529>
|
||||
config = configparser.ConfigParser(strict=False)
|
||||
config.read(git_config_path)
|
||||
|
||||
for k, v in config.items():
|
||||
if k.startswith('remote ') and 'url' in v:
|
||||
if 'Comfy-Org/ComfyUI-Manager' in v['url']:
|
||||
return "https://github.com/ltdrdata/ComfyUI-Manager"
|
||||
return v['url']
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def normalize_url(url) -> str:
|
||||
github_id = normalize_to_github_id(url)
|
||||
if github_id is not None:
|
||||
url = f"https://github.com/{github_id}"
|
||||
|
||||
return url
|
||||
|
||||
|
||||
def normalize_to_github_id(url) -> str:
|
||||
if 'github' in url or (GITHUB_ENDPOINT is not None and GITHUB_ENDPOINT in url):
|
||||
author = os.path.basename(os.path.dirname(url))
|
||||
|
||||
if author.startswith('git@github.com:'):
|
||||
author = author.split(':')[1]
|
||||
|
||||
repo_name = os.path.basename(url)
|
||||
if repo_name.endswith('.git'):
|
||||
repo_name = repo_name[:-4]
|
||||
|
||||
return f"{author}/{repo_name}"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_url_for_clone(url):
|
||||
url = normalize_url(url)
|
||||
|
||||
if GITHUB_ENDPOINT is not None and url.startswith('https://github.com/'):
|
||||
url = GITHUB_ENDPOINT + url[18:] # url[18:] -> remove `https://github.com`
|
||||
|
||||
return url
|
||||
|
||||
3065
glob/manager_core.py
@ -1,9 +1,17 @@
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
import urllib
|
||||
import sys
|
||||
import logging
|
||||
import requests
|
||||
from huggingface_hub import HfApi
|
||||
from tqdm.auto import tqdm
|
||||
|
||||
|
||||
aria2 = os.getenv('COMFYUI_MANAGER_ARIA2_SERVER')
|
||||
HF_ENDPOINT = os.getenv('HF_ENDPOINT')
|
||||
|
||||
|
||||
if aria2 is not None:
|
||||
secret = os.getenv('COMFYUI_MANAGER_ARIA2_SECRET')
|
||||
url = urlparse(aria2)
|
||||
@ -14,13 +22,44 @@ if aria2 is not None:
|
||||
aria2 = aria2p.API(aria2p.Client(host=host, port=port, secret=secret))
|
||||
|
||||
|
||||
def basic_download_url(url, dest_folder: str, filename: str):
|
||||
'''
|
||||
Download file from url to dest_folder with filename
|
||||
using requests library.
|
||||
'''
|
||||
import requests
|
||||
|
||||
# Ensure the destination folder exists
|
||||
if not os.path.exists(dest_folder):
|
||||
os.makedirs(dest_folder)
|
||||
|
||||
# Full path to save the file
|
||||
dest_path = os.path.join(dest_folder, filename)
|
||||
|
||||
# Download the file
|
||||
response = requests.get(url, stream=True)
|
||||
if response.status_code == 200:
|
||||
with open(dest_path, 'wb') as file:
|
||||
for chunk in response.iter_content(chunk_size=1024):
|
||||
if chunk:
|
||||
file.write(chunk)
|
||||
else:
|
||||
raise Exception(f"Failed to download file from {url}")
|
||||
|
||||
|
||||
def download_url(model_url: str, model_dir: str, filename: str):
|
||||
if HF_ENDPOINT:
|
||||
model_url = model_url.replace('https://huggingface.co', HF_ENDPOINT)
|
||||
logging.info(f"model_url replaced by HF_ENDPOINT, new = {model_url}")
|
||||
if aria2:
|
||||
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):
|
||||
@ -44,9 +83,6 @@ def aria2_download_url(model_url: str, model_dir: str, filename: str):
|
||||
if model_dir.startswith(core.comfy_path):
|
||||
model_dir = model_dir[len(core.comfy_path) :]
|
||||
|
||||
if HF_ENDPOINT:
|
||||
model_url = model_url.replace('https://huggingface.co', HF_ENDPOINT)
|
||||
|
||||
download_dir = model_dir if model_dir.startswith('/') else os.path.join('/models', model_dir)
|
||||
|
||||
download = aria2_find_task(download_dir, filename)
|
||||
@ -68,3 +104,60 @@ def aria2_download_url(model_url: str, model_dir: str, filename: str):
|
||||
progress_bar.update(download.completed_length - progress_bar.n)
|
||||
time.sleep(1)
|
||||
download.update()
|
||||
|
||||
|
||||
def download_url_with_agent(url, save_path):
|
||||
try:
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'}
|
||||
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
response = urllib.request.urlopen(req)
|
||||
data = response.read()
|
||||
|
||||
if not os.path.exists(os.path.dirname(save_path)):
|
||||
os.makedirs(os.path.dirname(save_path))
|
||||
|
||||
with open(save_path, 'wb') as f:
|
||||
f.write(data)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Download error: {url} / {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
print("Installation was successful.")
|
||||
return True
|
||||
|
||||
# NOTE: snapshot_download doesn't provide file size tqdm.
|
||||
def download_repo_in_bytes(repo_id, local_dir):
|
||||
api = HfApi()
|
||||
repo_info = api.repo_info(repo_id=repo_id, files_metadata=True)
|
||||
|
||||
os.makedirs(local_dir, exist_ok=True)
|
||||
|
||||
total_size = 0
|
||||
for file_info in repo_info.siblings:
|
||||
if file_info.size is not None:
|
||||
total_size += file_info.size
|
||||
|
||||
pbar = tqdm(total=total_size, unit="B", unit_scale=True, desc="Downloading")
|
||||
|
||||
for file_info in repo_info.siblings:
|
||||
out_path = os.path.join(local_dir, file_info.rfilename)
|
||||
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
||||
|
||||
if file_info.size is None:
|
||||
continue
|
||||
|
||||
download_url = f"https://huggingface.co/{repo_id}/resolve/main/{file_info.rfilename}"
|
||||
|
||||
with requests.get(download_url, stream=True) as r, open(out_path, "wb") as f:
|
||||
r.raise_for_status()
|
||||
for chunk in r.iter_content(chunk_size=65536):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
pbar.update(len(chunk))
|
||||
|
||||
pbar.close()
|
||||
|
||||
|
||||
|
||||
356
glob/manager_migration.py
Normal file
@ -0,0 +1,356 @@
|
||||
"""
|
||||
ComfyUI-Manager migration module.
|
||||
Handles migration from legacy paths to new __manager path structure.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import configparser
|
||||
|
||||
# Startup notices for notice board
|
||||
startup_notices = [] # List of (message, level) tuples
|
||||
|
||||
|
||||
def add_startup_notice(message, level='warning'):
|
||||
"""Add a notice to be displayed on Manager notice board.
|
||||
|
||||
Args:
|
||||
message: HTML-formatted message string
|
||||
level: 'warning', 'error', 'info'
|
||||
"""
|
||||
global startup_notices
|
||||
startup_notices.append((message, level))
|
||||
|
||||
|
||||
# Cache for API check (computed once per session)
|
||||
_cached_has_system_user_api = None
|
||||
|
||||
|
||||
def has_system_user_api():
|
||||
"""Check if ComfyUI has the System User Protection API (PR #10966).
|
||||
|
||||
Result is cached for performance.
|
||||
"""
|
||||
global _cached_has_system_user_api
|
||||
if _cached_has_system_user_api is None:
|
||||
try:
|
||||
import folder_paths
|
||||
_cached_has_system_user_api = hasattr(folder_paths, 'get_system_user_directory')
|
||||
except Exception:
|
||||
_cached_has_system_user_api = False
|
||||
return _cached_has_system_user_api
|
||||
|
||||
|
||||
def get_manager_path(user_dir):
|
||||
"""Get the appropriate manager files path based on ComfyUI version.
|
||||
|
||||
Returns:
|
||||
str: manager_files_path
|
||||
"""
|
||||
if has_system_user_api():
|
||||
return os.path.abspath(os.path.join(user_dir, '__manager'))
|
||||
else:
|
||||
return os.path.abspath(os.path.join(user_dir, 'default', 'ComfyUI-Manager'))
|
||||
|
||||
|
||||
def run_migration_checks(user_dir, manager_files_path):
|
||||
"""Run all migration and security checks.
|
||||
|
||||
Call this after get_manager_path() to handle:
|
||||
- Legacy config migration (new ComfyUI)
|
||||
- Legacy backup notification (every startup)
|
||||
- Suspicious directory detection (old ComfyUI)
|
||||
- Outdated ComfyUI warning (old ComfyUI)
|
||||
"""
|
||||
if has_system_user_api():
|
||||
migrated = migrate_legacy_config(user_dir, manager_files_path)
|
||||
# Only check for legacy backup if migration didn't just happen
|
||||
# (migration already shows backup location in its message)
|
||||
if not migrated:
|
||||
check_legacy_backup(manager_files_path)
|
||||
else:
|
||||
check_suspicious_manager(user_dir)
|
||||
warn_outdated_comfyui()
|
||||
|
||||
|
||||
def check_legacy_backup(manager_files_path):
|
||||
"""Check for legacy backup and notify user to verify and remove it.
|
||||
|
||||
This runs on every startup to remind users about pending legacy backup.
|
||||
"""
|
||||
backup_dir = os.path.join(manager_files_path, '.legacy-manager-backup')
|
||||
if not os.path.exists(backup_dir):
|
||||
return
|
||||
|
||||
# Terminal output
|
||||
print("\n" + "-"*70)
|
||||
print("[ComfyUI-Manager] NOTICE: Legacy backup exists")
|
||||
print(" - Your old Manager data was backed up to:")
|
||||
print(f" {backup_dir}")
|
||||
print(" - Please verify and remove it when no longer needed.")
|
||||
print("-"*70 + "\n")
|
||||
|
||||
# Notice board output
|
||||
add_startup_notice(
|
||||
"Legacy ComfyUI-Manager data backup exists. Please verify and remove when no longer needed. See terminal for details.",
|
||||
level='info'
|
||||
)
|
||||
|
||||
|
||||
def check_suspicious_manager(user_dir):
|
||||
"""Check for suspicious __manager directory on old ComfyUI.
|
||||
|
||||
On old ComfyUI without System User API, if __manager exists with low security,
|
||||
warn the user to verify manually.
|
||||
|
||||
Returns:
|
||||
bool: True if suspicious setup detected
|
||||
"""
|
||||
if has_system_user_api():
|
||||
return False # Not suspicious on new ComfyUI
|
||||
|
||||
suspicious_path = os.path.abspath(os.path.join(user_dir, '__manager'))
|
||||
if not os.path.exists(suspicious_path):
|
||||
return False
|
||||
|
||||
config_path = os.path.join(suspicious_path, 'config.ini')
|
||||
if not os.path.exists(config_path):
|
||||
return False
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.read(config_path)
|
||||
sec_level = config.get('default', 'security_level', fallback='normal').lower()
|
||||
|
||||
if sec_level in ['weak', 'normal-']:
|
||||
# Terminal output
|
||||
print("\n" + "!"*70)
|
||||
print("[ComfyUI-Manager] ERROR: Suspicious path detected!")
|
||||
print(f" - '__manager' exists with low security level: '{sec_level}'")
|
||||
print(" - Please verify manually:")
|
||||
print(f" {config_path}")
|
||||
print("!"*70 + "\n")
|
||||
|
||||
# Notice board output
|
||||
add_startup_notice(
|
||||
"[Security Alert] Suspicious path detected. See terminal log for details.",
|
||||
level='error'
|
||||
)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def warn_outdated_comfyui():
|
||||
"""Warn user about outdated ComfyUI without System User API."""
|
||||
if has_system_user_api():
|
||||
return
|
||||
|
||||
# Terminal output
|
||||
print("\n" + "!"*70)
|
||||
print("[ComfyUI-Manager] ERROR: ComfyUI version is outdated!")
|
||||
print(" - Most operations are blocked for security.")
|
||||
print(" - ComfyUI update is still allowed.")
|
||||
print(" - Please update ComfyUI to use Manager normally.")
|
||||
print("!"*70 + "\n")
|
||||
|
||||
# Notice board output
|
||||
add_startup_notice(
|
||||
"[Security Alert] ComfyUI outdated. Installations blocked (update allowed).<BR>"
|
||||
"Update ComfyUI for normal operation.",
|
||||
level='error'
|
||||
)
|
||||
|
||||
|
||||
def migrate_legacy_config(user_dir, manager_files_path):
|
||||
"""Migrate ONLY config.ini to new __manager path if needed.
|
||||
|
||||
IMPORTANT: Only config.ini is migrated. Other files (snapshots, cache, etc.)
|
||||
are NOT migrated - users must recreate them.
|
||||
|
||||
Scenarios:
|
||||
1. Legacy exists, New doesn't exist → Migrate config.ini
|
||||
2. Legacy exists, New exists → First update after upgrade
|
||||
- Run ComfyUI dependency installation
|
||||
- Rename legacy to .backup
|
||||
3. Legacy doesn't exist → No migration needed
|
||||
|
||||
Returns:
|
||||
bool: True if migration was performed
|
||||
"""
|
||||
if not has_system_user_api():
|
||||
return False
|
||||
|
||||
legacy_dir = os.path.join(user_dir, 'default', 'ComfyUI-Manager')
|
||||
legacy_config = os.path.join(legacy_dir, 'config.ini')
|
||||
new_config = os.path.join(manager_files_path, 'config.ini')
|
||||
|
||||
if not os.path.exists(legacy_dir):
|
||||
return False # No legacy directory, nothing to migrate
|
||||
|
||||
# IMPORTANT: Check for config.ini existence, not just directory
|
||||
# (because makedirs() creates __manager before this function is called)
|
||||
|
||||
# Case: Both configs exist (first update after ComfyUI upgrade)
|
||||
# This means user ran new ComfyUI at least once, creating __manager/config.ini
|
||||
if os.path.exists(legacy_config) and os.path.exists(new_config):
|
||||
_handle_first_update_migration(user_dir, legacy_dir, manager_files_path)
|
||||
return True
|
||||
|
||||
# Case: Legacy config exists but new config doesn't (normal migration)
|
||||
# This is the first run after ComfyUI upgrade
|
||||
if os.path.exists(legacy_config) and not os.path.exists(new_config):
|
||||
pass # Continue with normal migration below
|
||||
else:
|
||||
return False
|
||||
|
||||
# Terminal output
|
||||
print("\n" + "-"*70)
|
||||
print("[ComfyUI-Manager] NOTICE: Legacy config.ini detected")
|
||||
print(f" - Old: {legacy_config}")
|
||||
print(f" - New: {new_config}")
|
||||
print(" - Migrating config.ini only (other files are NOT migrated).")
|
||||
print(" - Security level below 'normal' will be raised.")
|
||||
print("-"*70 + "\n")
|
||||
|
||||
_migrate_config_with_security_check(legacy_config, new_config)
|
||||
|
||||
# Move legacy directory to backup
|
||||
_move_legacy_to_backup(legacy_dir, manager_files_path)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _handle_first_update_migration(user_dir, legacy_dir, manager_files_path):
|
||||
"""Handle first ComfyUI update when both legacy and new directories exist.
|
||||
|
||||
This scenario happens when:
|
||||
- User was on old ComfyUI (using default/ComfyUI-Manager)
|
||||
- ComfyUI was updated (now has System User API)
|
||||
- Manager already created __manager on first new run
|
||||
- But legacy directory still exists
|
||||
|
||||
Actions:
|
||||
1. Run ComfyUI dependency installation
|
||||
2. Move legacy to __manager/.legacy-manager-backup
|
||||
"""
|
||||
# Terminal output
|
||||
print("\n" + "-"*70)
|
||||
print("[ComfyUI-Manager] NOTICE: First update after ComfyUI upgrade detected")
|
||||
print(" - Both legacy and new directories exist.")
|
||||
print(" - Running ComfyUI dependency installation...")
|
||||
print("-"*70 + "\n")
|
||||
|
||||
# Run ComfyUI dependency installation
|
||||
# Path: glob/manager_migration.py → glob → comfyui-manager → custom_nodes → ComfyUI
|
||||
try:
|
||||
comfyui_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
||||
requirements_path = os.path.join(comfyui_path, 'requirements.txt')
|
||||
if os.path.exists(requirements_path):
|
||||
subprocess.run([sys.executable, '-m', 'pip', 'install', '-r', requirements_path],
|
||||
capture_output=True, check=False)
|
||||
print("[ComfyUI-Manager] ComfyUI dependencies installation completed.")
|
||||
except Exception as e:
|
||||
print(f"[ComfyUI-Manager] WARNING: Failed to install ComfyUI dependencies: {e}")
|
||||
|
||||
# Move legacy to backup inside __manager
|
||||
_move_legacy_to_backup(legacy_dir, manager_files_path)
|
||||
|
||||
|
||||
def _move_legacy_to_backup(legacy_dir, manager_files_path):
|
||||
"""Move legacy directory to backup inside __manager.
|
||||
|
||||
Returns:
|
||||
str: Path to backup directory if successful, None if failed
|
||||
"""
|
||||
import shutil
|
||||
|
||||
backup_dir = os.path.join(manager_files_path, '.legacy-manager-backup')
|
||||
|
||||
try:
|
||||
if os.path.exists(backup_dir):
|
||||
shutil.rmtree(backup_dir) # Remove old backup if exists
|
||||
shutil.move(legacy_dir, backup_dir)
|
||||
|
||||
# Terminal output (full paths shown here only)
|
||||
print("\n" + "-"*70)
|
||||
print("[ComfyUI-Manager] NOTICE: Legacy settings migrated")
|
||||
print(f" - Old location: {legacy_dir}")
|
||||
print(f" - Backed up to: {backup_dir}")
|
||||
print(" - Please verify and remove the backup when no longer needed.")
|
||||
print("-"*70 + "\n")
|
||||
|
||||
# Notice board output (no full paths for security)
|
||||
add_startup_notice(
|
||||
"Legacy ComfyUI-Manager data migrated. See terminal for details.",
|
||||
level='info'
|
||||
)
|
||||
return backup_dir
|
||||
except Exception as e:
|
||||
print(f"[ComfyUI-Manager] WARNING: Failed to backup legacy directory: {e}")
|
||||
add_startup_notice(
|
||||
f"[MIGRATION] Failed to backup legacy directory: {e}",
|
||||
level='warning'
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _migrate_config_with_security_check(legacy_path, new_path):
|
||||
"""Migrate legacy config, raising security level only if below default."""
|
||||
config = configparser.ConfigParser()
|
||||
try:
|
||||
config.read(legacy_path)
|
||||
except Exception as e:
|
||||
print(f"[ComfyUI-Manager] WARNING: Failed to parse config.ini: {e}")
|
||||
print(" - Creating fresh config with default settings.")
|
||||
add_startup_notice(
|
||||
"[MIGRATION] Failed to parse legacy config. Using defaults.",
|
||||
level='warning'
|
||||
)
|
||||
return # Skip migration, let Manager create fresh config
|
||||
|
||||
# Security level hierarchy: strong > normal > normal- > weak
|
||||
# Default is 'normal', only raise if below default
|
||||
if 'default' in config:
|
||||
current_level = config['default'].get('security_level', 'normal').lower()
|
||||
below_default_levels = ['weak', 'normal-']
|
||||
|
||||
if current_level in below_default_levels:
|
||||
config['default']['security_level'] = 'normal'
|
||||
|
||||
# Terminal output
|
||||
print("\n" + "="*70)
|
||||
print("[ComfyUI-Manager] WARNING: Security level adjusted")
|
||||
print(f" - Previous: '{current_level}' → New: 'normal'")
|
||||
print(" - Raised to prevent unauthorized remote access.")
|
||||
print("="*70 + "\n")
|
||||
|
||||
# Notice board output
|
||||
add_startup_notice(
|
||||
f"[MIGRATION] Security level raised: '{current_level}' → 'normal'.<BR>"
|
||||
"To prevent unauthorized remote access.",
|
||||
level='warning'
|
||||
)
|
||||
else:
|
||||
print(f" - Security level: '{current_level}' (no change needed)")
|
||||
|
||||
# Ensure directory exists
|
||||
os.makedirs(os.path.dirname(new_path), exist_ok=True)
|
||||
|
||||
with open(new_path, 'w') as f:
|
||||
config.write(f)
|
||||
|
||||
|
||||
def force_security_level_if_needed(config_dict):
|
||||
"""Force security level to 'strong' if on old ComfyUI.
|
||||
|
||||
Args:
|
||||
config_dict: Configuration dictionary to modify in-place
|
||||
|
||||
Returns:
|
||||
bool: True if security level was forced
|
||||
"""
|
||||
if not has_system_user_api():
|
||||
config_dict['security_level'] = 'strong'
|
||||
return True
|
||||
return False
|
||||
@ -1,8 +1,104 @@
|
||||
"""
|
||||
description:
|
||||
`manager_util` is the lightest module shared across the prestartup_script, main code, and cm-cli of ComfyUI-Manager.
|
||||
"""
|
||||
import traceback
|
||||
|
||||
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 functools import lru_cache
|
||||
|
||||
|
||||
cache_lock = threading.Lock()
|
||||
|
||||
comfyui_manager_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
cache_dir = os.path.join(comfyui_manager_path, '.cache') # This path is also updated together in **manager_core.update_user_directory**.
|
||||
|
||||
use_uv = False
|
||||
bypass_ssl = False
|
||||
|
||||
def add_python_path_to_env():
|
||||
if platform.system() != "Windows":
|
||||
sep = ':'
|
||||
else:
|
||||
sep = ';'
|
||||
|
||||
os.environ['PATH'] = os.path.dirname(sys.executable)+sep+os.environ['PATH']
|
||||
|
||||
|
||||
@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):
|
||||
"""
|
||||
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:
|
||||
# from distutils.version import StrictVersion
|
||||
# except:
|
||||
print(f"[ComfyUI-Manager] 'distutils' package not found. Activating fallback mode for compatibility.")
|
||||
# print(f"[ComfyUI-Manager] 'distutils' package not found. Activating fallback mode for compatibility.")
|
||||
class StrictVersion:
|
||||
def __init__(self, version_string):
|
||||
self.version_string = version_string
|
||||
@ -62,3 +158,476 @@ class StrictVersion:
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
|
||||
def simple_hash(input_string):
|
||||
hash_value = 0
|
||||
for char in input_string:
|
||||
hash_value = (hash_value * 31 + ord(char)) % (2**32)
|
||||
|
||||
return hash_value
|
||||
|
||||
|
||||
def is_file_created_within_one_day(file_path):
|
||||
if not os.path.exists(file_path):
|
||||
return False
|
||||
|
||||
file_creation_time = os.path.getctime(file_path)
|
||||
current_time = datetime.now().timestamp()
|
||||
time_difference = current_time - file_creation_time
|
||||
|
||||
return time_difference <= 86400
|
||||
|
||||
|
||||
async def get_data(uri, silent=False):
|
||||
if not silent:
|
||||
print(f"FETCH DATA from: {uri}", end="")
|
||||
|
||||
if uri.startswith("http"):
|
||||
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',
|
||||
'Expires': '0'
|
||||
}
|
||||
async with session.get(uri, headers=headers) as resp:
|
||||
json_text = await resp.text()
|
||||
else:
|
||||
with cache_lock:
|
||||
with open(uri, "r", encoding="utf-8") as f:
|
||||
json_text = f.read()
|
||||
|
||||
try:
|
||||
json_obj = json.loads(json_text)
|
||||
except Exception as e:
|
||||
logging.error(f"[ComfyUI-Manager] An error occurred while fetching '{uri}': {e}")
|
||||
|
||||
return {}
|
||||
|
||||
if not silent:
|
||||
print(" [DONE]")
|
||||
|
||||
return json_obj
|
||||
|
||||
|
||||
def get_cache_path(uri):
|
||||
cache_uri = str(simple_hash(uri)) + '_' + os.path.basename(uri).replace('&', "_").replace('?', "_").replace('=', "_")
|
||||
return os.path.join(cache_dir, cache_uri+'.json')
|
||||
|
||||
|
||||
def get_cache_state(uri):
|
||||
cache_uri = get_cache_path(uri)
|
||||
|
||||
if not os.path.exists(cache_uri):
|
||||
return "not-cached"
|
||||
elif is_file_created_within_one_day(cache_uri):
|
||||
return "cached"
|
||||
|
||||
return "expired"
|
||||
|
||||
|
||||
def save_to_cache(uri, json_obj, silent=False):
|
||||
cache_uri = get_cache_path(uri)
|
||||
|
||||
with cache_lock:
|
||||
with open(cache_uri, "w", encoding='utf-8') as file:
|
||||
json.dump(json_obj, file, indent=4, sort_keys=True)
|
||||
if not silent:
|
||||
logging.info(f"[ComfyUI-Manager] default cache updated: {uri}")
|
||||
|
||||
|
||||
async def get_data_with_cache(uri, silent=False, cache_mode=True, dont_wait=False, dont_cache=False):
|
||||
cache_uri = get_cache_path(uri)
|
||||
|
||||
if cache_mode and dont_wait:
|
||||
# NOTE: return the cache if possible, even if it is expired, so do not cache
|
||||
if not os.path.exists(cache_uri):
|
||||
logging.error(f"[ComfyUI-Manager] The network connection is unstable, so it is operating in fallback mode: {uri}")
|
||||
|
||||
return {}
|
||||
else:
|
||||
if not is_file_created_within_one_day(cache_uri):
|
||||
logging.error(f"[ComfyUI-Manager] The network connection is unstable, so it is operating in outdated cache mode: {uri}")
|
||||
|
||||
return await get_data(cache_uri, silent=silent)
|
||||
|
||||
if cache_mode and is_file_created_within_one_day(cache_uri):
|
||||
json_obj = await get_data(cache_uri, silent=silent)
|
||||
else:
|
||||
json_obj = await get_data(uri, silent=silent)
|
||||
if not dont_cache:
|
||||
with cache_lock:
|
||||
with open(cache_uri, "w", encoding='utf-8') as file:
|
||||
json.dump(json_obj, file, indent=4, sort_keys=True)
|
||||
if not silent:
|
||||
logging.info(f"[ComfyUI-Manager] default cache updated: {uri}")
|
||||
|
||||
return json_obj
|
||||
|
||||
|
||||
def sanitize_tag(x):
|
||||
return x.replace('<', '<').replace('>', '>')
|
||||
|
||||
|
||||
def extract_package_as_zip(file_path, extract_path):
|
||||
import zipfile
|
||||
try:
|
||||
with zipfile.ZipFile(file_path, "r") as zip_ref:
|
||||
zip_ref.extractall(extract_path)
|
||||
extracted_files = zip_ref.namelist()
|
||||
logging.info(f"Extracted zip file to {extract_path}")
|
||||
return extracted_files
|
||||
except zipfile.BadZipFile:
|
||||
logging.error(f"File '{file_path}' is not a zip or is corrupted.")
|
||||
return None
|
||||
|
||||
|
||||
pip_map = None
|
||||
|
||||
|
||||
def get_installed_packages(renew=False):
|
||||
global pip_map
|
||||
|
||||
if renew or pip_map is None:
|
||||
try:
|
||||
result = subprocess.check_output(make_pip_cmd(['list']), universal_newlines=True)
|
||||
|
||||
pip_map = {}
|
||||
for line in result.split('\n'):
|
||||
x = line.strip()
|
||||
if x:
|
||||
y = line.split()
|
||||
if y[0] == 'Package' or y[0].startswith('-'):
|
||||
continue
|
||||
|
||||
normalized_name = y[0].lower().replace('-', '_')
|
||||
pip_map[normalized_name] = y[1]
|
||||
except subprocess.CalledProcessError:
|
||||
logging.error("[ComfyUI-Manager] Failed to retrieve the information of installed pip packages.")
|
||||
return {}
|
||||
|
||||
return pip_map
|
||||
|
||||
|
||||
def clear_pip_cache():
|
||||
global pip_map
|
||||
pip_map = None
|
||||
|
||||
|
||||
def parse_requirement_line(line):
|
||||
tokens = shlex.split(line)
|
||||
if not tokens:
|
||||
return None
|
||||
|
||||
package_spec = tokens[0]
|
||||
|
||||
pattern = re.compile(
|
||||
r'^(?P<package>[A-Za-z0-9_.+-]+)'
|
||||
r'(?P<operator>==|>=|<=|!=|~=|>|<)?'
|
||||
r'(?P<version>[A-Za-z0-9_.+-]*)$'
|
||||
)
|
||||
m = pattern.match(package_spec)
|
||||
if not m:
|
||||
return None
|
||||
|
||||
package = m.group('package')
|
||||
operator = m.group('operator') or None
|
||||
version = m.group('version') or None
|
||||
|
||||
index_url = None
|
||||
if '--index-url' in tokens:
|
||||
idx = tokens.index('--index-url')
|
||||
if idx + 1 < len(tokens):
|
||||
index_url = tokens[idx + 1]
|
||||
|
||||
res = {'package': package}
|
||||
|
||||
if operator is not None:
|
||||
res['operator'] = operator
|
||||
|
||||
if version is not None:
|
||||
res['version'] = StrictVersion(version)
|
||||
|
||||
if index_url is not None:
|
||||
res['index_url'] = index_url
|
||||
|
||||
return res
|
||||
|
||||
|
||||
torch_torchvision_torchaudio_version_map = {
|
||||
'2.7.0': ('0.22.0', '2.7.0'),
|
||||
'2.6.0': ('0.21.0', '2.6.0'),
|
||||
'2.5.1': ('0.20.0', '2.5.0'),
|
||||
'2.5.0': ('0.20.0', '2.5.0'),
|
||||
'2.4.1': ('0.19.1', '2.4.1'),
|
||||
'2.4.0': ('0.19.0', '2.4.0'),
|
||||
'2.3.1': ('0.18.1', '2.3.1'),
|
||||
'2.3.0': ('0.18.0', '2.3.0'),
|
||||
'2.2.2': ('0.17.2', '2.2.2'),
|
||||
'2.2.1': ('0.17.1', '2.2.1'),
|
||||
'2.2.0': ('0.17.0', '2.2.0'),
|
||||
'2.1.2': ('0.16.2', '2.1.2'),
|
||||
'2.1.1': ('0.16.1', '2.1.1'),
|
||||
'2.1.0': ('0.16.0', '2.1.0'),
|
||||
'2.0.1': ('0.15.2', '2.0.1'),
|
||||
'2.0.0': ('0.15.1', '2.0.0'),
|
||||
}
|
||||
|
||||
|
||||
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):
|
||||
self.prev_pip_versions = { **prev_pip_versions }
|
||||
self.comfyui_path = comfyui_path
|
||||
self.manager_files_path = manager_files_path
|
||||
|
||||
def fix_broken(self):
|
||||
new_pip_versions = get_installed_packages(True)
|
||||
|
||||
# remove `comfy` python package
|
||||
try:
|
||||
if 'comfy' in new_pip_versions:
|
||||
cmd = make_pip_cmd(['uninstall', 'comfy'])
|
||||
subprocess.check_output(cmd, universal_newlines=True)
|
||||
|
||||
logging.warning("[ComfyUI-Manager] 'comfy' python package is uninstalled.\nWARN: The 'comfy' package is completely unrelated to ComfyUI and should never be installed as it causes conflicts with ComfyUI.")
|
||||
except Exception as e:
|
||||
logging.error("[ComfyUI-Manager] Failed to uninstall `comfy` python package")
|
||||
logging.error(e)
|
||||
|
||||
# fix torch - reinstall torch packages if version is changed
|
||||
try:
|
||||
if 'torch' not in self.prev_pip_versions or 'torchvision' not in self.prev_pip_versions or 'torchaudio' not in self.prev_pip_versions:
|
||||
logging.error("[ComfyUI-Manager] PyTorch is not installed")
|
||||
elif self.prev_pip_versions['torch'] != new_pip_versions['torch'] \
|
||||
or self.prev_pip_versions['torchvision'] != new_pip_versions['torchvision'] \
|
||||
or self.prev_pip_versions['torchaudio'] != new_pip_versions['torchaudio']:
|
||||
torch_rollback(self.prev_pip_versions['torch'])
|
||||
except Exception as e:
|
||||
logging.error("[ComfyUI-Manager] Failed to restore PyTorch")
|
||||
logging.error(e)
|
||||
|
||||
# fix opencv
|
||||
try:
|
||||
ocp = new_pip_versions.get('opencv-contrib-python')
|
||||
ocph = new_pip_versions.get('opencv-contrib-python-headless')
|
||||
op = new_pip_versions.get('opencv-python')
|
||||
oph = new_pip_versions.get('opencv-python-headless')
|
||||
|
||||
versions = [ocp, ocph, op, oph]
|
||||
versions = [StrictVersion(x) for x in versions if x is not None]
|
||||
versions.sort(reverse=True)
|
||||
|
||||
if len(versions) > 0:
|
||||
# upgrade to maximum version
|
||||
targets = []
|
||||
cur = versions[0]
|
||||
if ocp is not None and StrictVersion(ocp) != cur:
|
||||
targets.append('opencv-contrib-python')
|
||||
if ocph is not None and StrictVersion(ocph) != cur:
|
||||
targets.append('opencv-contrib-python-headless')
|
||||
if op is not None and StrictVersion(op) != cur:
|
||||
targets.append('opencv-python')
|
||||
if oph is not None and StrictVersion(oph) != cur:
|
||||
targets.append('opencv-python-headless')
|
||||
|
||||
if len(targets) > 0:
|
||||
for x in targets:
|
||||
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 missing frontend
|
||||
try:
|
||||
# NOTE: package name in requirements is 'comfyui-frontend-package'
|
||||
# but, package name from `pip freeze` is 'comfyui_frontend_package'
|
||||
# but, package name from `uv pip freeze` is 'comfyui-frontend-package'
|
||||
#
|
||||
# get_installed_packages returns normalized name (i.e. comfyui_frontend_package)
|
||||
if 'comfyui_frontend_package' not in new_pip_versions:
|
||||
requirements_path = os.path.join(self.comfyui_path, 'requirements.txt')
|
||||
|
||||
with open(requirements_path, 'r') as file:
|
||||
lines = file.readlines()
|
||||
|
||||
front_line = next((line.strip() for line in lines if line.startswith('comfyui-frontend-package')), None)
|
||||
if front_line is None:
|
||||
logging.info("[ComfyUI-Manager] Skipped fixing the 'comfyui-frontend-package' dependency because the ComfyUI is outdated.")
|
||||
else:
|
||||
cmd = make_pip_cmd(['install', front_line])
|
||||
subprocess.check_output(cmd , universal_newlines=True)
|
||||
logging.info("[ComfyUI-Manager] 'comfyui-frontend-package' dependency were fixed")
|
||||
except Exception as e:
|
||||
logging.error("[ComfyUI-Manager] Failed to restore comfyui-frontend-package")
|
||||
logging.error(e)
|
||||
|
||||
# restore based on custom list
|
||||
pip_auto_fix_path = os.path.join(self.manager_files_path, "pip_auto_fix.list")
|
||||
if os.path.exists(pip_auto_fix_path):
|
||||
with open(pip_auto_fix_path, 'r', encoding="UTF-8", errors="ignore") as f:
|
||||
fixed_list = []
|
||||
|
||||
for x in f.readlines():
|
||||
try:
|
||||
parsed = parse_requirement_line(x)
|
||||
need_to_reinstall = True
|
||||
|
||||
normalized_name = parsed['package'].lower().replace('-', '_')
|
||||
if normalized_name in new_pip_versions:
|
||||
if 'version' in parsed and 'operator' in parsed:
|
||||
cur = StrictVersion(new_pip_versions[normalized_name])
|
||||
dest = parsed['version']
|
||||
op = parsed['operator']
|
||||
if cur == dest:
|
||||
if op in ['==', '>=', '<=']:
|
||||
need_to_reinstall = False
|
||||
elif cur < dest:
|
||||
if op in ['<=', '<', '~=', '!=']:
|
||||
need_to_reinstall = False
|
||||
elif cur > dest:
|
||||
if op in ['>=', '>', '~=', '!=']:
|
||||
need_to_reinstall = False
|
||||
|
||||
if need_to_reinstall:
|
||||
cmd_args = ['install']
|
||||
if 'version' in parsed and 'operator' in parsed:
|
||||
cmd_args.append(parsed['package']+parsed['operator']+parsed['version'].version_string)
|
||||
|
||||
if 'index_url' in parsed:
|
||||
cmd_args.append('--index-url')
|
||||
cmd_args.append(parsed['index_url'])
|
||||
|
||||
cmd = make_pip_cmd(cmd_args)
|
||||
subprocess.check_output(cmd, universal_newlines=True)
|
||||
|
||||
fixed_list.append(parsed['package'])
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
logging.error(f"[ComfyUI-Manager] Failed to restore '{x}'")
|
||||
logging.error(e)
|
||||
|
||||
if len(fixed_list) > 0:
|
||||
logging.info(f"[ComfyUI-Manager] dependencies in pip_auto_fix.json were fixed: {fixed_list}")
|
||||
|
||||
def sanitize(data):
|
||||
return data.replace("<", "<").replace(">", ">")
|
||||
|
||||
|
||||
def sanitize_filename(input_string):
|
||||
result_string = re.sub(r'[^a-zA-Z0-9_]', '_', input_string)
|
||||
return result_string
|
||||
|
||||
|
||||
def robust_readlines(fullpath):
|
||||
import chardet
|
||||
try:
|
||||
with open(fullpath, "r") as f:
|
||||
return f.readlines()
|
||||
except:
|
||||
encoding = None
|
||||
with open(fullpath, "rb") as f:
|
||||
raw_data = f.read()
|
||||
result = chardet.detect(raw_data)
|
||||
encoding = result['encoding']
|
||||
|
||||
if encoding is not None:
|
||||
with open(fullpath, "r", encoding=encoding) as f:
|
||||
return f.readlines()
|
||||
|
||||
print(f"[ComfyUI-Manager] Failed to recognize encoding for: {fullpath}")
|
||||
return []
|
||||
|
||||
|
||||
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}")
|
||||
72
glob/node_package.py
Normal file
@ -0,0 +1,72 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
|
||||
from git_utils import get_commit_hash
|
||||
|
||||
|
||||
@dataclass
|
||||
class InstalledNodePackage:
|
||||
"""Information about an installed node package."""
|
||||
|
||||
id: str
|
||||
fullpath: str
|
||||
disabled: bool
|
||||
version: str
|
||||
|
||||
@property
|
||||
def is_unknown(self) -> bool:
|
||||
return self.version == "unknown"
|
||||
|
||||
@property
|
||||
def is_nightly(self) -> bool:
|
||||
return self.version == "nightly"
|
||||
|
||||
@property
|
||||
def is_from_cnr(self) -> bool:
|
||||
return not self.is_unknown and not self.is_nightly
|
||||
|
||||
@property
|
||||
def is_enabled(self) -> bool:
|
||||
return not self.disabled
|
||||
|
||||
@property
|
||||
def is_disabled(self) -> bool:
|
||||
return self.disabled
|
||||
|
||||
def get_commit_hash(self) -> str:
|
||||
return get_commit_hash(self.fullpath)
|
||||
|
||||
def isValid(self) -> bool:
|
||||
if self.is_from_cnr:
|
||||
return os.path.exists(os.path.join(self.fullpath, '.tracking'))
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def from_fullpath(fullpath: str, resolve_from_path) -> InstalledNodePackage:
|
||||
parent_folder_name = os.path.basename(os.path.dirname(fullpath))
|
||||
module_name = os.path.basename(fullpath)
|
||||
|
||||
if module_name.endswith(".disabled"):
|
||||
node_id = module_name[:-9]
|
||||
disabled = True
|
||||
elif parent_folder_name == ".disabled":
|
||||
# Nodes under custom_nodes/.disabled/* are disabled
|
||||
node_id = module_name
|
||||
disabled = True
|
||||
else:
|
||||
node_id = module_name
|
||||
disabled = False
|
||||
|
||||
info = resolve_from_path(fullpath)
|
||||
if info is None:
|
||||
version = 'unknown'
|
||||
else:
|
||||
node_id = info['id'] # robust module guessing
|
||||
version = info['ver']
|
||||
|
||||
return InstalledNodePackage(
|
||||
id=node_id, fullpath=fullpath, disabled=disabled, version=version
|
||||
)
|
||||
@ -2,6 +2,8 @@ import sys
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
import manager_util
|
||||
|
||||
|
||||
def security_check():
|
||||
print("[START] Security scan")
|
||||
@ -29,30 +31,60 @@ Detailed information: https://old.reddit.com/r/comfyui/comments/1dbls5n/psa_if_y
|
||||
2. Remove files: lolMiner*, 4G_Ethash_Linux_Readme.txt, mine* in ComfyUI dir.
|
||||
|
||||
(Reinstall ComfyUI is recommended.)
|
||||
""",
|
||||
"ultralytics==8.3.41": f"""
|
||||
Execute following commands:
|
||||
{sys.executable} -m pip uninstall ultralytics
|
||||
{sys.executable} -m pip install ultralytics==8.3.40
|
||||
|
||||
And kill and remove /tmp/ultralytics_runner
|
||||
|
||||
|
||||
The version 8.3.41 to 8.3.42 of the Ultralytics package you installed is compromised. Please uninstall that version and reinstall the latest version.
|
||||
https://blog.comfy.org/comfyui-statement-on-the-ultralytics-crypto-miner-situation/
|
||||
""",
|
||||
"ultralytics==8.3.42": f"""
|
||||
Execute following commands:
|
||||
{sys.executable} -m pip uninstall ultralytics
|
||||
{sys.executable} -m pip install ultralytics==8.3.40
|
||||
|
||||
And kill and remove /tmp/ultralytics_runner
|
||||
|
||||
|
||||
The version 8.3.41 to 8.3.42 of the Ultralytics package you installed is compromised. Please uninstall that version and reinstall the latest version.
|
||||
https://blog.comfy.org/comfyui-statement-on-the-ultralytics-crypto-miner-situation/
|
||||
"""
|
||||
}
|
||||
|
||||
node_blacklist = {"ComfyUI_LLMVISION": "ComfyUI_LLMVISION"}
|
||||
|
||||
pip_blacklist = {"AppleBotzz": "ComfyUI_LLMVISION"}
|
||||
pip_blacklist = {
|
||||
"AppleBotzz": "ComfyUI_LLMVISION",
|
||||
"ultralytics==8.3.41": "ultralytics==8.3.41"
|
||||
}
|
||||
|
||||
file_blacklist = {
|
||||
"ComfyUI_LLMVISION": ["%LocalAppData%\\rundll64.exe"],
|
||||
"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
|
||||
|
||||
@ -84,7 +116,7 @@ Detailed information: https://old.reddit.com/r/comfyui/comments/1dbls5n/psa_if_y
|
||||
|
||||
for x in detected:
|
||||
print(f"\n======== TARGET: {x} =========")
|
||||
print(f"\nTODO:")
|
||||
print("\nTODO:")
|
||||
print(guide.get(x))
|
||||
|
||||
exit(-1)
|
||||
|
||||
433
glob/share_3rdparty.py
Normal file
@ -0,0 +1,433 @@
|
||||
import mimetypes
|
||||
import manager_core as core
|
||||
import os
|
||||
from aiohttp import web
|
||||
import aiohttp
|
||||
import json
|
||||
import hashlib
|
||||
|
||||
import folder_paths
|
||||
from server import PromptServer
|
||||
|
||||
|
||||
def extract_model_file_names(json_data):
|
||||
"""Extract unique file names from the input JSON data."""
|
||||
file_names = set()
|
||||
model_filename_extensions = {'.safetensors', '.ckpt', '.pt', '.pth', '.bin'}
|
||||
|
||||
# Recursively search for file names in the JSON data
|
||||
def recursive_search(data):
|
||||
if isinstance(data, dict):
|
||||
for value in data.values():
|
||||
recursive_search(value)
|
||||
elif isinstance(data, list):
|
||||
for item in data:
|
||||
recursive_search(item)
|
||||
elif isinstance(data, str) and '.' in data:
|
||||
file_names.add(os.path.basename(data)) # file_names.add(data)
|
||||
|
||||
recursive_search(json_data)
|
||||
return [f for f in list(file_names) if os.path.splitext(f)[1] in model_filename_extensions]
|
||||
|
||||
|
||||
def find_file_paths(base_dir, file_names):
|
||||
"""Find the paths of the files in the base directory."""
|
||||
file_paths = {}
|
||||
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
# Exclude certain directories
|
||||
dirs[:] = [d for d in dirs if d not in ['.git']]
|
||||
|
||||
for file in files:
|
||||
if file in file_names:
|
||||
file_paths[file] = os.path.join(root, file)
|
||||
return file_paths
|
||||
|
||||
|
||||
def compute_sha256_checksum(filepath):
|
||||
"""Compute the SHA256 checksum of a file, in chunks"""
|
||||
sha256 = hashlib.sha256()
|
||||
with open(filepath, 'rb') as f:
|
||||
for chunk in iter(lambda: f.read(4096), b''):
|
||||
sha256.update(chunk)
|
||||
return sha256.hexdigest()
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/manager/share_option")
|
||||
async def share_option(request):
|
||||
if "value" in request.rel_url.query:
|
||||
core.get_config()['share_option'] = request.rel_url.query['value']
|
||||
core.write_config()
|
||||
else:
|
||||
return web.Response(text=core.get_config()['share_option'], status=200)
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
def get_openart_auth():
|
||||
if not os.path.exists(os.path.join(core.manager_files_path, ".openart_key")):
|
||||
return None
|
||||
try:
|
||||
with open(os.path.join(core.manager_files_path, ".openart_key"), "r") as f:
|
||||
openart_key = f.read().strip()
|
||||
return openart_key if openart_key else None
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def get_matrix_auth():
|
||||
if not os.path.exists(os.path.join(core.manager_files_path, "matrix_auth")):
|
||||
return None
|
||||
try:
|
||||
with open(os.path.join(core.manager_files_path, "matrix_auth"), "r") as f:
|
||||
matrix_auth = f.read()
|
||||
homeserver, username, password = matrix_auth.strip().split("\n")
|
||||
if not homeserver or not username or not password:
|
||||
return None
|
||||
return {
|
||||
"homeserver": homeserver,
|
||||
"username": username,
|
||||
"password": password,
|
||||
}
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def get_comfyworkflows_auth():
|
||||
if not os.path.exists(os.path.join(core.manager_files_path, "comfyworkflows_sharekey")):
|
||||
return None
|
||||
try:
|
||||
with open(os.path.join(core.manager_files_path, "comfyworkflows_sharekey"), "r") as f:
|
||||
share_key = f.read()
|
||||
if not share_key.strip():
|
||||
return None
|
||||
return share_key
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def get_youml_settings():
|
||||
if not os.path.exists(os.path.join(core.manager_files_path, ".youml")):
|
||||
return None
|
||||
try:
|
||||
with open(os.path.join(core.manager_files_path, ".youml"), "r") as f:
|
||||
youml_settings = f.read().strip()
|
||||
return youml_settings if youml_settings else None
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def set_youml_settings(settings):
|
||||
with open(os.path.join(core.manager_files_path, ".youml"), "w") as f:
|
||||
f.write(settings)
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/manager/get_openart_auth")
|
||||
async def api_get_openart_auth(request):
|
||||
# print("Getting stored Matrix credentials...")
|
||||
openart_key = get_openart_auth()
|
||||
if not openart_key:
|
||||
return web.Response(status=404)
|
||||
return web.json_response({"openart_key": openart_key})
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post("/manager/set_openart_auth")
|
||||
async def api_set_openart_auth(request):
|
||||
json_data = await request.json()
|
||||
openart_key = json_data['openart_key']
|
||||
with open(os.path.join(core.manager_files_path, ".openart_key"), "w") as f:
|
||||
f.write(openart_key)
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/manager/get_matrix_auth")
|
||||
async def api_get_matrix_auth(request):
|
||||
# print("Getting stored Matrix credentials...")
|
||||
matrix_auth = get_matrix_auth()
|
||||
if not matrix_auth:
|
||||
return web.Response(status=404)
|
||||
return web.json_response(matrix_auth)
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/manager/youml/settings")
|
||||
async def api_get_youml_settings(request):
|
||||
youml_settings = get_youml_settings()
|
||||
if not youml_settings:
|
||||
return web.Response(status=404)
|
||||
return web.json_response(json.loads(youml_settings))
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post("/manager/youml/settings")
|
||||
async def api_set_youml_settings(request):
|
||||
json_data = await request.json()
|
||||
set_youml_settings(json.dumps(json_data))
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/manager/get_comfyworkflows_auth")
|
||||
async def api_get_comfyworkflows_auth(request):
|
||||
# Check if the user has provided Matrix credentials in a file called 'matrix_accesstoken'
|
||||
# in the same directory as the ComfyUI base folder
|
||||
# print("Getting stored Comfyworkflows.com auth...")
|
||||
comfyworkflows_auth = get_comfyworkflows_auth()
|
||||
if not comfyworkflows_auth:
|
||||
return web.Response(status=404)
|
||||
return web.json_response({"comfyworkflows_sharekey": comfyworkflows_auth})
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post("/manager/set_esheep_workflow_and_images")
|
||||
async def set_esheep_workflow_and_images(request):
|
||||
json_data = await request.json()
|
||||
with open(os.path.join(core.manager_files_path, "esheep_share_message.json"), "w", encoding='utf-8') as file:
|
||||
json.dump(json_data, file, indent=4)
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
@PromptServer.instance.routes.get("/manager/get_esheep_workflow_and_images")
|
||||
async def get_esheep_workflow_and_images(request):
|
||||
with open(os.path.join(core.manager_files_path, "esheep_share_message.json"), 'r', encoding='utf-8') as file:
|
||||
data = json.load(file)
|
||||
return web.Response(status=200, text=json.dumps(data))
|
||||
|
||||
|
||||
def set_matrix_auth(json_data):
|
||||
homeserver = json_data['homeserver']
|
||||
username = json_data['username']
|
||||
password = json_data['password']
|
||||
with open(os.path.join(core.manager_files_path, "matrix_auth"), "w") as f:
|
||||
f.write("\n".join([homeserver, username, password]))
|
||||
|
||||
|
||||
def set_comfyworkflows_auth(comfyworkflows_sharekey):
|
||||
with open(os.path.join(core.manager_files_path, "comfyworkflows_sharekey"), "w") as f:
|
||||
f.write(comfyworkflows_sharekey)
|
||||
|
||||
|
||||
def has_provided_matrix_auth(matrix_auth):
|
||||
return matrix_auth['homeserver'].strip() and matrix_auth['username'].strip() and matrix_auth['password'].strip()
|
||||
|
||||
|
||||
def has_provided_comfyworkflows_auth(comfyworkflows_sharekey):
|
||||
return comfyworkflows_sharekey.strip()
|
||||
|
||||
|
||||
@PromptServer.instance.routes.post("/manager/share")
|
||||
async def share_art(request):
|
||||
# get json data
|
||||
json_data = await request.json()
|
||||
|
||||
matrix_auth = json_data['matrix_auth']
|
||||
comfyworkflows_sharekey = json_data['cw_auth']['cw_sharekey']
|
||||
|
||||
set_matrix_auth(matrix_auth)
|
||||
set_comfyworkflows_auth(comfyworkflows_sharekey)
|
||||
|
||||
share_destinations = json_data['share_destinations']
|
||||
credits = json_data['credits']
|
||||
title = json_data['title']
|
||||
description = json_data['description']
|
||||
is_nsfw = json_data['is_nsfw']
|
||||
prompt = json_data['prompt']
|
||||
potential_outputs = json_data['potential_outputs']
|
||||
selected_output_index = json_data['selected_output_index']
|
||||
|
||||
try:
|
||||
output_to_share = potential_outputs[int(selected_output_index)]
|
||||
except:
|
||||
# for now, pick the first output
|
||||
output_to_share = potential_outputs[0]
|
||||
|
||||
assert output_to_share['type'] in ('image', 'output')
|
||||
output_dir = folder_paths.get_output_directory()
|
||||
|
||||
if output_to_share['type'] == 'image':
|
||||
asset_filename = output_to_share['image']['filename']
|
||||
asset_subfolder = output_to_share['image']['subfolder']
|
||||
|
||||
if output_to_share['image']['type'] == 'temp':
|
||||
output_dir = folder_paths.get_temp_directory()
|
||||
else:
|
||||
asset_filename = output_to_share['output']['filename']
|
||||
asset_subfolder = output_to_share['output']['subfolder']
|
||||
|
||||
if asset_subfolder:
|
||||
asset_filepath = os.path.join(output_dir, asset_subfolder, asset_filename)
|
||||
else:
|
||||
asset_filepath = os.path.join(output_dir, asset_filename)
|
||||
|
||||
# get the mime type of the asset
|
||||
assetFileType = mimetypes.guess_type(asset_filepath)[0]
|
||||
|
||||
share_website_host = "UNKNOWN"
|
||||
if "comfyworkflows" in share_destinations:
|
||||
share_website_host = "https://comfyworkflows.com"
|
||||
share_endpoint = f"{share_website_host}/api"
|
||||
|
||||
# get presigned urls
|
||||
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
|
||||
async with session.post(
|
||||
f"{share_endpoint}/get_presigned_urls",
|
||||
json={
|
||||
"assetFileName": asset_filename,
|
||||
"assetFileType": assetFileType,
|
||||
"workflowJsonFileName": 'workflow.json',
|
||||
"workflowJsonFileType": 'application/json',
|
||||
},
|
||||
) as resp:
|
||||
assert resp.status == 200
|
||||
presigned_urls_json = await resp.json()
|
||||
assetFilePresignedUrl = presigned_urls_json["assetFilePresignedUrl"]
|
||||
assetFileKey = presigned_urls_json["assetFileKey"]
|
||||
workflowJsonFilePresignedUrl = presigned_urls_json["workflowJsonFilePresignedUrl"]
|
||||
workflowJsonFileKey = presigned_urls_json["workflowJsonFileKey"]
|
||||
|
||||
# upload asset
|
||||
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
|
||||
async with session.put(assetFilePresignedUrl, data=open(asset_filepath, "rb")) as resp:
|
||||
assert resp.status == 200
|
||||
|
||||
# upload workflow json
|
||||
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
|
||||
async with session.put(workflowJsonFilePresignedUrl, data=json.dumps(prompt['workflow']).encode('utf-8')) as resp:
|
||||
assert resp.status == 200
|
||||
|
||||
model_filenames = extract_model_file_names(prompt['workflow'])
|
||||
model_file_paths = find_file_paths(folder_paths.base_path, model_filenames)
|
||||
|
||||
models_info = {}
|
||||
for filename, filepath in model_file_paths.items():
|
||||
models_info[filename] = {
|
||||
"filename": filename,
|
||||
"sha256_checksum": compute_sha256_checksum(filepath),
|
||||
"relative_path": os.path.relpath(filepath, folder_paths.base_path),
|
||||
}
|
||||
|
||||
# make a POST request to /api/upload_workflow with form data key values
|
||||
async with aiohttp.ClientSession(trust_env=True, connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
|
||||
form = aiohttp.FormData()
|
||||
if comfyworkflows_sharekey:
|
||||
form.add_field("shareKey", comfyworkflows_sharekey)
|
||||
form.add_field("source", "comfyui_manager")
|
||||
form.add_field("assetFileKey", assetFileKey)
|
||||
form.add_field("assetFileType", assetFileType)
|
||||
form.add_field("workflowJsonFileKey", workflowJsonFileKey)
|
||||
form.add_field("sharedWorkflowWorkflowJsonString", json.dumps(prompt['workflow']))
|
||||
form.add_field("sharedWorkflowPromptJsonString", json.dumps(prompt['output']))
|
||||
form.add_field("shareWorkflowCredits", credits)
|
||||
form.add_field("shareWorkflowTitle", title)
|
||||
form.add_field("shareWorkflowDescription", description)
|
||||
form.add_field("shareWorkflowIsNSFW", str(is_nsfw).lower())
|
||||
form.add_field("currentSnapshot", json.dumps(await core.get_current_snapshot()))
|
||||
form.add_field("modelsInfo", json.dumps(models_info))
|
||||
|
||||
async with session.post(
|
||||
f"{share_endpoint}/upload_workflow",
|
||||
data=form,
|
||||
) as resp:
|
||||
assert resp.status == 200
|
||||
upload_workflow_json = await resp.json()
|
||||
workflowId = upload_workflow_json["workflowId"]
|
||||
|
||||
# check if the user has provided Matrix credentials
|
||||
if "matrix" in share_destinations:
|
||||
comfyui_share_room_id = '!LGYSoacpJPhIfBqVfb:matrix.org'
|
||||
filename = os.path.basename(asset_filepath)
|
||||
content_type = assetFileType
|
||||
|
||||
try:
|
||||
from nio import AsyncClient, LoginResponse, UploadResponse
|
||||
|
||||
homeserver = 'matrix.org'
|
||||
if matrix_auth:
|
||||
homeserver = matrix_auth.get('homeserver', 'matrix.org')
|
||||
homeserver = homeserver.replace("http://", "https://")
|
||||
if not homeserver.startswith("https://"):
|
||||
homeserver = "https://" + homeserver
|
||||
|
||||
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)
|
||||
|
||||
# Upload asset
|
||||
with open(asset_filepath, 'rb') as f:
|
||||
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
|
||||
|
||||
# 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"
|
||||
if description:
|
||||
text_content += f"{description}\n"
|
||||
if credits:
|
||||
text_content += f"\ncredits: {credits}\n"
|
||||
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({
|
||||
"comfyworkflows": {
|
||||
"url": None if "comfyworkflows" not in share_destinations else f"{share_website_host}/workflows/{workflowId}",
|
||||
},
|
||||
"matrix": {
|
||||
"success": None if "matrix" not in share_destinations else True
|
||||
}
|
||||
}, content_type='application/json', status=200)
|
||||
50
js/README.md
Normal file
@ -0,0 +1,50 @@
|
||||
# ComfyUI-Manager: Frontend (js)
|
||||
|
||||
This directory contains the JavaScript frontend implementation for ComfyUI-Manager, providing the user interface components that interact with the backend API.
|
||||
|
||||
## Core Components
|
||||
|
||||
- **comfyui-manager.js**: Main entry point that initializes the manager UI and integrates with ComfyUI.
|
||||
- **custom-nodes-manager.js**: Implements the UI for browsing, installing, and managing custom nodes.
|
||||
- **model-manager.js**: Handles the model management interface for downloading and organizing AI models.
|
||||
- **components-manager.js**: Manages reusable workflow components system.
|
||||
- **snapshot.js**: Implements the snapshot system for backing up and restoring installations.
|
||||
|
||||
## Sharing Components
|
||||
|
||||
- **comfyui-share-common.js**: Base functionality for workflow sharing features.
|
||||
- **comfyui-share-copus.js**: Integration with the ComfyUI Copus sharing platform.
|
||||
- **comfyui-share-openart.js**: Integration with the OpenArt sharing platform.
|
||||
- **comfyui-share-youml.js**: Integration with the YouML sharing platform.
|
||||
|
||||
## Utility Components
|
||||
|
||||
- **cm-api.js**: Client-side API wrapper for communication with the backend.
|
||||
- **common.js**: Shared utilities and helper functions used across the frontend.
|
||||
- **node_fixer.js**: Utilities for fixing disconnected links and repairing malformed nodes by recreating them while preserving connections.
|
||||
- **popover-helper.js**: UI component for popup tooltips and contextual information.
|
||||
- **turbogrid.esm.js**: Grid component library - https://github.com/cenfun/turbogrid
|
||||
- **workflow-metadata.js**: Handles workflow metadata parsing, validation and cross-repository compatibility including versioning, dependencies tracking, and resource management.
|
||||
|
||||
## Architecture
|
||||
|
||||
The frontend follows a modular component-based architecture:
|
||||
|
||||
1. **Integration Layer**: Connects with ComfyUI's existing UI system
|
||||
2. **Manager Components**: Individual functional UI components (node manager, model manager, etc.)
|
||||
3. **Sharing Components**: Platform-specific sharing implementations
|
||||
4. **Utility Layer**: Reusable UI components and helpers
|
||||
|
||||
## Implementation Details
|
||||
|
||||
- The frontend integrates directly with ComfyUI's UI system through `app.js`
|
||||
- Dialog-based UI for most manager functions to avoid cluttering the main interface
|
||||
- Asynchronous API calls to handle backend operations without blocking the UI
|
||||
|
||||
## Styling
|
||||
|
||||
CSS files are included for specific components:
|
||||
- **custom-nodes-manager.css**: Styling for the node management UI
|
||||
- **model-manager.css**: Styling for the model management UI
|
||||
|
||||
This frontend implementation provides a comprehensive yet user-friendly interface for managing the ComfyUI ecosystem.
|
||||
16
js/cm-api.js
@ -1,6 +1,6 @@
|
||||
import { api } from "../../scripts/api.js";
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { sleep } from "./common.js";
|
||||
import { sleep, customConfirm, customAlert, handle403Response, show_message } from "./common.js";
|
||||
|
||||
async function tryInstallCustomNode(event) {
|
||||
let msg = '-= [ComfyUI Manager] extension installation request =-\n\n';
|
||||
@ -19,11 +19,10 @@ async function tryInstallCustomNode(event) {
|
||||
msg += `\n\nRequest message:\n${event.detail.msg}`;
|
||||
|
||||
if(event.detail.target.installed == 'True') {
|
||||
alert(msg);
|
||||
customAlert(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
let res = confirm(msg);
|
||||
const res = await customConfirm(msg);
|
||||
if(res) {
|
||||
if(event.detail.target.installed == 'Disabled') {
|
||||
const response = await api.fetchApi(`/customnode/toggle_active`, {
|
||||
@ -43,14 +42,19 @@ async function tryInstallCustomNode(event) {
|
||||
});
|
||||
|
||||
if(response.status == 403) {
|
||||
show_message('This action is not allowed with this security level configuration.');
|
||||
await handle403Response(response);
|
||||
return false;
|
||||
}
|
||||
else if(response.status == 400) {
|
||||
let msg = await res.text();
|
||||
show_message(msg);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let response = await api.fetchApi("/manager/reboot");
|
||||
if(response.status == 403) {
|
||||
show_message('This action is not allowed with this security level configuration.');
|
||||
await handle403Response(response);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
227
js/comfyui-gui-builder.js
Normal file
@ -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;
|
||||
}
|
||||
@ -4,6 +4,7 @@ import { $el, ComfyDialog } from "../../scripts/ui.js";
|
||||
import { CopusShareDialog } from "./comfyui-share-copus.js";
|
||||
import { OpenArtShareDialog } from "./comfyui-share-openart.js";
|
||||
import { YouMLShareDialog } from "./comfyui-share-youml.js";
|
||||
import { customAlert } from "./common.js";
|
||||
|
||||
export const SUPPORTED_OUTPUT_NODE_TYPES = [
|
||||
"PreviewImage",
|
||||
@ -252,9 +253,9 @@ export const showShareDialog = async (share_option) => {
|
||||
if (potential_output_nodes.length === 0) {
|
||||
// todo: add support for other output node types (animatediff combine, etc.)
|
||||
const supported_nodes_string = SUPPORTED_OUTPUT_NODE_TYPES.join(", ");
|
||||
alert(`No supported output node found (${supported_nodes_string}). To share this workflow, please add an output node to your graph and re-run your prompt.`);
|
||||
customAlert(`No supported output node found (${supported_nodes_string}). To share this workflow, please add an output node to your graph and re-run your prompt.`);
|
||||
} else {
|
||||
alert("To share this, first run a prompt. Once it's done, click 'Share'.\n\nNOTE: Images of the Share target can only be selected in the PreviewImage, SaveImage, and VHS_VideoCombine nodes. In the case of VHS_VideoCombine, only the image/gif and image/webp formats are supported.");
|
||||
customAlert("To share this, first run a prompt. Once it's done, click 'Share'.\n\nNOTE: Images of the Share target can only be selected in the PreviewImage, SaveImage, and VHS_VideoCombine nodes. In the case of VHS_VideoCombine, only the image/gif and image/webp formats are supported.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -315,7 +316,7 @@ export class ShareDialogChooser extends ComfyDialog {
|
||||
key: "comfyworkflows",
|
||||
textContent: "ComfyWorkflows",
|
||||
website: "https://comfyworkflows.com",
|
||||
description: "Share & browse thousands of ComfyUI workflows and art 🎨<br/><br/><a style='color:white;' href='https://comfyworkflows.com' target='_blank'>ComfyWorkflows.com</a>",
|
||||
description: "Share & browse thousands of ComfyUI workflows and art 🎨<br/><br/><a style='color:var(--input-text);' href='https://comfyworkflows.com' target='_blank'>ComfyWorkflows.com</a>",
|
||||
onclick: () => {
|
||||
showShareDialog('comfyworkflows').then((suc) => {
|
||||
suc && this.close();
|
||||
@ -326,7 +327,7 @@ export class ShareDialogChooser extends ComfyDialog {
|
||||
key: "esheep",
|
||||
textContent: "eSheep",
|
||||
website: "https://www.esheep.com",
|
||||
description: "Share & download thousands of ComfyUI workflows on <a style='color:white;' href='https://www.esheep.com' target='_blank'>esheep.com</a>",
|
||||
description: "Share & download thousands of ComfyUI workflows on <a style='color:var(--input-text);' href='https://www.esheep.com' target='_blank'>esheep.com</a>",
|
||||
onclick: () => {
|
||||
shareToEsheep();
|
||||
this.close();
|
||||
@ -336,7 +337,7 @@ export class ShareDialogChooser extends ComfyDialog {
|
||||
key: "Copus",
|
||||
textContent: "Copus",
|
||||
website: "https://www.copus.io",
|
||||
description: "🔴 Permanently store and secure ownership of your workflow on the open-source platform: <a style='color:white;' href='https://copus.io' target='_blank'>Copus.io</a>",
|
||||
description: "🔴 Earn simple. Get paid from your ComfyUI workflows—no revenue sharing. Ever.",
|
||||
onclick: () => {
|
||||
showCopusShareDialog();
|
||||
this.close();
|
||||
@ -356,7 +357,8 @@ export class ShareDialogChooser extends ComfyDialog {
|
||||
});
|
||||
|
||||
buttons.forEach(b => {
|
||||
const button = $el("button", {
|
||||
const button = $el("button",
|
||||
{
|
||||
type: "button",
|
||||
textContent: b.textContent,
|
||||
onclick: b.onclick,
|
||||
@ -369,8 +371,14 @@ export class ShareDialogChooser extends ComfyDialog {
|
||||
'padding': '5px 5px',
|
||||
'margin-bottom': '5px',
|
||||
'transition': 'background-color 0.3s',
|
||||
'position':'relative'
|
||||
}
|
||||
});
|
||||
},
|
||||
[
|
||||
$el("span", { style: {
|
||||
} }),
|
||||
]
|
||||
);
|
||||
button.addEventListener('mouseover', () => {
|
||||
button.style.backgroundColor = '#007BFF'; // Change color on hover
|
||||
});
|
||||
@ -382,18 +390,40 @@ export class ShareDialogChooser extends ComfyDialog {
|
||||
innerHTML: b.description,
|
||||
style: {
|
||||
'text-align': 'left',
|
||||
color: 'white',
|
||||
color: 'var(--input-text)',
|
||||
'font-size': '14px',
|
||||
'margin-bottom': '0',
|
||||
},
|
||||
});
|
||||
|
||||
const copus_ui =$el("div", { style: {
|
||||
'position': 'absolute',
|
||||
'height': '100%',
|
||||
'left': '-25px',
|
||||
'top': '-26px',
|
||||
'width': '100%',
|
||||
'z-index':'-1',
|
||||
'background':'url("https://static.copus.io/images/client/202412/test/f28ac6ef8f4c6f3d5d50856a272ed02c.png")',
|
||||
'background-repeat': 'no-repeat',
|
||||
} });
|
||||
const copus_ui_bottom =$el("div", { style: {
|
||||
'position': 'absolute',
|
||||
'height': '100%',
|
||||
'left': '25px',
|
||||
'bottom': '-26px',
|
||||
'width': '100%',
|
||||
'transform':'scale(-1, -1)',
|
||||
'z-index':'-1',
|
||||
'background':'url("https://static.copus.io/images/client/202412/test/f28ac6ef8f4c6f3d5d50856a272ed02c.png")',
|
||||
'background-repeat': 'no-repeat',
|
||||
} });
|
||||
|
||||
const websiteLink = $el("a", {
|
||||
textContent: "🌐 Website",
|
||||
href: b.website,
|
||||
target: "_blank",
|
||||
style: {
|
||||
color: 'white',
|
||||
color: 'var(--input-text)',
|
||||
'margin-left': '10px',
|
||||
'font-size': '12px',
|
||||
'text-decoration': 'none',
|
||||
@ -417,7 +447,6 @@ export class ShareDialogChooser extends ComfyDialog {
|
||||
'margin-bottom': '10px',
|
||||
}
|
||||
}, [button, websiteLink]);
|
||||
|
||||
const column = $el("div", {
|
||||
style: {
|
||||
'flex-basis': '100%',
|
||||
@ -426,8 +455,17 @@ export class ShareDialogChooser extends ComfyDialog {
|
||||
'border': '1px solid #ddd',
|
||||
'border-radius': '5px',
|
||||
'box-shadow': '0 2px 4px rgba(0, 0, 0, 0.1)',
|
||||
'position':'relative'
|
||||
}
|
||||
}, [buttonLinkContainer, description]);
|
||||
}, [buttonLinkContainer, description
|
||||
,
|
||||
b.key ==='Copus' ?
|
||||
copus_ui
|
||||
:'',
|
||||
b.key ==='Copus' ?
|
||||
copus_ui_bottom
|
||||
:'',
|
||||
]);
|
||||
|
||||
container.appendChild(column);
|
||||
});
|
||||
@ -440,7 +478,7 @@ export class ShareDialogChooser extends ComfyDialog {
|
||||
textContent: 'Choose a platform to share your workflow',
|
||||
style: {
|
||||
'text-align': 'center',
|
||||
'color': 'white',
|
||||
'color': 'var(--input-text)',
|
||||
'font-size': '18px',
|
||||
'margin-bottom': '10px',
|
||||
},
|
||||
@ -475,7 +513,7 @@ export class ShareDialogChooser extends ComfyDialog {
|
||||
}
|
||||
show() {
|
||||
this.element.style.display = "block";
|
||||
this.element.style.zIndex = 10001;
|
||||
this.element.style.zIndex = 1099;
|
||||
}
|
||||
}
|
||||
export class ShareDialog extends ComfyDialog {
|
||||
@ -686,7 +724,7 @@ export class ShareDialog extends ComfyDialog {
|
||||
$el("div", {}, [
|
||||
$el("p", {
|
||||
size: 3, color: "white", style: {
|
||||
color: 'white'
|
||||
color: 'var(--input-text)'
|
||||
}
|
||||
}, [`Select where to share your art:`]),
|
||||
this.matrix_destination_checkbox,
|
||||
@ -701,7 +739,7 @@ export class ShareDialog extends ComfyDialog {
|
||||
size: 3,
|
||||
color: "white",
|
||||
style: {
|
||||
color: 'white'
|
||||
color: 'var(--input-text)'
|
||||
}
|
||||
}, []),
|
||||
this.credits_input,
|
||||
@ -712,7 +750,7 @@ export class ShareDialog extends ComfyDialog {
|
||||
size: 3,
|
||||
color: "white",
|
||||
style: {
|
||||
color: 'white'
|
||||
color: 'var(--input-text)'
|
||||
}
|
||||
}, []),
|
||||
this.title_input,
|
||||
@ -723,7 +761,7 @@ export class ShareDialog extends ComfyDialog {
|
||||
size: 3,
|
||||
color: "white",
|
||||
style: {
|
||||
color: 'white'
|
||||
color: 'var(--input-text)'
|
||||
}
|
||||
}, []),
|
||||
this.description_input,
|
||||
@ -824,7 +862,7 @@ export class ShareDialog extends ComfyDialog {
|
||||
if (destinations.includes("matrix")) {
|
||||
let definedMatrixAuth = !!this.matrix_homeserver_input.value && !!this.matrix_username_input.value && !!this.matrix_password_input.value;
|
||||
if (!definedMatrixAuth) {
|
||||
alert("Please set your Matrix account details.");
|
||||
customAlert("Please set your Matrix account details.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -841,9 +879,9 @@ export class ShareDialog extends ComfyDialog {
|
||||
if (potential_output_nodes.length === 0) {
|
||||
// todo: add support for other output node types (animatediff combine, etc.)
|
||||
const supported_nodes_string = SUPPORTED_OUTPUT_NODE_TYPES.join(", ");
|
||||
alert(`No supported output node found (${supported_nodes_string}). To share this workflow, please add an output node to your graph and re-run your prompt.`);
|
||||
customAlert(`No supported output node found (${supported_nodes_string}). To share this workflow, please add an output node to your graph and re-run your prompt.`);
|
||||
} else {
|
||||
alert("To share this, first run a prompt. Once it's done, click 'Share'.\n\nNOTE: Images of the Share target can only be selected in the PreviewImage, SaveImage, and VHS_VideoCombine nodes. In the case of VHS_VideoCombine, only the image/gif and image/webp formats are supported.");
|
||||
customAlert("To share this, first run a prompt. Once it's done, click 'Share'.\n\nNOTE: Images of the Share target can only be selected in the PreviewImage, SaveImage, and VHS_VideoCombine nodes. In the case of VHS_VideoCombine, only the image/gif and image/webp formats are supported.");
|
||||
}
|
||||
this.selectedOutputIndex = 0;
|
||||
this.close();
|
||||
@ -881,16 +919,16 @@ export class ShareDialog extends ComfyDialog {
|
||||
try {
|
||||
const response_json = await response.json();
|
||||
if (response_json.error) {
|
||||
alert(response_json.error);
|
||||
customAlert(response_json.error);
|
||||
this.close();
|
||||
return;
|
||||
} else {
|
||||
alert("Failed to share your art. Please try again.");
|
||||
customAlert("Failed to share your art. Please try again.");
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
alert("Failed to share your art. Please try again.");
|
||||
customAlert("Failed to share your art. Please try again.");
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
@ -989,7 +1027,7 @@ export class ShareDialog extends ComfyDialog {
|
||||
}
|
||||
const radio_button_text = $el("label", {
|
||||
// style: {
|
||||
// color: 'white'
|
||||
// color: 'var(--input-text)'
|
||||
// }
|
||||
}, [output.title])
|
||||
radio_button.style.color = "var(--fg-color)";
|
||||
@ -1028,7 +1066,7 @@ export class ShareDialog extends ComfyDialog {
|
||||
color: "white",
|
||||
style: {
|
||||
'text-align': 'center',
|
||||
color: 'white',
|
||||
color: 'var(--input-text)',
|
||||
backgroundColor: 'black',
|
||||
padding: '10px',
|
||||
'margin-top': '0px',
|
||||
@ -1040,7 +1078,7 @@ export class ShareDialog extends ComfyDialog {
|
||||
color: "white",
|
||||
style: {
|
||||
'text-align': 'center',
|
||||
color: 'white',
|
||||
color: 'var(--input-text)',
|
||||
'margin-bottom': '5px',
|
||||
'font-style': 'italic',
|
||||
'font-size': '12px',
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { $el, ComfyDialog } from "../../scripts/ui.js";
|
||||
import { customAlert } from "./common.js";
|
||||
|
||||
const env = "prod";
|
||||
|
||||
let DEFAULT_HOMEPAGE_URL = "https://copus.io";
|
||||
|
||||
let API_ENDPOINT = "https://api.client.prod.copus.io/copus-client";
|
||||
let API_ENDPOINT = "https://api.client.prod.copus.io";
|
||||
|
||||
if (env !== "prod") {
|
||||
API_ENDPOINT = "https://api.dev.copus.io/copus-client";
|
||||
API_ENDPOINT = "https://api.test.copus.io";
|
||||
DEFAULT_HOMEPAGE_URL = "https://test.copus.io";
|
||||
}
|
||||
|
||||
@ -61,6 +63,7 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
[$el("div.comfy-modal-content", {}, [...this.createButtons()])]
|
||||
);
|
||||
this.selectedOutputIndex = 0;
|
||||
this.selectedOutput_lock = 0;
|
||||
this.selectedNodeId = null;
|
||||
this.uploadedImages = [];
|
||||
this.allFilesImages = [];
|
||||
@ -68,7 +71,7 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
this.allFiles = [];
|
||||
this.titleNum = 0;
|
||||
}
|
||||
|
||||
|
||||
createButtons() {
|
||||
const inputStyle = {
|
||||
display: "block",
|
||||
@ -190,10 +193,38 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
type: "text",
|
||||
placeholder: "Subtitle (Optional)",
|
||||
style: inputStyle,
|
||||
maxLength: "70",
|
||||
maxLength: "350",
|
||||
oninput: () => {
|
||||
const titleNum = this.SubTitleInput.value.length;
|
||||
subTitleNumDom.textContent = `${titleNum}/70`;
|
||||
subTitleNumDom.textContent = `${titleNum}/350`;
|
||||
},
|
||||
});
|
||||
this.LockInput = $el("input", {
|
||||
type: "text",
|
||||
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;
|
||||
// Use a regular expression to match a number with up to two decimal places
|
||||
const regex = /^\d*\.?\d{0,2}$/;
|
||||
if (!regex.test(input)) {
|
||||
// If the input doesn't match, remove the last entered character
|
||||
event.target.value = input.slice(0, -1);
|
||||
}
|
||||
const numericValue = parseFloat(input);
|
||||
if (numericValue > 9999) {
|
||||
input = "9999";
|
||||
}
|
||||
// Update the input field with the valid value
|
||||
event.target.value = input;
|
||||
},
|
||||
});
|
||||
this.descriptionInput = $el("textarea", {
|
||||
@ -272,7 +303,7 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
|
||||
const titleNumDom = $el(
|
||||
"label",
|
||||
{
|
||||
@ -297,7 +328,7 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
color: "#999",
|
||||
},
|
||||
},
|
||||
["0/70"]
|
||||
["0/350"]
|
||||
);
|
||||
const descriptionNumDom = $el(
|
||||
"label",
|
||||
@ -313,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,
|
||||
@ -333,6 +360,101 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
// descriptionNumDom,
|
||||
]);
|
||||
// switch between outputs section and additional inputs section
|
||||
this.radioButtons_lock = [];
|
||||
|
||||
this.radioButtonsCheck_lock = $el("input", {
|
||||
type: "radio",
|
||||
name: "output_type_lock",
|
||||
value: "0",
|
||||
id: "blockchain1_lock",
|
||||
checked: true,
|
||||
});
|
||||
this.radioButtonsCheckOff_lock = $el("input", {
|
||||
type: "radio",
|
||||
name: "output_type_lock",
|
||||
value: "1",
|
||||
id: "blockchain_lock",
|
||||
});
|
||||
|
||||
const blockChainSection_lock = $el("div", { style: sectionStyle }, [
|
||||
$el("label", { style: labelStyle }, ["6️⃣ Download threshold"]),
|
||||
$el(
|
||||
"label",
|
||||
{
|
||||
style: {
|
||||
marginTop: "10px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
cursor: "pointer",
|
||||
},
|
||||
},
|
||||
[
|
||||
this.radioButtonsCheck_lock,
|
||||
$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(
|
||||
"label",
|
||||
{ style: { display: "flex", alignItems: "center", cursor: "pointer" } },
|
||||
[
|
||||
this.radioButtonsCheckOff_lock,
|
||||
$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" } },
|
||||
[
|
||||
]
|
||||
),
|
||||
]);
|
||||
|
||||
this.radioButtons = [];
|
||||
|
||||
this.radioButtonsCheck = $el("input", {
|
||||
@ -350,7 +472,7 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
});
|
||||
|
||||
const blockChainSection = $el("div", { style: sectionStyle }, [
|
||||
$el("label", { style: labelStyle }, ["6️⃣ Store on blockchain "]),
|
||||
$el("label", { style: labelStyle }, ["8️⃣ Store on blockchain "]),
|
||||
$el(
|
||||
"label",
|
||||
{
|
||||
@ -380,6 +502,141 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
["Secure ownership with a permanent & decentralized storage"]
|
||||
),
|
||||
]);
|
||||
|
||||
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(
|
||||
"div",
|
||||
@ -441,6 +698,8 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
SubtitleSection,
|
||||
DescriptionSection,
|
||||
// contestSection,
|
||||
blockChainSection_lock,
|
||||
contentRatingSection,
|
||||
blockChainSection,
|
||||
this.message,
|
||||
buttonsSection,
|
||||
@ -449,7 +708,7 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
return layout;
|
||||
}
|
||||
/**
|
||||
* api
|
||||
* api
|
||||
* @param {url} path
|
||||
* @param {params} options
|
||||
* @param {statusText} statusText
|
||||
@ -502,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) {
|
||||
@ -520,7 +781,7 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
this.shareButton.textContent = "Sharing...";
|
||||
await this.share();
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
customAlert(e.message);
|
||||
}
|
||||
this.shareButton.disabled = false;
|
||||
this.shareButton.textContent = "Share";
|
||||
@ -543,6 +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,
|
||||
rating: this.ratingRadioButtonsCheck0.checked
|
||||
? 0
|
||||
: this.ratingRadioButtonsCheck1.checked
|
||||
? 1
|
||||
: this.ratingRadioButtonsCheck2.checked
|
||||
? 2
|
||||
: -1,
|
||||
};
|
||||
|
||||
if (!this.keyInput.value) {
|
||||
@ -557,6 +827,12 @@ export class CopusShareDialog extends ComfyDialog {
|
||||
throw new Error("Title is required");
|
||||
}
|
||||
|
||||
if (this.radioButtonsCheck_lock.checked) {
|
||||
if (!this.LockInput.value) {
|
||||
throw new Error("Price is required");
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.uploadedImages.length) {
|
||||
if (this.selectedFile) {
|
||||
await this.uploadThumbnail(this.selectedFile);
|
||||
@ -602,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);
|
||||
}
|
||||
@ -664,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,6 +1,7 @@
|
||||
import {app} from "../../scripts/app.js";
|
||||
import {api} from "../../scripts/api.js";
|
||||
import {ComfyDialog, $el} from "../../scripts/ui.js";
|
||||
import { customAlert } from "./common.js";
|
||||
|
||||
const LOCAL_STORAGE_KEY = "openart_comfy_workflow_key";
|
||||
const DEFAULT_HOMEPAGE_URL = "https://openart.ai/workflows/dev?developer=true";
|
||||
@ -199,7 +200,7 @@ export class OpenArtShareDialog extends ComfyDialog {
|
||||
color: "white",
|
||||
style: {
|
||||
'text-align': 'center',
|
||||
color: 'white',
|
||||
color: 'var(--input-text)',
|
||||
margin: '0 0 10px 0',
|
||||
}
|
||||
});
|
||||
@ -431,7 +432,7 @@ export class OpenArtShareDialog extends ComfyDialog {
|
||||
this.shareButton.textContent = "Sharing...";
|
||||
await this.share();
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
customAlert(e.message);
|
||||
}
|
||||
this.shareButton.disabled = false;
|
||||
this.shareButton.textContent = "Share";
|
||||
@ -733,7 +734,7 @@ export class OpenArtShareDialog extends ComfyDialog {
|
||||
size: 2,
|
||||
color: "white",
|
||||
style: {
|
||||
color: 'white',
|
||||
color: 'var(--input-text)',
|
||||
margin: '0 0 5px 0',
|
||||
fontSize: '12px',
|
||||
},
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {app} from "../../scripts/app.js";
|
||||
import {api} from "../../scripts/api.js";
|
||||
import {ComfyDialog, $el} from "../../scripts/ui.js";
|
||||
import { customAlert } from "./common.js";
|
||||
|
||||
const BASE_URL = "https://youml.com";
|
||||
//const BASE_URL = "http://localhost:3000";
|
||||
@ -347,7 +348,7 @@ export class YouMLShareDialog extends ComfyDialog {
|
||||
this.shareButton.textContent = "Sharing...";
|
||||
await this.share();
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
customAlert(e.message);
|
||||
} finally {
|
||||
this.shareButton.disabled = false;
|
||||
this.shareButton.textContent = "Share";
|
||||
|
||||
462
js/common.js
@ -1,30 +1,203 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
import { $el, ComfyDialog } from "../../scripts/ui.js";
|
||||
import { getBestPosition, getPositionStyle, getRect } from './popover-helper.js';
|
||||
|
||||
|
||||
function internalCustomConfirm(message, confirmMessage, cancelMessage) {
|
||||
return new Promise((resolve) => {
|
||||
// transparent bg
|
||||
const modalOverlay = document.createElement('div');
|
||||
modalOverlay.style.position = 'fixed';
|
||||
modalOverlay.style.top = 0;
|
||||
modalOverlay.style.left = 0;
|
||||
modalOverlay.style.width = '100%';
|
||||
modalOverlay.style.height = '100%';
|
||||
modalOverlay.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
|
||||
modalOverlay.style.display = 'flex';
|
||||
modalOverlay.style.alignItems = 'center';
|
||||
modalOverlay.style.justifyContent = 'center';
|
||||
modalOverlay.style.zIndex = '1101';
|
||||
|
||||
// Modal window container (dark bg)
|
||||
const modalDialog = document.createElement('div');
|
||||
modalDialog.style.backgroundColor = '#333';
|
||||
modalDialog.style.padding = '20px';
|
||||
modalDialog.style.borderRadius = '4px';
|
||||
modalDialog.style.maxWidth = '400px';
|
||||
modalDialog.style.width = '80%';
|
||||
modalDialog.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.5)';
|
||||
modalDialog.style.color = '#fff';
|
||||
|
||||
// Display message
|
||||
const modalMessage = document.createElement('p');
|
||||
modalMessage.textContent = message;
|
||||
modalMessage.style.margin = '0';
|
||||
modalMessage.style.padding = '0 0 20px';
|
||||
modalMessage.style.wordBreak = 'keep-all';
|
||||
|
||||
// Button container
|
||||
const modalButtons = document.createElement('div');
|
||||
modalButtons.style.display = 'flex';
|
||||
modalButtons.style.justifyContent = 'flex-end';
|
||||
|
||||
// Confirm button (green)
|
||||
const confirmButton = document.createElement('button');
|
||||
if(confirmMessage)
|
||||
confirmButton.textContent = confirmMessage;
|
||||
else
|
||||
confirmButton.textContent = 'Confirm';
|
||||
confirmButton.style.marginLeft = '10px';
|
||||
confirmButton.style.backgroundColor = '#28a745'; // green
|
||||
confirmButton.style.color = '#fff';
|
||||
confirmButton.style.border = 'none';
|
||||
confirmButton.style.padding = '6px 12px';
|
||||
confirmButton.style.borderRadius = '4px';
|
||||
confirmButton.style.cursor = 'pointer';
|
||||
confirmButton.style.fontWeight = 'bold';
|
||||
|
||||
// Cancel button (red)
|
||||
const cancelButton = document.createElement('button');
|
||||
if(cancelMessage)
|
||||
cancelButton.textContent = cancelMessage;
|
||||
else
|
||||
cancelButton.textContent = 'Cancel';
|
||||
|
||||
cancelButton.style.marginLeft = '10px';
|
||||
cancelButton.style.backgroundColor = '#dc3545'; // red
|
||||
cancelButton.style.color = '#fff';
|
||||
cancelButton.style.border = 'none';
|
||||
cancelButton.style.padding = '6px 12px';
|
||||
cancelButton.style.borderRadius = '4px';
|
||||
cancelButton.style.cursor = 'pointer';
|
||||
cancelButton.style.fontWeight = 'bold';
|
||||
|
||||
const closeModal = () => {
|
||||
document.body.removeChild(modalOverlay);
|
||||
};
|
||||
|
||||
confirmButton.addEventListener('click', () => {
|
||||
closeModal();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
cancelButton.addEventListener('click', () => {
|
||||
closeModal();
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
modalButtons.appendChild(confirmButton);
|
||||
modalButtons.appendChild(cancelButton);
|
||||
modalDialog.appendChild(modalMessage);
|
||||
modalDialog.appendChild(modalButtons);
|
||||
modalOverlay.appendChild(modalDialog);
|
||||
document.body.appendChild(modalOverlay);
|
||||
});
|
||||
}
|
||||
|
||||
export function show_message(msg) {
|
||||
app.ui.dialog.show(msg);
|
||||
app.ui.dialog.element.style.zIndex = 10010;
|
||||
app.ui.dialog.element.style.zIndex = 1100;
|
||||
}
|
||||
|
||||
export async function handle403Response(res, defaultMessage) {
|
||||
try {
|
||||
const data = await res.json();
|
||||
if(data.error === 'comfyui_outdated') {
|
||||
show_message('ComfyUI version is outdated.<BR>Please update ComfyUI to use Manager normally.');
|
||||
} else {
|
||||
show_message(defaultMessage || 'This action is not allowed with this security level configuration.');
|
||||
}
|
||||
} catch {
|
||||
show_message(defaultMessage || 'This action is not allowed with this security level configuration.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function rebootAPI() {
|
||||
if (confirm("Are you sure you'd like to reboot the server?")) {
|
||||
try {
|
||||
api.fetchApi("/manager/reboot");
|
||||
}
|
||||
catch(exception) {
|
||||
export async function customConfirm(message) {
|
||||
try {
|
||||
let res = await
|
||||
window['app'].extensionManager.dialog
|
||||
.confirm({
|
||||
title: 'Confirm',
|
||||
message: message
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
catch {
|
||||
let res = await internalCustomConfirm(message);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function customAlert(message) {
|
||||
try {
|
||||
window['app'].extensionManager.toast.addAlert(message);
|
||||
}
|
||||
catch {
|
||||
alert(message);
|
||||
}
|
||||
}
|
||||
|
||||
export function infoToast(summary, message) {
|
||||
try {
|
||||
app.extensionManager.toast.add({
|
||||
severity: 'info',
|
||||
summary: summary,
|
||||
detail: message,
|
||||
life: 3000
|
||||
})
|
||||
}
|
||||
catch {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function customPrompt(title, message) {
|
||||
try {
|
||||
let res = await
|
||||
window['app'].extensionManager.dialog
|
||||
.prompt({
|
||||
title: title,
|
||||
message: message
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
catch {
|
||||
return prompt(title, message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function rebootAPI() {
|
||||
if ('electronAPI' in window) {
|
||||
window.electronAPI.restartApp();
|
||||
return true;
|
||||
}
|
||||
|
||||
const isConfirmed = await customConfirm("Are you sure you'd like to reboot the server?");
|
||||
if (isConfirmed) {
|
||||
try {
|
||||
const response = await api.fetchApi("/manager/reboot");
|
||||
if (response.status == 403) {
|
||||
await handle403Response(response);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
catch(exception) {}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
export var manager_instance = null;
|
||||
|
||||
export function setManagerInstance(obj) {
|
||||
@ -59,7 +232,7 @@ export async function install_pip(packages) {
|
||||
});
|
||||
|
||||
if(res.status == 403) {
|
||||
show_message('This action is not allowed with this security level configuration.');
|
||||
await handle403Response(res);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -94,7 +267,7 @@ export async function install_via_git_url(url, manager_dialog) {
|
||||
});
|
||||
|
||||
if(res.status == 403) {
|
||||
show_message('This action is not allowed with this security level configuration.');
|
||||
await handle403Response(res);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -105,9 +278,9 @@ export async function install_via_git_url(url, manager_dialog) {
|
||||
const self = this;
|
||||
|
||||
rebootButton.addEventListener("click",
|
||||
function() {
|
||||
if(rebootAPI()) {
|
||||
manager_dialog.close();
|
||||
async function() {
|
||||
if(await rebootAPI()) {
|
||||
manager_instance.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -231,10 +404,267 @@ export async function fetchData(route, options) {
|
||||
}
|
||||
}
|
||||
|
||||
// https://cenfun.github.io/open-icons/
|
||||
export const icons = {
|
||||
search: '<svg viewBox="0 0 24 24" width="100%" height="100%" pointer-events="none" xmlns="http://www.w3.org/2000/svg"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m21 21-4.486-4.494M19 10.5a8.5 8.5 0 1 1-17 0 8.5 8.5 0 0 1 17 0"/></svg>',
|
||||
extensions: '<svg viewBox="64 64 896 896" width="100%" height="100%" pointer-events="none" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M843.5 737.4c-12.4-75.2-79.2-129.1-155.3-125.4S550.9 676 546 752c-153.5-4.8-208-40.7-199.1-113.7 3.3-27.3 19.8-41.9 50.1-49 18.4-4.3 38.8-4.9 57.3-3.2 1.7.2 3.5.3 5.2.5 11.3 2.7 22.8 5 34.3 6.8 34.1 5.6 68.8 8.4 101.8 6.6 92.8-5 156-45.9 159.2-132.7 3.1-84.1-54.7-143.7-147.9-183.6-29.9-12.8-61.6-22.7-93.3-30.2-14.3-3.4-26.3-5.7-35.2-7.2-7.9-75.9-71.5-133.8-147.8-134.4S189.7 168 180.5 243.8s40 146.3 114.2 163.9 149.9-23.3 175.7-95.1c9.4 1.7 18.7 3.6 28 5.8 28.2 6.6 56.4 15.4 82.4 26.6 70.7 30.2 109.3 70.1 107.5 119.9-1.6 44.6-33.6 65.2-96.2 68.6-27.5 1.5-57.6-.9-87.3-5.8-8.3-1.4-15.9-2.8-22.6-4.3-3.9-.8-6.6-1.5-7.8-1.8l-3.1-.6c-2.2-.3-5.9-.8-10.7-1.3-25-2.3-52.1-1.5-78.5 4.6-55.2 12.9-93.9 47.2-101.1 105.8-15.7 126.2 78.6 184.7 276 188.9 29.1 70.4 106.4 107.9 179.6 87 73.3-20.9 119.3-93.4 106.9-168.6M329.1 345.2a83.3 83.3 0 1 1 .01-166.61 83.3 83.3 0 0 1-.01 166.61M695.6 845a83.3 83.3 0 1 1 .01-166.61A83.3 83.3 0 0 1 695.6 845"/></svg>',
|
||||
conflicts: '<svg viewBox="0 0 400 400" width="100%" height="100%" pointer-events="none" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="m397.2 350.4.2-.2-180-320-.2.2C213.8 24.2 207.4 20 200 20s-13.8 4.2-17.2 10.4l-.2-.2-180 320 .2.2c-1.6 2.8-2.8 6-2.8 9.6 0 11 9 20 20 20h360c11 0 20-9 20-20 0-3.6-1.2-6.8-2.8-9.6M220 340h-40v-40h40zm0-60h-40V120h40z"/></svg>',
|
||||
passed: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 426.667 426.667"><path fill="#6AC259" d="M213.333,0C95.518,0,0,95.514,0,213.333s95.518,213.333,213.333,213.333c117.828,0,213.333-95.514,213.333-213.333S331.157,0,213.333,0z M174.199,322.918l-93.935-93.931l31.309-31.309l62.626,62.622l140.894-140.898l31.309,31.309L174.199,322.918z"/></svg>',
|
||||
download: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" width="100%" height="100%" viewBox="0 0 32 32"><path fill="currentColor" d="M26 24v4H6v-4H4v4a2 2 0 0 0 2 2h20a2 2 0 0 0 2-2v-4zm0-10l-1.41-1.41L17 20.17V2h-2v18.17l-7.59-7.58L6 14l10 10l10-10z"></path></svg>'
|
||||
}
|
||||
download: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" width="100%" height="100%" viewBox="0 0 32 32"><path fill="currentColor" d="M26 24v4H6v-4H4v4a2 2 0 0 0 2 2h20a2 2 0 0 0 2-2v-4zm0-10l-1.41-1.41L17 20.17V2h-2v18.17l-7.59-7.58L6 14l10 10l10-10z"></path></svg>',
|
||||
close: '<svg xmlns="http://www.w3.org/2000/svg" pointer-events="none" width="100%" height="100%" viewBox="0 0 16 16"><g fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="m7.116 8-4.558 4.558.884.884L8 8.884l4.558 4.558.884-.884L8.884 8l4.558-4.558-.884-.884L8 7.116 3.442 2.558l-.884.884L7.116 8z"/></g></svg>',
|
||||
arrowRight: '<svg xmlns="http://www.w3.org/2000/svg" pointer-events="none" width="100%" height="100%" viewBox="0 0 20 20"><path fill="currentColor" fill-rule="evenodd" d="m2.542 2.154 7.254 7.26c.136.14.204.302.204.483a.73.73 0 0 1-.204.5l-7.575 7.398c-.383.317-.724.317-1.022 0-.299-.317-.299-.643 0-.98l7.08-6.918-6.754-6.763c-.237-.343-.215-.654.066-.935.281-.28.598-.295.951-.045Zm9 0 7.254 7.26c.136.14.204.302.204.483a.73.73 0 0 1-.204.5l-7.575 7.398c-.383.317-.724.317-1.022 0-.299-.317-.299-.643 0-.98l7.08-6.918-6.754-6.763c-.237-.343-.215-.654.066-.935.281-.28.598-.295.951-.045Z"/></svg>'
|
||||
}
|
||||
|
||||
export function sanitizeHTML(str) {
|
||||
return str
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
export function showTerminal() {
|
||||
try {
|
||||
const panel = app.extensionManager.bottomPanel;
|
||||
const isTerminalVisible = panel.bottomPanelVisible && panel.activeBottomPanelTab.id === 'logs-terminal';
|
||||
if (!isTerminalVisible)
|
||||
panel.toggleBottomPanelTab('logs-terminal');
|
||||
}
|
||||
catch(exception) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
let need_restart = false;
|
||||
|
||||
export function setNeedRestart(value) {
|
||||
need_restart = value;
|
||||
}
|
||||
|
||||
async function onReconnected(event) {
|
||||
if(need_restart) {
|
||||
setNeedRestart(false);
|
||||
|
||||
const confirmed = await customConfirm("To apply the changes to the node pack's installation status, you need to refresh the browser. Would you like to refresh?");
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.reload(true);
|
||||
}
|
||||
}
|
||||
|
||||
api.addEventListener('reconnected', onReconnected);
|
||||
|
||||
const storeId = "comfyui-manager-grid";
|
||||
let timeId;
|
||||
export function storeColumnWidth(gridId, columnItem) {
|
||||
clearTimeout(timeId);
|
||||
timeId = setTimeout(() => {
|
||||
let data = {};
|
||||
const dataStr = localStorage.getItem(storeId);
|
||||
if (dataStr) {
|
||||
try {
|
||||
data = JSON.parse(dataStr);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (!data[gridId]) {
|
||||
data[gridId] = {};
|
||||
}
|
||||
|
||||
data[gridId][columnItem.id] = columnItem.width;
|
||||
|
||||
localStorage.setItem(storeId, JSON.stringify(data));
|
||||
|
||||
}, 200)
|
||||
}
|
||||
|
||||
export function restoreColumnWidth(gridId, columns) {
|
||||
const dataStr = localStorage.getItem(storeId);
|
||||
if (!dataStr) {
|
||||
return;
|
||||
}
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(dataStr);
|
||||
} catch (e) {}
|
||||
if(!data) {
|
||||
return;
|
||||
}
|
||||
const widthMap = data[gridId];
|
||||
if (!widthMap) {
|
||||
return;
|
||||
}
|
||||
|
||||
columns.forEach(columnItem => {
|
||||
const w = widthMap[columnItem.id];
|
||||
if (w) {
|
||||
columnItem.width = w;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
export function getTimeAgo(dateStr) {
|
||||
const date = new Date(dateStr);
|
||||
|
||||
if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const units = [
|
||||
{ max: 2760000, value: 60000, name: 'minute', past: 'a minute ago', future: 'in a minute' },
|
||||
{ max: 72000000, value: 3600000, name: 'hour', past: 'an hour ago', future: 'in an hour' },
|
||||
{ max: 518400000, value: 86400000, name: 'day', past: 'yesterday', future: 'tomorrow' },
|
||||
{ max: 2419200000, value: 604800000, name: 'week', past: 'last week', future: 'in a week' },
|
||||
{ max: 28512000000, value: 2592000000, name: 'month', past: 'last month', future: 'in a month' }
|
||||
];
|
||||
const diff = Date.now() - date.getTime();
|
||||
// less than a minute
|
||||
if (Math.abs(diff) < 60000)
|
||||
return 'just now';
|
||||
for (let i = 0; i < units.length; i++) {
|
||||
if (Math.abs(diff) < units[i].max) {
|
||||
return format(diff, units[i].value, units[i].name, units[i].past, units[i].future, diff < 0);
|
||||
}
|
||||
}
|
||||
function format(diff, divisor, unit, past, future, isInTheFuture) {
|
||||
const val = Math.round(Math.abs(diff) / divisor);
|
||||
if (isInTheFuture)
|
||||
return val <= 1 ? future : 'in ' + val + ' ' + unit + 's';
|
||||
return val <= 1 ? past : val + ' ' + unit + 's ago';
|
||||
}
|
||||
return format(diff, 31536000000, 'year', 'last year', 'in a year', diff < 0);
|
||||
};
|
||||
|
||||
export const loadCss = (cssFile) => {
|
||||
const cssPath = import.meta.resolve(cssFile);
|
||||
//console.log(cssPath);
|
||||
const $link = document.createElement("link");
|
||||
$link.setAttribute("rel", 'stylesheet');
|
||||
$link.setAttribute("href", cssPath);
|
||||
document.head.appendChild($link);
|
||||
};
|
||||
|
||||
export const copyText = (text) => {
|
||||
return new Promise((resolve) => {
|
||||
let err;
|
||||
try {
|
||||
navigator.clipboard.writeText(text);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
if (err) {
|
||||
resolve(false);
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function renderPopover($elem, target, options = {}) {
|
||||
// async microtask
|
||||
queueMicrotask(() => {
|
||||
|
||||
const containerRect = getRect(window);
|
||||
const targetRect = getRect(target);
|
||||
const elemRect = getRect($elem);
|
||||
|
||||
const positionInfo = getBestPosition(
|
||||
containerRect,
|
||||
targetRect,
|
||||
elemRect,
|
||||
options.positions
|
||||
);
|
||||
const style = getPositionStyle(positionInfo, {
|
||||
bgColor: options.bgColor,
|
||||
borderColor: options.borderColor,
|
||||
borderRadius: options.borderRadius
|
||||
});
|
||||
|
||||
$elem.style.top = positionInfo.top + "px";
|
||||
$elem.style.left = positionInfo.left + "px";
|
||||
$elem.style.background = style.background;
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
let $popover;
|
||||
export function hidePopover() {
|
||||
if ($popover) {
|
||||
$popover.remove();
|
||||
$popover = null;
|
||||
}
|
||||
}
|
||||
export function showPopover(target, text, className, options) {
|
||||
hidePopover();
|
||||
$popover = document.createElement("div");
|
||||
$popover.className = ['cn-popover', className].filter(it => it).join(" ");
|
||||
document.body.appendChild($popover);
|
||||
$popover.innerHTML = text;
|
||||
$popover.style.display = "block";
|
||||
renderPopover($popover, target, {
|
||||
borderRadius: 10,
|
||||
... options
|
||||
});
|
||||
}
|
||||
|
||||
let $tooltip;
|
||||
export function hideTooltip(target) {
|
||||
if ($tooltip) {
|
||||
$tooltip.style.display = "none";
|
||||
$tooltip.innerHTML = "";
|
||||
$tooltip.style.top = "0px";
|
||||
$tooltip.style.left = "0px";
|
||||
}
|
||||
}
|
||||
export function showTooltip(target, text, className = 'cn-tooltip', styleMap = {}) {
|
||||
if (!$tooltip) {
|
||||
$tooltip = document.createElement("div");
|
||||
$tooltip.className = className;
|
||||
$tooltip.style.cssText = `
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
z-index: 10001;
|
||||
padding: 20px;
|
||||
color: #1e1e1e;
|
||||
max-width: 350px;
|
||||
filter: drop-shadow(1px 5px 5px rgb(0 0 0 / 30%));
|
||||
${Object.keys(styleMap).map(k=>k+":"+styleMap[k]+";").join("")}
|
||||
`;
|
||||
document.body.appendChild($tooltip);
|
||||
}
|
||||
|
||||
$tooltip.innerHTML = text;
|
||||
$tooltip.style.display = "block";
|
||||
renderPopover($tooltip, target, {
|
||||
positions: ['top', 'bottom', 'right', 'center'],
|
||||
bgColor: "#ffffff",
|
||||
borderColor: "#cccccc",
|
||||
borderRadius: 5
|
||||
});
|
||||
}
|
||||
|
||||
function initTooltip () {
|
||||
const mouseenterHandler = (e) => {
|
||||
const target = e.target;
|
||||
const text = target.getAttribute('tooltip');
|
||||
if (text) {
|
||||
showTooltip(target, text);
|
||||
}
|
||||
};
|
||||
const mouseleaveHandler = (e) => {
|
||||
const target = e.target;
|
||||
const text = target.getAttribute('tooltip');
|
||||
if (text) {
|
||||
hideTooltip(target);
|
||||
}
|
||||
};
|
||||
document.body.removeEventListener('mouseenter', mouseenterHandler, true);
|
||||
document.body.removeEventListener('mouseleave', mouseleaveHandler, true);
|
||||
document.body.addEventListener('mouseenter', mouseenterHandler, true);
|
||||
document.body.addEventListener('mouseleave', mouseleaveHandler, true);
|
||||
}
|
||||
|
||||
initTooltip();
|
||||
@ -1,9 +1,11 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js"
|
||||
import { sleep, show_message } from "./common.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 = {};
|
||||
|
||||
@ -20,7 +22,7 @@ export function getPureName(node) {
|
||||
let purename = node.comfyClass.substring(category.length+1);
|
||||
return purename;
|
||||
}
|
||||
else if(node.comfyClass.startsWith('workflow/')) {
|
||||
else if(node.comfyClass.startsWith('workflow/') || node.comfyClass.startsWith(`workflow${SEPARATOR}`)) {
|
||||
return node.comfyClass.substring(9);
|
||||
}
|
||||
else {
|
||||
@ -76,7 +78,7 @@ export async function load_components() {
|
||||
|
||||
let category = data.packname;
|
||||
if(data.category) {
|
||||
category += "/" + data.category;
|
||||
category += SEPARATOR + data.category;
|
||||
}
|
||||
if(category == '') {
|
||||
category = 'components';
|
||||
@ -100,7 +102,7 @@ export async function load_components() {
|
||||
try {
|
||||
let category = nodeData.packname;
|
||||
if(nodeData.category) {
|
||||
category += "/" + nodeData.category;
|
||||
category += SEPARATOR + nodeData.category;
|
||||
}
|
||||
if(category == '') {
|
||||
category = 'components';
|
||||
@ -139,7 +141,7 @@ export async function load_components() {
|
||||
try {
|
||||
let category = nodeData.packname;
|
||||
if(nodeData.workflow.category) {
|
||||
category += "/" + nodeData.category;
|
||||
category += SEPARATOR + nodeData.category;
|
||||
}
|
||||
if(category == '') {
|
||||
category = 'components';
|
||||
@ -174,7 +176,7 @@ export async function load_components() {
|
||||
try {
|
||||
let category = nodeData.workflow.packname;
|
||||
if(nodeData.workflow.category) {
|
||||
category += "/" + nodeData.category;
|
||||
category += SEPARATOR + nodeData.category;
|
||||
}
|
||||
if(category == '') {
|
||||
category = 'components';
|
||||
@ -234,7 +236,7 @@ async function save_as_component(node, version, author, prefix, nodename, packna
|
||||
|
||||
let category = body.workflow.packname;
|
||||
if(body.workflow.category) {
|
||||
category += "/" + body.workflow.category;
|
||||
category += SEPARATOR + body.workflow.category;
|
||||
}
|
||||
if(category == '') {
|
||||
category = 'components';
|
||||
@ -266,7 +268,7 @@ async function import_component(component_name, component, mode) {
|
||||
|
||||
let category = component.packname;
|
||||
if(component.category) {
|
||||
category += "/" + component.category;
|
||||
category += SEPARATOR + component.category;
|
||||
}
|
||||
if(category == '') {
|
||||
category = 'components';
|
||||
@ -363,7 +365,7 @@ function checkVersion(name, component) {
|
||||
return msg;
|
||||
}
|
||||
|
||||
function handle_import_components(components) {
|
||||
async function handle_import_components(components) {
|
||||
let msg = 'Components:\n';
|
||||
let cnt = 0;
|
||||
for(let name in components) {
|
||||
@ -385,8 +387,9 @@ function handle_import_components(components) {
|
||||
|
||||
let last_name = null;
|
||||
msg += '\nWill you load components?\n';
|
||||
if(confirm(msg)) {
|
||||
let mode = confirm('\nWill you save components?\n(cancel=load without save)');
|
||||
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];
|
||||
@ -403,13 +406,13 @@ function handle_import_components(components) {
|
||||
}
|
||||
|
||||
if(cnt == 1 && last_name) {
|
||||
const node = LiteGraph.createNode(`workflow/${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);
|
||||
}
|
||||
}
|
||||
|
||||
function handlePaste(e) {
|
||||
async function handlePaste(e) {
|
||||
let data = (e.clipboardData || window.clipboardData);
|
||||
const items = data.items;
|
||||
for(const item of items) {
|
||||
@ -419,7 +422,7 @@ function handlePaste(e) {
|
||||
let json_data = JSON.parse(data);
|
||||
if(json_data.kind == 'ComfyUI Components' && last_paste_timestamp != json_data.timestamp) {
|
||||
last_paste_timestamp = json_data.timestamp;
|
||||
handle_import_components(json_data.components);
|
||||
await handle_import_components(json_data.components);
|
||||
|
||||
// disable paste node
|
||||
localStorage.removeItem("litegrapheditor_clipboard", null);
|
||||
@ -453,7 +456,7 @@ export class ComponentBuilderDialog extends ComfyDialog {
|
||||
this.invalidateControl();
|
||||
|
||||
this.element.style.display = "block";
|
||||
this.element.style.zIndex = 10001;
|
||||
this.element.style.zIndex = 1099;
|
||||
this.element.style.width = "500px";
|
||||
this.element.style.height = "480px";
|
||||
}
|
||||
@ -619,7 +622,7 @@ export class ComponentBuilderDialog extends ComfyDialog {
|
||||
self.version_string.value = self.default_ver;
|
||||
}
|
||||
else {
|
||||
alert('If you are not the author, it is not recommended to change the version, as it may cause component update issues.');
|
||||
customAlert('If you are not the author, it is not recommended to change the version, as it may cause component update issues.');
|
||||
}
|
||||
};
|
||||
|
||||
@ -675,7 +678,7 @@ export class ComponentBuilderDialog extends ComfyDialog {
|
||||
|
||||
let orig_handleFile = app.handleFile;
|
||||
|
||||
function handleFile(file) {
|
||||
async function handleFile(file) {
|
||||
if (file.name?.endsWith(".json") || file.name?.endsWith(".pack")) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
@ -688,7 +691,7 @@ function handleFile(file) {
|
||||
}
|
||||
|
||||
if(is_component) {
|
||||
handle_import_components(jsonContent);
|
||||
await handle_import_components(jsonContent);
|
||||
}
|
||||
else {
|
||||
orig_handleFile.call(app, file);
|
||||
@ -706,7 +709,7 @@ app.handleFile = handleFile;
|
||||
|
||||
let current_component_policy = 'workflow';
|
||||
try {
|
||||
api.fetchApi('/manager/component/policy')
|
||||
api.fetchApi('/manager/policy/component')
|
||||
.then(response => response.text())
|
||||
.then(data => { current_component_policy = data; });
|
||||
}
|
||||
@ -786,7 +789,7 @@ app.graphToPrompt = async function () {
|
||||
// get used group nodes
|
||||
let used_group_nodes = new Set();
|
||||
for(let node of p.workflow.nodes) {
|
||||
if(node.type.startsWith('workflow/')) {
|
||||
if(node.type.startsWith(`workflow/`) || node.type.startsWith(`workflow${SEPARATOR}`)) {
|
||||
used_group_nodes.add(node.type.substring(9));
|
||||
}
|
||||
}
|
||||
|
||||
729
js/custom-nodes-manager.css
Normal file
@ -0,0 +1,729 @@
|
||||
.cn-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);
|
||||
}
|
||||
|
||||
.cn-manager .cn-flex-auto {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.cn-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;
|
||||
}
|
||||
|
||||
.cn-manager button:hover {
|
||||
filter: brightness(125%);
|
||||
}
|
||||
|
||||
.cn-manager button:disabled,
|
||||
.cn-manager input:disabled,
|
||||
.cn-manager select:disabled {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.cn-manager button:disabled {
|
||||
background-color: var(--comfy-input-bg);
|
||||
}
|
||||
|
||||
.cn-manager .cn-manager-restart {
|
||||
display: none;
|
||||
background-color: #500000 !important;
|
||||
border-color: #88181b !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.cn-manager .cn-manager-restart:hover {
|
||||
background-color: #88181b !important;
|
||||
}
|
||||
|
||||
.cn-manager .cn-manager-stop {
|
||||
display: none;
|
||||
background-color: #500000;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cn-manager .cn-manager-back {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
margin-right: 5px;
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
.cn-icon {
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.cn-icon svg {
|
||||
display: block;
|
||||
margin: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cn-manager-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cn-manager-header label {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.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 {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.cn-manager-grid {
|
||||
flex: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cn-manager-selection {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cn-manager-message {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cn-manager-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cn-manager-grid .tg-turbogrid {
|
||||
font-family: var(--grid-font);
|
||||
font-size: 15px;
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.cn-manager-grid .tg-turbogrid .tg-highlight::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
content: "";
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
background-color: #80bdff11;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cn-manager-grid .cn-pack-name a {
|
||||
color: skyblue;
|
||||
text-decoration: none;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.cn-manager-grid .cn-pack-desc a {
|
||||
color: #5555FF;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.cn-manager-grid .tg-cell a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.cn-manager-grid .cn-pack-version {
|
||||
line-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.cn-manager-grid .cn-pack-nodes {
|
||||
line-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cn-manager-grid .cn-pack-nodes:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.cn-manager-grid .cn-pack-conflicts {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.cn-popover {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
padding: 20px;
|
||||
color: #1e1e1e;
|
||||
filter: drop-shadow(1px 5px 5px rgb(0 0 0 / 30%));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cn-flyover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
background-color: var(--comfy-menu-bg);
|
||||
animation-duration: 0.2s;
|
||||
animation-fill-mode: both;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cn-flyover::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
content: "";
|
||||
z-index: 10;
|
||||
display: block;
|
||||
width: 10px;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
left: -10px;
|
||||
background-image: linear-gradient(to left, rgb(0 0 0 / 20%), rgb(0 0 0 / 0%));
|
||||
}
|
||||
|
||||
.cn-flyover-header {
|
||||
height: 45px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.cn-flyover-close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cn-flyover-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.cn-flyover-close svg {
|
||||
display: block;
|
||||
margin: 0;
|
||||
pointer-events: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.cn-flyover-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
gap: 10px;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.cn-flyover-body {
|
||||
height: calc(100% - 45px);
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
background-color: var(--comfy-menu-secondary-bg);
|
||||
}
|
||||
|
||||
@keyframes cn-slide-in-right {
|
||||
from {
|
||||
visibility: visible;
|
||||
transform: translate3d(100%, 0, 0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.cn-slide-in-right {
|
||||
animation-name: cn-slide-in-right;
|
||||
}
|
||||
|
||||
@keyframes cn-slide-out-right {
|
||||
from {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
to {
|
||||
visibility: hidden;
|
||||
transform: translate3d(100%, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.cn-slide-out-right {
|
||||
animation-name: cn-slide-out-right;
|
||||
}
|
||||
|
||||
.cn-nodes-list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cn-nodes-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cn-nodes-row:nth-child(odd) {
|
||||
background-color: rgb(0 0 0 / 5%);
|
||||
}
|
||||
|
||||
.cn-nodes-row:hover {
|
||||
background-color: rgb(0 0 0 / 10%);
|
||||
}
|
||||
|
||||
.cn-nodes-sn {
|
||||
text-align: right;
|
||||
min-width: 35px;
|
||||
color: var(--drag-text);
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
padding: 8px 5px;
|
||||
}
|
||||
|
||||
.cn-nodes-name {
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
padding: 8px 5px;
|
||||
}
|
||||
|
||||
.cn-nodes-name::after {
|
||||
content: attr(action);
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
top: 50%;
|
||||
left: 100%;
|
||||
transform: translate(5px, -50%);
|
||||
font-size: 12px;
|
||||
color: var(--drag-text);
|
||||
background-color: var(--comfy-input-bg);
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 3px 8px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cn-nodes-name.action::after {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cn-nodes-name:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.cn-nodes-conflict .cn-nodes-name,
|
||||
.cn-nodes-conflict .cn-icon {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.cn-conflicts-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.cn-conflicts-list b {
|
||||
font-weight: normal;
|
||||
color: var(--descrip-text);
|
||||
}
|
||||
|
||||
.cn-nodes-pack {
|
||||
cursor: pointer;
|
||||
color: skyblue;
|
||||
}
|
||||
|
||||
.cn-nodes-pack:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.cn-pack-badge {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
background-color: var(--comfy-input-bg);
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 3px 8px;
|
||||
color: var(--error-text);
|
||||
}
|
||||
|
||||
.cn-preview {
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
min-height: 120px;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
padding: 12px;
|
||||
color: var(--fg-color);
|
||||
}
|
||||
|
||||
.cn-preview-header {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--comfy-input-bg);
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.cn-preview-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: grey;
|
||||
position: relative;
|
||||
filter: drop-shadow(1px 2px 3px rgb(0 0 0 / 30%));
|
||||
}
|
||||
|
||||
.cn-preview-dot.cn-preview-optional::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: var(--comfy-input-bg);
|
||||
border-radius: 50%;
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.cn-preview-dot.cn-preview-grid {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.cn-preview-dot.cn-preview-grid::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-left: 1px solid var(--comfy-input-bg);
|
||||
border-right: 1px solid var(--comfy-input-bg);
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
left: 2px;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.cn-preview-dot.cn-preview-grid::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-top: 1px solid var(--comfy-input-bg);
|
||||
border-bottom: 1px solid var(--comfy-input-bg);
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
left: 0;
|
||||
top: 2px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.cn-preview-name {
|
||||
flex: auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.cn-preview-io {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 10px;
|
||||
}
|
||||
|
||||
.cn-preview-column > div {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
height: 18px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.cn-preview-input {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.cn-preview-output {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.cn-preview-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
padding: 0 10px 10px 10px;
|
||||
}
|
||||
|
||||
.cn-preview-switch {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--bg-color);
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
text-wrap: nowrap;
|
||||
padding: 2px 20px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cn-preview-switch::before,
|
||||
.cn-preview-switch::after {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
color: var(--fg-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.cn-preview-switch::before {
|
||||
content: "◀";
|
||||
left: 5px;
|
||||
}
|
||||
|
||||
.cn-preview-switch::after {
|
||||
content: "▶";
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
.cn-preview-value {
|
||||
color: var(--descrip-text);
|
||||
}
|
||||
|
||||
.cn-preview-string {
|
||||
min-height: 30px;
|
||||
max-height: 300px;
|
||||
background: var(--bg-color);
|
||||
color: var(--descrip-text);
|
||||
border-radius: 3px;
|
||||
padding: 3px 5px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.cn-preview-description {
|
||||
margin: 0px 10px 10px 10px;
|
||||
padding: 6px;
|
||||
background: var(--border-color);
|
||||
color: var(--descrip-text);
|
||||
border-radius: 5px;
|
||||
font-style: italic;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.cn-tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.cn-tag-list > div {
|
||||
background-color: var(--border-color);
|
||||
border-radius: 5px;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.cn-install-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
padding: 3px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cn-install-buttons button {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.cn-selected-buttons {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.cn-manager .cn-btn-enable {
|
||||
background-color: #333399;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cn-manager .cn-btn-disable {
|
||||
background-color: #442277;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cn-manager .cn-btn-update {
|
||||
background-color: #1155AA;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cn-manager .cn-btn-try-update {
|
||||
background-color: Gray;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cn-manager .cn-btn-try-fix {
|
||||
background-color: #6495ED;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cn-manager .cn-btn-import-failed {
|
||||
background-color: #AA1111;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cn-manager .cn-btn-install {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cn-manager .cn-btn-try-install {
|
||||
background-color: Gray;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cn-manager .cn-btn-uninstall {
|
||||
background-color: #993333;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cn-manager .cn-btn-reinstall {
|
||||
background-color: #993333;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cn-manager .cn-btn-switch {
|
||||
background-color: #448833;
|
||||
color: white;
|
||||
|
||||
}
|
||||
|
||||
@keyframes cn-btn-loading-bg {
|
||||
0% {
|
||||
left: 0;
|
||||
}
|
||||
100% {
|
||||
left: -105px;
|
||||
}
|
||||
}
|
||||
|
||||
.cn-manager button.cn-btn-loading {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-color: rgb(0 119 207 / 80%);
|
||||
background-color: var(--comfy-input-bg);
|
||||
}
|
||||
|
||||
.cn-manager button.cn-btn-loading::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
content: "";
|
||||
width: 500px;
|
||||
height: 100%;
|
||||
background-image: repeating-linear-gradient(
|
||||
-45deg,
|
||||
rgb(0 119 207 / 30%),
|
||||
rgb(0 119 207 / 30%) 10px,
|
||||
transparent 10px,
|
||||
transparent 15px
|
||||
);
|
||||
animation: cn-btn-loading-bg 2s linear infinite;
|
||||
}
|
||||
|
||||
.cn-manager-light .cn-pack-name a {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.cn-manager-light .cm-warn-note {
|
||||
background-color: #ccc !important;
|
||||
}
|
||||
|
||||
.cn-manager-light .cn-btn-install {
|
||||
background-color: #333;
|
||||
}
|
||||
237
js/model-manager.css
Normal file
@ -0,0 +1,237 @@
|
||||
.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: 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 {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.cmm-manager button {
|
||||
font-size: 16px;
|
||||
color: var(--input-text);
|
||||
background-color: var(--comfy-input-bg);
|
||||
border-color: var(--border-color);
|
||||
margin: 0;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.cmm-manager button:hover {
|
||||
filter: brightness(125%);
|
||||
}
|
||||
|
||||
.cmm-manager button:disabled,
|
||||
.cmm-manager input:disabled,
|
||||
.cmm-manager select:disabled {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.cmm-manager button:disabled {
|
||||
background-color: var(--comfy-input-bg);
|
||||
}
|
||||
|
||||
.cmm-manager .cmm-manager-refresh {
|
||||
display: none;
|
||||
background-color: #000080 !important;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cmm-manager .cmm-manager-stop {
|
||||
display: none;
|
||||
background-color: #500000;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cmm-manager-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cmm-manager-header label {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cmm-manager-type,
|
||||
.cmm-manager-base,
|
||||
.cmm-manager-filter {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
|
||||
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 {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.cmm-manager-grid {
|
||||
flex: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cmm-manager-selection {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cmm-manager-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cmm-manager-grid .tg-turbogrid {
|
||||
font-family: var(--grid-font);
|
||||
font-size: 15px;
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.cmm-manager-grid .cmm-node-name a {
|
||||
color: skyblue;
|
||||
text-decoration: none;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.cmm-manager-grid .cmm-node-desc a {
|
||||
color: #5555FF;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.cmm-manager-grid .tg-cell a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.cmm-icon-passed {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
left: calc(50% - 10px);
|
||||
top: calc(50% - 10px);
|
||||
}
|
||||
|
||||
.cmm-manager .cmm-btn-enable {
|
||||
background-color: blue;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cmm-manager .cmm-btn-disable {
|
||||
background-color: MediumSlateBlue;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cmm-manager .cmm-btn-install {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cmm-btn-install {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.cmm-btn-download {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
position: absolute;
|
||||
left: calc(50% - 10px);
|
||||
top: calc(50% - 10px);
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.cmm-btn-download:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.cmm-manager-light .cmm-btn-download {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
@keyframes cmm-btn-loading-bg {
|
||||
0% {
|
||||
left: 0;
|
||||
}
|
||||
100% {
|
||||
left: -105px;
|
||||
}
|
||||
}
|
||||
|
||||
.cmm-manager button.cmm-btn-loading {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-color: rgb(0 119 207 / 80%);
|
||||
background-color: var(--comfy-input-bg);
|
||||
}
|
||||
|
||||
.cmm-manager button.cmm-btn-loading::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
content: "";
|
||||
width: 500px;
|
||||
height: 100%;
|
||||
background-image: repeating-linear-gradient(
|
||||
-45deg,
|
||||
rgb(0 119 207 / 30%),
|
||||
rgb(0 119 207 / 30%) 10px,
|
||||
transparent 10px,
|
||||
transparent 15px
|
||||
);
|
||||
animation: cmm-btn-loading-bg 2s linear infinite;
|
||||
}
|
||||
|
||||
.cmm-manager-light .cmm-node-name a {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.cmm-manager-light .cm-warn-note {
|
||||
background-color: #ccc !important;
|
||||
}
|
||||
|
||||
.cmm-manager-light .cmm-btn-install {
|
||||
background-color: #333;
|
||||
}
|
||||
@ -1,242 +1,30 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { $el } from "../../scripts/ui.js";
|
||||
import {
|
||||
manager_instance, rebootAPI,
|
||||
fetchData, md5, icons
|
||||
import {
|
||||
manager_instance, rebootAPI,
|
||||
fetchData, md5, icons, show_message, customAlert, infoToast, showTerminal,
|
||||
storeColumnWidth, restoreColumnWidth, loadCss, handle403Response
|
||||
} from "./common.js";
|
||||
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";
|
||||
|
||||
const pageCss = `
|
||||
.cmm-manager {
|
||||
--grid-font: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
z-index: 10001;
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
color: var(--fg-color);
|
||||
font-family: arial, sans-serif;
|
||||
}
|
||||
loadCss("./model-manager.css");
|
||||
|
||||
.cmm-manager .cmm-flex-auto {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.cmm-manager button {
|
||||
font-size: 16px;
|
||||
color: var(--input-text);
|
||||
background-color: var(--comfy-input-bg);
|
||||
border-radius: 8px;
|
||||
border-color: var(--border-color);
|
||||
border-style: solid;
|
||||
margin: 0;
|
||||
padding: 4px 8px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.cmm-manager button:disabled,
|
||||
.cmm-manager input:disabled,
|
||||
.cmm-manager select:disabled {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.cmm-manager button:disabled {
|
||||
background-color: var(--comfy-input-bg);
|
||||
}
|
||||
|
||||
.cmm-manager-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.cmm-manager-header label {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cmm-manager-type,
|
||||
.cmm-manager-base,
|
||||
.cmm-manager-filter {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.cmm-manager-keywords {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
padding: 0 5px 0 26px;
|
||||
background-size: 16px;
|
||||
background-position: 5px center;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url("data:image/svg+xml;charset=utf8,${encodeURIComponent(icons.search.replace("currentColor", "#888"))}");
|
||||
}
|
||||
|
||||
.cmm-manager-status {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.cmm-manager-grid {
|
||||
flex: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cmm-manager-selection {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cmm-manager-message {
|
||||
|
||||
}
|
||||
|
||||
.cmm-manager-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cmm-manager-grid .tg-turbogrid {
|
||||
font-family: var(--grid-font);
|
||||
font-size: 15px;
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.cmm-manager-grid .cmm-node-name a {
|
||||
color: skyblue;
|
||||
text-decoration: none;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.cmm-manager-grid .cmm-node-desc a {
|
||||
color: #5555FF;
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.cmm-manager-grid .tg-cell a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.cmm-icon-passed {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
left: calc(50% - 10px);
|
||||
top: calc(50% - 10px);
|
||||
}
|
||||
|
||||
.cmm-manager .cmm-btn-enable {
|
||||
background-color: blue;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cmm-manager .cmm-btn-disable {
|
||||
background-color: MediumSlateBlue;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cmm-manager .cmm-btn-install {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cmm-btn-download {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
position: absolute;
|
||||
left: calc(50% - 10px);
|
||||
top: calc(50% - 10px);
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.cmm-btn-download:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.cmm-manager-light .cmm-btn-download {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
@keyframes cmm-btn-loading-bg {
|
||||
0% {
|
||||
left: 0;
|
||||
}
|
||||
100% {
|
||||
left: -105px;
|
||||
}
|
||||
}
|
||||
|
||||
.cmm-manager button.cmm-btn-loading {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-color: rgb(0 119 207 / 80%);
|
||||
background-color: var(--comfy-input-bg);
|
||||
}
|
||||
|
||||
.cmm-manager button.cmm-btn-loading::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
content: "";
|
||||
width: 500px;
|
||||
height: 100%;
|
||||
background-image: repeating-linear-gradient(
|
||||
-45deg,
|
||||
rgb(0 119 207 / 30%),
|
||||
rgb(0 119 207 / 30%) 10px,
|
||||
transparent 10px,
|
||||
transparent 15px
|
||||
);
|
||||
animation: cmm-btn-loading-bg 2s linear infinite;
|
||||
}
|
||||
|
||||
.cmm-manager-light .cmm-node-name a {
|
||||
color: blue;
|
||||
}
|
||||
|
||||
.cmm-manager-light .cm-warn-note {
|
||||
background-color: #ccc !important;
|
||||
}
|
||||
|
||||
.cmm-manager-light .cmm-btn-install {
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
`;
|
||||
const 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-close">Close</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>
|
||||
`;
|
||||
|
||||
@ -254,22 +42,28 @@ export class ModelManager {
|
||||
this.keywords = '';
|
||||
|
||||
this.init();
|
||||
|
||||
api.addEventListener("cm-queue-status", this.onQueueStatus);
|
||||
}
|
||||
|
||||
init() {
|
||||
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")
|
||||
]);
|
||||
|
||||
if (!document.querySelector(`style[context="${this.id}"]`)) {
|
||||
const $style = document.createElement("style");
|
||||
$style.setAttribute("context", this.id);
|
||||
$style.innerHTML = pageCss;
|
||||
document.head.appendChild($style);
|
||||
}
|
||||
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 = $el("div", {
|
||||
parent: document.body,
|
||||
className: "comfy-modal cmm-manager"
|
||||
});
|
||||
this.element.innerHTML = pageHtml;
|
||||
this.element = frame;
|
||||
this.initFilter();
|
||||
this.bindEvents();
|
||||
this.initGrid();
|
||||
@ -282,10 +76,13 @@ export class ModelManager {
|
||||
value: ""
|
||||
}, {
|
||||
label: "Installed",
|
||||
value: "True"
|
||||
value: "installed"
|
||||
}, {
|
||||
label: "Not Installed",
|
||||
value: "False"
|
||||
value: "not_installed"
|
||||
}, {
|
||||
label: "In Workflow",
|
||||
value: "in_workflow"
|
||||
}];
|
||||
|
||||
this.typeList = [{
|
||||
@ -365,10 +162,25 @@ export class ModelManager {
|
||||
}
|
||||
},
|
||||
|
||||
".cmm-manager-close": {
|
||||
click: (e) => this.close()
|
||||
".cmm-manager-refresh": {
|
||||
click: () => {
|
||||
app.refreshComboInNodes();
|
||||
}
|
||||
},
|
||||
|
||||
".cmm-manager-stop": {
|
||||
click: () => {
|
||||
api.fetchApi('/manager/queue/reset');
|
||||
infoToast('Cancel', 'Remaining tasks will stop after completing the current task.');
|
||||
}
|
||||
},
|
||||
|
||||
".cmm-manager-back": {
|
||||
click: (e) => {
|
||||
this.close()
|
||||
manager_instance.show();
|
||||
}
|
||||
}
|
||||
};
|
||||
Object.keys(eventsMap).forEach(selector => {
|
||||
const target = this.element.querySelector(selector);
|
||||
@ -400,6 +212,10 @@ export class ModelManager {
|
||||
this.renderSelected();
|
||||
});
|
||||
|
||||
grid.bind("onColumnWidthChanged", (e, columnItem) => {
|
||||
storeColumnWidth(gridId, columnItem)
|
||||
});
|
||||
|
||||
grid.bind('onClick', (e, d) => {
|
||||
const { rowItem } = d;
|
||||
const target = d.e.target;
|
||||
@ -436,12 +252,31 @@ export class ModelManager {
|
||||
rowFilter: (rowItem) => {
|
||||
|
||||
const searchableColumns = ["name", "type", "base", "description", "filename", "save_path"];
|
||||
const models_extensions = ['.ckpt', '.pt', '.pt2', '.bin', '.pth', '.safetensors', '.pkl', '.sft'];
|
||||
|
||||
let shouldShown = grid.highlightKeywordsFilter(rowItem, searchableColumns, this.keywords);
|
||||
|
||||
if (shouldShown) {
|
||||
if(this.filter && rowItem.installed !== this.filter) {
|
||||
return false;
|
||||
if(this.filter) {
|
||||
if (this.filter == "in_workflow") {
|
||||
rowItem.in_workflow = null;
|
||||
if (Array.isArray(app.graph._nodes)) {
|
||||
app.graph._nodes.forEach((item, i) => {
|
||||
if (Array.isArray(item.widgets_values)) {
|
||||
item.widgets_values.forEach((_item, i) => {
|
||||
if (rowItem.in_workflow === null && _item !== null && models_extensions.includes("." + _item.toString().split('.').pop())) {
|
||||
let filename = _item.match(/([^\/]+)(?=\.\w+$)/)[0];
|
||||
if (grid.highlightKeywordsFilter(rowItem, searchableColumns, filename)) {
|
||||
rowItem.in_workflow = "True";
|
||||
grid.highlightKeywordsFilter(rowItem, searchableColumns, "");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return ((this.filter == "installed" && rowItem.installed == "True") || (this.filter == "not_installed" && rowItem.installed == "False") || (this.filter == "in_workflow" && rowItem.in_workflow == "True"));
|
||||
}
|
||||
|
||||
if(this.type && rowItem.type !== this.type) {
|
||||
@ -507,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',
|
||||
@ -516,7 +351,7 @@ export class ModelManager {
|
||||
sortable: false,
|
||||
align: 'center',
|
||||
formatter: (url, rowItem, columnItem) => {
|
||||
return `<a class="cmm-btn-download" title="Download file" href="${url}" target="_blank">${icons.download}</a>`;
|
||||
return `<a class="cmm-btn-download" tooltip="Download file" href="${url}" target="_blank">${icons.download}</a>`;
|
||||
}
|
||||
}, {
|
||||
id: 'size',
|
||||
@ -551,6 +386,8 @@ export class ModelManager {
|
||||
width: 200
|
||||
}];
|
||||
|
||||
restoreColumnWidth(gridId, columns);
|
||||
|
||||
this.grid.setData({
|
||||
options,
|
||||
rows,
|
||||
@ -578,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) {
|
||||
@ -593,17 +430,27 @@ export class ModelManager {
|
||||
}
|
||||
|
||||
async installModels(list, btn) {
|
||||
|
||||
let stats = await api.fetchApi('/manager/queue/status');
|
||||
|
||||
stats = await stats.json();
|
||||
if(stats.is_processing) {
|
||||
customAlert(`[ComfyUI-Manager] There are already tasks in progress. Please try again after it is completed. (${stats.done_count}/${stats.total_count})`);
|
||||
return;
|
||||
}
|
||||
|
||||
btn.classList.add("cmm-btn-loading");
|
||||
this.showLoading();
|
||||
this.showError("");
|
||||
|
||||
let needRestart = false;
|
||||
let needRefresh = false;
|
||||
let errorMsg = "";
|
||||
|
||||
await api.fetchApi('/manager/queue/reset');
|
||||
|
||||
let target_items = [];
|
||||
|
||||
for (const item of list) {
|
||||
|
||||
this.grid.scrollRowIntoView(item);
|
||||
target_items.push(item);
|
||||
|
||||
if (!this.focusInstall(item)) {
|
||||
this.grid.onNextUpdated(() => {
|
||||
@ -614,48 +461,121 @@ export class ModelManager {
|
||||
this.showStatus(`Install ${item.name} ...`);
|
||||
|
||||
const data = item.originalData;
|
||||
const res = await fetchData('/model/install', {
|
||||
data.ui_id = item.hash;
|
||||
|
||||
const res = await api.fetchApi(`/manager/queue/install_model`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (res.status != 200) {
|
||||
errorMsg = `'${item.name}': `;
|
||||
|
||||
if (res.error) {
|
||||
errorMsg = `Install failed: ${item.name} ${res.error.message}`;
|
||||
break;;
|
||||
if(res.status == 403) {
|
||||
try {
|
||||
const data = await res.json();
|
||||
if(data.error === 'comfyui_outdated') {
|
||||
errorMsg += `ComfyUI version is outdated. Please update ComfyUI to use Manager normally.\n`;
|
||||
} else {
|
||||
errorMsg += `This action is not allowed with this security level configuration.\n`;
|
||||
}
|
||||
} catch {
|
||||
errorMsg += `This action is not allowed with this security level configuration.\n`;
|
||||
}
|
||||
} else {
|
||||
errorMsg += await res.text() + '\n';
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
needRestart = true;
|
||||
this.install_context = {btn: btn, targets: target_items};
|
||||
|
||||
this.grid.setRowSelected(item, false);
|
||||
if(errorMsg) {
|
||||
this.showError(errorMsg);
|
||||
show_message("[Installation Errors]\n"+errorMsg);
|
||||
|
||||
// reset
|
||||
for(let k in target_items) {
|
||||
const item = target_items[k];
|
||||
this.grid.updateCell(item, "installed");
|
||||
}
|
||||
}
|
||||
else {
|
||||
await api.fetchApi('/manager/queue/start');
|
||||
this.showStop();
|
||||
showTerminal();
|
||||
}
|
||||
}
|
||||
|
||||
async onQueueStatus(event) {
|
||||
let self = ModelManager.instance;
|
||||
|
||||
if(event.detail.status == 'in_progress' && event.detail.ui_target == 'model_manager') {
|
||||
const hash = event.detail.target;
|
||||
|
||||
const item = self.grid.getRowItemBy("hash", hash);
|
||||
|
||||
item.refresh = true;
|
||||
self.grid.setRowSelected(item, false);
|
||||
item.selectable = false;
|
||||
this.grid.updateCell(item, "installed");
|
||||
this.grid.updateCell(item, "tg-column-select");
|
||||
// self.grid.updateCell(item, "tg-column-select");
|
||||
self.grid.updateRow(item);
|
||||
}
|
||||
else if(event.detail.status == 'done') {
|
||||
self.hideStop();
|
||||
self.onQueueCompleted(event.detail);
|
||||
}
|
||||
}
|
||||
|
||||
this.showStatus(`Install ${item.name} successfully`);
|
||||
async onQueueCompleted(info) {
|
||||
let result = info.model_result;
|
||||
|
||||
if(result.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hideLoading();
|
||||
let self = ModelManager.instance;
|
||||
|
||||
if(!self.install_context) {
|
||||
return;
|
||||
}
|
||||
|
||||
let btn = self.install_context.btn;
|
||||
|
||||
self.hideLoading();
|
||||
btn.classList.remove("cmm-btn-loading");
|
||||
|
||||
let errorMsg = "";
|
||||
|
||||
for(let hash in result){
|
||||
let v = result[hash];
|
||||
|
||||
if(v != 'success')
|
||||
errorMsg += v + '\n';
|
||||
}
|
||||
|
||||
for(let k in self.install_context.targets) {
|
||||
let item = self.install_context.targets[k];
|
||||
self.grid.updateCell(item, "installed");
|
||||
}
|
||||
|
||||
if (errorMsg) {
|
||||
this.showError(errorMsg);
|
||||
self.showError(errorMsg);
|
||||
show_message("Installation Error:\n"+errorMsg);
|
||||
} else {
|
||||
this.showStatus(`Install ${list.length} models successfully`);
|
||||
self.showStatus(`Install ${result.length} models successfully`);
|
||||
}
|
||||
|
||||
if (needRestart) {
|
||||
this.showMessage(`To apply the installed model, please click the 'Refresh' button on the main menu.`, "red")
|
||||
}
|
||||
self.showRefresh();
|
||||
self.showMessage(`To apply the installed model, please click the 'Refresh' button.`, "red")
|
||||
|
||||
infoToast('Tasks done', `[ComfyUI-Manager] All model downloading tasks in the queue have been completed.\n${info.done_count}/${info.total_count}`);
|
||||
self.install_context = undefined;
|
||||
}
|
||||
|
||||
getModelList(models) {
|
||||
|
||||
const typeMap = new Map();
|
||||
const baseMap = new Map();
|
||||
|
||||
@ -824,7 +744,7 @@ export class ModelManager {
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
this.setDisabled(true);
|
||||
// this.setDisabled(true);
|
||||
if (this.grid) {
|
||||
this.grid.showLoading();
|
||||
this.grid.showMask({
|
||||
@ -834,7 +754,7 @@ export class ModelManager {
|
||||
}
|
||||
|
||||
hideLoading() {
|
||||
this.setDisabled(false);
|
||||
// this.setDisabled(false);
|
||||
if (this.grid) {
|
||||
this.grid.hideLoading();
|
||||
this.grid.hideMask();
|
||||
@ -842,8 +762,9 @@ export class ModelManager {
|
||||
}
|
||||
|
||||
setDisabled(disabled) {
|
||||
|
||||
const $close = this.element.querySelector(".cmm-manager-close");
|
||||
const $refresh = this.element.querySelector(".cmm-manager-refresh");
|
||||
const $stop = this.element.querySelector(".cmm-manager-stop");
|
||||
|
||||
const list = [
|
||||
".cmm-manager-header input",
|
||||
@ -855,7 +776,7 @@ export class ModelManager {
|
||||
})
|
||||
.flat()
|
||||
.filter(it => {
|
||||
return it !== $close;
|
||||
return it !== $close && it !== $refresh && it !== $stop;
|
||||
});
|
||||
|
||||
list.forEach($elem => {
|
||||
@ -872,6 +793,18 @@ export class ModelManager {
|
||||
|
||||
}
|
||||
|
||||
showRefresh() {
|
||||
this.element.querySelector(".cmm-manager-refresh").style.display = "block";
|
||||
}
|
||||
|
||||
showStop() {
|
||||
this.element.querySelector(".cmm-manager-stop").style.display = "block";
|
||||
}
|
||||
|
||||
hideStop() {
|
||||
this.element.querySelector(".cmm-manager-stop").style.display = "none";
|
||||
}
|
||||
|
||||
setKeywords(keywords = "") {
|
||||
this.keywords = keywords;
|
||||
this.element.querySelector(".cmm-manager-keywords").value = keywords;
|
||||
@ -888,4 +821,4 @@ export class ModelManager {
|
||||
close() {
|
||||
this.element.style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +1,6 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
let double_click_policy = "copy-all";
|
||||
|
||||
api.fetchApi('/manager/dbl_click/policy')
|
||||
.then(response => response.text())
|
||||
.then(data => set_double_click_policy(data));
|
||||
|
||||
export function set_double_click_policy(mode) {
|
||||
double_click_policy = mode;
|
||||
}
|
||||
|
||||
function addMenuHandler(nodeType, cb) {
|
||||
const getOpts = nodeType.prototype.getExtraMenuOptions;
|
||||
nodeType.prototype.getExtraMenuOptions = function () {
|
||||
@ -101,7 +91,7 @@ function connect_inputs(nearest_inputs, node) {
|
||||
}
|
||||
}
|
||||
|
||||
function node_info_copy(src, dest, connect_both) {
|
||||
function node_info_copy(src, dest, connect_both, copy_shape) {
|
||||
// copy input connections
|
||||
for(let i in src.inputs) {
|
||||
let input = src.inputs[i];
|
||||
@ -142,66 +132,17 @@ function node_info_copy(src, dest, connect_both) {
|
||||
}
|
||||
}
|
||||
|
||||
dest.color = src.color;
|
||||
dest.bgcolor = src.bgcolor;
|
||||
dest.size = src.size;
|
||||
if(copy_shape) {
|
||||
dest.color = src.color;
|
||||
dest.bgcolor = src.bgcolor;
|
||||
dest.size = max(src.size, dest.size);
|
||||
}
|
||||
|
||||
app.graph.afterChange();
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.Manager.NodeFixer",
|
||||
|
||||
async nodeCreated(node, app) {
|
||||
let orig_dblClick = node.onDblClick;
|
||||
node.onDblClick = function (e, pos, self) {
|
||||
orig_dblClick?.apply?.(this, arguments);
|
||||
|
||||
if((!node.inputs && !node.outputs) || pos[1] > 0)
|
||||
return;
|
||||
|
||||
switch(double_click_policy) {
|
||||
case "copy-all":
|
||||
case "copy-input":
|
||||
{
|
||||
if(node.inputs?.some(x => x.link != null) || node.outputs?.some(x => x.links != null && x.links.length > 0) )
|
||||
return;
|
||||
|
||||
let src_node = lookup_nearest_nodes(node);
|
||||
if(src_node)
|
||||
node_info_copy(src_node, node, double_click_policy == "copy-all");
|
||||
}
|
||||
break;
|
||||
case "possible-input":
|
||||
{
|
||||
let nearest_inputs = lookup_nearest_inputs(node);
|
||||
if(nearest_inputs)
|
||||
connect_inputs(nearest_inputs, node);
|
||||
}
|
||||
break;
|
||||
case "dual":
|
||||
{
|
||||
if(pos[0] < node.size[0]/2) {
|
||||
// left: possible-input
|
||||
let nearest_inputs = lookup_nearest_inputs(node);
|
||||
if(nearest_inputs)
|
||||
connect_inputs(nearest_inputs, node);
|
||||
}
|
||||
else {
|
||||
// right: copy-all
|
||||
if(node.inputs?.some(x => x.link != null) || node.outputs?.some(x => x.links != null && x.links.length > 0) )
|
||||
return;
|
||||
|
||||
let src_node = lookup_nearest_nodes(node);
|
||||
if(src_node)
|
||||
node_info_copy(src_node, node, true);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
addMenuHandler(nodeType, function (_, options) {
|
||||
options.push({
|
||||
@ -212,6 +153,7 @@ app.registerExtension({
|
||||
app.canvas.graph.add(new_node, false);
|
||||
node_info_copy(this, new_node, true);
|
||||
app.canvas.graph.remove(this);
|
||||
requestAnimationFrame(() => app.canvas.setDirty(true, true))
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
619
js/popover-helper.js
Normal file
@ -0,0 +1,619 @@
|
||||
const hasOwn = function(obj, key) {
|
||||
return Object.prototype.hasOwnProperty.call(obj, key);
|
||||
};
|
||||
|
||||
const isNum = function(num) {
|
||||
if (typeof num !== 'number' || isNaN(num)) {
|
||||
return false;
|
||||
}
|
||||
const isInvalid = function(n) {
|
||||
if (n === Number.MAX_VALUE || n === Number.MIN_VALUE || n === Number.NEGATIVE_INFINITY || n === Number.POSITIVE_INFINITY) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
if (isInvalid(num)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const toNum = (num) => {
|
||||
if (typeof (num) !== 'number') {
|
||||
num = parseFloat(num);
|
||||
}
|
||||
if (isNaN(num)) {
|
||||
num = 0;
|
||||
}
|
||||
num = Math.round(num);
|
||||
return num;
|
||||
};
|
||||
|
||||
const clamp = function(value, min, max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
};
|
||||
|
||||
const isWindow = (obj) => {
|
||||
return Boolean(obj && obj === obj.window);
|
||||
};
|
||||
|
||||
const isDocument = (obj) => {
|
||||
return Boolean(obj && obj.nodeType === 9);
|
||||
};
|
||||
|
||||
const isElement = (obj) => {
|
||||
return Boolean(obj && obj.nodeType === 1);
|
||||
};
|
||||
|
||||
// ===========================================================================================
|
||||
|
||||
export const toRect = (obj) => {
|
||||
if (obj) {
|
||||
return {
|
||||
left: toNum(obj.left || obj.x),
|
||||
top: toNum(obj.top || obj.y),
|
||||
width: toNum(obj.width),
|
||||
height: toNum(obj.height)
|
||||
};
|
||||
}
|
||||
return {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
};
|
||||
};
|
||||
|
||||
export const getElement = (selector) => {
|
||||
if (typeof selector === 'string' && selector) {
|
||||
if (selector.startsWith('#')) {
|
||||
return document.getElementById(selector.slice(1));
|
||||
}
|
||||
return document.querySelector(selector);
|
||||
}
|
||||
|
||||
if (isDocument(selector)) {
|
||||
return selector.body;
|
||||
}
|
||||
if (isElement(selector)) {
|
||||
return selector;
|
||||
}
|
||||
};
|
||||
|
||||
export const getRect = (target, fixed) => {
|
||||
if (!target) {
|
||||
return toRect();
|
||||
}
|
||||
|
||||
if (isWindow(target)) {
|
||||
return {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
};
|
||||
}
|
||||
|
||||
const elem = getElement(target);
|
||||
if (!elem) {
|
||||
return toRect(target);
|
||||
}
|
||||
|
||||
const br = elem.getBoundingClientRect();
|
||||
const rect = toRect(br);
|
||||
|
||||
// fix offset
|
||||
if (!fixed) {
|
||||
rect.left += window.scrollX;
|
||||
rect.top += window.scrollY;
|
||||
}
|
||||
|
||||
rect.width = elem.offsetWidth;
|
||||
rect.height = elem.offsetHeight;
|
||||
|
||||
return rect;
|
||||
};
|
||||
|
||||
// ===========================================================================================
|
||||
|
||||
const calculators = {
|
||||
|
||||
bottom: (info, containerRect, targetRect) => {
|
||||
info.space = containerRect.top + containerRect.height - targetRect.top - targetRect.height - info.height;
|
||||
info.top = targetRect.top + targetRect.height;
|
||||
info.left = Math.round(targetRect.left + targetRect.width * 0.5 - info.width * 0.5);
|
||||
},
|
||||
|
||||
top: (info, containerRect, targetRect) => {
|
||||
info.space = targetRect.top - info.height - containerRect.top;
|
||||
info.top = targetRect.top - info.height;
|
||||
info.left = Math.round(targetRect.left + targetRect.width * 0.5 - info.width * 0.5);
|
||||
},
|
||||
|
||||
right: (info, containerRect, targetRect) => {
|
||||
info.space = containerRect.left + containerRect.width - targetRect.left - targetRect.width - info.width;
|
||||
info.top = Math.round(targetRect.top + targetRect.height * 0.5 - info.height * 0.5);
|
||||
info.left = targetRect.left + targetRect.width;
|
||||
},
|
||||
|
||||
left: (info, containerRect, targetRect) => {
|
||||
info.space = targetRect.left - info.width - containerRect.left;
|
||||
info.top = Math.round(targetRect.top + targetRect.height * 0.5 - info.height * 0.5);
|
||||
info.left = targetRect.left - info.width;
|
||||
}
|
||||
};
|
||||
|
||||
// with order
|
||||
export const getDefaultPositions = () => {
|
||||
return Object.keys(calculators);
|
||||
};
|
||||
|
||||
const calculateSpace = (info, containerRect, targetRect) => {
|
||||
const calculator = calculators[info.position];
|
||||
calculator(info, containerRect, targetRect);
|
||||
if (info.space >= 0) {
|
||||
info.passed += 1;
|
||||
}
|
||||
};
|
||||
|
||||
// ===========================================================================================
|
||||
|
||||
const calculateAlignOffset = (info, containerRect, targetRect, alignType, sizeType) => {
|
||||
|
||||
const popoverStart = info[alignType];
|
||||
const popoverSize = info[sizeType];
|
||||
|
||||
const containerStart = containerRect[alignType];
|
||||
const containerSize = containerRect[sizeType];
|
||||
|
||||
const targetStart = targetRect[alignType];
|
||||
const targetSize = targetRect[sizeType];
|
||||
|
||||
const targetCenter = targetStart + targetSize * 0.5;
|
||||
|
||||
// size overflow
|
||||
if (popoverSize > containerSize) {
|
||||
const overflow = (popoverSize - containerSize) * 0.5;
|
||||
info[alignType] = containerStart - overflow;
|
||||
info.offset = targetCenter - containerStart + overflow;
|
||||
return;
|
||||
}
|
||||
|
||||
const space1 = popoverStart - containerStart;
|
||||
const space2 = (containerStart + containerSize) - (popoverStart + popoverSize);
|
||||
|
||||
// both side passed, default to center
|
||||
if (space1 >= 0 && space2 >= 0) {
|
||||
if (info.passed) {
|
||||
info.passed += 2;
|
||||
}
|
||||
info.offset = popoverSize * 0.5;
|
||||
return;
|
||||
}
|
||||
|
||||
// one side passed
|
||||
if (info.passed) {
|
||||
info.passed += 1;
|
||||
}
|
||||
|
||||
if (space1 < 0) {
|
||||
const min = containerStart;
|
||||
info[alignType] = min;
|
||||
info.offset = targetCenter - min;
|
||||
return;
|
||||
}
|
||||
|
||||
// space2 < 0
|
||||
const max = containerStart + containerSize - popoverSize;
|
||||
info[alignType] = max;
|
||||
info.offset = targetCenter - max;
|
||||
|
||||
};
|
||||
|
||||
const calculateHV = (info, containerRect) => {
|
||||
if (['top', 'bottom'].includes(info.position)) {
|
||||
info.top = clamp(info.top, containerRect.top, containerRect.top + containerRect.height - info.height);
|
||||
return ['left', 'width'];
|
||||
}
|
||||
info.left = clamp(info.left, containerRect.left, containerRect.left + containerRect.width - info.width);
|
||||
return ['top', 'height'];
|
||||
};
|
||||
|
||||
const calculateOffset = (info, containerRect, targetRect) => {
|
||||
|
||||
const [alignType, sizeType] = calculateHV(info, containerRect);
|
||||
|
||||
calculateAlignOffset(info, containerRect, targetRect, alignType, sizeType);
|
||||
|
||||
info.offset = clamp(info.offset, 0, info[sizeType]);
|
||||
|
||||
};
|
||||
|
||||
// ===========================================================================================
|
||||
|
||||
const calculateDistance = (info, previousPositionInfo) => {
|
||||
if (!previousPositionInfo) {
|
||||
return;
|
||||
}
|
||||
// no change if position no change with previous
|
||||
if (info.position === previousPositionInfo.position) {
|
||||
return;
|
||||
}
|
||||
const ax = info.left + info.width * 0.5;
|
||||
const ay = info.top + info.height * 0.5;
|
||||
const bx = previousPositionInfo.left + previousPositionInfo.width * 0.5;
|
||||
const by = previousPositionInfo.top + previousPositionInfo.height * 0.5;
|
||||
const dx = Math.abs(ax - bx);
|
||||
const dy = Math.abs(ay - by);
|
||||
info.distance = Math.round(Math.sqrt(dx * dx + dy * dy));
|
||||
};
|
||||
|
||||
// ===========================================================================================
|
||||
|
||||
const calculatePositionInfo = (info, containerRect, targetRect, previousPositionInfo) => {
|
||||
calculateSpace(info, containerRect, targetRect);
|
||||
calculateOffset(info, containerRect, targetRect);
|
||||
calculateDistance(info, previousPositionInfo);
|
||||
};
|
||||
|
||||
// ===========================================================================================
|
||||
|
||||
const calculateBestPosition = (containerRect, targetRect, infoMap, withOrder, previousPositionInfo) => {
|
||||
|
||||
// position space: +1
|
||||
// align space:
|
||||
// two side passed: +2
|
||||
// one side passed: +1
|
||||
|
||||
const safePassed = 3;
|
||||
|
||||
if (previousPositionInfo) {
|
||||
const prevInfo = infoMap[previousPositionInfo.position];
|
||||
if (prevInfo) {
|
||||
calculatePositionInfo(prevInfo, containerRect, targetRect);
|
||||
if (prevInfo.passed >= safePassed) {
|
||||
return prevInfo;
|
||||
}
|
||||
prevInfo.calculated = true;
|
||||
}
|
||||
}
|
||||
|
||||
const positionList = [];
|
||||
Object.values(infoMap).forEach((info) => {
|
||||
if (!info.calculated) {
|
||||
calculatePositionInfo(info, containerRect, targetRect, previousPositionInfo);
|
||||
}
|
||||
positionList.push(info);
|
||||
});
|
||||
|
||||
positionList.sort((a, b) => {
|
||||
if (a.passed !== b.passed) {
|
||||
return b.passed - a.passed;
|
||||
}
|
||||
|
||||
if (withOrder && a.passed >= safePassed && b.passed >= safePassed) {
|
||||
return a.index - b.index;
|
||||
}
|
||||
|
||||
if (a.space !== b.space) {
|
||||
return b.space - a.space;
|
||||
}
|
||||
|
||||
return a.index - b.index;
|
||||
});
|
||||
|
||||
// logTable(positionList);
|
||||
|
||||
return positionList[0];
|
||||
};
|
||||
|
||||
// const logTable = (() => {
|
||||
// let time_id;
|
||||
// return (info) => {
|
||||
// clearTimeout(time_id);
|
||||
// time_id = setTimeout(() => {
|
||||
// console.table(info);
|
||||
// }, 10);
|
||||
// };
|
||||
// })();
|
||||
|
||||
// ===========================================================================================
|
||||
|
||||
const getAllowPositions = (positions, defaultAllowPositions) => {
|
||||
if (!positions) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(positions)) {
|
||||
positions = positions.join(',');
|
||||
}
|
||||
positions = String(positions).split(',').map((it) => it.trim().toLowerCase()).filter((it) => it);
|
||||
positions = positions.filter((it) => defaultAllowPositions.includes(it));
|
||||
if (!positions.length) {
|
||||
return;
|
||||
}
|
||||
return positions;
|
||||
};
|
||||
|
||||
const isPositionChanged = (info, previousPositionInfo) => {
|
||||
if (!previousPositionInfo) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (info.left !== previousPositionInfo.left) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (info.top !== previousPositionInfo.top) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// ===========================================================================================
|
||||
|
||||
// const log = (name, time) => {
|
||||
// if (time > 0.1) {
|
||||
// console.log(name, time);
|
||||
// }
|
||||
// };
|
||||
|
||||
export const getBestPosition = (containerRect, targetRect, popoverRect, positions, previousPositionInfo) => {
|
||||
|
||||
const defaultAllowPositions = getDefaultPositions();
|
||||
let withOrder = true;
|
||||
let allowPositions = getAllowPositions(positions, defaultAllowPositions);
|
||||
if (!allowPositions) {
|
||||
allowPositions = defaultAllowPositions;
|
||||
withOrder = false;
|
||||
}
|
||||
|
||||
// console.log('withOrder', withOrder);
|
||||
|
||||
// const start_time = performance.now();
|
||||
|
||||
const infoMap = {};
|
||||
allowPositions.forEach((k, i) => {
|
||||
infoMap[k] = {
|
||||
position: k,
|
||||
index: i,
|
||||
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: popoverRect.width,
|
||||
height: popoverRect.height,
|
||||
|
||||
space: 0,
|
||||
|
||||
offset: 0,
|
||||
passed: 0,
|
||||
|
||||
distance: 0
|
||||
};
|
||||
});
|
||||
|
||||
// log('infoMap', performance.now() - start_time);
|
||||
|
||||
|
||||
const bestPosition = calculateBestPosition(containerRect, targetRect, infoMap, withOrder, previousPositionInfo);
|
||||
|
||||
// check left/top
|
||||
bestPosition.changed = isPositionChanged(bestPosition, previousPositionInfo);
|
||||
|
||||
return bestPosition;
|
||||
};
|
||||
|
||||
// ===========================================================================================
|
||||
|
||||
const getTemplatePath = (width, height, arrowOffset, arrowSize, borderRadius) => {
|
||||
const p = (px, py) => {
|
||||
return [px, py].join(',');
|
||||
};
|
||||
|
||||
const px = function(num, alignEnd) {
|
||||
const floor = Math.floor(num);
|
||||
let n = num < floor + 0.5 ? floor + 0.5 : floor + 1.5;
|
||||
if (alignEnd) {
|
||||
n -= 1;
|
||||
}
|
||||
return n;
|
||||
};
|
||||
|
||||
const pxe = function(num) {
|
||||
return px(num, true);
|
||||
};
|
||||
|
||||
const ls = [];
|
||||
|
||||
const innerLeft = px(arrowSize);
|
||||
const innerRight = pxe(width - arrowSize);
|
||||
arrowOffset = clamp(arrowOffset, innerLeft, innerRight);
|
||||
|
||||
const innerTop = px(arrowSize);
|
||||
const innerBottom = pxe(height - arrowSize);
|
||||
|
||||
const startPoint = p(innerLeft, innerTop + borderRadius);
|
||||
const arrowPoint = p(arrowOffset, 1);
|
||||
|
||||
const LT = p(innerLeft, innerTop);
|
||||
const RT = p(innerRight, innerTop);
|
||||
|
||||
const AOT = p(arrowOffset - arrowSize, innerTop);
|
||||
const RRT = p(innerRight - borderRadius, innerTop);
|
||||
|
||||
ls.push(`M${startPoint}`);
|
||||
ls.push(`V${innerBottom - borderRadius}`);
|
||||
ls.push(`Q${p(innerLeft, innerBottom)} ${p(innerLeft + borderRadius, innerBottom)}`);
|
||||
ls.push(`H${innerRight - borderRadius}`);
|
||||
ls.push(`Q${p(innerRight, innerBottom)} ${p(innerRight, innerBottom - borderRadius)}`);
|
||||
ls.push(`V${innerTop + borderRadius}`);
|
||||
|
||||
if (arrowOffset < innerLeft + arrowSize + borderRadius) {
|
||||
ls.push(`Q${RT} ${RRT}`);
|
||||
ls.push(`H${arrowOffset + arrowSize}`);
|
||||
ls.push(`L${arrowPoint}`);
|
||||
if (arrowOffset < innerLeft + arrowSize) {
|
||||
ls.push(`L${LT}`);
|
||||
ls.push(`L${startPoint}`);
|
||||
} else {
|
||||
ls.push(`L${AOT}`);
|
||||
ls.push(`Q${LT} ${startPoint}`);
|
||||
}
|
||||
} else if (arrowOffset > innerRight - arrowSize - borderRadius) {
|
||||
if (arrowOffset > innerRight - arrowSize) {
|
||||
ls.push(`L${RT}`);
|
||||
} else {
|
||||
ls.push(`Q${RT} ${p(arrowOffset + arrowSize, innerTop)}`);
|
||||
}
|
||||
ls.push(`L${arrowPoint}`);
|
||||
ls.push(`L${AOT}`);
|
||||
ls.push(`H${innerLeft + borderRadius}`);
|
||||
ls.push(`Q${LT} ${startPoint}`);
|
||||
} else {
|
||||
ls.push(`Q${RT} ${RRT}`);
|
||||
ls.push(`H${arrowOffset + arrowSize}`);
|
||||
ls.push(`L${arrowPoint}`);
|
||||
ls.push(`L${AOT}`);
|
||||
ls.push(`H${innerLeft + borderRadius}`);
|
||||
ls.push(`Q${LT} ${startPoint}`);
|
||||
}
|
||||
return ls.join('');
|
||||
};
|
||||
|
||||
const getPathData = function(position, width, height, arrowOffset, arrowSize, borderRadius) {
|
||||
|
||||
const handlers = {
|
||||
|
||||
bottom: () => {
|
||||
const d = getTemplatePath(width, height, arrowOffset, arrowSize, borderRadius);
|
||||
return {
|
||||
d,
|
||||
transform: ''
|
||||
};
|
||||
},
|
||||
|
||||
top: () => {
|
||||
const d = getTemplatePath(width, height, width - arrowOffset, arrowSize, borderRadius);
|
||||
return {
|
||||
d,
|
||||
transform: `rotate(180,${width * 0.5},${height * 0.5})`
|
||||
};
|
||||
},
|
||||
|
||||
left: () => {
|
||||
const d = getTemplatePath(height, width, arrowOffset, arrowSize, borderRadius);
|
||||
const x = (width - height) * 0.5;
|
||||
const y = (height - width) * 0.5;
|
||||
return {
|
||||
d,
|
||||
transform: `translate(${x} ${y}) rotate(90,${height * 0.5},${width * 0.5})`
|
||||
};
|
||||
},
|
||||
|
||||
right: () => {
|
||||
const d = getTemplatePath(height, width, height - arrowOffset, arrowSize, borderRadius);
|
||||
const x = (width - height) * 0.5;
|
||||
const y = (height - width) * 0.5;
|
||||
return {
|
||||
d,
|
||||
transform: `translate(${x} ${y}) rotate(-90,${height * 0.5},${width * 0.5})`
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return handlers[position]();
|
||||
};
|
||||
|
||||
// ===========================================================================================
|
||||
|
||||
// position style cache
|
||||
const styleCache = {
|
||||
// position: '',
|
||||
// top: {},
|
||||
// bottom: {},
|
||||
// left: {},
|
||||
// right: {}
|
||||
};
|
||||
|
||||
export const getPositionStyle = (info, options = {}) => {
|
||||
|
||||
const o = {
|
||||
bgColor: '#fff',
|
||||
borderColor: '#ccc',
|
||||
borderRadius: 5,
|
||||
arrowSize: 10
|
||||
};
|
||||
Object.keys(o).forEach((k) => {
|
||||
|
||||
if (hasOwn(options, k)) {
|
||||
const d = o[k];
|
||||
const v = options[k];
|
||||
|
||||
if (typeof d === 'string') {
|
||||
// string
|
||||
if (typeof v === 'string' && v) {
|
||||
o[k] = v;
|
||||
}
|
||||
} else {
|
||||
// number
|
||||
if (isNum(v) && v >= 0) {
|
||||
o[k] = v;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
const key = [
|
||||
info.width,
|
||||
info.height,
|
||||
info.offset,
|
||||
o.arrowSize,
|
||||
o.borderRadius,
|
||||
o.bgColor,
|
||||
o.borderColor
|
||||
].join('-');
|
||||
|
||||
const positionCache = styleCache[info.position];
|
||||
if (positionCache && key === positionCache.key) {
|
||||
const st = positionCache.style;
|
||||
st.changed = styleCache.position !== info.position;
|
||||
styleCache.position = info.position;
|
||||
return st;
|
||||
}
|
||||
|
||||
// console.log(options);
|
||||
|
||||
const data = getPathData(info.position, info.width, info.height, info.offset, o.arrowSize, o.borderRadius);
|
||||
// console.log(data);
|
||||
|
||||
const viewBox = [0, 0, info.width, info.height].join(' ');
|
||||
const svg = [
|
||||
`<svg viewBox="${viewBox}" xmlns="http://www.w3.org/2000/svg">`,
|
||||
`<path d="${data.d}" fill="${o.bgColor}" stroke="${o.borderColor}" transform="${data.transform}" />`,
|
||||
'</svg>'
|
||||
].join('');
|
||||
|
||||
// console.log(svg);
|
||||
const backgroundImage = `url("data:image/svg+xml;charset=utf8,${encodeURIComponent(svg)}")`;
|
||||
|
||||
const background = `${backgroundImage} center no-repeat`;
|
||||
|
||||
const padding = `${o.arrowSize + o.borderRadius}px`;
|
||||
|
||||
const style = {
|
||||
background,
|
||||
backgroundImage,
|
||||
padding,
|
||||
changed: true
|
||||
};
|
||||
|
||||
styleCache.position = info.position;
|
||||
styleCache[info.position] = {
|
||||
key,
|
||||
style
|
||||
};
|
||||
|
||||
return style;
|
||||
};
|
||||
65
js/snapshot.css
Normal file
@ -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, handle403Response, loadCss } from "./common.js";
|
||||
import { buildGuiFrame } from "./comfyui-gui-builder.js";
|
||||
|
||||
loadCss("./snapshot.css");
|
||||
|
||||
async function restore_snapshot(target) {
|
||||
if(SnapshotManager.instance) {
|
||||
@ -10,7 +12,7 @@ async function restore_snapshot(target) {
|
||||
const response = await api.fetchApi(`/snapshot/restore?target=${target}`, { cache: "no-store" });
|
||||
|
||||
if(response.status == 403) {
|
||||
show_message('This action is not allowed with this security level configuration.');
|
||||
await handle403Response(response);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -38,7 +40,7 @@ async function remove_snapshot(target) {
|
||||
const response = await api.fetchApi(`/snapshot/remove?target=${target}`, { cache: "no-store" });
|
||||
|
||||
if(response.status == 403) {
|
||||
show_message('This action is not allowed with this security level configuration.');
|
||||
await handle403Response(response);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
@ -145,8 +158,8 @@ export class SnapshotManager extends ComfyDialog {
|
||||
if(btn_id) {
|
||||
const rebootButton = document.getElementById(btn_id);
|
||||
const self = this;
|
||||
rebootButton.onclick = function() {
|
||||
if(rebootAPI()) {
|
||||
rebootButton.onclick = async function() {
|
||||
if(await rebootAPI()) {
|
||||
self.close();
|
||||
self.manager_dialog.close();
|
||||
}
|
||||
@ -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,16 +289,20 @@ 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.zIndex = 10001;
|
||||
this.element.style.display = "flex";
|
||||
this.element.style.zIndex = 1099;
|
||||
}
|
||||
catch(exception) {
|
||||
app.ui.dialog.show(`Failed to get external model list. / ${exception}`);
|
||||
|
||||
@ -1,81 +0,0 @@
|
||||
import {app} from "../../scripts/app.js";
|
||||
import {ComfyWidgets} from "../../scripts/widgets.js";
|
||||
// Node that add notes to your project
|
||||
|
||||
let terminal_node;
|
||||
let log_mode = false;
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.Manager.Terminal",
|
||||
|
||||
registerCustomNodes() {
|
||||
class TerminalNode {
|
||||
color = "#222222";
|
||||
bgcolor = "#000000";
|
||||
groupcolor = LGraphCanvas.node_colors.black.groupcolor;
|
||||
constructor() {
|
||||
this.logs = [];
|
||||
|
||||
if (!this.properties) {
|
||||
this.properties = {};
|
||||
this.properties.text="";
|
||||
}
|
||||
|
||||
ComfyWidgets.STRING(this, "", ["", {default:this.properties.text, multiline: true}], app)
|
||||
ComfyWidgets.BOOLEAN(this, "mode", ["", {default:true, label_on:'Logging', label_off:'Stop'}], app)
|
||||
ComfyWidgets.INT(this, "lines", ["", {default:500, min:10, max:10000, steps:1}], app)
|
||||
|
||||
let self = this;
|
||||
Object.defineProperty(this.widgets[1], 'value', {
|
||||
set: (v) => {
|
||||
api.fetchApi(`/manager/terminal?mode=${v}`, {});
|
||||
log_mode = v;
|
||||
},
|
||||
get: () => {
|
||||
return log_mode;
|
||||
}
|
||||
});
|
||||
|
||||
this.serialize_widgets = false;
|
||||
this.isVirtualNode = true;
|
||||
|
||||
if(terminal_node) {
|
||||
try {
|
||||
terminal_node.widgets[0].value = 'The output of this node is disabled because another terminal node has appeared.';
|
||||
}
|
||||
catch {}
|
||||
}
|
||||
terminal_node = this;
|
||||
}
|
||||
}
|
||||
|
||||
// Load default visibility
|
||||
LiteGraph.registerNodeType(
|
||||
"Terminal Log //CM",
|
||||
Object.assign(TerminalNode, {
|
||||
title_mode: LiteGraph.NORMAL_TITLE,
|
||||
title: "Terminal Log (Manager)",
|
||||
collapsable: true,
|
||||
})
|
||||
);
|
||||
|
||||
TerminalNode.category = "utils";
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
function terminalFeedback(event) {
|
||||
if(terminal_node) {
|
||||
terminal_node.logs.push(event.detail.data);
|
||||
if(terminal_node.logs.length > terminal_node.widgets[2].value) {
|
||||
terminal_node.logs.shift();
|
||||
if(terminal_node.logs[0] == '' || terminal_node.logs[0] == '\n')
|
||||
terminal_node.logs.shift();
|
||||
}
|
||||
terminal_node.widgets[0].value = [...terminal_node.logs].reverse().join('').trim();
|
||||
}
|
||||
}
|
||||
|
||||
api.addEventListener("manager-terminal-feedback", terminalFeedback);
|
||||
84
js/workflow-metadata.js
Normal file
@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Attaches metadata to the workflow on save
|
||||
* - custom node pack version to all custom nodes used in the workflow
|
||||
*
|
||||
* Example metadata:
|
||||
* "nodes": {
|
||||
* "1": {
|
||||
* type: "CheckpointLoaderSimple",
|
||||
* ...
|
||||
* properties: {
|
||||
* cnr_id: "comfy-core",
|
||||
* version: "0.3.8",
|
||||
* },
|
||||
* },
|
||||
* }
|
||||
*
|
||||
* @typedef {Object} NodeInfo
|
||||
* @property {string} ver - Version (git hash or semantic version)
|
||||
* @property {string} cnr_id - ComfyRegistry node ID
|
||||
* @property {boolean} enabled - Whether the node is enabled
|
||||
*/
|
||||
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
class WorkflowMetadataExtension {
|
||||
constructor() {
|
||||
this.name = "Comfy.CustomNodesManager.WorkflowMetadata";
|
||||
this.installedNodes = {};
|
||||
this.comfyCoreVersion = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the installed nodes info
|
||||
* @returns {Promise<Record<string, NodeInfo>>} The mapping from node name to its info.
|
||||
* ver can either be a git commit hash or a semantic version such as "1.0.0"
|
||||
* cnr_id is the id of the node in the ComfyRegistry
|
||||
* enabled is true if the node is enabled, false if it is disabled
|
||||
*/
|
||||
async getInstalledNodes() {
|
||||
const res = await api.fetchApi("/customnode/installed");
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.installedNodes = await this.getInstalledNodes();
|
||||
this.comfyCoreVersion = (await api.getSystemStats()).system.comfyui_version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when any node is created
|
||||
* @param {LGraphNode} node The newly created node
|
||||
*/
|
||||
nodeCreated(node) {
|
||||
try {
|
||||
// nodeData doesn't exist if node is missing or node is frontend only node
|
||||
if (!node?.constructor?.nodeData?.python_module) return;
|
||||
|
||||
const nodeProperties = (node.properties ??= {});
|
||||
const modules = node.constructor.nodeData.python_module.split(".");
|
||||
const moduleType = modules[0];
|
||||
|
||||
if (moduleType === "custom_nodes") {
|
||||
const nodePackageName = modules[1];
|
||||
const { cnr_id, aux_id, ver } =
|
||||
this.installedNodes[nodePackageName] ??
|
||||
this.installedNodes[nodePackageName.toLowerCase()] ??
|
||||
{};
|
||||
|
||||
if (cnr_id === "comfy-core") return; // don't allow hijacking comfy-core name
|
||||
if (cnr_id) nodeProperties.cnr_id = cnr_id;
|
||||
else nodeProperties.aux_id = aux_id;
|
||||
if (ver) nodeProperties.ver = ver.trim();
|
||||
} else if (["nodes", "comfy_extras", "comfy_api_nodes"].includes(moduleType)) {
|
||||
nodeProperties.cnr_id = "comfy-core";
|
||||
nodeProperties.ver = this.comfyCoreVersion;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.registerExtension(new WorkflowMetadataExtension());
|
||||
271
json-checker.py
@ -1,23 +1,264 @@
|
||||
import json
|
||||
import argparse
|
||||
#!/usr/bin/env python3
|
||||
"""JSON Entry Validator
|
||||
|
||||
def check_json_syntax(file_path):
|
||||
Validates JSON entries based on content structure.
|
||||
|
||||
Validation rules based on JSON content:
|
||||
- {"custom_nodes": [...]}: Validates required fields (author, title, reference, files, install_type, description)
|
||||
- {"models": [...]}: Validates JSON syntax only (no required fields)
|
||||
- Other JSON structures: Validates JSON syntax only
|
||||
|
||||
Git repository URL validation (for custom_nodes):
|
||||
1. URLs must NOT end with .git
|
||||
2. URLs must follow format: https://github.com/{author}/{reponame}
|
||||
3. .py and .js files are exempt from this check
|
||||
|
||||
Supported formats:
|
||||
- Array format: [{...}, {...}]
|
||||
- Object format: {"custom_nodes": [...]} or {"models": [...]}
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
|
||||
# Required fields for each entry type
|
||||
REQUIRED_FIELDS_CUSTOM_NODE = ['author', 'title', 'reference', 'files', 'install_type', 'description']
|
||||
REQUIRED_FIELDS_MODEL = [] # model-list.json doesn't require field validation
|
||||
|
||||
# Pattern for valid GitHub repository URL (without .git suffix)
|
||||
GITHUB_REPO_PATTERN = re.compile(r'^https://github\.com/[^/]+/[^/]+$')
|
||||
|
||||
|
||||
def get_entry_context(entry: Dict) -> str:
|
||||
"""Get identifying information from entry for error messages
|
||||
|
||||
Args:
|
||||
entry: JSON entry
|
||||
|
||||
Returns:
|
||||
String with author and reference info
|
||||
"""
|
||||
parts = []
|
||||
if 'author' in entry:
|
||||
parts.append(f"author={entry['author']}")
|
||||
if 'reference' in entry:
|
||||
parts.append(f"ref={entry['reference']}")
|
||||
if 'title' in entry:
|
||||
parts.append(f"title={entry['title']}")
|
||||
|
||||
if parts:
|
||||
return " | ".join(parts)
|
||||
else:
|
||||
# No identifying info - show actual entry content (truncated)
|
||||
import json
|
||||
entry_str = json.dumps(entry, ensure_ascii=False)
|
||||
if len(entry_str) > 100:
|
||||
entry_str = entry_str[:100] + "..."
|
||||
return f"content={entry_str}"
|
||||
|
||||
|
||||
def validate_required_fields(entry: Dict, entry_index: int, required_fields: List[str]) -> List[str]:
|
||||
"""Validate that all required fields are present
|
||||
|
||||
Args:
|
||||
entry: JSON entry to validate
|
||||
entry_index: Index of entry in array (for error reporting)
|
||||
required_fields: List of required field names
|
||||
|
||||
Returns:
|
||||
List of error descriptions (without entry prefix/context)
|
||||
"""
|
||||
errors = []
|
||||
|
||||
for field in required_fields:
|
||||
if field not in entry:
|
||||
errors.append(f"Missing required field '{field}'")
|
||||
elif entry[field] is None:
|
||||
errors.append(f"Field '{field}' is null")
|
||||
elif isinstance(entry[field], str) and not entry[field].strip():
|
||||
errors.append(f"Field '{field}' is empty")
|
||||
elif field == 'files' and not entry[field]: # Empty array
|
||||
errors.append("Field 'files' is empty array")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def validate_git_repo_urls(entry: Dict, entry_index: int) -> List[str]:
|
||||
"""Validate git repository URLs in 'files' array
|
||||
|
||||
Requirements:
|
||||
- Git repo URLs must NOT end with .git
|
||||
- Must follow format: https://github.com/{author}/{reponame}
|
||||
- .py and .js files are exempt
|
||||
|
||||
Args:
|
||||
entry: JSON entry to validate
|
||||
entry_index: Index of entry in array (for error reporting)
|
||||
|
||||
Returns:
|
||||
List of error descriptions (without entry prefix/context)
|
||||
"""
|
||||
errors = []
|
||||
|
||||
if 'files' not in entry or not isinstance(entry['files'], list):
|
||||
return errors
|
||||
|
||||
for file_url in entry['files']:
|
||||
if not isinstance(file_url, str):
|
||||
continue
|
||||
|
||||
# Skip .py and .js files - they're exempt from git repo validation
|
||||
if file_url.endswith('.py') or file_url.endswith('.js'):
|
||||
continue
|
||||
|
||||
# Check if it's a GitHub URL (likely a git repo)
|
||||
if 'github.com' in file_url:
|
||||
# Error if URL ends with .git
|
||||
if file_url.endswith('.git'):
|
||||
errors.append(f"Git repo URL must NOT end with .git: {file_url}")
|
||||
continue
|
||||
|
||||
# Validate format: https://github.com/{author}/{reponame}
|
||||
if not GITHUB_REPO_PATTERN.match(file_url):
|
||||
errors.append(f"Invalid git repo URL format (expected https://github.com/author/reponame): {file_url}")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def validate_entry(entry: Dict, entry_index: int, required_fields: List[str]) -> List[str]:
|
||||
"""Validate a single JSON entry
|
||||
|
||||
Args:
|
||||
entry: JSON entry to validate
|
||||
entry_index: Index of entry in array (for error reporting)
|
||||
required_fields: List of required field names
|
||||
|
||||
Returns:
|
||||
List of error messages (empty if valid)
|
||||
"""
|
||||
errors = []
|
||||
|
||||
# Check required fields
|
||||
errors.extend(validate_required_fields(entry, entry_index, required_fields))
|
||||
|
||||
# Check git repository URLs
|
||||
errors.extend(validate_git_repo_urls(entry, entry_index))
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def validate_json_file(file_path: str) -> Tuple[bool, List[str]]:
|
||||
"""Validate JSON file containing entries
|
||||
|
||||
Args:
|
||||
file_path: Path to JSON file
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, error_messages)
|
||||
"""
|
||||
errors = []
|
||||
|
||||
# Check file exists
|
||||
path = Path(file_path)
|
||||
if not path.exists():
|
||||
return False, [f"File not found: {file_path}"]
|
||||
|
||||
# Load JSON
|
||||
try:
|
||||
with open(file_path, 'r') as file:
|
||||
json_str = file.read()
|
||||
json.loads(json_str)
|
||||
print(f"[ OK ] {file_path}")
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"[FAIL] {file_path}\n\n {e}\n")
|
||||
except FileNotFoundError:
|
||||
print(f"[FAIL] {file_path}\n\n File not found\n")
|
||||
return False, [f"Invalid JSON: {e}"]
|
||||
except Exception as e:
|
||||
return False, [f"Error reading file: {e}"]
|
||||
|
||||
# Determine required fields based on JSON content
|
||||
required_fields = []
|
||||
|
||||
# Validate structure - support both array and object formats
|
||||
entries_to_validate = []
|
||||
|
||||
if isinstance(data, list):
|
||||
# Direct array format: [{...}, {...}]
|
||||
entries_to_validate = data
|
||||
elif isinstance(data, dict):
|
||||
# Object format: {"custom_nodes": [...]} or {"models": [...]}
|
||||
# Determine validation based on keys
|
||||
if 'custom_nodes' in data and isinstance(data['custom_nodes'], list):
|
||||
required_fields = REQUIRED_FIELDS_CUSTOM_NODE
|
||||
entries_to_validate = data['custom_nodes']
|
||||
elif 'models' in data and isinstance(data['models'], list):
|
||||
required_fields = REQUIRED_FIELDS_MODEL
|
||||
entries_to_validate = data['models']
|
||||
else:
|
||||
# Other JSON structures (extension-node-map.json, etc.) - just validate JSON syntax
|
||||
return True, []
|
||||
else:
|
||||
return False, ["JSON root must be either an array or an object containing arrays"]
|
||||
|
||||
# Validate each entry
|
||||
for idx, entry in enumerate(entries_to_validate, start=1):
|
||||
if not isinstance(entry, dict):
|
||||
# Show actual value for type errors
|
||||
entry_str = json.dumps(entry, ensure_ascii=False) if not isinstance(entry, str) else repr(entry)
|
||||
if len(entry_str) > 150:
|
||||
entry_str = entry_str[:150] + "..."
|
||||
errors.append(f"\n❌ Entry #{idx}: Must be an object, got {type(entry).__name__}")
|
||||
errors.append(f" Actual value: {entry_str}")
|
||||
continue
|
||||
|
||||
entry_errors = validate_entry(entry, idx, required_fields)
|
||||
if entry_errors:
|
||||
# Group errors by entry with context
|
||||
context = get_entry_context(entry)
|
||||
errors.append(f"\n❌ Entry #{idx} ({context}):")
|
||||
for error in entry_errors:
|
||||
errors.append(f" - {error}")
|
||||
|
||||
is_valid = len(errors) == 0
|
||||
return is_valid, errors
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="JSON File Syntax Checker")
|
||||
parser.add_argument("file_path", type=str, help="Path to the JSON file for syntax checking")
|
||||
"""Main entry point"""
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python json-checker.py <json-file>")
|
||||
print("\nValidates JSON entries based on content:")
|
||||
print(" - {\"custom_nodes\": [...]}: Validates required fields (author, title, reference, files, install_type, description)")
|
||||
print(" - {\"models\": [...]}: Validates JSON syntax only (no required fields)")
|
||||
print(" - Other JSON structures: Validates JSON syntax only")
|
||||
print("\nGit repo URL validation (for custom_nodes):")
|
||||
print(" - URLs must NOT end with .git")
|
||||
print(" - URLs must follow: https://github.com/{author}/{reponame}")
|
||||
sys.exit(1)
|
||||
|
||||
args = parser.parse_args()
|
||||
check_json_syntax(args.file_path)
|
||||
file_path = sys.argv[1]
|
||||
|
||||
if __name__ == "__main__":
|
||||
is_valid, errors = validate_json_file(file_path)
|
||||
|
||||
if is_valid:
|
||||
print(f"✅ {file_path}: Validation passed")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print(f"Validating: {file_path}")
|
||||
print("=" * 60)
|
||||
print("❌ Validation failed!\n")
|
||||
print("Errors:")
|
||||
# Count actual errors (lines starting with " -")
|
||||
error_count = sum(1 for e in errors if e.strip().startswith('-'))
|
||||
for error in errors:
|
||||
# Don't add ❌ prefix to grouped entries (they already have it)
|
||||
if error.strip().startswith('❌'):
|
||||
print(error)
|
||||
else:
|
||||
print(error)
|
||||
print(f"\nTotal errors: {error_count}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
|
Before Width: | Height: | Size: 160 KiB |
BIN
misc/main.jpg
|
Before Width: | Height: | Size: 31 KiB |
BIN
misc/menu.jpg
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 129 KiB |
BIN
misc/models.png
|
Before Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 29 KiB |
BIN
misc/share.jpg
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 91 KiB |
2810
model-list.json
95
node_db/README.md
Normal file
@ -0,0 +1,95 @@
|
||||
# ComfyUI-Manager: Node Database (node_db)
|
||||
|
||||
This directory contains the JSON database files that power ComfyUI-Manager's legacy node registry system. While the manager is gradually transitioning to the online Custom Node Registry (CNR), these local JSON files continue to provide important metadata about custom nodes, models, and their integrations.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
The node_db directory is organized into several subdirectories, each serving a specific purpose:
|
||||
|
||||
- **dev/**: Development channel files with latest additions and experimental nodes
|
||||
- **legacy/**: Historical/legacy nodes that may require special handling
|
||||
- **new/**: New nodes that have passed initial verification but are still being evaluated
|
||||
- **forked/**: Forks of existing nodes with modifications
|
||||
- **tutorial/**: Example and tutorial nodes designed for learning purposes
|
||||
|
||||
## Core Database Files
|
||||
|
||||
Each subdirectory contains a standard set of JSON files:
|
||||
|
||||
- **custom-node-list.json**: Primary database of custom nodes with metadata
|
||||
- **extension-node-map.json**: Maps between extensions and individual nodes they provide
|
||||
- **model-list.json**: Catalog of models that can be downloaded through the manager
|
||||
- **alter-list.json**: Alternative implementations of nodes for compatibility or functionality
|
||||
- **github-stats.json**: GitHub repository statistics for node popularity metrics
|
||||
|
||||
## Database Schema
|
||||
|
||||
### custom-node-list.json
|
||||
```json
|
||||
{
|
||||
"custom_nodes": [
|
||||
{
|
||||
"title": "Node display name",
|
||||
"name": "Repository name",
|
||||
"reference": "Original repository if forked",
|
||||
"files": ["GitHub URL or other source location"],
|
||||
"install_type": "git",
|
||||
"description": "Description of the node's functionality",
|
||||
"pip": ["optional pip dependencies"],
|
||||
"js": ["optional JavaScript files"],
|
||||
"tags": ["categorization tags"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### extension-node-map.json
|
||||
```json
|
||||
{
|
||||
"extension-id": [
|
||||
["list", "of", "node", "classes"],
|
||||
{
|
||||
"author": "Author name",
|
||||
"description": "Extension description",
|
||||
"nodename_pattern": "Optional regex pattern for node name matching"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Transition to Custom Node Registry (CNR)
|
||||
|
||||
This local database system is being progressively replaced by the online Custom Node Registry (CNR), which provides:
|
||||
- Real-time updates without manual JSON maintenance
|
||||
- Improved versioning support
|
||||
- Better security validation
|
||||
- Enhanced metadata
|
||||
|
||||
The Manager supports both systems simultaneously during the transition period.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
- The database follows a channel-based architecture for different sources
|
||||
- Multiple database modes are supported: Channel, Local, and Remote
|
||||
- The system supports differential updates to minimize bandwidth usage
|
||||
- Security levels are enforced for different node installations based on source
|
||||
|
||||
## Usage in the Application
|
||||
|
||||
The Manager's backend uses these database files to:
|
||||
|
||||
1. Provide browsable lists of available nodes and models
|
||||
2. Resolve dependencies for installation
|
||||
3. Track updates and new versions
|
||||
4. Map node classes to their source repositories
|
||||
5. Assess risk levels for installation security
|
||||
|
||||
## Maintenance Scripts
|
||||
|
||||
Each subdirectory contains a `scan.sh` script that assists with:
|
||||
- Scanning repositories for new nodes
|
||||
- Updating metadata
|
||||
- Validating database integrity
|
||||
- Generating proper JSON structures
|
||||
|
||||
This database system enables a flexible, secure, and comprehensive management system for the ComfyUI ecosystem while the transition to CNR continues.
|
||||
@ -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,115 @@
|
||||
{
|
||||
"custom_nodes": [
|
||||
{
|
||||
"author": "Fossiel",
|
||||
"title": "ComfyUI-MultiGPU-Patched",
|
||||
"reference": "https://github.com/Fossiel/ComfyUI-MultiGPU-Patched",
|
||||
"files": [
|
||||
"https://github.com/Fossiel/ComfyUI-MultiGPU-Patched"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Patched fork of ComfyUI-MultiGPU providing universal .safetensors and GGUF multi-GPU distribution with DisTorch 2.0 engine, model-driven allocation options (bytes/ratio modes), WanVideoWrapper integration, and up to 10% faster GGUF inference. (Description by CC)"
|
||||
},
|
||||
{
|
||||
"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]",
|
||||
"reference": "https://github.com/SanDiegoDude/ComfyUI-HiDream-Sampler",
|
||||
"files": [
|
||||
"https://github.com/SanDiegoDude/ComfyUI-HiDream-Sampler"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A collection of enhanced nodes for ComfyUI that provide powerful additional functionality to your workflows.\nNOTE: The files in the repo are not organized."
|
||||
},
|
||||
{
|
||||
"author": "PramaLLC",
|
||||
"title": "ComfyUI BEN - Background Erase Network",
|
||||
"reference": "https://github.com/PramaLLC/BEN2_ComfyUI",
|
||||
"files": [
|
||||
"https://github.com/PramaLLC/BEN2_ComfyUI"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Remove backgrounds from images with [a/BEN2](https://huggingface.co/PramaLLC/BEN2) in ComfyUI\nOriginal repo: [a/https://github.com/DoctorDiffusion/ComfyUI-BEN](https://github.com/DoctorDiffusion/ComfyUI-BEN)"
|
||||
},
|
||||
{
|
||||
"author": "BlenderNeko",
|
||||
"title": "ltdrdata/ComfyUI_TiledKSampler",
|
||||
"reference": "https://github.com/ltdrdata/ComfyUI_TiledKSampler",
|
||||
"files": [
|
||||
"https://github.com/ltdrdata/ComfyUI_TiledKSampler"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "PR for [a/https://github.com/BlenderNeko/ComfyUI_TiledKSampler/pull/59](https://github.com/BlenderNeko/ComfyUI_TiledKSampler/pull/59)"
|
||||
},
|
||||
{
|
||||
"author": "leeooo001",
|
||||
"title": "ComfyUI-leo-Hamer",
|
||||
"reference": "https://github.com/leeooo001/ComfyUI-leo-Hamer",
|
||||
"files": [
|
||||
"https://github.com/leeooo001/ComfyUI-leo-Hamer"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Unoffice Hamer-ComfyUI by leo\nNOTE:base on [a/hamer](https://github.com/geopavlakos/hamer)"
|
||||
},
|
||||
{
|
||||
"author": "leeooo001",
|
||||
"title": "ComfyUI-leo-GVHMR",
|
||||
"reference": "https://github.com/leeooo001/ComfyUI-leo-GVHMR",
|
||||
"files": [
|
||||
"https://github.com/leeooo001/ComfyUI-leo-GVHMR"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Unoffice Hamer-ComfyUI by leo\nNOTE:base on [a/GVHMR](https://github.com/zju3dv/GVHMR)"
|
||||
},
|
||||
{
|
||||
"author": "leeooo001",
|
||||
"title": "RealisDance-ComfyUI",
|
||||
"reference": "https://github.com/leeooo001/ComfyUI-leo-RealisDance",
|
||||
"files": [
|
||||
"https://github.com/leeooo001/ComfyUI-leo-RealisDance"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Unoffice RealisDance-ComfyUI by leo\nNOTE:base on [a/RealisDance](https://github.com/damo-cv/RealisDance), modified on [a/RealisDanceComfyui](https://github.com/AIFSH/RealisDance-ComfyUI)"
|
||||
},
|
||||
{
|
||||
"author": "jags111",
|
||||
"title": "NyaamZ/efficiency-nodes-ED",
|
||||
"reference": "https://github.com/NyaamZ/efficiency-nodes-ED",
|
||||
"files": [
|
||||
"https://github.com/NyaamZ/efficiency-nodes-ED"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This forked repo supports efficiency-nodes-comfyui. Additional features."
|
||||
},
|
||||
{
|
||||
"author": "SeaArtLab",
|
||||
"title": "zer0int/ComfyUI-Long-CLIP",
|
||||
"reference": "https://github.com/zer0int/ComfyUI-Long-CLIP",
|
||||
"files": [
|
||||
"https://github.com/zer0int/ComfyUI-Long-CLIP"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This forked repo supports FLUX.1 not only SD1.5, SDXL."
|
||||
},
|
||||
{
|
||||
"author": "meimeilook",
|
||||
"title": "ComfyUI_IPAdapter_plus.old [backward compatbility]",
|
||||
@ -49,6 +159,36 @@
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This is a development respository for debugging migration of StableSR to ComfyUI\n\nNOTE:Forked from [https://github.com/gameltb/Comfyui-StableSR]\nPut the StableSR [a/webui_786v_139.ckpt](https://huggingface.co/Iceclear/StableSR/resolve/main/webui_768v_139.ckpt) model into Comyfui/models/stablesr/, Put the StableSR [a/stablesr_768v_000139.ckpt](https://huggingface.co/Iceclear/StableSR/resolve/main/stablesr_768v_000139.ckpt) model into Comyfui/models/checkpoints/"
|
||||
},
|
||||
{
|
||||
"author": "city96",
|
||||
"title": "Efficient-Large-Model/Extra Models for ComfyUI",
|
||||
"reference": "https://github.com/Efficient-Large-Model/ComfyUI_ExtraModels",
|
||||
"files": [
|
||||
"https://github.com/Efficient-Large-Model/ComfyUI_ExtraModels"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A forked version of ComfyUI_ExtraModels. (modified by Efficient-Large-Model)"
|
||||
},
|
||||
{
|
||||
"author": "Pablerdo",
|
||||
"title": "ComfyUI-PSNodes",
|
||||
"reference": "https://github.com/Pablerdo/ComfyUI-PSNodes",
|
||||
"files": [
|
||||
"https://github.com/Pablerdo/ComfyUI-PSNodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A fork of KJNodes for ComfyUI.\nVarious quality of life -nodes for ComfyUI, mostly just visual stuff to improve usability"
|
||||
},
|
||||
{
|
||||
"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)"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,3 +1,148 @@
|
||||
{
|
||||
"models": []
|
||||
"models": [
|
||||
{
|
||||
"name": "Inswapper-fp16 (face swap) [REMOVED]",
|
||||
"type": "insightface",
|
||||
"base": "inswapper",
|
||||
"save_path": "insightface",
|
||||
"description": "Checkpoint of the insightface swapper model\n(used by ComfyUI-FaceSwap, comfyui-reactor-node, CharacterFaceSwap,\nComfyUI roop and comfy_mtb)",
|
||||
"reference": "https://github.com/facefusion/facefusion-assets",
|
||||
"filename": "inswapper_128_fp16.onnx",
|
||||
"url": "https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128_fp16.onnx",
|
||||
"size": "277.7MB"
|
||||
},
|
||||
{
|
||||
"name": "Inswapper (face swap) [REMOVED]",
|
||||
"type": "insightface",
|
||||
"base": "inswapper",
|
||||
"save_path": "insightface",
|
||||
"description": "Checkpoint of the insightface swapper model\n(used by ComfyUI-FaceSwap, comfyui-reactor-node, CharacterFaceSwap,\nComfyUI roop and comfy_mtb)",
|
||||
"reference": "https://github.com/facefusion/facefusion-assets",
|
||||
"filename": "inswapper_128.onnx",
|
||||
"url": "https://github.com/facefusion/facefusion-assets/releases/download/models/inswapper_128.onnx",
|
||||
"size": "555.3MB"
|
||||
},
|
||||
{
|
||||
"name": "pfg-novel-n10.pt",
|
||||
"type": "PFG",
|
||||
"base": "SD1.5",
|
||||
"save_path": "custom_nodes/pfg-ComfyUI/models",
|
||||
"description": "Pressing 'install' directly downloads the model from the pfg-ComfyUI/models extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
|
||||
"reference": "https://huggingface.co/furusu/PFG",
|
||||
"filename": "pfg-novel-n10.pt",
|
||||
"url": "https://huggingface.co/furusu/PFG/resolve/main/pfg-novel-n10.pt",
|
||||
"size": "23.6MB"
|
||||
},
|
||||
{
|
||||
"name": "pfg-wd14-n10.pt",
|
||||
"type": "PFG",
|
||||
"base": "SD1.5",
|
||||
"save_path": "custom_nodes/pfg-ComfyUI/models",
|
||||
"description": "Pressing 'install' directly downloads the model from the pfg-ComfyUI/models extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
|
||||
"reference": "https://huggingface.co/furusu/PFG",
|
||||
"filename": "pfg-wd14-n10.pt",
|
||||
"url": "https://huggingface.co/furusu/PFG/resolve/main/pfg-wd14-n10.pt",
|
||||
"size": "31.5MB"
|
||||
},
|
||||
{
|
||||
"name": "pfg-wd15beta2-n10.pt",
|
||||
"type": "PFG",
|
||||
"base": "SD1.5",
|
||||
"save_path": "custom_nodes/pfg-ComfyUI/models",
|
||||
"description": "Pressing 'install' directly downloads the model from the pfg-ComfyUI/models extension node. (Note: Requires ComfyUI-Manager V0.24 or above)",
|
||||
"reference": "https://huggingface.co/furusu/PFG",
|
||||
"filename": "pfg-wd15beta2-n10.pt",
|
||||
"url": "https://huggingface.co/furusu/PFG/resolve/main/pfg-wd15beta2-n10.pt",
|
||||
"size": "31.5MB"
|
||||
},
|
||||
{
|
||||
"name": "shape_predictor_68_face_landmarks.dat [Face Analysis]",
|
||||
"type": "Shape Predictor",
|
||||
"base": "DLIB",
|
||||
"save_path": "custom_nodes/comfyui_faceanalysis/dlib",
|
||||
"description": "To use the Face Analysis for ComfyUI custom node, installation of this model is needed.",
|
||||
"reference": "https://huggingface.co/matt3ounstable/dlib_predictor_recognition/tree/main",
|
||||
"filename": "shape_predictor_68_face_landmarks.dat",
|
||||
"url": "https://huggingface.co/matt3ounstable/dlib_predictor_recognition/resolve/main/shape_predictor_68_face_landmarks.dat",
|
||||
"size": "99.7MB"
|
||||
},
|
||||
{
|
||||
"name": "dlib_face_recognition_resnet_model_v1.dat [Face Analysis]",
|
||||
"type": "Face Recognition",
|
||||
"base": "DLIB",
|
||||
"save_path": "custom_nodes/comfyui_faceanalysis/dlib",
|
||||
"description": "To use the Face Analysis for ComfyUI custom node, installation of this model is needed.",
|
||||
"reference": "https://huggingface.co/matt3ounstable/dlib_predictor_recognition/tree/main",
|
||||
"filename": "dlib_face_recognition_resnet_model_v1.dat",
|
||||
"url": "https://huggingface.co/matt3ounstable/dlib_predictor_recognition/resolve/main/dlib_face_recognition_resnet_model_v1.dat",
|
||||
"size": "22.5MB"
|
||||
},
|
||||
{
|
||||
"name": "ID-Animator/animator.ckpt",
|
||||
"type": "ID-Animator",
|
||||
"base": "SD1.5",
|
||||
"save_path": "custom_nodes/comfyui_id_animator/models",
|
||||
"description": "ID-Animator checkpoint",
|
||||
"reference": "https://huggingface.co/spaces/ID-Animator/ID-Animator",
|
||||
"filename": "animator.ckpt",
|
||||
"url": "https://huggingface.co/spaces/ID-Animator/ID-Animator/resolve/main/animator.ckpt",
|
||||
"size": "247.3MB"
|
||||
},
|
||||
{
|
||||
"name": "ID-Animator/mm_sd_v15_v2.ckpt",
|
||||
"type": "ID-Animator",
|
||||
"base": "SD1.5",
|
||||
"save_path": "custom_nodes/comfyui_id_animator/models/animatediff_models",
|
||||
"description": "AnimateDiff checkpoint for ID-Animator",
|
||||
"reference": "https://huggingface.co/spaces/ID-Animator/ID-Animator",
|
||||
"filename": "mm_sd_v15_v2.ckpt",
|
||||
"url": "https://huggingface.co/spaces/ID-Animator/ID-Animator/resolve/main/mm_sd_v15_v2.ckpt",
|
||||
"size": "1.82GB"
|
||||
},
|
||||
{
|
||||
"name": "ID-Animator/image_encoder",
|
||||
"type": "ID-Animator",
|
||||
"base": "SD1.5",
|
||||
"save_path": "custom_nodes/comfyui_id_animator/models/image_encoder",
|
||||
"description": "CLIP Image encoder for ID-Animator",
|
||||
"reference": "https://huggingface.co/spaces/ID-Animator/ID-Animator",
|
||||
"filename": "model.safetensors",
|
||||
"url": "https://huggingface.co/spaces/ID-Animator/ID-Animator/resolve/main/image_encoder/model.safetensors",
|
||||
"size": "2.53GB"
|
||||
},
|
||||
{
|
||||
"name": "Doubiiu/ToonCrafter model checkpoint",
|
||||
"type": "checkpoint",
|
||||
"base": "ToonCrafter",
|
||||
"save_path": "custom_nodes/comfyui-tooncrafter/ToonCrafter/checkpoints/tooncrafter_512_interp_v1",
|
||||
"description": "ToonCrafter checkpoint model for ComfyUI-ToonCrafter",
|
||||
"reference": "https://huggingface.co/Doubiiu/ToonCrafter/tree/main",
|
||||
"filename": "model.ckpt",
|
||||
"url": "https://huggingface.co/Doubiiu/ToonCrafter/resolve/main/model.ckpt",
|
||||
"size": "10.5GB"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "BAAI/SegGPT",
|
||||
"type": "SegGPT",
|
||||
"base": "SegGPT",
|
||||
"save_path": "custom_nodes/comfyui-seggpt",
|
||||
"description": "SegGPT",
|
||||
"reference": "https://huggingface.co/BAAI/SegGPT",
|
||||
"filename": "seggpt_vit_large.pth",
|
||||
"url": "https://huggingface.co/BAAI/SegGPT/resolve/main/seggpt_vit_large.pth",
|
||||
"size": "1.48GB"
|
||||
},
|
||||
{
|
||||
"name": "kohya-ss/ControlNet-LLLite: SDXL Canny Anime",
|
||||
"type": "controlnet",
|
||||
"base": "SDXL",
|
||||
"save_path": "custom_nodes/ControlNet-LLLite-ComfyUI/models",
|
||||
"description": "An extremely compactly designed controlnet model (a.k.a. ControlNet-LLLite). Note: The model structure is highly experimental and may be subject to change in the future.",
|
||||
"reference": "https://huggingface.co/kohya-ss/controlnet-lllite",
|
||||
"filename": "controllllite_v01032064e_sdxl_canny_anime.safetensors",
|
||||
"url": "https://huggingface.co/kohya-ss/controlnet-lllite/resolve/main/controllllite_v01032064e_sdxl_canny_anime.safetensors",
|
||||
"size": "46.2MB"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,5 +1,25 @@
|
||||
{
|
||||
"custom_nodes": [
|
||||
{
|
||||
"author": "Comfy-Org",
|
||||
"title": "ComfyUI React Extension Template",
|
||||
"reference": "https://github.com/Comfy-Org/ComfyUI-React-Extension-Template",
|
||||
"files": [
|
||||
"https://github.com/Comfy-Org/ComfyUI-React-Extension-Template"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A minimal template for creating React/TypeScript frontend extensions for ComfyUI, with complete boilerplate setup including internationalization and unit testing."
|
||||
},
|
||||
{
|
||||
"author": "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",
|
||||
@ -90,16 +110,6 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:WW_ImageResize"
|
||||
},
|
||||
{
|
||||
"author": "bmz55",
|
||||
"title": "bmz nodes",
|
||||
"reference": "https://github.com/bmz55/comfyui-bmz-nodes",
|
||||
"files": [
|
||||
"https://github.com/bmz55/comfyui-bmz-nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:Load Images From Dir With Name (Inspire - BMZ), Count Images In Dir (BMZ), Get Level Text (BMZ), Get Level Float (BMZ)"
|
||||
},
|
||||
{
|
||||
"author": "azure-dragon-ai",
|
||||
"title": "ComfyUI-HPSv2-Nodes",
|
||||
@ -150,36 +160,6 @@
|
||||
"install_type": "git-clone",
|
||||
"description": "This module provides an annotation @ComfyFunc to streamline adding custom node types in ComfyUI. It processes your function's signature to create a wrapped function and custom node definition required for ComfyUI, eliminating all the boilerplate code. In most cases you can just add a @ComfyFunc(\"category\") annotation to your existing function."
|
||||
},
|
||||
{
|
||||
"author": "MokkaBoss1",
|
||||
"title": "Woman_in_a_dress",
|
||||
"reference": "https://github.com/MokkaBoss1/Woman_in_a_dress",
|
||||
"files": [
|
||||
"https://github.com/MokkaBoss1/Woman_in_a_dress/raw/main/Woman_In_A_Dress.py"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:Woman_in_a_dress"
|
||||
},
|
||||
{
|
||||
"author": "shinich39",
|
||||
"title": "comfyui-concat-text-39",
|
||||
"reference": "https://github.com/shinich39/comfyui-concat-text-39",
|
||||
"files": [
|
||||
"https://github.com/shinich39/comfyui-concat-text-39"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:Concatenate multiple text nodes."
|
||||
},
|
||||
{
|
||||
"author": "nilor-corp",
|
||||
"title": "nilor-nodes",
|
||||
"reference": "https://github.com/nilor-corp/nilor-nodes",
|
||||
"files": [
|
||||
"https://github.com/nilor-corp/nilor-nodes"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Nodes:Nilor Floats, Nilor Int To List Of Bools, Nilor Bool From List Of Bools, Nilor Int From List Of Ints, Nilor List of Ints, Nilor Count Images In Directory"
|
||||
},
|
||||
{
|
||||
"author": "OuticNZ",
|
||||
"title": "ComfyUI-Simple-Of-Complex",
|
||||
@ -260,6 +240,137 @@
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "The primitive node and dummy input are required because comfy doesn't accept requests with identical graphs. You'll also need a show text node. I like the one from ComfyUI-Custom-Scripts. I got the generic tetris remake from claude so it may or may not be ripped from somewhere else."
|
||||
},
|
||||
{
|
||||
"author": "FlyMyAI",
|
||||
"title": "ComfyUI-ExampleNode",
|
||||
"reference": "https://github.com/FlyMyAI/ComfyUI-ExampleNode",
|
||||
"files": [
|
||||
"https://github.com/FlyMyAI/ComfyUI-ExampleNode"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Node to provide convenient ComfyUI standard, supported by flymy_comfyui."
|
||||
},
|
||||
{
|
||||
"author": "Wanghanying",
|
||||
"title": "ComfyUI_RAGDemo",
|
||||
"reference": "https://github.com/Wanghanying/ComfyUI_RAGDemo",
|
||||
"files": [
|
||||
"https://github.com/Wanghanying/ComfyUI_RAGDemo"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "RAG Demo for LLM"
|
||||
},
|
||||
{
|
||||
"author": "FelixTeutsch",
|
||||
"title": "BachelorThesis",
|
||||
"reference": "https://github.com/FelixTeutsch/BachelorThesis",
|
||||
"files": [
|
||||
"https://github.com/FelixTeutsch/BachelorThesis"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This is a ComfyUi custom node, that build a new UI on top of the already existing AI, to enable the use of custom controllers"
|
||||
},
|
||||
{
|
||||
"author": "jhj0517",
|
||||
"title": "ComfyUI-CustomNodes-Template",
|
||||
"reference": "https://github.com/jhj0517/ComfyUI-CustomNodes-Template",
|
||||
"files": [
|
||||
"https://github.com/jhj0517/ComfyUI-CustomNodes-Template"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This is the ComfyUI custom node template repository that anyone can use to create their own custom nodes."
|
||||
},
|
||||
{
|
||||
"author": "laogou666",
|
||||
"title": "Comfyui_LG_Advertisement",
|
||||
"reference": "https://github.com/LAOGOU-666/Comfyui_LG_Advertisement",
|
||||
"files": [
|
||||
"https://github.com/LAOGOU-666/Comfyui_LG_Advertisement"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "A node for demonstration."
|
||||
},
|
||||
{
|
||||
"author": "amorano",
|
||||
"title": "cozy_spoke",
|
||||
"reference": "https://github.com/cozy-comfyui/cozy_spoke",
|
||||
"files": [
|
||||
"https://github.com/cozy-comfyui/cozy_spoke"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Example node communicating between ComfyUI Javascript and Python."
|
||||
},
|
||||
{
|
||||
"author": "amorano",
|
||||
"title": "Cozy Link Toggle",
|
||||
"id": "cozyLinkToggle",
|
||||
"reference": "https://github.com/cozy-comfyui/cozy_link_toggle",
|
||||
"files": [
|
||||
"https://github.com/cozy-comfyui/cozy_link_toggle"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "Example of using ComfyUI Toolbar to Toggle ComfyUI links on/off"
|
||||
},
|
||||
{
|
||||
"author": "xhiroga",
|
||||
"title": "ComfyUI-TypeScript-CustomNode",
|
||||
"reference": "https://github.com/xhiroga/ComfyUI-TypeScript-CustomNode",
|
||||
"files": [
|
||||
"https://github.com/xhiroga/ComfyUI-TypeScript-CustomNode"
|
||||
],
|
||||
"install_type": "git-clone",
|
||||
"description": "This project is generated from xhiroga/ComfyUI-TypeScript-CustomNode"
|
||||
},
|
||||
{
|
||||
"author": "zentrocdot",
|
||||
"title": "ComfyUI-Turtle_Graphics_Demos",
|
||||
"reference": "https://github.com/zentrocdot/ComfyUI-Turtle_Graphics_Demo",
|
||||
"files": [
|
||||
"https://github.com/zentrocdot/ComfyUI-Turtle_Graphics_Demo"
|
||||
],
|
||||
"description": "ComfyUI node for creating some Turtle Graphic demos.",
|
||||
"install_type": "git-clone"
|
||||
},
|
||||
{
|
||||
"author": "cozy-comfyui",
|
||||
"title": "cozy_ex_dynamic",
|
||||
"reference": "https://github.com/cozy-comfyui/cozy_ex_dynamic",
|
||||
"files": [
|
||||
"https://github.com/cozy-comfyui/cozy_ex_dynamic"
|
||||
],
|
||||
"description": "Dynamic Node examples for ComfyUI",
|
||||
"install_type": "git-clone"
|
||||
},
|
||||
{
|
||||
"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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1 +1,573 @@
|
||||
{}
|
||||
{
|
||||
"https://github.com/BadCafeCode/execution-inversion-demo-comfyui": [
|
||||
[
|
||||
"AccumulateNode",
|
||||
"AccumulationGetItemNode",
|
||||
"AccumulationGetLengthNode",
|
||||
"AccumulationHeadNode",
|
||||
"AccumulationSetItemNode",
|
||||
"AccumulationTailNode",
|
||||
"AccumulationToListNode",
|
||||
"BoolOperationNode",
|
||||
"ComponentInput",
|
||||
"ComponentMetadata",
|
||||
"ComponentOutput",
|
||||
"DebugPrint",
|
||||
"ExecutionBlocker",
|
||||
"FloatConditions",
|
||||
"ForLoopClose",
|
||||
"ForLoopOpen",
|
||||
"IntConditions",
|
||||
"IntMathOperation",
|
||||
"InversionDemoAdvancedPromptNode",
|
||||
"InversionDemoLazyConditional",
|
||||
"InversionDemoLazyIndexSwitch",
|
||||
"InversionDemoLazyMixImages",
|
||||
"InversionDemoLazySwitch",
|
||||
"ListToAccumulationNode",
|
||||
"MakeListNode",
|
||||
"StringConditions",
|
||||
"ToBoolNode",
|
||||
"WhileLoopClose",
|
||||
"WhileLoopOpen"
|
||||
],
|
||||
{
|
||||
"title_aux": "execution-inversion-demo-comfyui"
|
||||
}
|
||||
],
|
||||
"https://github.com/BetaDoggo/ComfyUI-Tetris": [
|
||||
[
|
||||
"Tetris"
|
||||
],
|
||||
{
|
||||
"title_aux": "ComfyUI Tetris"
|
||||
}
|
||||
],
|
||||
"https://github.com/BoosterCore/ComfyUI-BC-Experimental": [
|
||||
[
|
||||
"ClipTextEncodeBC",
|
||||
"ClipTextEncodeBCA",
|
||||
"FluxEmptyLatentSize",
|
||||
"LoraWithTriggerWord",
|
||||
"SaveAnyText",
|
||||
"SimpleText"
|
||||
],
|
||||
{
|
||||
"title_aux": "ComfyUI-BC-Experimental"
|
||||
}
|
||||
],
|
||||
"https://github.com/FlyMyAI/ComfyUI-ExampleNode": [
|
||||
[
|
||||
"ExampleT2IFMANode"
|
||||
],
|
||||
{
|
||||
"title_aux": "ComfyUI-ExampleNode"
|
||||
}
|
||||
],
|
||||
"https://github.com/IvanRybakov/comfyui-node-int-to-string-convertor": [
|
||||
[
|
||||
"Int To String"
|
||||
],
|
||||
{
|
||||
"title_aux": "comfyui-node-int-to-string-convertor"
|
||||
}
|
||||
],
|
||||
"https://github.com/LarryJane491/Custom-Node-Base": [
|
||||
[
|
||||
"My First Node"
|
||||
],
|
||||
{
|
||||
"title_aux": "Custom-Node-Base"
|
||||
}
|
||||
],
|
||||
"https://github.com/OuticNZ/ComfyUI-Simple-Of-Complex": [
|
||||
[
|
||||
"Pipe From Parameters",
|
||||
"Pipe To Parameters",
|
||||
"Prompt Tidy",
|
||||
"Text Switch 2 Way",
|
||||
"Text With Context"
|
||||
],
|
||||
{
|
||||
"title_aux": "ComfyUI-Simple-Of-Complex"
|
||||
}
|
||||
],
|
||||
"https://github.com/Suzie1/ComfyUI_Guide_To_Making_Custom_Nodes": [
|
||||
[
|
||||
"Concatenate Hello World",
|
||||
"Hello World Overlay Text",
|
||||
"Print Hello World"
|
||||
],
|
||||
{
|
||||
"title_aux": "Guide To Making Custom Nodes in ComfyUI"
|
||||
}
|
||||
],
|
||||
"https://github.com/Wanghanying/ComfyUI_RAGDemo": [
|
||||
[
|
||||
"testRAG"
|
||||
],
|
||||
{
|
||||
"title_aux": "ComfyUI_RAGDemo"
|
||||
}
|
||||
],
|
||||
"https://github.com/azure-dragon-ai/ComfyUI-HPSv2-Nodes": [
|
||||
[
|
||||
"GetImageSize",
|
||||
"HaojihuiHPSv2ImageProcessor",
|
||||
"HaojihuiHPSv2ImageScore",
|
||||
"HaojihuiHPSv2ImageScores",
|
||||
"HaojihuiHPSv2Loader",
|
||||
"HaojihuiHPSv2SaveAnimatedWEBP",
|
||||
"HaojihuiHPSv2SaveImage",
|
||||
"HaojihuiHPSv2SaveWEBP",
|
||||
"HaojihuiHPSv2SaveWebpImage",
|
||||
"HaojihuiHPSv2TextProcessor",
|
||||
"SaveImageWebp",
|
||||
"ScaleShort"
|
||||
],
|
||||
{
|
||||
"title_aux": "ComfyUI-HPSv2-Nodes"
|
||||
}
|
||||
],
|
||||
"https://github.com/bamboodia/BAM_Nodes": [
|
||||
[
|
||||
"BAM Crop To Ratio",
|
||||
"BAM Empty Latent By Ratio",
|
||||
"BAM Get Shortest Side",
|
||||
"BAM OnOff INT",
|
||||
"BAM Random Float",
|
||||
"BAM Random Image From Folder"
|
||||
],
|
||||
{
|
||||
"title_aux": "BAM Nodes"
|
||||
}
|
||||
],
|
||||
"https://github.com/boricuapab/ComfyUI_BoricuapabWFNodePack": [
|
||||
[
|
||||
"BoricuapabWF Concatenate Hello World",
|
||||
"BoricuapabWF Integer",
|
||||
"BoricuapabWF Print Hello Puerto Rican World",
|
||||
"BoricuapabWF Print Puerto Rican"
|
||||
],
|
||||
{
|
||||
"title_aux": "ComfyUI_BoricuapabWFNodePack"
|
||||
}
|
||||
],
|
||||
"https://github.com/comfyanonymous/ComfyUI": [
|
||||
[
|
||||
"AddNoise",
|
||||
"AlignYourStepsScheduler",
|
||||
"BasicGuider",
|
||||
"BasicScheduler",
|
||||
"BetaSamplingScheduler",
|
||||
"CFGGuider",
|
||||
"CLIPAttentionMultiply",
|
||||
"CLIPLoader",
|
||||
"CLIPMergeAdd",
|
||||
"CLIPMergeSimple",
|
||||
"CLIPMergeSubtract",
|
||||
"CLIPSave",
|
||||
"CLIPSetLastLayer",
|
||||
"CLIPTextEncode",
|
||||
"CLIPTextEncodeControlnet",
|
||||
"CLIPTextEncodeFlux",
|
||||
"CLIPTextEncodeHunyuanDiT",
|
||||
"CLIPTextEncodePixArtAlpha",
|
||||
"CLIPTextEncodeSD3",
|
||||
"CLIPTextEncodeSDXL",
|
||||
"CLIPTextEncodeSDXLRefiner",
|
||||
"CLIPVisionEncode",
|
||||
"CLIPVisionLoader",
|
||||
"Canny",
|
||||
"CheckpointLoader",
|
||||
"CheckpointLoaderSimple",
|
||||
"CheckpointSave",
|
||||
"ConditioningAverage",
|
||||
"ConditioningCombine",
|
||||
"ConditioningConcat",
|
||||
"ConditioningSetArea",
|
||||
"ConditioningSetAreaPercentage",
|
||||
"ConditioningSetAreaStrength",
|
||||
"ConditioningSetMask",
|
||||
"ConditioningSetTimestepRange",
|
||||
"ConditioningStableAudio",
|
||||
"ConditioningZeroOut",
|
||||
"ControlNetApply",
|
||||
"ControlNetApplyAdvanced",
|
||||
"ControlNetApplySD3",
|
||||
"ControlNetInpaintingAliMamaApply",
|
||||
"ControlNetLoader",
|
||||
"CropMask",
|
||||
"DiffControlNetLoader",
|
||||
"DifferentialDiffusion",
|
||||
"DiffusersLoader",
|
||||
"DisableNoise",
|
||||
"DualCFGGuider",
|
||||
"DualCLIPLoader",
|
||||
"EmptyHunyuanLatentVideo",
|
||||
"EmptyImage",
|
||||
"EmptyLTXVLatentVideo",
|
||||
"EmptyLatentAudio",
|
||||
"EmptyLatentImage",
|
||||
"EmptyMochiLatentVideo",
|
||||
"EmptySD3LatentImage",
|
||||
"ExponentialScheduler",
|
||||
"FeatherMask",
|
||||
"FlipSigmas",
|
||||
"FluxGuidance",
|
||||
"FreeU",
|
||||
"FreeU_V2",
|
||||
"GITSScheduler",
|
||||
"GLIGENLoader",
|
||||
"GLIGENTextBoxApply",
|
||||
"GrowMask",
|
||||
"HyperTile",
|
||||
"HypernetworkLoader",
|
||||
"ImageBatch",
|
||||
"ImageBlend",
|
||||
"ImageBlur",
|
||||
"ImageColorToMask",
|
||||
"ImageCompositeMasked",
|
||||
"ImageCrop",
|
||||
"ImageFromBatch",
|
||||
"ImageInvert",
|
||||
"ImageOnlyCheckpointLoader",
|
||||
"ImageOnlyCheckpointSave",
|
||||
"ImagePadForOutpaint",
|
||||
"ImageQuantize",
|
||||
"ImageScale",
|
||||
"ImageScaleBy",
|
||||
"ImageScaleToTotalPixels",
|
||||
"ImageSharpen",
|
||||
"ImageToMask",
|
||||
"ImageUpscaleWithModel",
|
||||
"InpaintModelConditioning",
|
||||
"InstructPixToPixConditioning",
|
||||
"InvertMask",
|
||||
"JoinImageWithAlpha",
|
||||
"KSampler",
|
||||
"KSamplerAdvanced",
|
||||
"KSamplerSelect",
|
||||
"KarrasScheduler",
|
||||
"LTXVConditioning",
|
||||
"LTXVImgToVideo",
|
||||
"LTXVScheduler",
|
||||
"LaplaceScheduler",
|
||||
"LatentAdd",
|
||||
"LatentApplyOperation",
|
||||
"LatentApplyOperationCFG",
|
||||
"LatentBatch",
|
||||
"LatentBatchSeedBehavior",
|
||||
"LatentBlend",
|
||||
"LatentComposite",
|
||||
"LatentCompositeMasked",
|
||||
"LatentCrop",
|
||||
"LatentFlip",
|
||||
"LatentFromBatch",
|
||||
"LatentInterpolate",
|
||||
"LatentMultiply",
|
||||
"LatentOperationSharpen",
|
||||
"LatentOperationTonemapReinhard",
|
||||
"LatentRotate",
|
||||
"LatentSubtract",
|
||||
"LatentUpscale",
|
||||
"LatentUpscaleBy",
|
||||
"Load3D",
|
||||
"Load3DAnimation",
|
||||
"LoadAudio",
|
||||
"LoadImage",
|
||||
"LoadImageMask",
|
||||
"LoadLatent",
|
||||
"LoraLoader",
|
||||
"LoraLoaderModelOnly",
|
||||
"LoraSave",
|
||||
"Mahiro",
|
||||
"MaskComposite",
|
||||
"MaskToImage",
|
||||
"ModelMergeAdd",
|
||||
"ModelMergeAuraflow",
|
||||
"ModelMergeBlocks",
|
||||
"ModelMergeFlux1",
|
||||
"ModelMergeLTXV",
|
||||
"ModelMergeMochiPreview",
|
||||
"ModelMergeSD1",
|
||||
"ModelMergeSD2",
|
||||
"ModelMergeSD35_Large",
|
||||
"ModelMergeSD3_2B",
|
||||
"ModelMergeSDXL",
|
||||
"ModelMergeSimple",
|
||||
"ModelMergeSubtract",
|
||||
"ModelSamplingAuraFlow",
|
||||
"ModelSamplingContinuousEDM",
|
||||
"ModelSamplingContinuousV",
|
||||
"ModelSamplingDiscrete",
|
||||
"ModelSamplingFlux",
|
||||
"ModelSamplingLTXV",
|
||||
"ModelSamplingSD3",
|
||||
"ModelSamplingStableCascade",
|
||||
"ModelSave",
|
||||
"Morphology",
|
||||
"PatchModelAddDownscale",
|
||||
"PerpNeg",
|
||||
"PerpNegGuider",
|
||||
"PerturbedAttentionGuidance",
|
||||
"PhotoMakerEncode",
|
||||
"PhotoMakerLoader",
|
||||
"PolyexponentialScheduler",
|
||||
"PorterDuffImageComposite",
|
||||
"Preview3D",
|
||||
"PreviewAudio",
|
||||
"PreviewImage",
|
||||
"RandomNoise",
|
||||
"RebatchImages",
|
||||
"RebatchLatents",
|
||||
"RepeatImageBatch",
|
||||
"RepeatLatentBatch",
|
||||
"RescaleCFG",
|
||||
"SDTurboScheduler",
|
||||
"SD_4XUpscale_Conditioning",
|
||||
"SV3D_Conditioning",
|
||||
"SVD_img2vid_Conditioning",
|
||||
"SamplerCustom",
|
||||
"SamplerCustomAdvanced",
|
||||
"SamplerDPMAdaptative",
|
||||
"SamplerDPMPP_2M_SDE",
|
||||
"SamplerDPMPP_2S_Ancestral",
|
||||
"SamplerDPMPP_3M_SDE",
|
||||
"SamplerDPMPP_SDE",
|
||||
"SamplerEulerAncestral",
|
||||
"SamplerEulerAncestralCFGPP",
|
||||
"SamplerEulerCFGpp",
|
||||
"SamplerLCMUpscale",
|
||||
"SamplerLMS",
|
||||
"SaveAnimatedPNG",
|
||||
"SaveAnimatedWEBP",
|
||||
"SaveAudio",
|
||||
"SaveImage",
|
||||
"SaveImageWebsocket",
|
||||
"SaveLatent",
|
||||
"SelfAttentionGuidance",
|
||||
"SetLatentNoiseMask",
|
||||
"SetUnionControlNetType",
|
||||
"SkipLayerGuidanceDiT",
|
||||
"SkipLayerGuidanceSD3",
|
||||
"SolidMask",
|
||||
"SplitImageWithAlpha",
|
||||
"SplitSigmas",
|
||||
"SplitSigmasDenoise",
|
||||
"StableCascade_EmptyLatentImage",
|
||||
"StableCascade_StageB_Conditioning",
|
||||
"StableCascade_StageC_VAEEncode",
|
||||
"StableCascade_SuperResolutionControlnet",
|
||||
"StableZero123_Conditioning",
|
||||
"StableZero123_Conditioning_Batched",
|
||||
"StubConstantImage",
|
||||
"StubFloat",
|
||||
"StubImage",
|
||||
"StubInt",
|
||||
"StubMask",
|
||||
"StyleModelApply",
|
||||
"StyleModelLoader",
|
||||
"TestAccumulateNode",
|
||||
"TestAccumulationGetItemNode",
|
||||
"TestAccumulationGetLengthNode",
|
||||
"TestAccumulationHeadNode",
|
||||
"TestAccumulationSetItemNode",
|
||||
"TestAccumulationTailNode",
|
||||
"TestAccumulationToListNode",
|
||||
"TestBoolOperationNode",
|
||||
"TestCustomIsChanged",
|
||||
"TestCustomValidation1",
|
||||
"TestCustomValidation2",
|
||||
"TestCustomValidation3",
|
||||
"TestCustomValidation4",
|
||||
"TestCustomValidation5",
|
||||
"TestDynamicDependencyCycle",
|
||||
"TestExecutionBlocker",
|
||||
"TestFloatConditions",
|
||||
"TestForLoopClose",
|
||||
"TestForLoopOpen",
|
||||
"TestIntConditions",
|
||||
"TestIntMathOperation",
|
||||
"TestIsChangedWithConstants",
|
||||
"TestLazyMixImages",
|
||||
"TestListToAccumulationNode",
|
||||
"TestMakeListNode",
|
||||
"TestMixedExpansionReturns",
|
||||
"TestStringConditions",
|
||||
"TestToBoolNode",
|
||||
"TestVariadicAverage",
|
||||
"TestWhileLoopClose",
|
||||
"TestWhileLoopOpen",
|
||||
"ThresholdMask",
|
||||
"TomePatchModel",
|
||||
"TorchCompileModel",
|
||||
"TripleCLIPLoader",
|
||||
"UNETLoader",
|
||||
"UNetCrossAttentionMultiply",
|
||||
"UNetSelfAttentionMultiply",
|
||||
"UNetTemporalAttentionMultiply",
|
||||
"UpscaleModelLoader",
|
||||
"VAEDecode",
|
||||
"VAEDecodeAudio",
|
||||
"VAEDecodeTiled",
|
||||
"VAEEncode",
|
||||
"VAEEncodeAudio",
|
||||
"VAEEncodeForInpaint",
|
||||
"VAEEncodeTiled",
|
||||
"VAELoader",
|
||||
"VAESave",
|
||||
"VPScheduler",
|
||||
"VideoLinearCFGGuidance",
|
||||
"VideoTriangleCFGGuidance",
|
||||
"WebcamCapture",
|
||||
"unCLIPCheckpointLoader",
|
||||
"unCLIPConditioning"
|
||||
],
|
||||
{
|
||||
"title_aux": "ComfyUI"
|
||||
}
|
||||
],
|
||||
"https://github.com/dynamixar/Atluris": [
|
||||
[
|
||||
"RandomLine"
|
||||
],
|
||||
{
|
||||
"title_aux": "Atluris"
|
||||
}
|
||||
],
|
||||
"https://github.com/ecjojo/ecjojo-example-nodes": [
|
||||
[
|
||||
"BiggerNote_Example",
|
||||
"DisplayTextNode_Example",
|
||||
"EmptyNode_Example",
|
||||
"ExampleNode_Example",
|
||||
"FilePrefixNode_Example",
|
||||
"HelloWorldNode_Example",
|
||||
"RandomSizeNode_Example",
|
||||
"StringNode_Example",
|
||||
"TextOverlayNode_Example"
|
||||
],
|
||||
{
|
||||
"title_aux": "ecjojo_example_nodes"
|
||||
}
|
||||
],
|
||||
"https://github.com/et118/ComfyUI-ElGogh-Nodes": [
|
||||
[
|
||||
"ElGoghCLIPSetLastLayer",
|
||||
"ElGoghCheckpointLoaderSimple",
|
||||
"ElGoghEmptyLatentImage",
|
||||
"ElGoghKSamplerAdvanced",
|
||||
"ElGoghNegativePrompt",
|
||||
"ElGoghPositivePrompt",
|
||||
"ElGoghPrimaryLoraLoader",
|
||||
"ElGoghSecondaryLoraLoader",
|
||||
"ElGoghSendWebsocketNSFWBool",
|
||||
"ElGoghTertiaryLoraLoader",
|
||||
"ElGoghVAELoader"
|
||||
],
|
||||
{
|
||||
"title_aux": "ComfyUI-ElGogh-Nodes"
|
||||
}
|
||||
],
|
||||
"https://github.com/foxtrot-roger/comfyui-custom-nodes": [
|
||||
[
|
||||
"RF_Tutorial"
|
||||
],
|
||||
{
|
||||
"title_aux": "comfyui-custom-nodes"
|
||||
}
|
||||
],
|
||||
"https://github.com/jhj0517/ComfyUI-CustomNodes-Template": [
|
||||
[
|
||||
"(Down)Load My Model",
|
||||
"Calculate Minus",
|
||||
"Calculate Plus",
|
||||
"Example Output Node"
|
||||
],
|
||||
{
|
||||
"title_aux": "ComfyUI-CustomNodes-Template"
|
||||
}
|
||||
],
|
||||
"https://github.com/jtong/comfyui-jtong-workflow": [
|
||||
[
|
||||
"Example",
|
||||
"high_workflow_caller",
|
||||
"jtong.Highend",
|
||||
"jtong.Highway"
|
||||
],
|
||||
{
|
||||
"author": "Trung0246",
|
||||
"description": "Random nodes for ComfyUI I made to solve my struggle with ComfyUI (ex: pipe, process). Have varying quality.",
|
||||
"nickname": "ComfyUI-0246",
|
||||
"title": "ComfyUI-0246",
|
||||
"title_aux": "comfyui-jtong-workflow"
|
||||
}
|
||||
],
|
||||
"https://github.com/kappa54m/ComfyUI_Usability": [
|
||||
[
|
||||
"KLoadImageByPath",
|
||||
"KLoadImageByPathAdvanced",
|
||||
"KLoadImageDedup"
|
||||
],
|
||||
{
|
||||
"title_aux": "ComfyUI-HPSv2-Nodes"
|
||||
}
|
||||
],
|
||||
"https://github.com/mira-6/mira-wildcard-node": [
|
||||
[
|
||||
"MiraWildcard"
|
||||
],
|
||||
{
|
||||
"author": "mira-6",
|
||||
"description": "Single-node wildcard implementation.",
|
||||
"nickname": "mira-wildcard-node",
|
||||
"title": "mira-wildcard-node",
|
||||
"title_aux": "mira-wildcard-node"
|
||||
}
|
||||
],
|
||||
"https://github.com/sonyeon-sj/ComfyUI-easy_ImageSize_Selecter": [
|
||||
[
|
||||
"ImageSizer",
|
||||
"promptSelecter"
|
||||
],
|
||||
{
|
||||
"title_aux": "ComfyUI-easy_ImageSize_Selecter"
|
||||
}
|
||||
],
|
||||
"https://github.com/thinkthinking/ComfyUI-Ye": [
|
||||
[
|
||||
"CheckpointLoader|Ye",
|
||||
"OllamaVision|Ye",
|
||||
"PrintHelloWorld|Ye",
|
||||
"Signature|Ye"
|
||||
],
|
||||
{
|
||||
"title_aux": "ComfyUI-Ye"
|
||||
}
|
||||
],
|
||||
"https://github.com/wailovet/ComfyUI-WW": [
|
||||
[
|
||||
"WW_AccumulationPreviewImages",
|
||||
"WW_AppendString",
|
||||
"WW_CurrentPreviewImages",
|
||||
"WW_ImageResize",
|
||||
"WW_PreviewTextNode",
|
||||
"WW_RandString"
|
||||
],
|
||||
{
|
||||
"title_aux": "ComfyUI-WW"
|
||||
}
|
||||
],
|
||||
"https://github.com/yowipr/ComfyUI-Manual": [
|
||||
[
|
||||
"EXAMPLE",
|
||||
"M_Layer",
|
||||
"M_Output",
|
||||
"M_RenderArea"
|
||||
],
|
||||
{
|
||||
"title_aux": "ComfyUI-Manual"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
#!/bin/bash
|
||||
source ../../../../venv/bin/activate
|
||||
rm .tmp/*.py > /dev/null
|
||||
python ../../scanner.py
|
||||
rm ~/.tmp/dev/*.py > /dev/null 2>&1
|
||||
python ../../scanner.py ~/.tmp/tutorial
|
||||
|
||||
@ -1,372 +1,373 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "aaaaaaaaaa"
|
||||
},
|
||||
"source": [
|
||||
"Git clone the repo and install the requirements. (ignore the pip errors about protobuf)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "bbbbbbbbbb"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# #@title Environment Setup\n",
|
||||
"\n",
|
||||
"from pathlib import Path\n",
|
||||
"\n",
|
||||
"OPTIONS = {}\n",
|
||||
"\n",
|
||||
"USE_GOOGLE_DRIVE = True #@param {type:\"boolean\"}\n",
|
||||
"UPDATE_COMFY_UI = True #@param {type:\"boolean\"}\n",
|
||||
"USE_COMFYUI_MANAGER = True #@param {type:\"boolean\"}\n",
|
||||
"INSTALL_CUSTOM_NODES_DEPENDENCIES = True #@param {type:\"boolean\"}\n",
|
||||
"OPTIONS['USE_GOOGLE_DRIVE'] = USE_GOOGLE_DRIVE\n",
|
||||
"OPTIONS['UPDATE_COMFY_UI'] = UPDATE_COMFY_UI\n",
|
||||
"OPTIONS['USE_COMFYUI_MANAGER'] = USE_COMFYUI_MANAGER\n",
|
||||
"OPTIONS['INSTALL_CUSTOM_NODES_DEPENDENCIES'] = INSTALL_CUSTOM_NODES_DEPENDENCIES\n",
|
||||
"\n",
|
||||
"current_dir = !pwd\n",
|
||||
"WORKSPACE = f\"{current_dir[0]}/ComfyUI\"\n",
|
||||
"\n",
|
||||
"if OPTIONS['USE_GOOGLE_DRIVE']:\n",
|
||||
" !echo \"Mounting Google Drive...\"\n",
|
||||
" %cd /\n",
|
||||
"\n",
|
||||
" from google.colab import drive\n",
|
||||
" drive.mount('/content/drive')\n",
|
||||
"\n",
|
||||
" WORKSPACE = \"/content/drive/MyDrive/ComfyUI\"\n",
|
||||
" %cd /content/drive/MyDrive\n",
|
||||
"\n",
|
||||
"![ ! -d $WORKSPACE ] && echo -= Initial setup ComfyUI =- && git clone https://github.com/comfyanonymous/ComfyUI\n",
|
||||
"%cd $WORKSPACE\n",
|
||||
"\n",
|
||||
"if OPTIONS['UPDATE_COMFY_UI']:\n",
|
||||
" !echo -= Updating ComfyUI =-\n",
|
||||
"\n",
|
||||
" # Correction of the issue of permissions being deleted on Google Drive.\n",
|
||||
" ![ -f \".ci/nightly/update_windows/update_comfyui_and_python_dependencies.bat\" ] && chmod 755 .ci/nightly/update_windows/update_comfyui_and_python_dependencies.bat\n",
|
||||
" ![ -f \".ci/nightly/windows_base_files/run_nvidia_gpu.bat\" ] && chmod 755 .ci/nightly/windows_base_files/run_nvidia_gpu.bat\n",
|
||||
" ![ -f \".ci/update_windows/update_comfyui_and_python_dependencies.bat\" ] && chmod 755 .ci/update_windows/update_comfyui_and_python_dependencies.bat\n",
|
||||
" ![ -f \".ci/update_windows_cu118/update_comfyui_and_python_dependencies.bat\" ] && chmod 755 .ci/update_windows_cu118/update_comfyui_and_python_dependencies.bat\n",
|
||||
" ![ -f \".ci/update_windows/update.py\" ] && chmod 755 .ci/update_windows/update.py\n",
|
||||
" ![ -f \".ci/update_windows/update_comfyui.bat\" ] && chmod 755 .ci/update_windows/update_comfyui.bat\n",
|
||||
" ![ -f \".ci/update_windows/README_VERY_IMPORTANT.txt\" ] && chmod 755 .ci/update_windows/README_VERY_IMPORTANT.txt\n",
|
||||
" ![ -f \".ci/update_windows/run_cpu.bat\" ] && chmod 755 .ci/update_windows/run_cpu.bat\n",
|
||||
" ![ -f \".ci/update_windows/run_nvidia_gpu.bat\" ] && chmod 755 .ci/update_windows/run_nvidia_gpu.bat\n",
|
||||
"\n",
|
||||
" !git pull\n",
|
||||
"\n",
|
||||
"!echo -= Install dependencies =-\n",
|
||||
"!pip3 install accelerate\n",
|
||||
"!pip3 install einops transformers>=4.28.1 safetensors>=0.4.2 aiohttp pyyaml Pillow scipy tqdm psutil tokenizers>=0.13.3\n",
|
||||
"!pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121\n",
|
||||
"!pip3 install torchsde\n",
|
||||
"!pip3 install kornia>=0.7.1 spandrel soundfile sentencepiece\n",
|
||||
"\n",
|
||||
"if OPTIONS['USE_COMFYUI_MANAGER']:\n",
|
||||
" %cd custom_nodes\n",
|
||||
"\n",
|
||||
" # Correction of the issue of permissions being deleted on Google Drive.\n",
|
||||
" ![ -f \"ComfyUI-Manager/check.sh\" ] && chmod 755 ComfyUI-Manager/check.sh\n",
|
||||
" ![ -f \"ComfyUI-Manager/scan.sh\" ] && chmod 755 ComfyUI-Manager/scan.sh\n",
|
||||
" ![ -f \"ComfyUI-Manager/node_db/dev/scan.sh\" ] && chmod 755 ComfyUI-Manager/node_db/dev/scan.sh\n",
|
||||
" ![ -f \"ComfyUI-Manager/scripts/install-comfyui-venv-linux.sh\" ] && chmod 755 ComfyUI-Manager/scripts/install-comfyui-venv-linux.sh\n",
|
||||
" ![ -f \"ComfyUI-Manager/scripts/install-comfyui-venv-win.bat\" ] && chmod 755 ComfyUI-Manager/scripts/install-comfyui-venv-win.bat\n",
|
||||
"\n",
|
||||
" ![ ! -d ComfyUI-Manager ] && echo -= Initial setup ComfyUI-Manager =- && git clone https://github.com/ltdrdata/ComfyUI-Manager\n",
|
||||
" %cd ComfyUI-Manager\n",
|
||||
" !git pull\n",
|
||||
"\n",
|
||||
"%cd $WORKSPACE\n",
|
||||
"\n",
|
||||
"if OPTIONS['INSTALL_CUSTOM_NODES_DEPENDENCIES']:\n",
|
||||
" !echo -= Install custom nodes dependencies =-\n",
|
||||
" !pip install GitPython\n",
|
||||
" !python custom_nodes/ComfyUI-Manager/cm-cli.py restore-dependencies\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "cccccccccc"
|
||||
},
|
||||
"source": [
|
||||
"Download some models/checkpoints/vae or custom comfyui nodes (uncomment the commands for the ones you want)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "dddddddddd"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Checkpoints\n",
|
||||
"\n",
|
||||
"### SDXL\n",
|
||||
"### I recommend these workflow examples: https://comfyanonymous.github.io/ComfyUI_examples/sdxl/\n",
|
||||
"\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors -P ./models/checkpoints/\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-refiner-1.0/resolve/main/sd_xl_refiner_1.0.safetensors -P ./models/checkpoints/\n",
|
||||
"\n",
|
||||
"# SDXL ReVision\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/clip_vision_g/resolve/main/clip_vision_g.safetensors -P ./models/clip_vision/\n",
|
||||
"\n",
|
||||
"# SD1.5\n",
|
||||
"!wget -c https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.ckpt -P ./models/checkpoints/\n",
|
||||
"\n",
|
||||
"# SD2\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-2-1-base/resolve/main/v2-1_512-ema-pruned.safetensors -P ./models/checkpoints/\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-2-1/resolve/main/v2-1_768-ema-pruned.safetensors -P ./models/checkpoints/\n",
|
||||
"\n",
|
||||
"# Some SD1.5 anime style\n",
|
||||
"#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix2/AbyssOrangeMix2_hard.safetensors -P ./models/checkpoints/\n",
|
||||
"#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix3/AOM3A1_orangemixs.safetensors -P ./models/checkpoints/\n",
|
||||
"#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix3/AOM3A3_orangemixs.safetensors -P ./models/checkpoints/\n",
|
||||
"#!wget -c https://huggingface.co/Linaqruf/anything-v3.0/resolve/main/anything-v3-fp16-pruned.safetensors -P ./models/checkpoints/\n",
|
||||
"\n",
|
||||
"# Waifu Diffusion 1.5 (anime style SD2.x 768-v)\n",
|
||||
"#!wget -c https://huggingface.co/waifu-diffusion/wd-1-5-beta3/resolve/main/wd-illusion-fp16.safetensors -P ./models/checkpoints/\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# unCLIP models\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/illuminatiDiffusionV1_v11_unCLIP/resolve/main/illuminatiDiffusionV1_v11-unclip-h-fp16.safetensors -P ./models/checkpoints/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/wd-1.5-beta2_unCLIP/resolve/main/wd-1-5-beta2-aesthetic-unclip-h-fp16.safetensors -P ./models/checkpoints/\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# VAE\n",
|
||||
"!wget -c https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.safetensors -P ./models/vae/\n",
|
||||
"#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/VAEs/orangemix.vae.pt -P ./models/vae/\n",
|
||||
"#!wget -c https://huggingface.co/hakurei/waifu-diffusion-v1-4/resolve/main/vae/kl-f8-anime2.ckpt -P ./models/vae/\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Loras\n",
|
||||
"#!wget -c https://civitai.com/api/download/models/10350 -O ./models/loras/theovercomer8sContrastFix_sd21768.safetensors #theovercomer8sContrastFix SD2.x 768-v\n",
|
||||
"#!wget -c https://civitai.com/api/download/models/10638 -O ./models/loras/theovercomer8sContrastFix_sd15.safetensors #theovercomer8sContrastFix SD1.x\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_offset_example-lora_1.0.safetensors -P ./models/loras/ #SDXL offset noise lora\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# T2I-Adapter\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_depth_sd14v1.pth -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_seg_sd14v1.pth -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_sketch_sd14v1.pth -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_keypose_sd14v1.pth -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_openpose_sd14v1.pth -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_color_sd14v1.pth -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_canny_sd14v1.pth -P ./models/controlnet/\n",
|
||||
"\n",
|
||||
"# T2I Styles Model\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_style_sd14v1.pth -P ./models/style_models/\n",
|
||||
"\n",
|
||||
"# CLIPVision model (needed for styles model)\n",
|
||||
"#!wget -c https://huggingface.co/openai/clip-vit-large-patch14/resolve/main/pytorch_model.bin -O ./models/clip_vision/clip_vit14.bin\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# ControlNet\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11e_sd15_ip2p_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11e_sd15_shuffle_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_canny_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11f1p_sd15_depth_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_inpaint_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_lineart_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_mlsd_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_normalbae_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_openpose_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_scribble_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_seg_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_softedge_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15s2_lineart_anime_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11u_sd15_tile_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"\n",
|
||||
"# ControlNet SDXL\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-canny-rank256.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-depth-rank256.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-recolor-rank256.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-sketch-rank256.safetensors -P ./models/controlnet/\n",
|
||||
"\n",
|
||||
"# Controlnet Preprocessor nodes by Fannovel16\n",
|
||||
"#!cd custom_nodes && git clone https://github.com/Fannovel16/comfy_controlnet_preprocessors; cd comfy_controlnet_preprocessors && python install.py\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# GLIGEN\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/GLIGEN_pruned_safetensors/resolve/main/gligen_sd14_textbox_pruned_fp16.safetensors -P ./models/gligen/\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# ESRGAN upscale model\n",
|
||||
"#!wget -c https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth -P ./models/upscale_models/\n",
|
||||
"#!wget -c https://huggingface.co/sberbank-ai/Real-ESRGAN/resolve/main/RealESRGAN_x2.pth -P ./models/upscale_models/\n",
|
||||
"#!wget -c https://huggingface.co/sberbank-ai/Real-ESRGAN/resolve/main/RealESRGAN_x4.pth -P ./models/upscale_models/\n",
|
||||
"\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "kkkkkkkkkkkkkkk"
|
||||
},
|
||||
"source": [
|
||||
"### Run ComfyUI with cloudflared (Recommended Way)\n",
|
||||
"\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "jjjjjjjjjjjjjj"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!wget -P ~ https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb\n",
|
||||
"!dpkg -i ~/cloudflared-linux-amd64.deb\n",
|
||||
"\n",
|
||||
"import subprocess\n",
|
||||
"import threading\n",
|
||||
"import time\n",
|
||||
"import socket\n",
|
||||
"import urllib.request\n",
|
||||
"\n",
|
||||
"def iframe_thread(port):\n",
|
||||
" while True:\n",
|
||||
" time.sleep(0.5)\n",
|
||||
" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
|
||||
" result = sock.connect_ex(('127.0.0.1', port))\n",
|
||||
" if result == 0:\n",
|
||||
" break\n",
|
||||
" sock.close()\n",
|
||||
" print(\"\\nComfyUI finished loading, trying to launch cloudflared (if it gets stuck here cloudflared is having issues)\\n\")\n",
|
||||
"\n",
|
||||
" p = subprocess.Popen([\"cloudflared\", \"tunnel\", \"--url\", \"http://127.0.0.1:{}\".format(port)], stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
|
||||
" for line in p.stderr:\n",
|
||||
" l = line.decode()\n",
|
||||
" if \"trycloudflare.com \" in l:\n",
|
||||
" print(\"This is the URL to access ComfyUI:\", l[l.find(\"http\"):], end='')\n",
|
||||
" #print(l, end='')\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
|
||||
"\n",
|
||||
"!python main.py --dont-print-server"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "kkkkkkkkkkkkkk"
|
||||
},
|
||||
"source": [
|
||||
"### Run ComfyUI with localtunnel\n",
|
||||
"\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "jjjjjjjjjjjjj"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!npm install -g localtunnel\n",
|
||||
"\n",
|
||||
"import subprocess\n",
|
||||
"import threading\n",
|
||||
"import time\n",
|
||||
"import socket\n",
|
||||
"import urllib.request\n",
|
||||
"\n",
|
||||
"def iframe_thread(port):\n",
|
||||
" while True:\n",
|
||||
" time.sleep(0.5)\n",
|
||||
" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
|
||||
" result = sock.connect_ex(('127.0.0.1', port))\n",
|
||||
" if result == 0:\n",
|
||||
" break\n",
|
||||
" sock.close()\n",
|
||||
" print(\"\\nComfyUI finished loading, trying to launch localtunnel (if it gets stuck here localtunnel is having issues)\\n\")\n",
|
||||
"\n",
|
||||
" print(\"The password/enpoint ip for localtunnel is:\", urllib.request.urlopen('https://ipv4.icanhazip.com').read().decode('utf8').strip(\"\\n\"))\n",
|
||||
" p = subprocess.Popen([\"lt\", \"--port\", \"{}\".format(port)], stdout=subprocess.PIPE)\n",
|
||||
" for line in p.stdout:\n",
|
||||
" print(line.decode(), end='')\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
|
||||
"\n",
|
||||
"!python main.py --dont-print-server"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "gggggggggg"
|
||||
},
|
||||
"source": [
|
||||
"### Run ComfyUI with colab iframe (use only in case the previous way with localtunnel doesn't work)\n",
|
||||
"\n",
|
||||
"You should see the ui appear in an iframe. If you get a 403 error, it's your firefox settings or an extension that's messing things up.\n",
|
||||
"\n",
|
||||
"If you want to open it in another window use the link.\n",
|
||||
"\n",
|
||||
"Note that some UI features like live image previews won't work because the colab iframe blocks websockets."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "hhhhhhhhhh"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import threading\n",
|
||||
"import time\n",
|
||||
"import socket\n",
|
||||
"def iframe_thread(port):\n",
|
||||
" while True:\n",
|
||||
" time.sleep(0.5)\n",
|
||||
" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
|
||||
" result = sock.connect_ex(('127.0.0.1', port))\n",
|
||||
" if result == 0:\n",
|
||||
" break\n",
|
||||
" sock.close()\n",
|
||||
" from google.colab import output\n",
|
||||
" output.serve_kernel_port_as_iframe(port, height=1024)\n",
|
||||
" print(\"to open it in a window you can open this link here:\")\n",
|
||||
" output.serve_kernel_port_as_window(port)\n",
|
||||
"\n",
|
||||
"threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
|
||||
"\n",
|
||||
"!python main.py --dont-print-server"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"accelerator": "GPU",
|
||||
"colab": {
|
||||
"provenance": []
|
||||
},
|
||||
"gpuClass": "standard",
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python"
|
||||
}
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "aaaaaaaaaa"
|
||||
},
|
||||
"source": [
|
||||
"Git clone the repo and install the requirements. (ignore the pip errors about protobuf)"
|
||||
]
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "bbbbbbbbbb"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# #@title Environment Setup\n",
|
||||
"\n",
|
||||
"from pathlib import Path\n",
|
||||
"\n",
|
||||
"OPTIONS = {}\n",
|
||||
"\n",
|
||||
"USE_GOOGLE_DRIVE = True #@param {type:\"boolean\"}\n",
|
||||
"UPDATE_COMFY_UI = True #@param {type:\"boolean\"}\n",
|
||||
"USE_COMFYUI_MANAGER = True #@param {type:\"boolean\"}\n",
|
||||
"INSTALL_CUSTOM_NODES_DEPENDENCIES = True #@param {type:\"boolean\"}\n",
|
||||
"OPTIONS['USE_GOOGLE_DRIVE'] = USE_GOOGLE_DRIVE\n",
|
||||
"OPTIONS['UPDATE_COMFY_UI'] = UPDATE_COMFY_UI\n",
|
||||
"OPTIONS['USE_COMFYUI_MANAGER'] = USE_COMFYUI_MANAGER\n",
|
||||
"OPTIONS['INSTALL_CUSTOM_NODES_DEPENDENCIES'] = INSTALL_CUSTOM_NODES_DEPENDENCIES\n",
|
||||
"\n",
|
||||
"current_dir = !pwd\n",
|
||||
"WORKSPACE = f\"{current_dir[0]}/ComfyUI\"\n",
|
||||
"\n",
|
||||
"if OPTIONS['USE_GOOGLE_DRIVE']:\n",
|
||||
" !echo \"Mounting Google Drive...\"\n",
|
||||
" %cd /\n",
|
||||
"\n",
|
||||
" from google.colab import drive\n",
|
||||
" drive.mount('/content/drive')\n",
|
||||
"\n",
|
||||
" WORKSPACE = \"/content/drive/MyDrive/ComfyUI\"\n",
|
||||
" %cd /content/drive/MyDrive\n",
|
||||
"\n",
|
||||
"![ ! -d $WORKSPACE ] && echo -= Initial setup ComfyUI =- && git clone https://github.com/comfyanonymous/ComfyUI\n",
|
||||
"%cd $WORKSPACE\n",
|
||||
"\n",
|
||||
"if OPTIONS['UPDATE_COMFY_UI']:\n",
|
||||
" !echo -= Updating ComfyUI =-\n",
|
||||
"\n",
|
||||
" # Correction of the issue of permissions being deleted on Google Drive.\n",
|
||||
" ![ -f \".ci/nightly/update_windows/update_comfyui_and_python_dependencies.bat\" ] && chmod 755 .ci/nightly/update_windows/update_comfyui_and_python_dependencies.bat\n",
|
||||
" ![ -f \".ci/nightly/windows_base_files/run_nvidia_gpu.bat\" ] && chmod 755 .ci/nightly/windows_base_files/run_nvidia_gpu.bat\n",
|
||||
" ![ -f \".ci/update_windows/update_comfyui_and_python_dependencies.bat\" ] && chmod 755 .ci/update_windows/update_comfyui_and_python_dependencies.bat\n",
|
||||
" ![ -f \".ci/update_windows_cu118/update_comfyui_and_python_dependencies.bat\" ] && chmod 755 .ci/update_windows_cu118/update_comfyui_and_python_dependencies.bat\n",
|
||||
" ![ -f \".ci/update_windows/update.py\" ] && chmod 755 .ci/update_windows/update.py\n",
|
||||
" ![ -f \".ci/update_windows/update_comfyui.bat\" ] && chmod 755 .ci/update_windows/update_comfyui.bat\n",
|
||||
" ![ -f \".ci/update_windows/README_VERY_IMPORTANT.txt\" ] && chmod 755 .ci/update_windows/README_VERY_IMPORTANT.txt\n",
|
||||
" ![ -f \".ci/update_windows/run_cpu.bat\" ] && chmod 755 .ci/update_windows/run_cpu.bat\n",
|
||||
" ![ -f \".ci/update_windows/run_nvidia_gpu.bat\" ] && chmod 755 .ci/update_windows/run_nvidia_gpu.bat\n",
|
||||
"\n",
|
||||
" !git pull\n",
|
||||
"\n",
|
||||
"!echo -= Install dependencies =-\n",
|
||||
"!pip3 install accelerate\n",
|
||||
"!pip3 install einops transformers>=4.28.1 safetensors>=0.4.2 aiohttp pyyaml Pillow scipy tqdm psutil tokenizers>=0.13.3\n",
|
||||
"!pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121\n",
|
||||
"!pip3 install torchsde\n",
|
||||
"!pip3 install kornia>=0.7.1 spandrel soundfile sentencepiece\n",
|
||||
"\n",
|
||||
"if OPTIONS['USE_COMFYUI_MANAGER']:\n",
|
||||
" %cd custom_nodes\n",
|
||||
"\n",
|
||||
" # Correction of the issue of permissions being deleted on Google Drive.\n",
|
||||
" ![ -f \"ComfyUI-Manager/check.sh\" ] && chmod 755 ComfyUI-Manager/check.sh\n",
|
||||
" ![ -f \"ComfyUI-Manager/scan.sh\" ] && chmod 755 ComfyUI-Manager/scan.sh\n",
|
||||
" ![ -f \"ComfyUI-Manager/node_db/dev/scan.sh\" ] && chmod 755 ComfyUI-Manager/node_db/dev/scan.sh\n",
|
||||
" ![ -f \"ComfyUI-Manager/node_db/tutorial/scan.sh\" ] && chmod 755 ComfyUI-Manager/node_db/tutorial/scan.sh\n",
|
||||
" ![ -f \"ComfyUI-Manager/scripts/install-comfyui-venv-linux.sh\" ] && chmod 755 ComfyUI-Manager/scripts/install-comfyui-venv-linux.sh\n",
|
||||
" ![ -f \"ComfyUI-Manager/scripts/install-comfyui-venv-win.bat\" ] && chmod 755 ComfyUI-Manager/scripts/install-comfyui-venv-win.bat\n",
|
||||
"\n",
|
||||
" ![ ! -d ComfyUI-Manager ] && echo -= Initial setup ComfyUI-Manager =- && git clone https://github.com/ltdrdata/ComfyUI-Manager\n",
|
||||
" %cd ComfyUI-Manager\n",
|
||||
" !git pull\n",
|
||||
"\n",
|
||||
"%cd $WORKSPACE\n",
|
||||
"\n",
|
||||
"if OPTIONS['INSTALL_CUSTOM_NODES_DEPENDENCIES']:\n",
|
||||
" !echo -= Install custom nodes dependencies =-\n",
|
||||
" !pip install GitPython\n",
|
||||
" !python custom_nodes/ComfyUI-Manager/cm-cli.py restore-dependencies\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "cccccccccc"
|
||||
},
|
||||
"source": [
|
||||
"Download some models/checkpoints/vae or custom comfyui nodes (uncomment the commands for the ones you want)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "dddddddddd"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Checkpoints\n",
|
||||
"\n",
|
||||
"### SDXL\n",
|
||||
"### I recommend these workflow examples: https://comfyanonymous.github.io/ComfyUI_examples/sdxl/\n",
|
||||
"\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors -P ./models/checkpoints/\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-refiner-1.0/resolve/main/sd_xl_refiner_1.0.safetensors -P ./models/checkpoints/\n",
|
||||
"\n",
|
||||
"# SDXL ReVision\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/clip_vision_g/resolve/main/clip_vision_g.safetensors -P ./models/clip_vision/\n",
|
||||
"\n",
|
||||
"# SD1.5\n",
|
||||
"!wget -c https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.ckpt -P ./models/checkpoints/\n",
|
||||
"\n",
|
||||
"# SD2\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-2-1-base/resolve/main/v2-1_512-ema-pruned.safetensors -P ./models/checkpoints/\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-2-1/resolve/main/v2-1_768-ema-pruned.safetensors -P ./models/checkpoints/\n",
|
||||
"\n",
|
||||
"# Some SD1.5 anime style\n",
|
||||
"#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix2/AbyssOrangeMix2_hard.safetensors -P ./models/checkpoints/\n",
|
||||
"#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix3/AOM3A1_orangemixs.safetensors -P ./models/checkpoints/\n",
|
||||
"#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix3/AOM3A3_orangemixs.safetensors -P ./models/checkpoints/\n",
|
||||
"#!wget -c https://huggingface.co/Linaqruf/anything-v3.0/resolve/main/anything-v3-fp16-pruned.safetensors -P ./models/checkpoints/\n",
|
||||
"\n",
|
||||
"# Waifu Diffusion 1.5 (anime style SD2.x 768-v)\n",
|
||||
"#!wget -c https://huggingface.co/waifu-diffusion/wd-1-5-beta3/resolve/main/wd-illusion-fp16.safetensors -P ./models/checkpoints/\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# unCLIP models\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/illuminatiDiffusionV1_v11_unCLIP/resolve/main/illuminatiDiffusionV1_v11-unclip-h-fp16.safetensors -P ./models/checkpoints/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/wd-1.5-beta2_unCLIP/resolve/main/wd-1-5-beta2-aesthetic-unclip-h-fp16.safetensors -P ./models/checkpoints/\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# VAE\n",
|
||||
"!wget -c https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.safetensors -P ./models/vae/\n",
|
||||
"#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/VAEs/orangemix.vae.pt -P ./models/vae/\n",
|
||||
"#!wget -c https://huggingface.co/hakurei/waifu-diffusion-v1-4/resolve/main/vae/kl-f8-anime2.ckpt -P ./models/vae/\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# Loras\n",
|
||||
"#!wget -c https://civitai.com/api/download/models/10350 -O ./models/loras/theovercomer8sContrastFix_sd21768.safetensors #theovercomer8sContrastFix SD2.x 768-v\n",
|
||||
"#!wget -c https://civitai.com/api/download/models/10638 -O ./models/loras/theovercomer8sContrastFix_sd15.safetensors #theovercomer8sContrastFix SD1.x\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_offset_example-lora_1.0.safetensors -P ./models/loras/ #SDXL offset noise lora\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# T2I-Adapter\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_depth_sd14v1.pth -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_seg_sd14v1.pth -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_sketch_sd14v1.pth -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_keypose_sd14v1.pth -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_openpose_sd14v1.pth -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_color_sd14v1.pth -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_canny_sd14v1.pth -P ./models/controlnet/\n",
|
||||
"\n",
|
||||
"# T2I Styles Model\n",
|
||||
"#!wget -c https://huggingface.co/TencentARC/T2I-Adapter/resolve/main/models/t2iadapter_style_sd14v1.pth -P ./models/style_models/\n",
|
||||
"\n",
|
||||
"# CLIPVision model (needed for styles model)\n",
|
||||
"#!wget -c https://huggingface.co/openai/clip-vit-large-patch14/resolve/main/pytorch_model.bin -O ./models/clip_vision/clip_vit14.bin\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# ControlNet\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11e_sd15_ip2p_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11e_sd15_shuffle_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_canny_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11f1p_sd15_depth_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_inpaint_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_lineart_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_mlsd_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_normalbae_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_openpose_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_scribble_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_seg_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_softedge_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15s2_lineart_anime_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11u_sd15_tile_fp16.safetensors -P ./models/controlnet/\n",
|
||||
"\n",
|
||||
"# ControlNet SDXL\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-canny-rank256.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-depth-rank256.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-recolor-rank256.safetensors -P ./models/controlnet/\n",
|
||||
"#!wget -c https://huggingface.co/stabilityai/control-lora/resolve/main/control-LoRAs-rank256/control-lora-sketch-rank256.safetensors -P ./models/controlnet/\n",
|
||||
"\n",
|
||||
"# Controlnet Preprocessor nodes by Fannovel16\n",
|
||||
"#!cd custom_nodes && git clone https://github.com/Fannovel16/comfy_controlnet_preprocessors; cd comfy_controlnet_preprocessors && python install.py\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# GLIGEN\n",
|
||||
"#!wget -c https://huggingface.co/comfyanonymous/GLIGEN_pruned_safetensors/resolve/main/gligen_sd14_textbox_pruned_fp16.safetensors -P ./models/gligen/\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# ESRGAN upscale model\n",
|
||||
"#!wget -c https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth -P ./models/upscale_models/\n",
|
||||
"#!wget -c https://huggingface.co/sberbank-ai/Real-ESRGAN/resolve/main/RealESRGAN_x2.pth -P ./models/upscale_models/\n",
|
||||
"#!wget -c https://huggingface.co/sberbank-ai/Real-ESRGAN/resolve/main/RealESRGAN_x4.pth -P ./models/upscale_models/\n",
|
||||
"\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "kkkkkkkkkkkkkkk"
|
||||
},
|
||||
"source": [
|
||||
"### Run ComfyUI with cloudflared (Recommended Way)\n",
|
||||
"\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "jjjjjjjjjjjjjj"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!wget -P ~ https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb\n",
|
||||
"!dpkg -i ~/cloudflared-linux-amd64.deb\n",
|
||||
"\n",
|
||||
"import subprocess\n",
|
||||
"import threading\n",
|
||||
"import time\n",
|
||||
"import socket\n",
|
||||
"import urllib.request\n",
|
||||
"\n",
|
||||
"def iframe_thread(port):\n",
|
||||
" while True:\n",
|
||||
" time.sleep(0.5)\n",
|
||||
" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
|
||||
" result = sock.connect_ex(('127.0.0.1', port))\n",
|
||||
" if result == 0:\n",
|
||||
" break\n",
|
||||
" sock.close()\n",
|
||||
" print(\"\\nComfyUI finished loading, trying to launch cloudflared (if it gets stuck here cloudflared is having issues)\\n\")\n",
|
||||
"\n",
|
||||
" p = subprocess.Popen([\"cloudflared\", \"tunnel\", \"--url\", \"http://127.0.0.1:{}\".format(port)], stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n",
|
||||
" for line in p.stderr:\n",
|
||||
" l = line.decode()\n",
|
||||
" if \"trycloudflare.com \" in l:\n",
|
||||
" print(\"This is the URL to access ComfyUI:\", l[l.find(\"http\"):], end='')\n",
|
||||
" #print(l, end='')\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
|
||||
"\n",
|
||||
"!python main.py --dont-print-server"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "kkkkkkkkkkkkkk"
|
||||
},
|
||||
"source": [
|
||||
"### Run ComfyUI with localtunnel\n",
|
||||
"\n",
|
||||
"\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "jjjjjjjjjjjjj"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!npm install -g localtunnel\n",
|
||||
"\n",
|
||||
"import subprocess\n",
|
||||
"import threading\n",
|
||||
"import time\n",
|
||||
"import socket\n",
|
||||
"import urllib.request\n",
|
||||
"\n",
|
||||
"def iframe_thread(port):\n",
|
||||
" while True:\n",
|
||||
" time.sleep(0.5)\n",
|
||||
" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
|
||||
" result = sock.connect_ex(('127.0.0.1', port))\n",
|
||||
" if result == 0:\n",
|
||||
" break\n",
|
||||
" sock.close()\n",
|
||||
" print(\"\\nComfyUI finished loading, trying to launch localtunnel (if it gets stuck here localtunnel is having issues)\\n\")\n",
|
||||
"\n",
|
||||
" print(\"The password/enpoint ip for localtunnel is:\", urllib.request.urlopen('https://ipv4.icanhazip.com').read().decode('utf8').strip(\"\\n\"))\n",
|
||||
" p = subprocess.Popen([\"lt\", \"--port\", \"{}\".format(port)], stdout=subprocess.PIPE)\n",
|
||||
" for line in p.stdout:\n",
|
||||
" print(line.decode(), end='')\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
|
||||
"\n",
|
||||
"!python main.py --dont-print-server"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"id": "gggggggggg"
|
||||
},
|
||||
"source": [
|
||||
"### Run ComfyUI with colab iframe (use only in case the previous way with localtunnel doesn't work)\n",
|
||||
"\n",
|
||||
"You should see the ui appear in an iframe. If you get a 403 error, it's your firefox settings or an extension that's messing things up.\n",
|
||||
"\n",
|
||||
"If you want to open it in another window use the link.\n",
|
||||
"\n",
|
||||
"Note that some UI features like live image previews won't work because the colab iframe blocks websockets."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"id": "hhhhhhhhhh"
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import threading\n",
|
||||
"import time\n",
|
||||
"import socket\n",
|
||||
"def iframe_thread(port):\n",
|
||||
" while True:\n",
|
||||
" time.sleep(0.5)\n",
|
||||
" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n",
|
||||
" result = sock.connect_ex(('127.0.0.1', port))\n",
|
||||
" if result == 0:\n",
|
||||
" break\n",
|
||||
" sock.close()\n",
|
||||
" from google.colab import output\n",
|
||||
" output.serve_kernel_port_as_iframe(port, height=1024)\n",
|
||||
" print(\"to open it in a window you can open this link here:\")\n",
|
||||
" output.serve_kernel_port_as_window(port)\n",
|
||||
"\n",
|
||||
"threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
|
||||
"\n",
|
||||
"!python main.py --dont-print-server"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"accelerator": "GPU",
|
||||
"colab": {
|
||||
"provenance": []
|
||||
},
|
||||
"gpuClass": "standard",
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0
|
||||
}
|
||||
|
||||
903
openapi.yaml
Normal file
@ -0,0 +1,903 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: ComfyUI-Manager API
|
||||
description: |
|
||||
API for ComfyUI-Manager, a comprehensive management tool for ComfyUI custom nodes, models, and components.
|
||||
This API enables programmatic access to node management, model downloading, snapshot operations,
|
||||
and overall system configuration.
|
||||
version: "3.32.3"
|
||||
contact:
|
||||
name: ComfyUI-Manager Maintainers
|
||||
servers:
|
||||
- url: '/'
|
||||
description: Default ComfyUI server
|
||||
|
||||
# Common API components
|
||||
components:
|
||||
schemas:
|
||||
Error:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Error message
|
||||
|
||||
NodePackageMetadata:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
description: Display name of the node package
|
||||
name:
|
||||
type: string
|
||||
description: Repository/package name
|
||||
files:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Source URLs for the package
|
||||
description:
|
||||
type: string
|
||||
description: Description of the node package functionality
|
||||
install_type:
|
||||
type: string
|
||||
enum: [git, copy, pip]
|
||||
description: Installation method
|
||||
version:
|
||||
type: string
|
||||
description: Version identifier
|
||||
id:
|
||||
type: string
|
||||
description: Unique identifier for the node package
|
||||
ui_id:
|
||||
type: string
|
||||
description: ID for UI reference
|
||||
channel:
|
||||
type: string
|
||||
description: Source channel
|
||||
mode:
|
||||
type: string
|
||||
description: Source mode
|
||||
|
||||
ModelMetadata:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Name of the model
|
||||
type:
|
||||
type: string
|
||||
description: Type of model
|
||||
base:
|
||||
type: string
|
||||
description: Base model type
|
||||
save_path:
|
||||
type: string
|
||||
description: Path for saving the model
|
||||
url:
|
||||
type: string
|
||||
description: Download URL
|
||||
filename:
|
||||
type: string
|
||||
description: Target filename
|
||||
ui_id:
|
||||
type: string
|
||||
description: ID for UI reference
|
||||
|
||||
SnapshotItem:
|
||||
type: string
|
||||
description: Name of the snapshot
|
||||
|
||||
QueueStatus:
|
||||
type: object
|
||||
properties:
|
||||
total_count:
|
||||
type: integer
|
||||
description: Total number of tasks
|
||||
done_count:
|
||||
type: integer
|
||||
description: Number of completed tasks
|
||||
in_progress_count:
|
||||
type: integer
|
||||
description: Number of tasks in progress
|
||||
is_processing:
|
||||
type: boolean
|
||||
description: Whether the queue is currently processing
|
||||
|
||||
ImportFailInfoBulkRequest:
|
||||
type: object
|
||||
properties:
|
||||
cnr_ids:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: A list of CNR IDs to check.
|
||||
urls:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: A list of repository URLs to check.
|
||||
|
||||
ImportFailInfoBulkResponse:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/ImportFailInfoItem'
|
||||
description: >-
|
||||
A dictionary where each key is a cnr_id or url from the request,
|
||||
and the value is the corresponding error info.
|
||||
|
||||
ImportFailInfoItem:
|
||||
oneOf:
|
||||
- type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
traceback:
|
||||
type: string
|
||||
- type: "null"
|
||||
|
||||
securitySchemes:
|
||||
securityLevel:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: Security-Level
|
||||
description: Security level for sensitive operations
|
||||
|
||||
parameters:
|
||||
modeParam:
|
||||
name: mode
|
||||
in: query
|
||||
description: Source mode (e.g., "local", "remote")
|
||||
schema:
|
||||
type: string
|
||||
enum: [local, remote, default]
|
||||
|
||||
targetParam:
|
||||
name: target
|
||||
in: query
|
||||
description: Target identifier
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
|
||||
valueParam:
|
||||
name: value
|
||||
in: query
|
||||
description: New value to set
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
|
||||
# API Paths
|
||||
paths:
|
||||
# Custom Nodes Endpoints
|
||||
/customnode/getmappings:
|
||||
get:
|
||||
summary: Get node-to-package mappings
|
||||
description: Provides unified mapping between nodes and node packages
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/modeParam'
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: array
|
||||
items:
|
||||
type: array
|
||||
description: Mapping of node packages to node classes
|
||||
|
||||
/customnode/fetch_updates:
|
||||
get:
|
||||
summary: Check for updates
|
||||
description: Fetches updates for custom nodes
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/modeParam'
|
||||
responses:
|
||||
'200':
|
||||
description: No updates available
|
||||
'201':
|
||||
description: Updates found
|
||||
'400':
|
||||
description: Error occurred
|
||||
|
||||
/customnode/installed:
|
||||
get:
|
||||
summary: Get installed custom nodes
|
||||
description: Returns a list of installed node packages
|
||||
parameters:
|
||||
- name: mode
|
||||
in: query
|
||||
description: Lists mode, default or imported
|
||||
schema:
|
||||
type: string
|
||||
enum: [default, imported]
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/NodePackageMetadata'
|
||||
|
||||
/customnode/getlist:
|
||||
get:
|
||||
summary: Get custom node list
|
||||
description: Provides a list of available custom nodes
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/modeParam'
|
||||
- name: skip_update
|
||||
in: query
|
||||
description: Skip update check
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
channel:
|
||||
type: string
|
||||
node_packs:
|
||||
type: object
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/NodePackageMetadata'
|
||||
|
||||
/customnode/alternatives:
|
||||
get:
|
||||
summary: Get alternative node options
|
||||
description: Provides alternatives for nodes
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/modeParam'
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: object
|
||||
|
||||
/customnode/versions/{node_name}:
|
||||
get:
|
||||
summary: Get available versions for a node
|
||||
description: Lists all available versions for a specific node
|
||||
parameters:
|
||||
- name: node_name
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
'400':
|
||||
description: Node not found
|
||||
|
||||
/customnode/disabled_versions/{node_name}:
|
||||
get:
|
||||
summary: Get disabled versions for a node
|
||||
description: Lists all disabled versions for a specific node
|
||||
parameters:
|
||||
- name: node_name
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
'400':
|
||||
description: Node not found
|
||||
|
||||
/customnode/import_fail_info:
|
||||
post:
|
||||
summary: Get import failure information
|
||||
description: Returns information about why a node failed to import
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
cnr_id:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
'400':
|
||||
description: No information available
|
||||
|
||||
/v2/customnode/import_fail_info_bulk:
|
||||
post:
|
||||
summary: Get import failure info for multiple nodes
|
||||
description: Retrieves recorded import failure information for a list of custom nodes.
|
||||
tags:
|
||||
- customnode
|
||||
requestBody:
|
||||
description: A list of CNR IDs or repository URLs to check.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ImportFailInfoBulkRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: A dictionary containing the import failure information.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ImportFailInfoBulkResponse'
|
||||
'400':
|
||||
description: Bad Request. The request body is invalid.
|
||||
'500':
|
||||
description: Internal Server Error.
|
||||
|
||||
/customnode/install/git_url:
|
||||
post:
|
||||
summary: Install custom node via Git URL
|
||||
description: Installs a custom node from a Git repository URL
|
||||
security:
|
||||
- securityLevel: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Installation successful or already installed
|
||||
'400':
|
||||
description: Installation failed
|
||||
'403':
|
||||
description: Security policy violation
|
||||
|
||||
/customnode/install/pip:
|
||||
post:
|
||||
summary: Install custom node dependencies via pip
|
||||
description: Installs Python package dependencies for custom nodes
|
||||
security:
|
||||
- securityLevel: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Installation successful
|
||||
'403':
|
||||
description: Security policy violation
|
||||
|
||||
# Model Management Endpoints
|
||||
/externalmodel/getlist:
|
||||
get:
|
||||
summary: Get external model list
|
||||
description: Provides a list of available external models
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/modeParam'
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
models:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ModelMetadata'
|
||||
|
||||
# Queue Management Endpoints
|
||||
/manager/queue/update_all:
|
||||
get:
|
||||
summary: Update all custom nodes
|
||||
description: Queues update operations for all installed custom nodes
|
||||
security:
|
||||
- securityLevel: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/modeParam'
|
||||
responses:
|
||||
'200':
|
||||
description: Update queued successfully
|
||||
'401':
|
||||
description: Processing already in progress
|
||||
'403':
|
||||
description: Security policy violation
|
||||
|
||||
/manager/queue/reset:
|
||||
get:
|
||||
summary: Reset queue
|
||||
description: Resets the operation queue
|
||||
responses:
|
||||
'200':
|
||||
description: Queue reset successfully
|
||||
|
||||
/manager/queue/status:
|
||||
get:
|
||||
summary: Get queue status
|
||||
description: Returns the current status of the operation queue
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/QueueStatus'
|
||||
|
||||
/manager/queue/install:
|
||||
post:
|
||||
summary: Install custom node
|
||||
description: Queues installation of a custom node
|
||||
security:
|
||||
- securityLevel: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/NodePackageMetadata'
|
||||
responses:
|
||||
'200':
|
||||
description: Installation queued successfully
|
||||
'403':
|
||||
description: Security policy violation
|
||||
'404':
|
||||
description: Target node not found or security issue
|
||||
|
||||
/manager/queue/start:
|
||||
get:
|
||||
summary: Start queue processing
|
||||
description: Starts processing the operation queue
|
||||
responses:
|
||||
'200':
|
||||
description: Processing started
|
||||
'201':
|
||||
description: Processing already in progress
|
||||
|
||||
/manager/queue/fix:
|
||||
post:
|
||||
summary: Fix custom node
|
||||
description: Attempts to fix a broken custom node installation
|
||||
security:
|
||||
- securityLevel: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/NodePackageMetadata'
|
||||
responses:
|
||||
'200':
|
||||
description: Fix operation queued successfully
|
||||
'403':
|
||||
description: Security policy violation
|
||||
|
||||
/manager/queue/reinstall:
|
||||
post:
|
||||
summary: Reinstall custom node
|
||||
description: Uninstalls and then reinstalls a custom node
|
||||
security:
|
||||
- securityLevel: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/NodePackageMetadata'
|
||||
responses:
|
||||
'200':
|
||||
description: Reinstall operation queued successfully
|
||||
'403':
|
||||
description: Security policy violation
|
||||
|
||||
/manager/queue/uninstall:
|
||||
post:
|
||||
summary: Uninstall custom node
|
||||
description: Queues uninstallation of a custom node
|
||||
security:
|
||||
- securityLevel: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/NodePackageMetadata'
|
||||
responses:
|
||||
'200':
|
||||
description: Uninstallation queued successfully
|
||||
'403':
|
||||
description: Security policy violation
|
||||
|
||||
/manager/queue/update:
|
||||
post:
|
||||
summary: Update custom node
|
||||
description: Queues update of a custom node
|
||||
security:
|
||||
- securityLevel: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/NodePackageMetadata'
|
||||
responses:
|
||||
'200':
|
||||
description: Update queued successfully
|
||||
'403':
|
||||
description: Security policy violation
|
||||
|
||||
/manager/queue/disable:
|
||||
post:
|
||||
summary: Disable custom node
|
||||
description: Disables a custom node without uninstalling it
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/NodePackageMetadata'
|
||||
responses:
|
||||
'200':
|
||||
description: Disable operation queued successfully
|
||||
|
||||
/manager/queue/update_comfyui:
|
||||
get:
|
||||
summary: Update ComfyUI
|
||||
description: Queues an update operation for ComfyUI itself
|
||||
responses:
|
||||
'200':
|
||||
description: Update queued successfully
|
||||
|
||||
/manager/queue/install_model:
|
||||
post:
|
||||
summary: Install model
|
||||
description: Queues installation of a model
|
||||
security:
|
||||
- securityLevel: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ModelMetadata'
|
||||
responses:
|
||||
'200':
|
||||
description: Installation queued successfully
|
||||
'400':
|
||||
description: Invalid model request
|
||||
'403':
|
||||
description: Security policy violation
|
||||
|
||||
# Snapshot Management Endpoints
|
||||
/snapshot/getlist:
|
||||
get:
|
||||
summary: Get snapshot list
|
||||
description: Returns a list of available snapshots
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/SnapshotItem'
|
||||
|
||||
/snapshot/remove:
|
||||
get:
|
||||
summary: Remove snapshot
|
||||
description: Removes a specified snapshot
|
||||
security:
|
||||
- securityLevel: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/targetParam'
|
||||
responses:
|
||||
'200':
|
||||
description: Snapshot removed successfully
|
||||
'400':
|
||||
description: Error removing snapshot
|
||||
'403':
|
||||
description: Security policy violation
|
||||
|
||||
/snapshot/restore:
|
||||
get:
|
||||
summary: Restore snapshot
|
||||
description: Restores a specified snapshot
|
||||
security:
|
||||
- securityLevel: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/targetParam'
|
||||
responses:
|
||||
'200':
|
||||
description: Snapshot restoration scheduled
|
||||
'400':
|
||||
description: Error restoring snapshot
|
||||
'403':
|
||||
description: Security policy violation
|
||||
|
||||
/snapshot/get_current:
|
||||
get:
|
||||
summary: Get current snapshot
|
||||
description: Returns the current system state as a snapshot
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
'400':
|
||||
description: Error creating snapshot
|
||||
|
||||
/snapshot/save:
|
||||
get:
|
||||
summary: Save snapshot
|
||||
description: Saves the current system state as a new snapshot
|
||||
responses:
|
||||
'200':
|
||||
description: Snapshot saved successfully
|
||||
'400':
|
||||
description: Error saving snapshot
|
||||
|
||||
# ComfyUI Management Endpoints
|
||||
/comfyui_manager/comfyui_versions:
|
||||
get:
|
||||
summary: Get ComfyUI versions
|
||||
description: Returns available and current ComfyUI versions
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
versions:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
current:
|
||||
type: string
|
||||
'400':
|
||||
description: Error retrieving versions
|
||||
|
||||
/comfyui_manager/comfyui_switch_version:
|
||||
get:
|
||||
summary: Switch ComfyUI version
|
||||
description: Switches to a specified ComfyUI version
|
||||
parameters:
|
||||
- name: ver
|
||||
in: query
|
||||
description: Target version
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Version switch successful
|
||||
'400':
|
||||
description: Error switching version
|
||||
|
||||
/manager/reboot:
|
||||
get:
|
||||
summary: Reboot ComfyUI
|
||||
description: Restarts the ComfyUI server
|
||||
security:
|
||||
- securityLevel: []
|
||||
responses:
|
||||
'200':
|
||||
description: Reboot initiated
|
||||
'403':
|
||||
description: Security policy violation
|
||||
|
||||
# Configuration Endpoints
|
||||
/manager/preview_method:
|
||||
get:
|
||||
summary: Get or set preview method
|
||||
description: Gets or sets the latent preview method
|
||||
parameters:
|
||||
- name: value
|
||||
in: query
|
||||
required: false
|
||||
description: New preview method
|
||||
schema:
|
||||
type: string
|
||||
enum: [auto, latent2rgb, taesd, none]
|
||||
responses:
|
||||
'200':
|
||||
description: Setting updated or current value returned
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/manager/db_mode:
|
||||
get:
|
||||
summary: Get or set database mode
|
||||
description: Gets or sets the database mode
|
||||
parameters:
|
||||
- name: value
|
||||
in: query
|
||||
required: false
|
||||
description: New database mode
|
||||
schema:
|
||||
type: string
|
||||
enum: [channel, local, remote]
|
||||
responses:
|
||||
'200':
|
||||
description: Setting updated or current value returned
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/manager/policy/component:
|
||||
get:
|
||||
summary: Get or set component policy
|
||||
description: Gets or sets the component policy
|
||||
parameters:
|
||||
- name: value
|
||||
in: query
|
||||
required: false
|
||||
description: New component policy
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Setting updated or current value returned
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/manager/policy/update:
|
||||
get:
|
||||
summary: Get or set update policy
|
||||
description: Gets or sets the update policy
|
||||
parameters:
|
||||
- name: value
|
||||
in: query
|
||||
required: false
|
||||
description: New update policy
|
||||
schema:
|
||||
type: string
|
||||
enum: [stable, nightly, nightly-comfyui]
|
||||
responses:
|
||||
'200':
|
||||
description: Setting updated or current value returned
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/manager/channel_url_list:
|
||||
get:
|
||||
summary: Get or set channel URL
|
||||
description: Gets or sets the channel URL for custom node sources
|
||||
parameters:
|
||||
- name: value
|
||||
in: query
|
||||
required: false
|
||||
description: New channel name
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Setting updated or channel list returned
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
selected:
|
||||
type: string
|
||||
list:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
|
||||
# Component Management Endpoints
|
||||
/manager/component/save:
|
||||
post:
|
||||
summary: Save component
|
||||
description: Saves a reusable workflow component
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
workflow:
|
||||
type: object
|
||||
responses:
|
||||
'200':
|
||||
description: Component saved successfully
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
'400':
|
||||
description: Error saving component
|
||||
|
||||
/manager/component/loads:
|
||||
post:
|
||||
summary: Load components
|
||||
description: Loads all available workflow components
|
||||
responses:
|
||||
'200':
|
||||
description: Components loaded successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
'400':
|
||||
description: Error loading components
|
||||
|
||||
# Miscellaneous Endpoints
|
||||
/manager/version:
|
||||
get:
|
||||
summary: Get manager version
|
||||
description: Returns the current version of ComfyUI-Manager
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/manager/notice:
|
||||
get:
|
||||
summary: Get manager notice
|
||||
description: Returns HTML content with notices and version information
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
type: string
|
||||
@ -4,8 +4,8 @@
|
||||
"imageio_ffmpeg": "imageio",
|
||||
"diffusers~=0.21.4": "diffusers",
|
||||
"huggingface_hub": "huggingface-hub",
|
||||
"numpy<1.24>=1.18": "numpy",
|
||||
"numpy>=1.18.5, <1.25.0": "numpy",
|
||||
"numpy<1.24>=1.18": "numpy==1.26.4",
|
||||
"numpy>=1.18.5, <1.25.0": "numpy==1.26.4",
|
||||
"opencv-contrib-python": "opencv-contrib-python-headless",
|
||||
"opencv-python": "opencv-contrib-python-headless",
|
||||
"opencv-python-headless": "opencv-contrib-python-headless",
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
"imageio_ffmpeg": "imageio",
|
||||
"diffusers~=0.21.4": "diffusers",
|
||||
"huggingface_hub": "huggingface-hub",
|
||||
"numpy<1.24>=1.18": "numpy",
|
||||
"numpy>=1.18.5, <1.25.0": "numpy",
|
||||
"numpy<1.24>=1.18": "numpy==1.26.4",
|
||||
"numpy>=1.18.5, <1.25.0": "numpy==1.26.4",
|
||||
"opencv-contrib-python": "opencv-contrib-python-headless",
|
||||
"opencv-python": "opencv-contrib-python-headless",
|
||||
"opencv-python-headless": "opencv-contrib-python-headless",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import datetime
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import atexit
|
||||
@ -10,17 +10,37 @@ import platform
|
||||
import json
|
||||
import ast
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
glob_path = os.path.join(os.path.dirname(__file__), "glob")
|
||||
sys.path.append(glob_path)
|
||||
|
||||
import security_check
|
||||
from manager_util import *
|
||||
import manager_util
|
||||
import cm_global
|
||||
import manager_downloader
|
||||
import folder_paths
|
||||
|
||||
security_check.security_check()
|
||||
manager_util.add_python_path_to_env()
|
||||
|
||||
cm_global.pip_downgrade_blacklist = ['torch', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']
|
||||
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]
|
||||
|
||||
|
||||
cm_global.pip_blacklist = {'torch', 'torchaudio', 'torchsde', 'torchvision'}
|
||||
cm_global.pip_downgrade_blacklist = ['torch', 'torchaudio', 'torchsde', 'torchvision', 'transformers', 'safetensors', 'kornia']
|
||||
|
||||
|
||||
def skip_pip_spam(x):
|
||||
@ -43,46 +63,85 @@ def is_import_failed_extension(name):
|
||||
return name in import_failed_extensions
|
||||
|
||||
|
||||
def check_file_logging():
|
||||
global enable_file_logging
|
||||
try:
|
||||
import configparser
|
||||
config_path = os.path.join(os.path.dirname(__file__), "config.ini")
|
||||
config = configparser.ConfigParser()
|
||||
config.read(config_path)
|
||||
default_conf = config['default']
|
||||
|
||||
if 'file_logging' in default_conf and default_conf['file_logging'].lower() == 'false':
|
||||
enable_file_logging = False
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
check_file_logging()
|
||||
|
||||
comfy_path = os.environ.get('COMFYUI_PATH')
|
||||
comfy_base_path = os.environ.get('COMFYUI_FOLDERS_BASE_PATH')
|
||||
|
||||
if comfy_path is None:
|
||||
# legacy env var
|
||||
comfy_path = os.environ.get('COMFYUI_PATH')
|
||||
|
||||
if comfy_path is None:
|
||||
comfy_path = os.path.abspath(os.path.dirname(sys.modules['__main__'].__file__))
|
||||
|
||||
if comfy_base_path is None:
|
||||
comfy_base_path = comfy_path
|
||||
|
||||
sys.__comfyui_manager_register_message_collapse = register_message_collapse
|
||||
sys.__comfyui_manager_is_import_failed_extension = is_import_failed_extension
|
||||
cm_global.register_api('cm.register_message_collapse', register_message_collapse)
|
||||
cm_global.register_api('cm.is_import_failed_extension', is_import_failed_extension)
|
||||
|
||||
|
||||
comfyui_manager_path = os.path.dirname(__file__)
|
||||
custom_nodes_path = os.path.abspath(os.path.join(comfyui_manager_path, ".."))
|
||||
startup_script_path = os.path.join(comfyui_manager_path, "startup-scripts")
|
||||
restore_snapshot_path = os.path.join(startup_script_path, "restore-snapshot.json")
|
||||
git_script_path = os.path.join(comfyui_manager_path, "git_helper.py")
|
||||
pip_overrides_path = os.path.join(comfyui_manager_path, "pip_overrides.json")
|
||||
comfyui_manager_path = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
custom_nodes_base_path = folder_paths.get_folder_paths('custom_nodes')[0]
|
||||
|
||||
# Check for System User API availability (PR #10966)
|
||||
_has_system_user_api = hasattr(folder_paths, 'get_system_user_directory')
|
||||
|
||||
if _has_system_user_api:
|
||||
manager_files_path = os.path.abspath(os.path.join(folder_paths.get_user_directory(), '__manager'))
|
||||
else:
|
||||
manager_files_path = os.path.abspath(os.path.join(folder_paths.get_user_directory(), 'default', 'ComfyUI-Manager'))
|
||||
|
||||
manager_pip_overrides_path = os.path.join(manager_files_path, "pip_overrides.json")
|
||||
manager_pip_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")
|
||||
manager_config_path = os.path.join(manager_files_path, 'config.ini')
|
||||
|
||||
cm_cli_path = os.path.join(comfyui_manager_path, "cm-cli.py")
|
||||
|
||||
|
||||
default_conf = {}
|
||||
|
||||
def read_config():
|
||||
global default_conf
|
||||
try:
|
||||
import configparser
|
||||
config = configparser.ConfigParser(strict=False)
|
||||
config.read(manager_config_path)
|
||||
default_conf = config['default']
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def read_uv_mode():
|
||||
if 'use_uv' in default_conf:
|
||||
manager_util.use_uv = default_conf['use_uv'].lower() == 'true'
|
||||
|
||||
def check_file_logging():
|
||||
global enable_file_logging
|
||||
if 'file_logging' in default_conf and default_conf['file_logging'].lower() == 'false':
|
||||
enable_file_logging = False
|
||||
|
||||
|
||||
read_config()
|
||||
read_uv_mode()
|
||||
security_check.security_check()
|
||||
check_file_logging()
|
||||
|
||||
cm_global.pip_overrides = {}
|
||||
if os.path.exists(pip_overrides_path):
|
||||
with open(pip_overrides_path, 'r', encoding="UTF-8", errors="ignore") as json_file:
|
||||
|
||||
if os.path.exists(manager_pip_overrides_path):
|
||||
with open(manager_pip_overrides_path, 'r', encoding="UTF-8", errors="ignore") as json_file:
|
||||
cm_global.pip_overrides = json.load(json_file)
|
||||
cm_global.pip_overrides['numpy'] = 'numpy<2'
|
||||
|
||||
|
||||
if os.path.exists(manager_pip_blacklist_path):
|
||||
with open(manager_pip_blacklist_path, 'r', encoding="UTF-8", errors="ignore") as f:
|
||||
for x in f.readlines():
|
||||
y = x.strip()
|
||||
if y != '':
|
||||
cm_global.pip_blacklist.add(y)
|
||||
|
||||
|
||||
def remap_pip_package(pkg):
|
||||
@ -97,36 +156,6 @@ def remap_pip_package(pkg):
|
||||
std_log_lock = threading.Lock()
|
||||
|
||||
|
||||
class TerminalHook:
|
||||
def __init__(self):
|
||||
self.hooks = {}
|
||||
|
||||
def add_hook(self, k, v):
|
||||
self.hooks[k] = v
|
||||
|
||||
def remove_hook(self, k):
|
||||
if k in self.hooks:
|
||||
del self.hooks[k]
|
||||
|
||||
def write_stderr(self, msg):
|
||||
for v in self.hooks.values():
|
||||
try:
|
||||
v.write_stderr(msg)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def write_stdout(self, msg):
|
||||
for v in self.hooks.values():
|
||||
try:
|
||||
v.write_stdout(msg)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
terminal_hook = TerminalHook()
|
||||
sys.__comfyui_manager_terminal_hook = terminal_hook
|
||||
|
||||
|
||||
def handle_stream(stream, prefix):
|
||||
stream.reconfigure(encoding=locale.getpreferredencoding(), errors='replace')
|
||||
for msg in stream:
|
||||
@ -160,6 +189,48 @@ def process_wrap(cmd_str, cwd_path, handler=None, env=None):
|
||||
return process.wait()
|
||||
|
||||
|
||||
original_stdout = sys.stdout
|
||||
|
||||
|
||||
def try_get_custom_nodes(x):
|
||||
for custom_nodes_dir in folder_paths.get_folder_paths('custom_nodes'):
|
||||
if x.startswith(custom_nodes_dir):
|
||||
relative_path = os.path.relpath(x, custom_nodes_dir)
|
||||
next_segment = relative_path.split(os.sep)[0]
|
||||
if next_segment.lower() != 'comfyui-manager':
|
||||
return next_segment, os.path.join(custom_nodes_dir, next_segment)
|
||||
return None
|
||||
|
||||
|
||||
def extract_origin_module():
|
||||
stack = traceback.extract_stack()[:-2]
|
||||
for frame in reversed(stack):
|
||||
info = try_get_custom_nodes(frame.filename)
|
||||
if info is None:
|
||||
continue
|
||||
else:
|
||||
return info
|
||||
return None
|
||||
|
||||
def extract_origin_module_from_strings(file_paths):
|
||||
for filepath in file_paths:
|
||||
info = try_get_custom_nodes(filepath)
|
||||
if info is None:
|
||||
continue
|
||||
else:
|
||||
return info
|
||||
return None
|
||||
|
||||
|
||||
def finalize_startup():
|
||||
res = {}
|
||||
for k, v in cm_global.error_dict.items():
|
||||
if v['path'] in import_failed_extensions:
|
||||
res[k] = v
|
||||
|
||||
cm_global.error_dict = res
|
||||
|
||||
|
||||
try:
|
||||
if '--port' in sys.argv:
|
||||
port_index = sys.argv.index('--port')
|
||||
@ -172,15 +243,21 @@ try:
|
||||
postfix = ""
|
||||
|
||||
# Logger setup
|
||||
log_path_base = None
|
||||
if enable_file_logging:
|
||||
if os.path.exists(f"comfyui{postfix}.log"):
|
||||
if os.path.exists(f"comfyui{postfix}.prev.log"):
|
||||
if os.path.exists(f"comfyui{postfix}.prev2.log"):
|
||||
os.remove(f"comfyui{postfix}.prev2.log")
|
||||
os.rename(f"comfyui{postfix}.prev.log", f"comfyui{postfix}.prev2.log")
|
||||
os.rename(f"comfyui{postfix}.log", f"comfyui{postfix}.prev.log")
|
||||
log_path_base = os.path.join(folder_paths.user_directory, 'comfyui')
|
||||
|
||||
log_file = open(f"comfyui{postfix}.log", "w", encoding="utf-8", errors="ignore")
|
||||
if not os.path.exists(folder_paths.user_directory):
|
||||
os.makedirs(folder_paths.user_directory)
|
||||
|
||||
if os.path.exists(f"{log_path_base}{postfix}.log"):
|
||||
if os.path.exists(f"{log_path_base}{postfix}.prev.log"):
|
||||
if os.path.exists(f"{log_path_base}{postfix}.prev2.log"):
|
||||
os.remove(f"{log_path_base}{postfix}.prev2.log")
|
||||
os.rename(f"{log_path_base}{postfix}.prev.log", f"{log_path_base}{postfix}.prev2.log")
|
||||
os.rename(f"{log_path_base}{postfix}.log", f"{log_path_base}{postfix}.prev.log")
|
||||
|
||||
log_file = open(f"{log_path_base}{postfix}.log", "w", encoding="utf-8", errors="ignore")
|
||||
|
||||
log_lock = threading.Lock()
|
||||
|
||||
@ -201,7 +278,7 @@ try:
|
||||
write_stderr = wrapper_stderr
|
||||
|
||||
pat_tqdm = r'\d+%.*\[(.*?)\]'
|
||||
pat_import_fail = r'seconds \(IMPORT FAILED\):.*[/\\]custom_nodes[/\\](.*)$'
|
||||
pat_import_fail = r'seconds \(IMPORT FAILED\):(.*)$'
|
||||
|
||||
is_start_mode = True
|
||||
|
||||
@ -234,10 +311,18 @@ try:
|
||||
if is_start_mode:
|
||||
match = re.search(pat_import_fail, message)
|
||||
if match:
|
||||
import_failed_extensions.add(match.group(1))
|
||||
import_failed_extensions.add(match.group(1).strip())
|
||||
|
||||
if 'Starting server' in message:
|
||||
is_start_mode = False
|
||||
if not self.is_stdout:
|
||||
origin_info = extract_origin_module()
|
||||
if origin_info is not None:
|
||||
name, origin_path = origin_info
|
||||
|
||||
if name != 'comfyui-manager':
|
||||
if name not in cm_global.error_dict:
|
||||
cm_global.error_dict[name] = {'name': name, 'path': origin_path, 'msg': ''}
|
||||
|
||||
cm_global.error_dict[name]['msg'] += message
|
||||
|
||||
if not self.is_stdout:
|
||||
match = re.search(pat_tqdm, message)
|
||||
@ -256,12 +341,17 @@ try:
|
||||
|
||||
def sync_write(self, message, file_only=False):
|
||||
with log_lock:
|
||||
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')[:-3]
|
||||
timestamp = current_timestamp()
|
||||
if self.last_char != '\n':
|
||||
log_file.write(message)
|
||||
else:
|
||||
log_file.write(f"[{timestamp}] {message}")
|
||||
log_file.flush()
|
||||
|
||||
try:
|
||||
log_file.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.last_char = message if message == '' else message[-1]
|
||||
|
||||
if not file_only:
|
||||
@ -269,14 +359,15 @@ try:
|
||||
if self.is_stdout:
|
||||
write_stdout(message)
|
||||
original_stdout.flush()
|
||||
terminal_hook.write_stderr(message)
|
||||
else:
|
||||
write_stderr(message)
|
||||
original_stderr.flush()
|
||||
terminal_hook.write_stdout(message)
|
||||
|
||||
def flush(self):
|
||||
log_file.flush()
|
||||
try:
|
||||
log_file.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
with std_log_lock:
|
||||
if self.is_stdout:
|
||||
@ -322,14 +413,36 @@ try:
|
||||
if is_start_mode:
|
||||
match = re.search(pat_import_fail, message)
|
||||
if match:
|
||||
import_failed_extensions.add(match.group(1))
|
||||
import_failed_extensions.add(match.group(1).strip())
|
||||
|
||||
if 'Traceback' in message:
|
||||
file_lists = self._extract_file_paths(message)
|
||||
origin_info = extract_origin_module_from_strings(file_lists)
|
||||
if origin_info is not None:
|
||||
name, origin_path = origin_info
|
||||
|
||||
if name != 'comfyui-manager':
|
||||
if name not in cm_global.error_dict:
|
||||
cm_global.error_dict[name] = {'name': name, 'path': origin_path, 'msg': ''}
|
||||
|
||||
cm_global.error_dict[name]['msg'] += message
|
||||
|
||||
if 'Starting server' in message:
|
||||
is_start_mode = False
|
||||
finalize_startup()
|
||||
|
||||
if stderr_wrapper:
|
||||
stderr_wrapper.sync_write(message+'\n', file_only=True)
|
||||
|
||||
def _extract_file_paths(self, msg):
|
||||
file_paths = []
|
||||
for line in msg.split('\n'):
|
||||
match = re.findall(r'File \"(.*?)\", line \d+', line)
|
||||
for x in match:
|
||||
if not x.startswith('<'):
|
||||
file_paths.extend(match)
|
||||
return file_paths
|
||||
|
||||
|
||||
logging.getLogger().addHandler(LoggingHandler())
|
||||
|
||||
@ -338,50 +451,53 @@ except Exception as e:
|
||||
print(f"[ComfyUI-Manager] Logging failed: {e}")
|
||||
|
||||
|
||||
try:
|
||||
import git
|
||||
except ModuleNotFoundError:
|
||||
my_path = os.path.dirname(__file__)
|
||||
requirements_path = os.path.join(my_path, "requirements.txt")
|
||||
|
||||
print(f"## ComfyUI-Manager: installing dependencies. (GitPython)")
|
||||
def ensure_dependencies():
|
||||
try:
|
||||
result = subprocess.check_output([sys.executable, '-s', '-m', 'pip', 'install', '-r', requirements_path])
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"## [ERROR] ComfyUI-Manager: Attempting to reinstall dependencies using an alternative method.")
|
||||
import git # noqa: F401
|
||||
import toml # noqa: F401
|
||||
import rich # noqa: F401
|
||||
import chardet # noqa: F401
|
||||
except ModuleNotFoundError:
|
||||
my_path = os.path.dirname(__file__)
|
||||
requirements_path = os.path.join(my_path, "requirements.txt")
|
||||
|
||||
print("## ComfyUI-Manager: installing dependencies. (GitPython)")
|
||||
try:
|
||||
result = subprocess.check_output([sys.executable, '-s', '-m', 'pip', 'install', '--user', '-r', requirements_path])
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"## [ERROR] ComfyUI-Manager: Failed to install the GitPython package in the correct Python environment. Please install it manually in the appropriate environment. (You can seek help at https://app.element.io/#/room/%23comfyui_space%3Amatrix.org)")
|
||||
subprocess.check_output(manager_util.make_pip_cmd(['install', '-r', requirements_path]))
|
||||
except subprocess.CalledProcessError:
|
||||
print("## [ERROR] ComfyUI-Manager: Attempting to reinstall dependencies using an alternative method.")
|
||||
try:
|
||||
subprocess.check_output(manager_util.make_pip_cmd(['install', '--user', '-r', requirements_path]))
|
||||
except subprocess.CalledProcessError:
|
||||
print("## [ERROR] ComfyUI-Manager: Failed to install the GitPython package in the correct Python environment. Please install it manually in the appropriate environment. (You can seek help at https://app.element.io/#/room/%23comfyui_space%3Amatrix.org)")
|
||||
|
||||
try:
|
||||
import git
|
||||
print(f"## ComfyUI-Manager: installing dependencies done.")
|
||||
except:
|
||||
# maybe we should sys.exit() here? there is at least two screens worth of error messages still being pumped after our error messages
|
||||
print(f"## [ERROR] ComfyUI-Manager: GitPython package seems to be installed, but failed to load somehow. Make sure you have a working git client installed")
|
||||
try:
|
||||
print("## ComfyUI-Manager: installing dependencies done.")
|
||||
except:
|
||||
# maybe we should sys.exit() here? there is at least two screens worth of error messages still being pumped after our error messages
|
||||
print("## [ERROR] ComfyUI-Manager: GitPython package seems to be installed, but failed to load somehow. Make sure you have a working git client installed")
|
||||
|
||||
ensure_dependencies()
|
||||
|
||||
|
||||
print("** ComfyUI startup time:", datetime.datetime.now())
|
||||
print("** ComfyUI startup time:", current_timestamp())
|
||||
print("** Platform:", platform.system())
|
||||
print("** Python version:", sys.version)
|
||||
print("** Python executable:", sys.executable)
|
||||
print("** ComfyUI Path:", comfy_path)
|
||||
print("** ComfyUI Base Folder Path:", comfy_base_path)
|
||||
print("** User directory:", folder_paths.user_directory)
|
||||
print("** ComfyUI-Manager config path:", manager_config_path)
|
||||
|
||||
if enable_file_logging:
|
||||
print("** Log path:", os.path.abspath('comfyui.log'))
|
||||
|
||||
if log_path_base is not None:
|
||||
print("** Log path:", os.path.abspath(f'{log_path_base}.log'))
|
||||
else:
|
||||
print("** Log path: file logging is disabled")
|
||||
|
||||
|
||||
def read_downgrade_blacklist():
|
||||
try:
|
||||
import configparser
|
||||
config_path = os.path.join(os.path.dirname(__file__), "config.ini")
|
||||
config = configparser.ConfigParser()
|
||||
config.read(config_path)
|
||||
default_conf = config['default']
|
||||
|
||||
if 'downgrade_blacklist' in default_conf:
|
||||
items = default_conf['downgrade_blacklist'].split(',')
|
||||
items = [x.strip() for x in items if x != '']
|
||||
@ -396,50 +512,21 @@ read_downgrade_blacklist()
|
||||
|
||||
def check_bypass_ssl():
|
||||
try:
|
||||
import configparser
|
||||
import ssl
|
||||
config_path = os.path.join(os.path.dirname(__file__), "config.ini")
|
||||
config = configparser.ConfigParser()
|
||||
config.read(config_path)
|
||||
default_conf = config['default']
|
||||
|
||||
if 'bypass_ssl' in default_conf and default_conf['bypass_ssl'].lower() == 'true':
|
||||
print(f"[ComfyUI-Manager] WARN: Unsafe - SSL verification bypass option is Enabled. (see ComfyUI-Manager/config.ini)")
|
||||
print(f"[ComfyUI-Manager] WARN: Unsafe - SSL verification bypass option is Enabled. (see {manager_config_path})")
|
||||
ssl._create_default_https_context = ssl._create_unverified_context # SSL certificate error fix.
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
check_bypass_ssl()
|
||||
|
||||
|
||||
# Perform install
|
||||
processed_install = set()
|
||||
script_list_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "startup-scripts", "install-scripts.txt")
|
||||
pip_map = None
|
||||
|
||||
|
||||
def get_installed_packages():
|
||||
global pip_map
|
||||
|
||||
if pip_map is None:
|
||||
try:
|
||||
result = subprocess.check_output([sys.executable, '-m', 'pip', 'list'], universal_newlines=True)
|
||||
|
||||
pip_map = {}
|
||||
for line in result.split('\n'):
|
||||
x = line.strip()
|
||||
if x:
|
||||
y = line.split()
|
||||
if y[0] == 'Package' or y[0].startswith('-'):
|
||||
continue
|
||||
|
||||
pip_map[y[0]] = y[1]
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"[ComfyUI-Manager] Failed to retrieve the information of installed pip packages.")
|
||||
return set()
|
||||
|
||||
return pip_map
|
||||
# Use manager_files_path for consistency (fixes path inconsistency bug)
|
||||
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)
|
||||
|
||||
|
||||
def is_installed(name):
|
||||
@ -448,25 +535,28 @@ def is_installed(name):
|
||||
if name.startswith('#'):
|
||||
return True
|
||||
|
||||
pattern = r'([^<>!=]+)([<>!=]=?)([0-9.a-zA-Z]*)'
|
||||
pattern = r'([^<>!~=]+)([<>!~=]=?)([0-9.a-zA-Z]*)'
|
||||
match = re.search(pattern, name)
|
||||
|
||||
if match:
|
||||
name = match.group(1)
|
||||
|
||||
if name in cm_global.pip_blacklist:
|
||||
return True
|
||||
|
||||
if name in cm_global.pip_downgrade_blacklist:
|
||||
pips = get_installed_packages()
|
||||
pips = manager_util.get_installed_packages()
|
||||
|
||||
if match is None:
|
||||
if name in pips:
|
||||
return True
|
||||
elif match.group(2) in ['<=', '==', '<']:
|
||||
elif match.group(2) in ['<=', '==', '<', '~=']:
|
||||
if name in pips:
|
||||
if StrictVersion(pips[name]) >= StrictVersion(match.group(3)):
|
||||
if manager_util.StrictVersion(pips[name]) >= manager_util.StrictVersion(match.group(3)):
|
||||
print(f"[ComfyUI-Manager] skip black listed pip installation: '{name}'")
|
||||
return True
|
||||
|
||||
pkg = get_installed_packages().get(name.lower())
|
||||
pkg = manager_util.get_installed_packages().get(name.lower())
|
||||
if pkg is None:
|
||||
return False # update if not installed
|
||||
|
||||
@ -474,11 +564,19 @@ def is_installed(name):
|
||||
return True # don't update if version is not specified
|
||||
|
||||
if match.group(2) in ['>', '>=']:
|
||||
if StrictVersion(pkg) < StrictVersion(match.group(3)):
|
||||
if manager_util.StrictVersion(pkg) < manager_util.StrictVersion(match.group(3)):
|
||||
return False
|
||||
elif StrictVersion(pkg) > StrictVersion(match.group(3)):
|
||||
elif manager_util.StrictVersion(pkg) > manager_util.StrictVersion(match.group(3)):
|
||||
print(f"[SKIP] Downgrading pip package isn't allowed: {name.lower()} (cur={pkg})")
|
||||
|
||||
if match.group(2) == '==':
|
||||
if manager_util.StrictVersion(pkg) < manager_util.StrictVersion(match.group(3)):
|
||||
return False
|
||||
|
||||
if match.group(2) == '~=':
|
||||
if manager_util.StrictVersion(pkg) == manager_util.StrictVersion(match.group(3)):
|
||||
return False
|
||||
|
||||
return True # prevent downgrade
|
||||
|
||||
|
||||
@ -507,63 +605,22 @@ if os.path.exists(restore_snapshot_path):
|
||||
else:
|
||||
print(prefix, msg, end="")
|
||||
|
||||
print(f"[ComfyUI-Manager] Restore snapshot.")
|
||||
cmd_str = [sys.executable, git_script_path, '--apply-snapshot', restore_snapshot_path]
|
||||
|
||||
print("[ComfyUI-Manager] Restore snapshot.")
|
||||
new_env = os.environ.copy()
|
||||
new_env["COMFYUI_PATH"] = comfy_path
|
||||
exit_code = process_wrap(cmd_str, custom_nodes_path, handler=msg_capture, env=new_env)
|
||||
if 'COMFYUI_FOLDERS_BASE_PATH' not in new_env:
|
||||
new_env["COMFYUI_FOLDERS_BASE_PATH"] = comfy_path
|
||||
|
||||
repository_name = ''
|
||||
for url in cloned_repos:
|
||||
try:
|
||||
repository_name = url.split("/")[-1].strip()
|
||||
repo_path = os.path.join(custom_nodes_path, repository_name)
|
||||
repo_path = os.path.abspath(repo_path)
|
||||
|
||||
requirements_path = os.path.join(repo_path, 'requirements.txt')
|
||||
install_script_path = os.path.join(repo_path, 'install.py')
|
||||
|
||||
this_exit_code = 0
|
||||
|
||||
if os.path.exists(requirements_path):
|
||||
with open(requirements_path, 'r', encoding="UTF-8", errors="ignore") as file:
|
||||
for line in file:
|
||||
package_name = remap_pip_package(line.strip())
|
||||
if package_name and not is_installed(package_name):
|
||||
if not package_name.startswith('#'):
|
||||
if '--index-url' in package_name:
|
||||
s = package_name.split('--index-url')
|
||||
install_cmd = [sys.executable, "-m", "pip", "install", s[0].strip(), '--index-url', s[1].strip()]
|
||||
else:
|
||||
install_cmd = [sys.executable, "-m", "pip", "install", package_name]
|
||||
|
||||
this_exit_code += process_wrap(install_cmd, repo_path)
|
||||
|
||||
if os.path.exists(install_script_path) and f'{repo_path}/install.py' not in processed_install:
|
||||
processed_install.add(f'{repo_path}/install.py')
|
||||
install_cmd = [sys.executable, install_script_path]
|
||||
print(f">>> {install_cmd} / {repo_path}")
|
||||
|
||||
new_env = os.environ.copy()
|
||||
new_env["COMFYUI_PATH"] = comfy_path
|
||||
this_exit_code += process_wrap(install_cmd, repo_path, env=new_env)
|
||||
|
||||
if this_exit_code != 0:
|
||||
print(f"[ComfyUI-Manager] Restoring '{repository_name}' is failed.")
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print(f"[ComfyUI-Manager] Restoring '{repository_name}' is failed.")
|
||||
cmd_str = [sys.executable, cm_cli_path, 'restore-snapshot', restore_snapshot_path]
|
||||
exit_code = process_wrap(cmd_str, custom_nodes_base_path, handler=msg_capture, env=new_env)
|
||||
|
||||
if exit_code != 0:
|
||||
print(f"[ComfyUI-Manager] Restore snapshot failed.")
|
||||
print("[ComfyUI-Manager] Restore snapshot failed.")
|
||||
else:
|
||||
print(f"[ComfyUI-Manager] Restore snapshot done.")
|
||||
print("[ComfyUI-Manager] Restore snapshot done.")
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print(f"[ComfyUI-Manager] Restore snapshot failed.")
|
||||
print("[ComfyUI-Manager] Restore snapshot failed.")
|
||||
|
||||
os.remove(restore_snapshot_path)
|
||||
|
||||
@ -576,17 +633,19 @@ def execute_lazy_install_script(repo_path, executable):
|
||||
|
||||
if os.path.exists(requirements_path):
|
||||
print(f"Install: pip packages for '{repo_path}'")
|
||||
with open(requirements_path, "r") as requirements_file:
|
||||
for line in requirements_file:
|
||||
package_name = remap_pip_package(line.strip())
|
||||
if package_name and not is_installed(package_name):
|
||||
if '--index-url' in package_name:
|
||||
s = package_name.split('--index-url')
|
||||
install_cmd = [sys.executable, "-m", "pip", "install", s[0].strip(), '--index-url', s[1].strip()]
|
||||
else:
|
||||
install_cmd = [sys.executable, "-m", "pip", "install", package_name]
|
||||
|
||||
process_wrap(install_cmd, repo_path)
|
||||
lines = manager_util.robust_readlines(requirements_path)
|
||||
for line in lines:
|
||||
package_name = remap_pip_package(line.strip())
|
||||
package_name = package_name.split('#')[0].strip()
|
||||
if package_name and not is_installed(package_name):
|
||||
if '--index-url' in package_name:
|
||||
s = package_name.split('--index-url')
|
||||
install_cmd = manager_util.make_pip_cmd(["install", s[0].strip(), '--index-url', s[1].strip()])
|
||||
else:
|
||||
install_cmd = manager_util.make_pip_cmd(["install", package_name])
|
||||
|
||||
process_wrap(install_cmd, repo_path)
|
||||
|
||||
if os.path.exists(install_script_path) and f'{repo_path}/install.py' not in processed_install:
|
||||
processed_install.add(f'{repo_path}/install.py')
|
||||
@ -594,15 +653,99 @@ def execute_lazy_install_script(repo_path, executable):
|
||||
install_cmd = [executable, "install.py"]
|
||||
|
||||
new_env = os.environ.copy()
|
||||
new_env["COMFYUI_PATH"] = comfy_path
|
||||
if 'COMFYUI_FOLDERS_BASE_PATH' not in new_env:
|
||||
new_env["COMFYUI_FOLDERS_BASE_PATH"] = comfy_path
|
||||
process_wrap(install_cmd, repo_path, env=new_env)
|
||||
|
||||
|
||||
# Check if script_list_path exists
|
||||
if os.path.exists(script_list_path):
|
||||
def execute_lazy_cnr_switch(target, zip_url, from_path, to_path, no_deps, custom_nodes_path):
|
||||
import uuid
|
||||
import shutil
|
||||
|
||||
# 1. download
|
||||
archive_name = f"CNR_temp_{str(uuid.uuid4())}.zip" # should be unpredictable name - security precaution
|
||||
download_path = os.path.join(custom_nodes_path, archive_name)
|
||||
manager_downloader.download_url(zip_url, custom_nodes_path, archive_name)
|
||||
|
||||
# 2. extract files into <node_id>@<cur_ver>
|
||||
extracted = manager_util.extract_package_as_zip(download_path, from_path)
|
||||
os.remove(download_path)
|
||||
|
||||
if extracted is None:
|
||||
if len(os.listdir(from_path)) == 0:
|
||||
shutil.rmtree(from_path)
|
||||
|
||||
print(f'Empty archive file: {target}')
|
||||
return False
|
||||
|
||||
|
||||
# 3. calculate garbage files (.tracking - extracted)
|
||||
tracking_info_file = os.path.join(from_path, '.tracking')
|
||||
prev_files = set()
|
||||
with open(tracking_info_file, 'r') as f:
|
||||
for line in f:
|
||||
prev_files.add(line.strip())
|
||||
garbage = prev_files.difference(extracted)
|
||||
garbage = [os.path.join(custom_nodes_path, x) for x in garbage]
|
||||
|
||||
# 4-1. remove garbage files
|
||||
for x in garbage:
|
||||
if os.path.isfile(x):
|
||||
os.remove(x)
|
||||
|
||||
# 4-2. remove garbage dir if empty
|
||||
for x in garbage:
|
||||
if os.path.isdir(x):
|
||||
if not os.listdir(x):
|
||||
os.rmdir(x)
|
||||
|
||||
# 5. rename dir name <node_id>@<prev_ver> ==> <node_id>@<cur_ver>
|
||||
print(f"'{from_path}' is moved to '{to_path}'")
|
||||
shutil.move(from_path, to_path)
|
||||
|
||||
# 6. create .tracking file
|
||||
tracking_info_file = os.path.join(to_path, '.tracking')
|
||||
with open(tracking_info_file, "w", encoding='utf-8') as file:
|
||||
file.write('\n'.join(list(extracted)))
|
||||
|
||||
|
||||
script_executed = False
|
||||
|
||||
def execute_startup_script():
|
||||
global script_executed
|
||||
print("\n#######################################################################")
|
||||
print("[ComfyUI-Manager] Starting dependency installation/(de)activation for the extension\n")
|
||||
|
||||
custom_nodelist_cache = None
|
||||
|
||||
def get_custom_node_paths():
|
||||
nonlocal custom_nodelist_cache
|
||||
if custom_nodelist_cache is None:
|
||||
custom_nodelist_cache = set()
|
||||
for base in folder_paths.get_folder_paths('custom_nodes'):
|
||||
for x in os.listdir(base):
|
||||
fullpath = os.path.join(base, x)
|
||||
if os.path.isdir(fullpath):
|
||||
custom_nodelist_cache.add(fullpath)
|
||||
|
||||
return custom_nodelist_cache
|
||||
|
||||
def execute_lazy_delete(path):
|
||||
# Validate to prevent arbitrary paths from being deleted
|
||||
if path not in get_custom_node_paths():
|
||||
logging.error(f"## ComfyUI-Manager: The scheduled '{path}' is not a custom node path, so the deletion has been canceled.")
|
||||
return
|
||||
|
||||
if not os.path.exists(path):
|
||||
logging.info(f"## ComfyUI-Manager: SKIP-DELETE => '{path}' (already deleted)")
|
||||
return
|
||||
|
||||
try:
|
||||
shutil.rmtree(path)
|
||||
logging.info(f"## ComfyUI-Manager: DELETE => '{path}'")
|
||||
except Exception as e:
|
||||
logging.error(f"## ComfyUI-Manager: Failed to delete '{path}' ({e})")
|
||||
|
||||
executed = set()
|
||||
# Read each line from the file and convert it to a list using eval
|
||||
with open(script_list_path, 'r', encoding="UTF-8", errors="ignore") as file:
|
||||
@ -619,6 +762,13 @@ if os.path.exists(script_list_path):
|
||||
if script[1] == "#LAZY-INSTALL-SCRIPT":
|
||||
execute_lazy_install_script(script[0], script[2])
|
||||
|
||||
elif script[1] == "#LAZY-CNR-SWITCH-SCRIPT":
|
||||
execute_lazy_cnr_switch(script[0], script[2], script[3], script[4], script[5], script[6])
|
||||
execute_lazy_install_script(script[3], script[7])
|
||||
|
||||
elif script[1] == "#LAZY-DELETE-NODEPACK":
|
||||
execute_lazy_delete(script[2])
|
||||
|
||||
elif os.path.exists(script[0]):
|
||||
if script[1] == "#FORCE":
|
||||
del script[1]
|
||||
@ -627,37 +777,77 @@ if os.path.exists(script_list_path):
|
||||
continue
|
||||
|
||||
print(f"\n## ComfyUI-Manager: EXECUTE => {script[1:]}")
|
||||
print(f"\n## Execute install/(de)activation script for '{script[0]}'")
|
||||
print(f"\n## Execute management script for '{script[0]}'")
|
||||
|
||||
new_env = os.environ.copy()
|
||||
new_env["COMFYUI_PATH"] = comfy_path
|
||||
if 'COMFYUI_FOLDERS_BASE_PATH' not in new_env:
|
||||
new_env["COMFYUI_FOLDERS_BASE_PATH"] = comfy_path
|
||||
exit_code = process_wrap(script[1:], script[0], env=new_env)
|
||||
|
||||
if exit_code != 0:
|
||||
print(f"install/(de)activation script failed: {script[0]}")
|
||||
print(f"management script failed: {script[0]}")
|
||||
else:
|
||||
print(f"\n## ComfyUI-Manager: CANCELED => {script[1:]}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to execute install/(de)activation script: {line} / {e}")
|
||||
print(f"[ERROR] Failed to execute management script: {line} / {e}")
|
||||
|
||||
# Remove the script_list_path file
|
||||
if os.path.exists(script_list_path):
|
||||
script_executed = True
|
||||
os.remove(script_list_path)
|
||||
|
||||
print("\n[ComfyUI-Manager] Startup script completed.")
|
||||
print("#######################################################################\n")
|
||||
|
||||
|
||||
# Check if script_list_path exists
|
||||
# Block startup-scripts on old ComfyUI (security measure)
|
||||
if not _has_system_user_api:
|
||||
if os.path.exists(script_list_path):
|
||||
print("[ComfyUI-Manager] Startup scripts blocked on old ComfyUI version.")
|
||||
elif os.path.exists(script_list_path):
|
||||
execute_startup_script()
|
||||
|
||||
|
||||
pip_fixer.fix_broken()
|
||||
|
||||
del processed_install
|
||||
del pip_map
|
||||
del pip_fixer
|
||||
manager_util.clear_pip_cache()
|
||||
|
||||
if script_executed:
|
||||
# Restart
|
||||
print("[ComfyUI-Manager] Restarting to reapply dependency installation.")
|
||||
|
||||
if '__COMFY_CLI_SESSION__' in os.environ:
|
||||
with open(os.path.join(os.environ['__COMFY_CLI_SESSION__'] + '.reboot'), 'w'):
|
||||
pass
|
||||
|
||||
print("--------------------------------------------------------------------------\n")
|
||||
exit(0)
|
||||
else:
|
||||
sys_argv = sys.argv.copy()
|
||||
|
||||
if sys_argv[0].endswith("__main__.py"): # this is a python module
|
||||
module_name = os.path.basename(os.path.dirname(sys_argv[0]))
|
||||
cmds = [sys.executable, '-m', module_name] + sys_argv[1:]
|
||||
elif sys.platform.startswith('win32'):
|
||||
cmds = ['"' + sys.executable + '"', '"' + sys_argv[0] + '"'] + sys_argv[1:]
|
||||
else:
|
||||
cmds = [sys.executable] + sys_argv
|
||||
|
||||
print(f"Command: {cmds}", flush=True)
|
||||
print("--------------------------------------------------------------------------\n")
|
||||
|
||||
os.execv(sys.executable, cmds)
|
||||
|
||||
|
||||
def check_windows_event_loop_policy():
|
||||
try:
|
||||
import configparser
|
||||
config_path = os.path.join(os.path.dirname(__file__), "config.ini")
|
||||
config = configparser.ConfigParser()
|
||||
config.read(config_path)
|
||||
config = configparser.ConfigParser(strict=False)
|
||||
config.read(manager_config_path)
|
||||
default_conf = config['default']
|
||||
|
||||
if 'windows_selector_event_loop_policy' in default_conf and default_conf['windows_selector_event_loop_policy'].lower() == 'true':
|
||||
@ -665,7 +855,7 @@ def check_windows_event_loop_policy():
|
||||
import asyncio
|
||||
import asyncio.windows_events
|
||||
asyncio.set_event_loop_policy(asyncio.windows_events.WindowsSelectorEventLoopPolicy())
|
||||
print(f"[ComfyUI-Manager] Windows event loop policy mode enabled")
|
||||
print("[ComfyUI-Manager] Windows event loop policy mode enabled")
|
||||
except Exception as e:
|
||||
print(f"[ComfyUI-Manager] WARN: Windows initialization fail: {e}")
|
||||
except Exception:
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
[project]
|
||||
name = "comfyui-manager"
|
||||
description = "ComfyUI-Manager provides features to install and manage custom nodes for ComfyUI, as well as various functionalities to assist with ComfyUI."
|
||||
version = "2.48.7"
|
||||
version = "3.39"
|
||||
license = { file = "LICENSE.txt" }
|
||||
dependencies = ["GitPython", "PyGithub", "matrix-client==0.4.0", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions"]
|
||||
dependencies = ["GitPython", "PyGithub", "matrix-nio", "transformers", "huggingface-hub>0.20", "typer", "rich", "typing-extensions", "toml", "uv", "chardet"]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/ltdrdata/ComfyUI-Manager"
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
GitPython
|
||||
PyGithub
|
||||
matrix-client==0.4.0
|
||||
matrix-nio
|
||||
transformers
|
||||
huggingface-hub>0.20
|
||||
huggingface-hub
|
||||
typer
|
||||
rich
|
||||
typing-extensions
|
||||
typing-extensions
|
||||
toml
|
||||
uv
|
||||
chardet
|
||||
|
||||
12
ruff.toml
Normal file
@ -0,0 +1,12 @@
|
||||
# Disable all rules by default
|
||||
lint.ignore = ["ALL"]
|
||||
|
||||
# Enable specific rules
|
||||
lint.select = [
|
||||
"S307", # suspicious-eval-usage
|
||||
# The "F" series in Ruff stands for "Pyflakes" rules, which catch various Python syntax errors and undefined names.
|
||||
# See all rules here: https://docs.astral.sh/ruff/rules/#pyflakes-f
|
||||
"F",
|
||||
]
|
||||
|
||||
exclude = ["*.ipynb"]
|
||||
8
scan.sh
@ -3,5 +3,9 @@ rm ~/.tmp/default/*.py > /dev/null 2>&1
|
||||
python scanner.py ~/.tmp/default $*
|
||||
cp extension-node-map.json node_db/new/.
|
||||
|
||||
echo Integrity check
|
||||
./check.sh
|
||||
echo "Integrity check"
|
||||
if [ -f "check2.sh" ]; then
|
||||
./check2.sh
|
||||
else
|
||||
./check.sh
|
||||
fi
|
||||
1205
scanner.py
@ -1,12 +1,12 @@
|
||||
git clone https://github.com/comfyanonymous/ComfyUI
|
||||
cd ComfyUI/custom_nodes
|
||||
git clone https://github.com/ltdrdata/ComfyUI-Manager
|
||||
git clone https://github.com/ltdrdata/ComfyUI-Manager comfyui-manager
|
||||
cd ..
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
python -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu121
|
||||
python -m pip install -r requirements.txt
|
||||
python -m pip install -r custom_nodes/ComfyUI-Manager/requirements.txt
|
||||
python -m pip install -r custom_nodes/comfyui-manager/requirements.txt
|
||||
cd ..
|
||||
echo "#!/bin/bash" > run_gpu.sh
|
||||
echo "cd ComfyUI" >> run_gpu.sh
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
git clone https://github.com/comfyanonymous/ComfyUI
|
||||
cd ComfyUI/custom_nodes
|
||||
git clone https://github.com/ltdrdata/ComfyUI-Manager
|
||||
git clone https://github.com/ltdrdata/ComfyUI-Manager comfyui-manager
|
||||
cd ..
|
||||
python -m venv venv
|
||||
call venv/Scripts/activate
|
||||
python -m pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu121
|
||||
python -m pip install -r requirements.txt
|
||||
python -m pip install -r custom_nodes/ComfyUI-Manager/requirements.txt
|
||||
python -m pip install -r custom_nodes/comfyui-manager/requirements.txt
|
||||
cd ..
|
||||
echo "cd ComfyUI" >> run_gpu.bat
|
||||
echo "call venv/Scripts/activate" >> run_gpu.bat
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
.\python_embeded\python.exe -s -m pip install gitpython
|
||||
.\python_embeded\python.exe -c "import git; git.Repo.clone_from('https://github.com/ltdrdata/ComfyUI-Manager', './ComfyUI/custom_nodes/ComfyUI-Manager')"
|
||||
.\python_embeded\python.exe -c "import git; git.Repo.clone_from('https://github.com/ltdrdata/ComfyUI-Manager', './ComfyUI/custom_nodes/comfyui-manager')"
|
||||
.\python_embeded\python.exe -m pip install -r ./ComfyUI/custom_nodes/comfyui-manager/requirements.txt
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
import git
|
||||
|
||||
commit_hash = "a361cc1"
|
||||
|
||||
repo = git.Repo('.')
|
||||
|
||||
if repo.is_dirty():
|
||||
repo.git.stash()
|
||||
|
||||
repo.git.update_ref("refs/remotes/origin/main", commit_hash)
|
||||
repo.remotes.origin.fetch()
|
||||
repo.git.pull("origin", "main")
|
||||