Compare commits
1874 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94f9e74d67 | ||
|
|
47432fe54e | ||
|
|
8660a79ccd | ||
|
|
547febafba | ||
|
|
85eaa305cc | ||
|
|
e77629c525 | ||
|
|
097780be26 | ||
|
|
398a0509d7 | ||
|
|
52cc99bf1f | ||
|
|
9bd4b3a6d0 | ||
|
|
8a98df4673 | ||
|
|
45a14a9be9 | ||
|
|
e419276e34 | ||
|
|
0a92b7b1d5 | ||
|
|
f110168366 | ||
|
|
ce24a05909 | ||
|
|
45facf64e5 | ||
|
|
e999832c26 | ||
|
|
4c8d5a7077 | ||
|
|
81643cb3af | ||
|
|
7a9bc883df | ||
|
|
6280a80129 | ||
|
|
a33908b6de | ||
|
|
0ea5620413 | ||
|
|
e23eb1fea8 | ||
|
|
41f2c82eaf | ||
|
|
91e3bfe58f | ||
|
|
83d5519a31 | ||
|
|
cc2666b9d6 | ||
|
|
954493fef5 | ||
|
|
967c3681cd | ||
|
|
87c9df5c0d | ||
|
|
62136768d2 | ||
|
|
b71b7804fc | ||
|
|
e8b7751374 | ||
|
|
54d4433141 | ||
|
|
14dbebbc35 | ||
|
|
d6a02a31a7 | ||
|
|
86e2ac40ae | ||
|
|
a12ed7533b | ||
|
|
9fb0ee2d1b | ||
|
|
6311b80474 | ||
|
|
c13d1093ee | ||
|
|
dd7deeba53 | ||
|
|
338aef3e95 | ||
|
|
134c98ccb5 | ||
|
|
d12877987f | ||
|
|
676316e5e4 | ||
|
|
52761ad88c | ||
|
|
f5e489ba87 | ||
|
|
982af1fff3 | ||
|
|
1cff398c20 | ||
|
|
a6271d2c4e | ||
|
|
60f8cc6883 | ||
|
|
ffb8feba6b | ||
|
|
4aca3c4639 | ||
|
|
120f9e567c | ||
|
|
c0492511df | ||
|
|
1075a5ed93 | ||
|
|
58d3507155 | ||
|
|
ae0c9b6a6b | ||
|
|
ad1374af1d | ||
|
|
8436e8a71e | ||
|
|
ea07483465 | ||
|
|
51f857c3f3 | ||
|
|
74c0ca0902 | ||
|
|
ad5641fa3e | ||
|
|
b0294f8cbd | ||
|
|
5d4498ff85 | ||
|
|
d52fb15746 | ||
|
|
ee6be74e72 | ||
|
|
4cbc86f945 | ||
|
|
3a5e0cb2d2 | ||
|
|
7916b8d26a | ||
|
|
a0842b4659 | ||
|
|
14ee87ca80 | ||
|
|
cec1d7d6c9 | ||
|
|
9aeae4d16e | ||
|
|
9c1b741d89 | ||
|
|
c71a74f857 | ||
|
|
524612cee5 | ||
|
|
11e47b3871 | ||
|
|
4a1b2be45c | ||
|
|
d641aa2f6e | ||
|
|
237c7a5348 | ||
|
|
19f37907d9 | ||
|
|
b8706da990 | ||
|
|
b458d57355 | ||
|
|
a5962dae33 | ||
|
|
670768e5b3 | ||
|
|
f02b915cd0 | ||
|
|
71bbbeb936 | ||
|
|
e084b78b53 | ||
|
|
013860e3c0 | ||
|
|
7a118eeb15 | ||
|
|
df408b25e5 | ||
|
|
536082c1a6 | ||
|
|
b986ca3059 | ||
|
|
4bf9e577b9 | ||
|
|
a7c12e61d8 | ||
|
|
847d27bffb | ||
|
|
781e812f22 | ||
|
|
e49b5e0e6b | ||
|
|
8f1c1b128e | ||
|
|
04cbb052d7 | ||
|
|
16f0950ebd | ||
|
|
e959a3d7ab | ||
|
|
fc9941abaa | ||
|
|
f177011395 | ||
|
|
80e47be5a5 | ||
|
|
9a9f6e3559 | ||
|
|
1a6e0234b3 | ||
|
|
56bea46e3a | ||
|
|
a09441b2c8 | ||
|
|
105994d96d | ||
|
|
d641647b1e | ||
|
|
672574d278 | ||
|
|
f1ded17399 | ||
|
|
d254e3e2fd | ||
|
|
ab5450bb27 | ||
|
|
a2e9e5eb57 | ||
|
|
8965f11ab4 | ||
|
|
1dd5644e7a | ||
|
|
37f813506e | ||
|
|
a5d5ed90e6 | ||
|
|
3792a1bc0d | ||
|
|
fbafa56ecb | ||
|
|
2f910c69b8 | ||
|
|
bf06cc48bb | ||
|
|
3ef67ebc73 | ||
|
|
0c4318fb31 | ||
|
|
c55ced93db | ||
|
|
4bd89ab2e1 | ||
|
|
807d940001 | ||
|
|
d4427b97ae | ||
|
|
4f336d9f25 | ||
|
|
1565530b0f | ||
|
|
a21b01a0cd | ||
|
|
1c7e90576d | ||
|
|
8c27fa136c | ||
|
|
c8de1cd49b | ||
|
|
5eb36e131d | ||
|
|
b5d1adaa19 | ||
|
|
b89d152540 | ||
|
|
e49772030d | ||
|
|
b1cb03962c | ||
|
|
a7b0858b22 | ||
|
|
ad227ca190 | ||
|
|
a8360484b2 | ||
|
|
80c4a50ca1 | ||
|
|
768b88a0ac | ||
|
|
82607573fa | ||
|
|
d07e00cd74 | ||
|
|
dfdd2b32e0 | ||
|
|
844edbc865 | ||
|
|
2bc66cc640 | ||
|
|
f9f9aba92d | ||
|
|
3f278cf2ad | ||
|
|
cb7ba96dad | ||
|
|
31edce4a60 | ||
|
|
1b6aae9678 | ||
|
|
9572ddf1c1 | ||
|
|
3bbce82454 | ||
|
|
1f44cebd0e | ||
|
|
843ea58c15 | ||
|
|
1e13c4e808 | ||
|
|
ab8f10ae4a | ||
|
|
c62161770d | ||
|
|
15b828b0f5 | ||
|
|
faa83a87df | ||
|
|
796c12bc4c | ||
|
|
50da182e30 | ||
|
|
dba573bf1a | ||
|
|
6a0eef3fe4 | ||
|
|
98f58e8672 | ||
|
|
04274f5839 | ||
|
|
f387b9f464 | ||
|
|
b8f533d0ea | ||
|
|
5a49818a10 | ||
|
|
ad9d9e0b04 | ||
|
|
c92470ff7e | ||
|
|
1cd9c7fdac | ||
|
|
e607035c65 | ||
|
|
bde8113414 | ||
|
|
1fd011b1be | ||
|
|
061380742c | ||
|
|
8f9feb3ed9 | ||
|
|
0dc01cb974 | ||
|
|
55af328181 | ||
|
|
a8c0abfd5d | ||
|
|
4807744aa7 | ||
|
|
669d40a9d2 | ||
|
|
18049d529a | ||
|
|
f2b441d9fc | ||
|
|
d2078d4dde | ||
|
|
41d4ad2096 | ||
|
|
29ec8291ad | ||
|
|
b93a206a48 | ||
|
|
be83336cf7 | ||
|
|
19fdba7d73 | ||
|
|
2c2b3b75d5 | ||
|
|
47d5cb9e33 | ||
|
|
7b8e1bc919 | ||
|
|
77aa7a0148 | ||
|
|
bdd7d2599f | ||
|
|
ca8a96f956 | ||
|
|
8957250db8 | ||
|
|
1b6ec418a1 | ||
|
|
3759d77945 | ||
|
|
ab4d34e509 | ||
|
|
7f878f365b | ||
|
|
5efabfaea6 | ||
|
|
4cd8ae45e3 | ||
|
|
8999f9450f | ||
|
|
1d54943d71 | ||
|
|
767d8fc35d | ||
|
|
894f34678e | ||
|
|
1190bedafd | ||
|
|
e80db71d1c | ||
|
|
846bb2134e | ||
|
|
38b2eec4be | ||
|
|
8dafe486a2 | ||
|
|
c895a96a43 | ||
|
|
67cae9725e | ||
|
|
a2d06f87f6 | ||
|
|
8e4afc8374 | ||
|
|
afd879a692 | ||
|
|
83de2b8de7 | ||
|
|
4930f36a1a | ||
|
|
fa3f196add | ||
|
|
95004be0e9 | ||
|
|
281a849c8f | ||
|
|
5d82ce665c | ||
|
|
d632cfcde9 | ||
|
|
07f797a5e4 | ||
|
|
121107dd13 | ||
|
|
a2479b74be | ||
|
|
7ee1d3cd91 | ||
|
|
4b28ddd691 | ||
|
|
7270b5fe0c | ||
|
|
285792f692 | ||
|
|
23a0a48b81 | ||
|
|
2baad73bb9 | ||
|
|
097dc99e77 | ||
|
|
edd10bcfe7 | ||
|
|
ac1c65fba1 | ||
|
|
b4cc21ea89 | ||
|
|
3dfc3f5ff7 | ||
|
|
7c012df1d5 | ||
|
|
a1854d3734 | ||
|
|
074c566826 | ||
|
|
a2e7bfb30e | ||
|
|
01c1c77564 | ||
|
|
34de4fe8fe | ||
|
|
4975f8167e | ||
|
|
6777459e62 | ||
|
|
253d0dbd5e | ||
|
|
e98bd70871 | ||
|
|
6a216be5cb | ||
|
|
0adb7831e7 | ||
|
|
30ca98b597 | ||
|
|
e80001e8c8 | ||
|
|
b5490f7712 | ||
|
|
dc5748624f | ||
|
|
91fb82e9b6 | ||
|
|
84c5a759d4 | ||
|
|
ec43aa2f18 | ||
|
|
8e7a6077e5 | ||
|
|
53a79c1a81 | ||
|
|
e9f54c8bae | ||
|
|
c978863e5f | ||
|
|
12fa08d7a7 | ||
|
|
50dea4cb52 | ||
|
|
20b06db359 | ||
|
|
b6e512e65f | ||
|
|
7d71c353b2 | ||
|
|
2adf43274c | ||
|
|
3216a68d63 | ||
|
|
df518f822c | ||
|
|
abdf0b6719 | ||
|
|
2d2a75f23c | ||
|
|
fcb59c68d4 | ||
|
|
d47816e7b9 | ||
|
|
21297d98f2 | ||
|
|
cc7452374d | ||
|
|
851aa7aaaf | ||
|
|
376d238ad8 | ||
|
|
e0998e227f | ||
|
|
07b584b3b4 | ||
|
|
d35a89bb01 | ||
|
|
22a6fe7721 | ||
|
|
404329f9b5 | ||
|
|
3929e88d87 | ||
|
|
83a5b5b46f | ||
|
|
b97c906128 | ||
|
|
b8328b6071 | ||
|
|
9a528496a3 | ||
|
|
6a95c602b1 | ||
|
|
f0f6578b9c | ||
|
|
83c93eb9ef | ||
|
|
befe8ad24e | ||
|
|
c5249e6144 | ||
|
|
9be3297c27 | ||
|
|
b6344ef6f9 | ||
|
|
76b7e32125 | ||
|
|
801a3dd598 | ||
|
|
d1fdf1766a | ||
|
|
35073adc1f | ||
|
|
d76930c7f4 | ||
|
|
7d496f4ad0 | ||
|
|
53b5ce6e2c | ||
|
|
38ab5b090f | ||
|
|
fa58996f37 | ||
|
|
56f92ccab0 | ||
|
|
4e444b418e | ||
|
|
3d9a9299dc | ||
|
|
ae34c9e84b | ||
|
|
eba7bab15e | ||
|
|
ee6db85768 | ||
|
|
05ed110519 | ||
|
|
9690fd1fa8 | ||
|
|
4cee1be99c | ||
|
|
d39e1da183 | ||
|
|
8538a684e7 | ||
|
|
47d7513dd8 | ||
|
|
432fd57581 | ||
|
|
9c06e2612a | ||
|
|
1d6742f463 | ||
|
|
2e849827d1 | ||
|
|
1e2c9ecb41 | ||
|
|
14679586a8 | ||
|
|
11fb83a2a7 | ||
|
|
4d3f55622a | ||
|
|
eedf6f0aad | ||
|
|
13592fae1a | ||
|
|
4dd05d3efe | ||
|
|
2e3059a7c8 | ||
|
|
3b53b5ebaf | ||
|
|
a9f1000af8 | ||
|
|
a9960ded01 | ||
|
|
ed84a23f36 | ||
|
|
8301cafb37 | ||
|
|
c906c5d14a | ||
|
|
6e52680fa8 | ||
|
|
7f32c531d7 | ||
|
|
17a11b94b2 | ||
|
|
e61549e0cd | ||
|
|
b93c624efa | ||
|
|
d118443a94 | ||
|
|
064a55f587 | ||
|
|
de3b43647a | ||
|
|
0b054b58d4 | ||
|
|
7b5e2b4a12 | ||
|
|
b408fc7cd2 | ||
|
|
a8ed1ebf52 | ||
|
|
357ab9596e | ||
|
|
cba1cfbbef | ||
|
|
c402867d45 | ||
|
|
158591bdcf | ||
|
|
8857249b48 | ||
|
|
c71d7ea14f | ||
|
|
721ab8a0c7 | ||
|
|
d0d9c185f1 | ||
|
|
ade0912ba1 | ||
|
|
192520eafe | ||
|
|
710208f376 | ||
|
|
9fd69b2519 | ||
|
|
636c3e5c02 | ||
|
|
0dbc195770 | ||
|
|
d03f16cbd3 | ||
|
|
eac0d49880 | ||
|
|
0ad97b8aa5 | ||
|
|
8ce5ebe885 | ||
|
|
fa2a929796 | ||
|
|
7debd2cd97 | ||
|
|
ca59866d52 | ||
|
|
4832c67167 | ||
|
|
6ec7b78d96 | ||
|
|
14c1d17632 | ||
|
|
5bd98d4aa0 | ||
|
|
a7b427c5ff | ||
|
|
de36489444 | ||
|
|
de6fec5fd7 | ||
|
|
47b157c24a | ||
|
|
e358a72925 | ||
|
|
6201ed30ff | ||
|
|
ace53d211f | ||
|
|
812fc20c39 | ||
|
|
08ff52235e | ||
|
|
66624f4011 | ||
|
|
b58b7660ab | ||
|
|
ef1e42dd7b | ||
|
|
6f065918e9 | ||
|
|
0f7f52fbc2 | ||
|
|
721f826376 | ||
|
|
74e7c35885 | ||
|
|
d61bc5958e | ||
|
|
3461bb669d | ||
|
|
8151bc57b1 | ||
|
|
0d610c5393 | ||
|
|
7c358c2842 | ||
|
|
f5b8044bad | ||
|
|
92ffbb5ed8 | ||
|
|
0cc2d26e97 | ||
|
|
bb5487efb8 | ||
|
|
0139111d49 | ||
|
|
fe6991c703 | ||
|
|
17c9d9b447 | ||
|
|
318136f4eb | ||
|
|
5344cb9723 | ||
|
|
959c1a196e | ||
|
|
c5a06eedbc | ||
|
|
025ee49680 | ||
|
|
935c75bcab | ||
|
|
7b632fe441 | ||
|
|
1e4cca54b6 | ||
|
|
4b89c3e7a5 | ||
|
|
837ad5b68c | ||
|
|
3980625be6 | ||
|
|
2242a76fed | ||
|
|
0661b9ba5b | ||
|
|
788404f66a | ||
|
|
fa457ad476 | ||
|
|
b005332840 | ||
|
|
ab52a95e45 | ||
|
|
ed59402e48 | ||
|
|
5e0839531e | ||
|
|
1c7acf7bcf | ||
|
|
d143e85760 | ||
|
|
2612c274d3 | ||
|
|
764ad1b8db | ||
|
|
b1dcfbd017 | ||
|
|
e43bf2b93a | ||
|
|
ab75527df2 | ||
|
|
b5a661eec8 | ||
|
|
df655eb2d7 | ||
|
|
db55064bb2 | ||
|
|
b4c3c4c650 | ||
|
|
6dfabb692d | ||
|
|
09f747a68e | ||
|
|
6d6a07f830 | ||
|
|
4313e7e701 | ||
|
|
2b9f5eb627 | ||
|
|
d4c1155ac3 | ||
|
|
37e8158175 | ||
|
|
c6c025353a | ||
|
|
21946ff824 | ||
|
|
2bd1cceb24 | ||
|
|
82561268ea | ||
|
|
99ab2d2a81 | ||
|
|
f9ff184b89 | ||
|
|
0118c7c808 | ||
|
|
d17ee88ced | ||
|
|
84574367b3 | ||
|
|
afdbcf267b | ||
|
|
4dd254263f | ||
|
|
2f9b492f5b | ||
|
|
0d9d01c9f5 | ||
|
|
36c4c0c8d7 | ||
|
|
c6d6446606 | ||
|
|
fa6716345d | ||
|
|
2dfa482b24 | ||
|
|
4959e52559 | ||
|
|
95334715ab | ||
|
|
5511d1090f | ||
|
|
9d319fd279 | ||
|
|
d023fd07b0 | ||
|
|
324226f87d | ||
|
|
3120b593c6 | ||
|
|
311ade1281 | ||
|
|
d98e4772ac | ||
|
|
cf87c34bef | ||
|
|
5a643c383b | ||
|
|
8618708fd1 | ||
|
|
f09c50ec90 | ||
|
|
75c57f646d | ||
|
|
45f99ab48a | ||
|
|
26042b1e26 | ||
|
|
656acafed3 | ||
|
|
ec353ba90d | ||
|
|
084ef5a28c | ||
|
|
81a24249e6 | ||
|
|
6df9a38a65 | ||
|
|
4c52dcb8a0 | ||
|
|
7306ac0168 | ||
|
|
9c34d42a50 | ||
|
|
fdc6a4d94b | ||
|
|
cc475f26f4 | ||
|
|
f252ca75e9 | ||
|
|
05b608831c | ||
|
|
7adf25ef97 | ||
|
|
837069648f | ||
|
|
1286e2d03c | ||
|
|
c425811b45 | ||
|
|
4b3de4c656 | ||
|
|
d6b996b28e | ||
|
|
3081a20bd0 | ||
|
|
fda30b1ecd | ||
|
|
74aa1a9db1 | ||
|
|
1f9a429a62 | ||
|
|
dbf9482303 | ||
|
|
845e6d1528 | ||
|
|
2e4807312a | ||
|
|
4bbb4b5e1e | ||
|
|
3a7281df3c | ||
|
|
800f275e91 | ||
|
|
ae930f3993 | ||
|
|
417daa264f | ||
|
|
19b42c91c0 | ||
|
|
aa53b868fc | ||
|
|
ab0d08b7a3 | ||
|
|
de0b082810 | ||
|
|
913550295c | ||
|
|
bce0373b11 | ||
|
|
95768cdb05 | ||
|
|
af7073d9b6 | ||
|
|
d56c23be9a | ||
|
|
5867baea35 | ||
|
|
f6bd05bcf1 | ||
|
|
13056f87d3 | ||
|
|
a72bae9cd2 | ||
|
|
df416a6a17 | ||
|
|
672571a36c | ||
|
|
848ff35e85 | ||
|
|
f05b815c5d | ||
|
|
e1e2a2a249 | ||
|
|
817436b65c | ||
|
|
c9a5ad9c3a | ||
|
|
c480b615ce | ||
|
|
5bc0d1f762 | ||
|
|
881fdc58ec | ||
|
|
569431dc72 | ||
|
|
07e30ae4ad | ||
|
|
c74be07c33 | ||
|
|
887d871d26 | ||
|
|
4dd1a46efa | ||
|
|
eb301a67d4 | ||
|
|
d9bddffc42 | ||
|
|
a5898aaf3b | ||
|
|
3dc62a8857 | ||
|
|
7811929b5b | ||
|
|
a43bd2fd3b | ||
|
|
aac9acf068 | ||
|
|
65bb01892f | ||
|
|
5b35c47360 | ||
|
|
4bf78521ce | ||
|
|
2a5b3040e2 | ||
|
|
ac4651c241 | ||
|
|
2c4cd21c8f | ||
|
|
31a7e178a1 | ||
|
|
ed59972b03 | ||
|
|
6ae4314b79 | ||
|
|
5e07432ae9 | ||
|
|
8ced5b7199 | ||
|
|
41d8847592 | ||
|
|
eb96bfe8a4 | ||
|
|
3037cceab3 | ||
|
|
324ffdefba | ||
|
|
9a81d17d33 | ||
|
|
3e34cdc884 | ||
|
|
e213f6cb95 | ||
|
|
0ba9f0549e | ||
|
|
f83af28e42 | ||
|
|
a2856b2b77 | ||
|
|
924fee394a | ||
|
|
e349fb1a23 | ||
|
|
4f799a2bf0 | ||
|
|
5398765fd7 | ||
|
|
48edce72a9 | ||
|
|
267c7b85ea | ||
|
|
e23f66a697 | ||
|
|
9a0031c47b | ||
|
|
0d8e73b206 | ||
|
|
9486c03a89 | ||
|
|
c09512bf12 | ||
|
|
05c2de9450 | ||
|
|
6ae5cb28cf | ||
|
|
cf6c1add1d | ||
|
|
d0184a1598 | ||
|
|
79d6ab9915 | ||
|
|
047390873c | ||
|
|
4b36ca75cb | ||
|
|
f7c52b700e | ||
|
|
c81d98ad0f | ||
|
|
046c00d844 | ||
|
|
b14653cb9e | ||
|
|
c72b287c82 | ||
|
|
a10aa92634 | ||
|
|
8a2c09c6de | ||
|
|
401fc30617 | ||
|
|
6ca7247c02 | ||
|
|
1d5309decb | ||
|
|
ab0218050c | ||
|
|
6dcf7539bb | ||
|
|
51d52d3a07 | ||
|
|
dd95df8f02 | ||
|
|
3045f5211f | ||
|
|
0860e35d17 | ||
|
|
32c4f10626 | ||
|
|
3e90eafafb | ||
|
|
16fcb4ed79 | ||
|
|
9be48b3fc5 | ||
|
|
7830ec7ca2 | ||
|
|
0ebf9df207 | ||
|
|
40682405cc | ||
|
|
9fdd482811 | ||
|
|
7202ffba6e | ||
|
|
30dcc7477f | ||
|
|
9ce076eb0d | ||
|
|
2080d6e27b | ||
|
|
6826435046 | ||
|
|
69d937e0b1 | ||
|
|
edd92b724f | ||
|
|
41ecc822df | ||
|
|
0990d8fc4d | ||
|
|
ce2a42ca13 | ||
|
|
1da35e89f6 | ||
|
|
d818107953 | ||
|
|
b3f65c0b3c | ||
|
|
59c322dc3b | ||
|
|
096f9ad3a6 | ||
|
|
5c8965b3ab | ||
|
|
090f8f6070 | ||
|
|
5f4fc63645 | ||
|
|
a0b3b5af53 | ||
|
|
351dd97500 | ||
|
|
b511000441 | ||
|
|
3a059bb919 | ||
|
|
a548c026b1 | ||
|
|
99c99ee9e3 | ||
|
|
523131de79 | ||
|
|
17e731dfe3 | ||
|
|
9dfa300083 | ||
|
|
3ea74af76d | ||
|
|
3d7e16cfd9 | ||
|
|
db265309a5 | ||
|
|
8554b0eab2 | ||
|
|
f641e6e69d | ||
|
|
30c07eab6b | ||
|
|
eba83386c1 | ||
|
|
d3334f9dfa | ||
|
|
a87dca1ef4 | ||
|
|
2bab4341a3 | ||
|
|
01fb2fde47 | ||
|
|
e93a49134a | ||
|
|
0127714929 | ||
|
|
29ec34169c | ||
|
|
d60cb61e58 | ||
|
|
d4582e9e6e | ||
|
|
a84d29c49c | ||
|
|
e76a91a78d | ||
|
|
ea9861d180 | ||
|
|
e48c73d277 | ||
|
|
0f6caaec33 | ||
|
|
a5a1d33589 | ||
|
|
cca6dd9230 | ||
|
|
7d936c72a4 | ||
|
|
cac4bd11d2 | ||
|
|
70a3beeaa2 | ||
|
|
566cb55f36 | ||
|
|
a6dbdf664b | ||
|
|
bdf36a8dab | ||
|
|
760909f495 | ||
|
|
40c9f1f51d | ||
|
|
4349c595b8 | ||
|
|
da27fc7782 | ||
|
|
11e1436e2e | ||
|
|
107323d8e7 | ||
|
|
83557d4b3c | ||
|
|
b08e9b7982 | ||
|
|
415213878d | ||
|
|
53b23756a4 | ||
|
|
063d14d2ac | ||
|
|
3d99f0dd9c | ||
|
|
f3ce5ed279 | ||
|
|
b77036443f | ||
|
|
00603ce124 | ||
|
|
d3dd15eb63 | ||
|
|
9d408a62bf | ||
|
|
e4a7537952 | ||
|
|
4313166dbf | ||
|
|
a25364732b | ||
|
|
0adaf6c0a0 | ||
|
|
9410879b73 | ||
|
|
1dc326cc41 | ||
|
|
1605c5fbcc | ||
|
|
7562a882f4 | ||
|
|
366bc72759 | ||
|
|
2f7990b9cf | ||
|
|
45db4bb036 | ||
|
|
4a04226cc0 | ||
|
|
8142fd0701 | ||
|
|
7240c91db7 | ||
|
|
a9318a9ba0 | ||
|
|
1cba62af24 | ||
|
|
add05228bd | ||
|
|
4bca739b3d | ||
|
|
566a83ce3f | ||
|
|
ca19a488a8 | ||
|
|
08f44472f8 | ||
|
|
2d1be6186e | ||
|
|
c0dcf1633c | ||
|
|
d8447ef1a9 | ||
|
|
ca362ef78d | ||
|
|
a255d74abf | ||
|
|
fec2140896 | ||
|
|
3f9ec378a0 | ||
|
|
64cfd55065 | ||
|
|
f9cfe1da45 | ||
|
|
b27a14b1b4 | ||
|
|
4dbdb802b6 | ||
|
|
cbd74e7510 | ||
|
|
8e416cef25 | ||
|
|
ae9afab6c1 | ||
|
|
06c990e94d | ||
|
|
4d31078579 | ||
|
|
01c7712961 | ||
|
|
c18bf3e413 | ||
|
|
5c95bcc65d | ||
|
|
75f0780bd1 | ||
|
|
843d22d0d4 | ||
|
|
33a49a57e6 | ||
|
|
eaba64a64a | ||
|
|
679b828cf5 | ||
|
|
d231c533ae | ||
|
|
5a9e74cef7 | ||
|
|
49599dc3ba | ||
|
|
c1e5c8dc86 | ||
|
|
35d36f9eb3 | ||
|
|
2b8c199e56 | ||
|
|
654749de40 | ||
|
|
dde51c0cef | ||
|
|
729f7eb24a | ||
|
|
51e067b050 | ||
|
|
4dc2a96d41 | ||
|
|
1b40a6baa3 | ||
|
|
50fdc32ff8 | ||
|
|
e7fd0b3a05 | ||
|
|
20d6e17d4d | ||
|
|
262a1464c3 | ||
|
|
31c54c4a41 | ||
|
|
6cf05df5ee | ||
|
|
f90a13571c | ||
|
|
a6fe023519 | ||
|
|
e550b15094 | ||
|
|
0ebad77083 | ||
|
|
3100fae118 | ||
|
|
01202c5c2e | ||
|
|
ae52d9ef22 | ||
|
|
70a37fda57 | ||
|
|
da3f894ed4 | ||
|
|
2e362d57eb | ||
|
|
03fedfd0d5 | ||
|
|
039395f221 | ||
|
|
1381be16ad | ||
|
|
9af511e457 | ||
|
|
d18cefc519 | ||
|
|
07f52c38ef | ||
|
|
a46ff731d8 | ||
|
|
469585ddda | ||
|
|
3000e53cc0 | ||
|
|
db0722aca7 | ||
|
|
af0058d2aa | ||
|
|
aad1afb70e | ||
|
|
228a5c4552 | ||
|
|
dc43eb29e1 | ||
|
|
400cb218ba | ||
|
|
0fbe3cfb8f | ||
|
|
9a01e917c6 | ||
|
|
e01d68fce3 | ||
|
|
60f2f5ea19 | ||
|
|
2333beda5f | ||
|
|
8174f94172 | ||
|
|
6a6ea5009a | ||
|
|
24d0e7566f | ||
|
|
fe8c208e7c | ||
|
|
ba7a49e834 | ||
|
|
216323fcf4 | ||
|
|
fb18c93bd6 | ||
|
|
382dee1fd1 | ||
|
|
9399fb5371 | ||
|
|
92d8dfe963 | ||
|
|
3ae851ab1f | ||
|
|
fb1e3de3c7 | ||
|
|
6fbb24ae3d | ||
|
|
943776dd14 | ||
|
|
bb607927d0 | ||
|
|
ce95072845 | ||
|
|
36344732ac | ||
|
|
1f4e4d8d82 | ||
|
|
4ef10222e1 | ||
|
|
d7b91db204 | ||
|
|
5acf5949a6 | ||
|
|
991f9cda42 | ||
|
|
6b075256e8 | ||
|
|
3d740555c3 | ||
|
|
49b2fc5b33 | ||
|
|
f7235cf82c | ||
|
|
56dbddd472 | ||
|
|
eb16296873 | ||
|
|
1967299417 | ||
|
|
c7d8164c48 | ||
|
|
1864921d1d | ||
|
|
75bdb214c7 | ||
|
|
0b19adba75 | ||
|
|
2e84a421f3 | ||
|
|
fea77e97a0 | ||
|
|
e1b6cc2a86 | ||
|
|
0921573644 | ||
|
|
5eec05c0c4 | ||
|
|
526fc989c1 | ||
|
|
023b78d1c9 | ||
|
|
670410b539 | ||
|
|
76e379d7e1 | ||
|
|
cde57109e4 | ||
|
|
bc142c9ecd | ||
|
|
6c148f1791 | ||
|
|
534bb2dd84 | ||
|
|
d0f4476ba5 | ||
|
|
6287bcd00a | ||
|
|
fcbcb7d471 | ||
|
|
cb527919a2 | ||
|
|
83c34ea52f | ||
|
|
35c75115de | ||
|
|
7c75a61700 | ||
|
|
34ea49147c | ||
|
|
c1e8637a9f | ||
|
|
becbef4fac | ||
|
|
f22ecc454a | ||
|
|
bf3df097b8 | ||
|
|
7fc2ed28b1 | ||
|
|
30a133bad9 | ||
|
|
d8d44c579c | ||
|
|
80384e6ee1 | ||
|
|
0898f98355 | ||
|
|
e7dc41e271 | ||
|
|
0f0f475241 | ||
|
|
127ee68486 | ||
|
|
b204b02b05 | ||
|
|
893b6d985c | ||
|
|
44824fb5f9 | ||
|
|
dc21cbe59d | ||
|
|
e16d9f4742 | ||
|
|
f2b5843e6c | ||
|
|
16229caa8e | ||
|
|
0c0525e11b | ||
|
|
11517b0969 | ||
|
|
1ba3a139d9 | ||
|
|
80bcfabc48 | ||
|
|
4192f87d6b | ||
|
|
03c8a0fca5 | ||
|
|
a3d2c71ed6 | ||
|
|
8ba0b34853 | ||
|
|
424ec40fa5 | ||
|
|
210429a259 | ||
|
|
df806d5dfa | ||
|
|
b07046f6a2 | ||
|
|
4cdb8a7d2a | ||
|
|
2f83f2bd48 | ||
|
|
8a6cf3cfae | ||
|
|
514e40569e | ||
|
|
f30f98abd8 | ||
|
|
c611d26306 | ||
|
|
9ee38d0b70 | ||
|
|
5e45f37232 | ||
|
|
4f2df2d188 | ||
|
|
ae470e35c8 | ||
|
|
0f4b62cb97 | ||
|
|
c086098af1 | ||
|
|
4c7b4c7592 | ||
|
|
9e244f758c | ||
|
|
e7c0b9bd76 | ||
|
|
d7317e8252 | ||
|
|
36a187d3c5 | ||
|
|
a6ec401440 | ||
|
|
7a2048b2cb | ||
|
|
37082ad430 | ||
|
|
c571521e87 | ||
|
|
c438cd47b9 | ||
|
|
3ce6c3dc61 | ||
|
|
17f60d3c7f | ||
|
|
7d4d85284b | ||
|
|
9c091a9edf | ||
|
|
60ca5641ae | ||
|
|
c115a9aa3d | ||
|
|
6529240808 | ||
|
|
0d570b3fae | ||
|
|
0778078350 | ||
|
|
888dc05cde | ||
|
|
687da5b64a | ||
|
|
5b00d54c76 | ||
|
|
38b4a7856e | ||
|
|
4f899bd83d | ||
|
|
7f80f7c46b | ||
|
|
d6cb0e48cc | ||
|
|
899125cc41 | ||
|
|
f38e8688b3 | ||
|
|
f3a0ab24c1 | ||
|
|
730424ebc4 | ||
|
|
84737eb271 | ||
|
|
e807bcdf90 | ||
|
|
73d947c4a6 | ||
|
|
f0dbd87ba9 | ||
|
|
88d415d3f9 | ||
|
|
3fc93e2c57 | ||
|
|
69380e3527 | ||
|
|
ec0b08e4d0 | ||
|
|
6aa048e3ad | ||
|
|
bc711414a8 | ||
|
|
07b467e4bc | ||
|
|
75445185c4 | ||
|
|
7772b6901a | ||
|
|
fad36d9c08 | ||
|
|
061e012cff | ||
|
|
02fdafc111 | ||
|
|
a03164f3bc | ||
|
|
8ab445bb31 | ||
|
|
1429a44f0e | ||
|
|
0b5c5b646f | ||
|
|
5ddf0bd164 | ||
|
|
ed60bab294 | ||
|
|
65348e1cb9 | ||
|
|
22ca01bbde | ||
|
|
c772347c09 | ||
|
|
afc18619db | ||
|
|
4f88939c77 | ||
|
|
5dd3f46cee | ||
|
|
11138a1d97 | ||
|
|
aea49bf739 | ||
|
|
8f877a2cee | ||
|
|
dc4344043e | ||
|
|
fe635db3ab | ||
|
|
7e53eb658c | ||
|
|
4836abb5bd | ||
|
|
67746981e7 | ||
|
|
8149f97388 | ||
|
|
e5a47ac964 | ||
|
|
40598c28af | ||
|
|
044e3746ca | ||
|
|
969eb5632f | ||
|
|
03800a45e0 | ||
|
|
9ee17ec5f1 | ||
|
|
72d991afaa | ||
|
|
a858d4d1ba | ||
|
|
590472cab2 | ||
|
|
d988c80874 | ||
|
|
8e705f0785 | ||
|
|
477a4fc0e3 | ||
|
|
77246aeab1 | ||
|
|
728e7c79fd | ||
|
|
a10dfd0386 | ||
|
|
d81be64711 | ||
|
|
e4550f7eb4 | ||
|
|
27fb8ccd92 | ||
|
|
811d3b916e | ||
|
|
a573e61d36 | ||
|
|
5a7675709b | ||
|
|
0730400bc1 | ||
|
|
e65ee76076 | ||
|
|
de02435015 | ||
|
|
48b233304f | ||
|
|
1c2e353fc5 | ||
|
|
356375677d | ||
|
|
2af1b5c064 | ||
|
|
e42ddbd652 | ||
|
|
d1f341678c | ||
|
|
696bb883d0 | ||
|
|
55da2988b3 | ||
|
|
d16783d0d1 | ||
|
|
d8cfd35a94 | ||
|
|
114363e22b | ||
|
|
23e7a6b8b0 | ||
|
|
20ad0d7f8c | ||
|
|
6ef72f03ff | ||
|
|
ac91a15aa9 | ||
|
|
872c09c220 | ||
|
|
8efde22580 | ||
|
|
9c693bd76f | ||
|
|
7a4002a17a | ||
|
|
fbaa11f08d | ||
|
|
3faaed9819 | ||
|
|
f5be941a4a | ||
|
|
eabe4cf58c | ||
|
|
593e9748b9 | ||
|
|
f8d260e0a7 | ||
|
|
f8bc50871a | ||
|
|
03a7e2e8bf | ||
|
|
3d2695c4ec | ||
|
|
4871f7fed7 | ||
|
|
0f3a3da5ed | ||
|
|
ec6922b885 | ||
|
|
c9a05e0290 | ||
|
|
856724b9a5 | ||
|
|
2b3e2b1de7 | ||
|
|
bcaa624b9b | ||
|
|
b5da4e2fa0 | ||
|
|
7917ac8ebc | ||
|
|
700bfc16bd | ||
|
|
8aead029a8 | ||
|
|
38aafd3577 | ||
|
|
a9da65c2cd | ||
|
|
a290fdd28c | ||
|
|
0324deec60 | ||
|
|
d7b3b5d87f | ||
|
|
28338612fa | ||
|
|
fe23cc558d | ||
|
|
394cb72e46 | ||
|
|
57c85a64c7 | ||
|
|
a3f357732c | ||
|
|
ca99b87319 | ||
|
|
3507f91090 | ||
|
|
aa3bc864ee | ||
|
|
db769dd995 | ||
|
|
a0f2097b1b | ||
|
|
6298df14e7 | ||
|
|
2d0a76c5a4 | ||
|
|
b7a7a7d31f | ||
|
|
841811e3bc | ||
|
|
0679fb2b20 | ||
|
|
230f7b478a | ||
|
|
c8f8f329e3 | ||
|
|
9c4d702434 | ||
|
|
ab7d74d2fa | ||
|
|
c9ddf4d15f | ||
|
|
7af620f66e | ||
|
|
24b631500a | ||
|
|
c27d57831c | ||
|
|
30eb729973 | ||
|
|
7f24241372 | ||
|
|
0ddcc98a57 | ||
|
|
9ca8cf810b | ||
|
|
3c2157111b | ||
|
|
d26e646a94 | ||
|
|
1fda12640f | ||
|
|
05316ae25b | ||
|
|
079402cb2f | ||
|
|
995bdc77b8 | ||
|
|
c52b72f500 | ||
|
|
a4e496abc1 | ||
|
|
6cb981655e | ||
|
|
2916a33fa2 | ||
|
|
ca555686ec | ||
|
|
03256f6bba | ||
|
|
257b14ee09 | ||
|
|
4e35580bd6 | ||
|
|
512b160fc0 | ||
|
|
250afd2355 | ||
|
|
7fa3e2d2f9 | ||
|
|
91f28fa38f | ||
|
|
a0dc82e1f9 | ||
|
|
fc3c2925e5 | ||
|
|
6b3511e15d | ||
|
|
59183aceec | ||
|
|
ec44a84915 | ||
|
|
1010837cfd | ||
|
|
aec7e6d32e | ||
|
|
bb0f7cd1cd | ||
|
|
5dd92b1d3f | ||
|
|
7548f7cdbb | ||
|
|
44da3d26f3 | ||
|
|
7c01c48297 | ||
|
|
7826870d99 | ||
|
|
bdb6649722 | ||
|
|
31ee73c5eb | ||
|
|
3b708e8d44 | ||
|
|
0fd706f392 | ||
|
|
8907dabd4c | ||
|
|
1496d6ec51 | ||
|
|
d1a45ed9ac | ||
|
|
f73d28ac10 | ||
|
|
1b7af75d4e | ||
|
|
ed0d78bf73 | ||
|
|
046e2acae1 | ||
|
|
b6efa71efc | ||
|
|
3bb835b5e1 | ||
|
|
fbeecda38c | ||
|
|
942904186a | ||
|
|
737a81570a | ||
|
|
3691aeb8e1 | ||
|
|
32d8f4d24b | ||
|
|
30ccd35dd3 | ||
|
|
11265c4034 | ||
|
|
8acff43028 | ||
|
|
660aa4f4ab | ||
|
|
1384c2f1bc | ||
|
|
459b9428d4 | ||
|
|
a82f16958b | ||
|
|
b9f436812b | ||
|
|
7c0ec9faaf | ||
|
|
88e3831bc6 | ||
|
|
2a597fcad7 | ||
|
|
6158f49400 | ||
|
|
9dd819e193 | ||
|
|
e706fae648 | ||
|
|
118a4862ab | ||
|
|
5e2f31e3bf | ||
|
|
f78b31b1bc | ||
|
|
8d698cb997 | ||
|
|
8945aac319 | ||
|
|
f2a960136e | ||
|
|
7a1170f1dd | ||
|
|
24cce08580 | ||
|
|
b425b43d3e | ||
|
|
353fe88226 | ||
|
|
1a3086230e | ||
|
|
0e57487774 | ||
|
|
3024465086 | ||
|
|
c95b43253a | ||
|
|
aedf7856e5 | ||
|
|
d83e034d5e | ||
|
|
05cafce1e8 | ||
|
|
b9676b51cb | ||
|
|
5698473891 | ||
|
|
de1d1ad961 | ||
|
|
bd82480fa3 | ||
|
|
fce8b96d3b | ||
|
|
37b47e7f05 | ||
|
|
a6f94959fe | ||
|
|
45a2c9f7ef | ||
|
|
c49ac6880d | ||
|
|
e0258d9e7b | ||
|
|
e3ff6f183b | ||
|
|
e6ec7393c6 | ||
|
|
f733b53c25 | ||
|
|
204a68b17d | ||
|
|
1379dde1a7 | ||
|
|
79eee62d42 | ||
|
|
7c1f18b6cd | ||
|
|
b59371988d | ||
|
|
30dbadb2ab | ||
|
|
a342de0207 | ||
|
|
6e6d236819 | ||
|
|
0e41483564 | ||
|
|
1023f5f7cc | ||
|
|
4bc7bca60d | ||
|
|
de7dbd27c0 | ||
|
|
14118f142c | ||
|
|
9b99be4c1d | ||
|
|
91c4b5865c | ||
|
|
1b4c14af71 | ||
|
|
7b85e50604 | ||
|
|
d64b2d8fbe | ||
|
|
f1a7aed1b6 | ||
|
|
75f758e792 | ||
|
|
e25e1bfe10 | ||
|
|
09deaefab0 | ||
|
|
f80ecbde71 | ||
|
|
5e1e198a1f | ||
|
|
bdbb741716 | ||
|
|
2f0e8a8a4a | ||
|
|
4f8424c544 | ||
|
|
ce3355d6aa | ||
|
|
fb67ef2df0 | ||
|
|
380e9aaf13 | ||
|
|
255e90d125 | ||
|
|
504f7f3799 | ||
|
|
9970e505de | ||
|
|
0ccacd5eca | ||
|
|
50e4683492 | ||
|
|
bc14bdc010 | ||
|
|
14b0dabfdf | ||
|
|
e140acd2a4 | ||
|
|
facfed07fe | ||
|
|
41a3309cbe | ||
|
|
bbda097aa8 | ||
|
|
4df9a22dd6 | ||
|
|
31a1c4b2b2 | ||
|
|
c2c33b7df1 | ||
|
|
6a2c2152e2 | ||
|
|
37f2755611 | ||
|
|
aa70f2849b | ||
|
|
e7a2dfa57f | ||
|
|
b43f9fc4ee | ||
|
|
51b6a2fd2a | ||
|
|
5fffb82b16 | ||
|
|
e051dbc2c7 | ||
|
|
c2fba39cc7 | ||
|
|
1050b13bbb | ||
|
|
92d3d9cd33 | ||
|
|
d8dec3e56a | ||
|
|
130f9678b2 | ||
|
|
29d13cb06d | ||
|
|
620f521e0c | ||
|
|
a36fb55b05 | ||
|
|
23f9bcb38b | ||
|
|
e73e820237 | ||
|
|
7e4735ae0f | ||
|
|
66ffcbbee6 | ||
|
|
4754743c84 | ||
|
|
09c1dfd92b | ||
|
|
7fc46f3672 | ||
|
|
df93fee034 | ||
|
|
fc2cf742c8 | ||
|
|
9bec441e94 | ||
|
|
1caab1da85 | ||
|
|
d612d7ab53 | ||
|
|
3d3994bbad | ||
|
|
d643ae0299 | ||
|
|
0a099434a3 | ||
|
|
16905a8999 | ||
|
|
282c4cca82 | ||
|
|
f36b7ce016 | ||
|
|
9fb5cac5d4 | ||
|
|
9f5f213cd3 | ||
|
|
932ee11c91 | ||
|
|
5d3b59b94e | ||
|
|
744c6e4725 | ||
|
|
c59745d346 | ||
|
|
9d1dd09a07 | ||
|
|
2eb317c6b6 | ||
|
|
0ad08c609d | ||
|
|
85f6f8b31d | ||
|
|
9799309db9 | ||
|
|
fa205f483a | ||
|
|
2df4286256 | ||
|
|
b89f689ea3 | ||
|
|
f58b21746e | ||
|
|
6971f9dcf1 | ||
|
|
3454a47f67 | ||
|
|
5922fd39c5 | ||
|
|
cdbddbae3b | ||
|
|
af4a26c1d0 | ||
|
|
d3f42e47a7 | ||
|
|
8821e471b5 | ||
|
|
d34aed0b14 | ||
|
|
b7391652ca | ||
|
|
074a14f056 | ||
|
|
b1db708af1 | ||
|
|
b2a66709b0 | ||
|
|
e3e43913ab | ||
|
|
c7fed0a42a | ||
|
|
c6c5e0734a | ||
|
|
73cbc58a50 | ||
|
|
8431395326 | ||
|
|
dd21c07d4a | ||
|
|
ce9591428e | ||
|
|
a801a5d8b6 | ||
|
|
04e8458ce2 | ||
|
|
4b4fa84879 | ||
|
|
1b3df8c4de | ||
|
|
7ce223771d | ||
|
|
ccf71ed445 | ||
|
|
aa7c031e8a | ||
|
|
8465bc1bc9 | ||
|
|
f2f3ed71d4 | ||
|
|
ab7ba35639 | ||
|
|
1cc09cbe5f | ||
|
|
fe7e398eb4 | ||
|
|
6ab3133b33 | ||
|
|
ef77c37a7e | ||
|
|
1dd165a9c9 | ||
|
|
3c74540615 | ||
|
|
ad249c4651 | ||
|
|
071a4d6f37 | ||
|
|
5f2fb19d71 | ||
|
|
ce61657f7a | ||
|
|
dc54e5bdce | ||
|
|
f7b8e000c5 | ||
|
|
73abf131a6 | ||
|
|
5741af2aba | ||
|
|
159af669f6 | ||
|
|
a517255653 | ||
|
|
573154633b | ||
|
|
baa8afd9eb | ||
|
|
9e718da70e | ||
|
|
4df442f169 | ||
|
|
1dc93c7a39 | ||
|
|
3d124986d3 | ||
|
|
a589d98cd4 | ||
|
|
ed9f18e22c | ||
|
|
14fb115fc8 | ||
|
|
c35a731a60 | ||
|
|
4f3d2bd120 | ||
|
|
69c8fc3236 | ||
|
|
840ff5c363 | ||
|
|
8386cd5cf7 | ||
|
|
666c2f8771 | ||
|
|
b342fa9661 | ||
|
|
63bf84fdd5 | ||
|
|
070e51fcab | ||
|
|
50fd64150e | ||
|
|
63c5de2612 | ||
|
|
c576d582e2 | ||
|
|
026a4b6c76 | ||
|
|
7bc95b68c8 | ||
|
|
0332cc8cb3 | ||
|
|
ce192f4ad7 | ||
|
|
cbdb715918 | ||
|
|
5537102fd3 | ||
|
|
1ea294f15c | ||
|
|
e7bf2ee58b | ||
|
|
a931aa59a3 | ||
|
|
4c8da67bb1 | ||
|
|
a0178e15b3 | ||
|
|
43a1c3901f | ||
|
|
a4c6f28a70 | ||
|
|
f8bca93170 | ||
|
|
f07d05a490 | ||
|
|
b3a988bc0b | ||
|
|
e0f22d29e8 | ||
|
|
07ee97b862 | ||
|
|
19b05659b5 | ||
|
|
7e5c7ca1b7 | ||
|
|
1156c159f9 | ||
|
|
5c6c2303ba | ||
|
|
a0a58bcfa8 | ||
|
|
8a28b265a3 | ||
|
|
86dc08130b | ||
|
|
5cd8a732c7 | ||
|
|
fafbbf68a4 | ||
|
|
0cbb553564 | ||
|
|
f4512bb291 | ||
|
|
99205b4d03 | ||
|
|
d48e6554d5 | ||
|
|
d0c4e95de3 | ||
|
|
0b3a35c4b6 | ||
|
|
ded6a41f86 | ||
|
|
f4063e63d3 | ||
|
|
23ba912db0 | ||
|
|
b99d9db8f9 | ||
|
|
b7047dafb2 | ||
|
|
368967fbcf | ||
|
|
a9d0fc9978 | ||
|
|
b6f3d2ec02 | ||
|
|
78e917a6fb | ||
|
|
96b45385e8 | ||
|
|
db47888a75 | ||
|
|
51443741b8 | ||
|
|
3e7f14af2c | ||
|
|
733439da07 | ||
|
|
6bff97d6fa | ||
|
|
efba81cb66 | ||
|
|
b2cc5dcf4b | ||
|
|
fab86ddf35 | ||
|
|
f3a90ce02d | ||
|
|
4886616c48 | ||
|
|
dcd8121009 | ||
|
|
59adaf6225 | ||
|
|
0055cd9b2e | ||
|
|
fe89d487f6 | ||
|
|
01368ac496 | ||
|
|
495064985e | ||
|
|
200f8fd245 | ||
|
|
64bf4356b4 | ||
|
|
8d4d409cd6 | ||
|
|
dd4937178f | ||
|
|
e12387a377 | ||
|
|
5d3fb9091a | ||
|
|
b044bc1791 | ||
|
|
409ec61be2 | ||
|
|
e2ae2715a3 | ||
|
|
52458ae273 | ||
|
|
79d112ca7b | ||
|
|
9b1a9cc7c8 | ||
|
|
42f9abdfe3 | ||
|
|
66d311258a | ||
|
|
0a1197055c | ||
|
|
649cbf07e3 | ||
|
|
5089ac5ad1 | ||
|
|
d99e3f7974 | ||
|
|
3d5133209b | ||
|
|
b5d1912c94 | ||
|
|
a8fba8f3fb | ||
|
|
9d9fc1683a | ||
|
|
8ee4364065 | ||
|
|
152aa7de09 | ||
|
|
85c90cbee1 | ||
|
|
7302927e4c | ||
|
|
df3d00ef94 | ||
|
|
bb47835256 | ||
|
|
037512ca5c | ||
|
|
a13713adaf | ||
|
|
ad073252e7 | ||
|
|
d24a7a5c5e | ||
|
|
192fd223b4 | ||
|
|
a671dd8e00 | ||
|
|
8b764a8fd3 | ||
|
|
aa576e68e3 | ||
|
|
ad5508a14d | ||
|
|
4fafc8aa67 | ||
|
|
0aab3d0f12 | ||
|
|
a5d88bdfcc | ||
|
|
5173957368 | ||
|
|
4b3e3d900d | ||
|
|
9ea51b174a | ||
|
|
80e265e547 | ||
|
|
c3e6e63023 | ||
|
|
9b5a262d63 | ||
|
|
1309f1480c | ||
|
|
12ba5b8096 | ||
|
|
156c5f4792 | ||
|
|
1da4b3d94a | ||
|
|
18aca98e41 | ||
|
|
a88afb0956 | ||
|
|
bfa1f57930 | ||
|
|
a5350eb3cc | ||
|
|
3ed4d792b3 | ||
|
|
fb0c9405cf | ||
|
|
a17a9044ad | ||
|
|
73af7f5481 | ||
|
|
57ead7f0c0 | ||
|
|
bf490c910a | ||
|
|
40f806efa8 | ||
|
|
226ba8b06e | ||
|
|
b11aa4833d | ||
|
|
8d9cd0e30b | ||
|
|
9532928998 | ||
|
|
420f7549a2 | ||
|
|
ed64b9bfed | ||
|
|
5d5ebfdef6 | ||
|
|
567c02bf5d | ||
|
|
60f7c73c8a | ||
|
|
ac4c5003f1 | ||
|
|
d5e76e662f | ||
|
|
23d5f85d17 | ||
|
|
f75adc1e22 | ||
|
|
15a1436c8b | ||
|
|
813edec808 | ||
|
|
21e3299b7a | ||
|
|
f7193966fb | ||
|
|
2d9853f1f4 | ||
|
|
ced79a187d | ||
|
|
7832524963 | ||
|
|
58c7f3ba15 | ||
|
|
90ec8f0575 | ||
|
|
64ced3b3f6 | ||
|
|
493526c478 | ||
|
|
b86617e3af | ||
|
|
f3db6d84fb | ||
|
|
f9b9ecf754 | ||
|
|
af43a92a2f | ||
|
|
4dbdc642f9 | ||
|
|
8f2c87ce94 | ||
|
|
5149040496 | ||
|
|
5b1078e0db | ||
|
|
ae31813239 | ||
|
|
f6b3cde286 | ||
|
|
0f05f9c32c | ||
|
|
89170af721 | ||
|
|
5fddae589b | ||
|
|
19c16af5fa | ||
|
|
019f8f69f4 | ||
|
|
ad8d1f77df | ||
|
|
e82a8a7f3d | ||
|
|
ad07aeb041 | ||
|
|
451ab7e84c | ||
|
|
083390da83 | ||
|
|
dc6d48580b | ||
|
|
27d69e2ac3 | ||
|
|
91274a4df8 | ||
|
|
6eafcdfafd | ||
|
|
5e44744ff7 | ||
|
|
37b293fe74 | ||
|
|
280f0be690 | ||
|
|
183bc8321c | ||
|
|
a973e4d1ef | ||
|
|
eed1066967 | ||
|
|
2859c94fea | ||
|
|
dbcce2ee5d | ||
|
|
25071c238c | ||
|
|
9995ffb5f3 | ||
|
|
c867c35e45 | ||
|
|
6f60e88ca6 | ||
|
|
11730dcbe4 | ||
|
|
e155bac445 | ||
|
|
15a4682665 | ||
|
|
08675b39f7 | ||
|
|
2c7d5adb80 | ||
|
|
51c7faee3c | ||
|
|
852e129f9c | ||
|
|
6eb2d800fa | ||
|
|
0a2c70595d | ||
|
|
f13e16af15 | ||
|
|
f364958c13 | ||
|
|
e65150647d | ||
|
|
3c435b9593 | ||
|
|
871b96a450 | ||
|
|
48a3254ad2 | ||
|
|
2c0bdd6377 | ||
|
|
8cedeb349d | ||
|
|
e241ef25e5 | ||
|
|
5e553dd958 | ||
|
|
19ee87d2cd | ||
|
|
72b3598687 | ||
|
|
33b120f6cd | ||
|
|
0bfb9d00c8 | ||
|
|
b1a2d36c2d | ||
|
|
517ddca22d | ||
|
|
41c7b08418 | ||
|
|
c7c1b5a570 | ||
|
|
87b6dfb1a9 | ||
|
|
46c56f3706 | ||
|
|
32bab80508 | ||
|
|
b6f1194c93 | ||
|
|
206f9b97bb | ||
|
|
13721f160e | ||
|
|
102e5623f7 | ||
|
|
9a975321db | ||
|
|
6743ec14f1 | ||
|
|
daec5e5426 | ||
|
|
a2b55c0df7 | ||
|
|
01320ac735 | ||
|
|
84bddee2ce | ||
|
|
e636dd3649 | ||
|
|
5f6b798e35 | ||
|
|
9137f3793e | ||
|
|
73e92a688f | ||
|
|
7a9f219037 | ||
|
|
a4728190c0 | ||
|
|
04d67a24b6 | ||
|
|
55049ba9d2 | ||
|
|
e0b33a4feb | ||
|
|
fb5c0a3db7 | ||
|
|
8154a5709b | ||
|
|
3a6780bd50 | ||
|
|
b7a76d4212 | ||
|
|
ba7cae683a | ||
|
|
243556656e | ||
|
|
6662dc66d5 | ||
|
|
107112d1c4 | ||
|
|
4eae540086 | ||
|
|
21108650f7 | ||
|
|
c5d343750c | ||
|
|
09b76dcd93 | ||
|
|
b87bc033f5 | ||
|
|
fb95d76e34 | ||
|
|
4e765a7948 | ||
|
|
cf2408013e | ||
|
|
d8543d1358 | ||
|
|
d8b79d8b5c | ||
|
|
c2bcf89f9a | ||
|
|
5cb24f992c | ||
|
|
21394b7d45 | ||
|
|
6d08082693 | ||
|
|
768fb2583a | ||
|
|
6e07b2354f | ||
|
|
00597879bc | ||
|
|
0cd0d6aadf | ||
|
|
9d201f82f1 | ||
|
|
d6c535c45c | ||
|
|
babdb5b718 | ||
|
|
0ea8d038be | ||
|
|
c804a9971e | ||
|
|
4d7f6e4236 | ||
|
|
5474d1786f | ||
|
|
7f36473544 | ||
|
|
9d19698bf3 | ||
|
|
582b2d936f | ||
|
|
6036ccdc1c | ||
|
|
5eeef41d8c | ||
|
|
bacf266f0d | ||
|
|
ba5c54043b | ||
|
|
e33c858829 | ||
|
|
e47e54de3f | ||
|
|
54f9e9bfe9 | ||
|
|
e1875c872c | ||
|
|
27b8e173e8 | ||
|
|
47e3884994 | ||
|
|
e483071894 | ||
|
|
af090cb289 | ||
|
|
9bbb25f16c | ||
|
|
3007f00c9b | ||
|
|
352dcfbe30 | ||
|
|
60b181a545 | ||
|
|
600482e2d7 | ||
|
|
39ccbbd72e | ||
|
|
6e69cbcdaf | ||
|
|
bf6c222a3b | ||
|
|
6afcf7570a | ||
|
|
c3126f7b4d | ||
|
|
cb3b542363 | ||
|
|
1a5e15608c | ||
|
|
64a751ad79 | ||
|
|
57efe31959 | ||
|
|
39350d554b | ||
|
|
8f4e03550c | ||
|
|
d03823fb20 | ||
|
|
00ec2b9d6f | ||
|
|
70e4bc4582 | ||
|
|
5e56a437ef | ||
|
|
22ffd25619 | ||
|
|
127949c56b | ||
|
|
cdfef16a0e | ||
|
|
1595f1ed05 | ||
|
|
1cae39b105 | ||
|
|
8189b38e6e | ||
|
|
c240d6932a | ||
|
|
c4548d9396 | ||
|
|
aea70e3dd4 | ||
|
|
3b01e65e11 | ||
|
|
341c810bbb | ||
|
|
85fd2dfaaa | ||
|
|
bf4bc38c6c | ||
|
|
aa8b50280b | ||
|
|
62553dc0fa | ||
|
|
25639cc3f8 | ||
|
|
7982a9ae25 | ||
|
|
aa01fd058e | ||
|
|
ef7e1575bd | ||
|
|
fb075a0013 | ||
|
|
d1738baf44 | ||
|
|
7eb29fa91b | ||
|
|
34c00fb77f | ||
|
|
7965318d9f | ||
|
|
e73a514e29 | ||
|
|
35ff4f439e | ||
|
|
12e0194c7f | ||
|
|
d1ac90e16d | ||
|
|
7dc7f70582 | ||
|
|
84d606408a | ||
|
|
d103693811 | ||
|
|
0dbce101ac | ||
|
|
cb81e2aacd | ||
|
|
6cd0b530c5 | ||
|
|
35571eb14d | ||
|
|
8e6102ad9a | ||
|
|
80bc80dc2c | ||
|
|
a483bd0800 | ||
|
|
47a39569bc | ||
|
|
f00e1a92d8 | ||
|
|
a289945e8e | ||
|
|
b750c0d7c3 | ||
|
|
a244a6873a | ||
|
|
ceff4f06c1 | ||
|
|
0307114c8e | ||
|
|
92030a3917 | ||
|
|
73ace121a4 | ||
|
|
44d5809e46 | ||
|
|
5c4e6f7e96 | ||
|
|
8c032579b8 | ||
|
|
b53935bfd4 | ||
|
|
d4db027cfa | ||
|
|
27963decc9 | ||
|
|
25f488c6e1 | ||
|
|
07bd580050 | ||
|
|
fb32a38d96 | ||
|
|
ac0961d7d4 | ||
|
|
6b943f88d1 | ||
|
|
4bbf683d15 | ||
|
|
d0e50584ea | ||
|
|
b57649828d | ||
|
|
1f44a283b3 | ||
|
|
9947c3bcfb | ||
|
|
8faf6b9f52 | ||
|
|
e45cbbf1ca | ||
|
|
1a5b6ef260 | ||
|
|
096556d8c9 | ||
|
|
97919c7e87 | ||
|
|
0aa7968503 | ||
|
|
bd1bc78953 | ||
|
|
6ce6dc3ff6 | ||
|
|
e6346775e7 | ||
|
|
d03eed3859 | ||
|
|
afb88616d8 | ||
|
|
543f13f9a3 | ||
|
|
af5c68051a | ||
|
|
5b7cd11de8 | ||
|
|
d3c3496e55 | ||
|
|
c08c8b2789 | ||
|
|
069315e434 | ||
|
|
7e4ad83a1c | ||
|
|
400f9fd680 | ||
|
|
38951f5581 | ||
|
|
b5329ee93d | ||
|
|
c568bca69e | ||
|
|
7b2be12587 | ||
|
|
099fde2652 | ||
|
|
83e5410945 | ||
|
|
b330c34b29 | ||
|
|
e3184622e8 | ||
|
|
28f822afe0 | ||
|
|
a2af811ad2 | ||
|
|
cde8c2d3bd | ||
|
|
79cc84b611 | ||
|
|
f1de0be679 | ||
|
|
854e3d3576 | ||
|
|
dbac2655f5 | ||
|
|
0f656dbf2f | ||
|
|
3fbb3f6773 | ||
|
|
8820814002 | ||
|
|
b40fb3a422 | ||
|
|
aa59575df3 | ||
|
|
accfec9007 | ||
|
|
16410d90b8 | ||
|
|
27c6113287 | ||
|
|
f4a6910ab4 | ||
|
|
bad89160cc | ||
|
|
5782966d63 | ||
|
|
ba2c966329 | ||
|
|
f8dee7e25f | ||
|
|
a8151176d7 | ||
|
|
9ee0b7fe2e | ||
|
|
fb6a7e04f5 | ||
|
|
bfdf487d52 | ||
|
|
b7aac1501d | ||
|
|
273525e6f9 | ||
|
|
064a4938c1 | ||
|
|
182236e742 | ||
|
|
75cb052cca | ||
|
|
d4a378827f | ||
|
|
592d5e8c40 | ||
|
|
733150111d | ||
|
|
cbe91251ac | ||
|
|
1283c6483d | ||
|
|
f24d3d69af | ||
|
|
7984327d81 | ||
|
|
ef90832aea | ||
|
|
9571b8addc | ||
|
|
9601f304a5 | ||
|
|
ff43dac2a7 | ||
|
|
0a43305455 | ||
|
|
54d8224de2 | ||
|
|
c9e34457cd | ||
|
|
47c8eb304f | ||
|
|
2dd39fa218 | ||
|
|
cb618efb98 | ||
|
|
e7ca8090fd | ||
|
|
7861c57317 | ||
|
|
f701b8dc29 | ||
|
|
bd10a850fa | ||
|
|
0f96688a54 | ||
|
|
8eeca90d55 | ||
|
|
367e7f7065 | ||
|
|
ee19eaae62 | ||
|
|
8eb3a3536b | ||
|
|
cfd50231e1 | ||
|
|
1c8ab9e1b4 | ||
|
|
6094cd8578 | ||
|
|
353c49a40b | ||
|
|
277140f218 | ||
|
|
ca9413ccf4 | ||
|
|
c9a0d090cb | ||
|
|
1cd783d3a3 | ||
|
|
1ead764a02 | ||
|
|
45f7b35954 | ||
|
|
6a41540749 | ||
|
|
5b47da67f6 | ||
|
|
292f68ff97 | ||
|
|
3b554d881a | ||
|
|
40ebf468d3 | ||
|
|
4bc6e51862 | ||
|
|
427861cf13 | ||
|
|
da3e7a2eb8 | ||
|
|
2979f04c82 | ||
|
|
1949d8a50c | ||
|
|
ee66c799e0 | ||
|
|
7c50b8bf94 | ||
|
|
141ff74ece | ||
|
|
321e5f1ed6 | ||
|
|
6d131d9d8e | ||
|
|
7e69b8eb31 | ||
|
|
4e0b33e6a4 | ||
|
|
54f7e6fcb8 | ||
|
|
529169c4da | ||
|
|
a2c8c99215 | ||
|
|
e8bf3fd009 | ||
|
|
465676e9ea | ||
|
|
af53b57047 | ||
|
|
54b5f75905 | ||
|
|
4348333497 | ||
|
|
cc31110bcf | ||
|
|
f7c04bf7a6 | ||
|
|
029509ebad | ||
|
|
65102bb64d | ||
|
|
b96b55c5ce | ||
|
|
1f5aba010e | ||
|
|
f0b3bea4e3 | ||
|
|
84fae2d9e0 | ||
|
|
0b96fa112d | ||
|
|
c64bcd23d3 | ||
|
|
efd9a22bb5 | ||
|
|
159c3edfe3 | ||
|
|
f74fa8657b | ||
|
|
648b142a4b | ||
|
|
426f92595e | ||
|
|
82a8d9b644 | ||
|
|
ff9430b8a2 | ||
|
|
2e69ffcb5e | ||
|
|
0ea38db7ef | ||
|
|
a69d4c279e | ||
|
|
2706149399 | ||
|
|
3d0cdc1cb6 | ||
|
|
ac605e9352 | ||
|
|
5432297691 | ||
|
|
e37be0f954 | ||
|
|
a99209b674 | ||
|
|
cb02b5ba18 | ||
|
|
69f14edd80 | ||
|
|
14714b950d | ||
|
|
13654cb8c0 | ||
|
|
00276228cf | ||
|
|
8583bb8d7b | ||
|
|
d48951fe00 | ||
|
|
99bdcfa0a5 | ||
|
|
e64e1a92e6 | ||
|
|
e278e639a3 | ||
|
|
c4bad5c454 | ||
|
|
da41a74efc | ||
|
|
0dc970562a | ||
|
|
2d8401473d | ||
|
|
9c91f57b19 | ||
|
|
f14afcd129 | ||
|
|
5c1a3d82d7 | ||
|
|
e02a917569 | ||
|
|
347fa0fda1 | ||
|
|
6510d4cb02 | ||
|
|
91e4ccf6f8 | ||
|
|
36249874bc | ||
|
|
d2b5d6cce9 | ||
|
|
b2922741c9 | ||
|
|
300f3e27db | ||
|
|
d7330b80a9 | ||
|
|
acdd7667b7 | ||
|
|
8114fa3f5d | ||
|
|
4bc5508f38 | ||
|
|
e503c6092e | ||
|
|
6a8985d8dd | ||
|
|
bee67fd883 | ||
|
|
a1d75d40aa | ||
|
|
29484867ca | ||
|
|
7fa983b971 | ||
|
|
617a8b2814 | ||
|
|
b924d323d4 | ||
|
|
a2efda41d3 | ||
|
|
642c114501 | ||
|
|
02dd3e457d | ||
|
|
ea7b28c9d5 | ||
|
|
472ab4a9ce | ||
|
|
fca84e3edf | ||
|
|
b70235ff92 | ||
|
|
6eff591df7 | ||
|
|
d0b2bf736e | ||
|
|
e5c11ea214 | ||
|
|
6b6443406d | ||
|
|
3452d7852a | ||
|
|
f1fa10badd | ||
|
|
1267621424 | ||
|
|
8a0ec95fe1 | ||
|
|
ba30a63407 | ||
|
|
c56a2adbcb | ||
|
|
2de96d4dc9 | ||
|
|
a486f20892 | ||
|
|
49535deb2e | ||
|
|
7cbf62cf12 | ||
|
|
3b0ace3410 | ||
|
|
5a9c8e1d87 | ||
|
|
daaa65dc0a | ||
|
|
ab4e371524 | ||
|
|
927fd304b0 | ||
|
|
5af84b8e90 | ||
|
|
d425dac499 | ||
|
|
d056459e76 | ||
|
|
3169485f33 | ||
|
|
d9b9f80a93 | ||
|
|
d429505b71 | ||
|
|
72ee708917 | ||
|
|
93bbfac29a | ||
|
|
040d7a6563 | ||
|
|
e8dd930a50 | ||
|
|
31c049ebfe | ||
|
|
d343a37fb2 | ||
|
|
7097175c6f | ||
|
|
8e57c49043 | ||
|
|
9f036ceefd | ||
|
|
ff3ca8b36b | ||
|
|
87a7b70a27 | ||
|
|
9c71c966ca | ||
|
|
6dc99e676e | ||
|
|
3bf5e11f94 | ||
|
|
eef9af2266 | ||
|
|
8316a002da | ||
|
|
c3bf767024 | ||
|
|
0a21a69a9f | ||
|
|
cbc48e31e1 |
3
.github/FUNDING.yml
vendored
@@ -1,3 +1,4 @@
|
|||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
ko_fi: cmdr2_stablediffusion_ui
|
ko_fi: easydiffusion
|
||||||
|
patreon: easydiffusion
|
||||||
|
|||||||
3
.gitignore
vendored
@@ -3,3 +3,6 @@ installer
|
|||||||
installer.tar
|
installer.tar
|
||||||
dist
|
dist
|
||||||
.idea/*
|
.idea/*
|
||||||
|
node_modules/*
|
||||||
|
.tmp1
|
||||||
|
.tmp2
|
||||||
|
|||||||
9
.prettierignore
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
*.min.*
|
||||||
|
*.py
|
||||||
|
*.json
|
||||||
|
*.html
|
||||||
|
/*
|
||||||
|
!/ui
|
||||||
|
/ui/easydiffusion
|
||||||
|
!/ui/plugins
|
||||||
|
!/ui/media
|
||||||
7
.prettierrc.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 120,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"semi": false,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
||||||
1122
3rd-PARTY-LICENSES
Normal file
225
CHANGES.md
@@ -1,10 +1,203 @@
|
|||||||
# What's new?
|
# What's new?
|
||||||
|
|
||||||
|
## v3.0
|
||||||
|
### Major Changes
|
||||||
|
- **ControlNet** - Full support for ControlNet, with native integration of the common ControlNet models. Just select a control image, then choose the ControlNet filter/model and run. No additional configuration or download necessary. Supports custom ControlNets as well.
|
||||||
|
- **SDXL** - Full support for SDXL. No configuration necessary, just put the SDXL model in the `models/stable-diffusion` folder.
|
||||||
|
- **Multiple LoRAs** - Use multiple LoRAs, including SDXL and SD2-compatible LoRAs. Put them in the `models/lora` folder.
|
||||||
|
- **Embeddings** - Use textual inversion embeddings easily, by putting them in the `models/embeddings` folder and using their names in the prompt (or by clicking the `+ Embeddings` button to select embeddings visually). Thanks @JeLuf.
|
||||||
|
- **Seamless Tiling** - Generate repeating textures that can be useful for games and other art projects. Works best in 512x512 resolution. Thanks @JeLuf.
|
||||||
|
- **Inpainting Models** - Full support for inpainting models, including custom inpainting models. No configuration (or yaml files) necessary.
|
||||||
|
- **Faster than v2.5** - Nearly 40% faster than Easy Diffusion v2.5, and can be even faster if you enable xFormers.
|
||||||
|
- **Even less VRAM usage** - Less than 2 GB for 512x512 images on 'low' VRAM usage setting (SD 1.5). Can generate large images with SDXL.
|
||||||
|
- **WebP images** - Supports saving images in the lossless webp format.
|
||||||
|
- **Undo/Redo in the UI** - Remove tasks or images from the queue easily, and undo the action if you removed anything accidentally. Thanks @JeLuf.
|
||||||
|
- **Three new samplers, and latent upscaler** - Added `DEIS`, `DDPM` and `DPM++ 2m SDE` as additional samplers. Thanks @ogmaresca and @rbertus2000.
|
||||||
|
- **Significantly faster 'Upscale' and 'Fix Faces' buttons on the images**
|
||||||
|
- **Major rewrite of the code** - We've switched to using diffusers under-the-hood, which allows us to release new features faster, and focus on making the UI and installer even easier to use.
|
||||||
|
|
||||||
|
### Detailed changelog
|
||||||
|
* 3.0.8 - 13 Mar 2024 - Update diffusers version to v0.26.3.
|
||||||
|
* 3.0.7 - 11 Dec 2023 - Setting to enable/disable VAE tiling (in the Image Settings panel). Sometimes VAE tiling reduces the quality of the image, so this setting will help control that.
|
||||||
|
* 3.0.6 - 18 Sep 2023 - Add thumbnails to embeddings from the UI, using the new `Upload Thumbnail` button in the Embeddings popup. Thanks @JeLuf.
|
||||||
|
* 3.0.6 - 15 Sep 2023 - Fix broken embeddings dialog when LoRA information couldn't be fetched.
|
||||||
|
* 3.0.6 - 14 Sep 2023 - UI for adding notes to LoRA files (to help you remember which prompts to use). Also added a button to automatically fetch prompts from Civitai for a LoRA file, using the `Import from Civitai` button. Thanks @JeLuf.
|
||||||
|
* 3.0.5 - 2 Sep 2023 - Support SDXL ControlNets.
|
||||||
|
* 3.0.4 - 1 Sep 2023 - Fix incorrect metadata generated for embeddings, when the exact word doesn't match the case, or is part of a larger word.
|
||||||
|
* 3.0.4 - 1 Sep 2023 - Simplify the installation for AMD users on Linux. Thanks @JeLuf.
|
||||||
|
* 3.0.4 - 1 Sep 2023 - Allow using a different folder for models. This is useful if you want to share a models folder across different software, or on a different drive. You can change this path in the Settings tab.
|
||||||
|
* 3.0.3 - 31 Aug 2023 - Auto-save images to disk (if enabled by the user) when upscaling/fixing using the buttons on the image.
|
||||||
|
* 3.0.3 - 30 Aug 2023 - Allow loading NovelAI-based custom models.
|
||||||
|
* 3.0.3 - 30 Aug 2023 - Fix broken VAE tiling. This allows you to create larger images with lesser VRAM usage.
|
||||||
|
* 3.0.3 - 30 Aug 2023 - Allow blocking NSFW images using a server-side config. This prevents the browser from generating NSFW images or changing the config. Open `config.yaml` in a text editor (e.g. Notepad), and add `block_nsfw: true` at the end, and save the file.
|
||||||
|
* 3.0.2 - 29 Aug 2023 - Fixed incorrect matching of embeddings from prompts.
|
||||||
|
* 3.0.2 - 24 Aug 2023 - Fix broken seamless tiling.
|
||||||
|
* 3.0.2 - 23 Aug 2023 - Fix styling on mobile devices.
|
||||||
|
* 3.0.2 - 22 Aug 2023 - Full support for inpainting models, including custom models. Support SD 1.x and SD 2.x inpainting models. Does not require you to specify a yaml config file.
|
||||||
|
* 3.0.2 - 22 Aug 2023 - Reduce VRAM consumption of controlnet in 'low' VRAM mode, and allow accelerating controlnets using xformers.
|
||||||
|
* 3.0.2 - 22 Aug 2023 - Improve auto-detection of SD 2.0 and 2.1 models, removing the need for custom yaml files for SD 2.x models. Improve the model load time by speeding-up the black image test.
|
||||||
|
* 3.0.1 - 18 Aug 2023 - Rotate an image if EXIF rotation is present. For e.g. this is common in images taken with a smartphone.
|
||||||
|
* 3.0.1 - 18 Aug 2023 - Resize control images to the task dimensions, to avoid memory errors with high-res control images.
|
||||||
|
* 3.0.1 - 18 Aug 2023 - Show controlnet filter preview in the task entry.
|
||||||
|
* 3.0.1 - 18 Aug 2023 - Fix drag-and-drop and 'Use these Settings' for LoRA and ControlNet.
|
||||||
|
* 3.0.1 - 18 Aug 2023 - Auto-save LoRA models and strengths.
|
||||||
|
* 3.0.1 - 17 Aug 2023 - Automatically use the correct yaml config file for custom SDXL models, even if a yaml file isn't present in the folder.
|
||||||
|
* 3.0.1 - 17 Aug 2023 - Fix broken embeddings with SDXL.
|
||||||
|
* 3.0.1 - 16 Aug 2023 - Fix broken LoRA with SDXL.
|
||||||
|
* 3.0.1 - 15 Aug 2023 - Fix broken seamless tiling.
|
||||||
|
* 3.0.1 - 15 Aug 2023 - Fix textual inversion embeddings not working in `low` VRAM usage mode.
|
||||||
|
* 3.0.1 - 15 Aug 2023 - Fix for custom VAEs not working in `low` VRAM usage mode.
|
||||||
|
* 3.0.1 - 14 Aug 2023 - Slider to change the image dimensions proportionally (in Image Settings). Thanks @JeLuf.
|
||||||
|
* 3.0.1 - 14 Aug 2023 - Show an error to the user if an embedding isn't compatible with the model, instead of failing silently without informing the user. Thanks @JeLuf.
|
||||||
|
* 3.0.1 - 14 Aug 2023 - Disable watermarking for SDXL img2img. Thanks @AvidGameFan.
|
||||||
|
* 3.0.0 - 3 Aug 2023 - Enabled diffusers for everyone by default. The old v2 engine can be used by disabling the "Use v3 engine" option in the Settings tab.
|
||||||
|
|
||||||
|
## v2.5
|
||||||
|
### Major Changes
|
||||||
|
- **Nearly twice as fast** - significantly faster speed of image generation. Code contributions are welcome to make our project even faster: https://github.com/easydiffusion/sdkit/#is-it-fast
|
||||||
|
- **Mac M1/M2 support** - Experimental support for Mac M1/M2. Thanks @michaelgallacher, @JeLuf and vishae.
|
||||||
|
- **AMD support for Linux** - Experimental support for AMD GPUs on Linux. Thanks @DianaNites and @JeLuf.
|
||||||
|
- **Full support for Stable Diffusion 2.1 (including CPU)** - supports loading v1.4 or v2.0 or v2.1 models seamlessly. No need to enable "Test SD2", and no need to add `sd2_` to your SD 2.0 model file names. Works on CPU as well.
|
||||||
|
- **Memory optimized Stable Diffusion 2.1** - you can now use Stable Diffusion 2.1 models, with the same low VRAM optimizations that we've always had for SD 1.4. Please note, the SD 2.0 and 2.1 models require more GPU and System RAM, as compared to the SD 1.4 and 1.5 models.
|
||||||
|
- **11 new samplers!** - explore the new samplers, some of which can generate great images in less than 10 inference steps! We've added the Karras and UniPC samplers. Thanks @Schorny for the UniPC samplers.
|
||||||
|
- **Model Merging** - You can now merge two models (`.ckpt` or `.safetensors`) and output `.ckpt` or `.safetensors` models, optionally in `fp16` precision. Details: https://github.com/easydiffusion/easydiffusion/wiki/Model-Merging . Thanks @JeLuf.
|
||||||
|
- **Fast loading/unloading of VAEs** - No longer needs to reload the entire Stable Diffusion model, each time you change the VAE
|
||||||
|
- **Database of known models** - automatically picks the right configuration for known models. E.g. we automatically detect and apply "v" parameterization (required for some SD 2.0 models), and "fp32" attention precision (required for some SD 2.1 models).
|
||||||
|
- **Color correction for img2img** - an option to preserve the color profile (histogram) of the initial image. This is especially useful if you're getting red-tinted images after inpainting/masking.
|
||||||
|
- **Three GPU Memory Usage Settings** - `High` (fastest, maximum VRAM usage), `Balanced` (default - almost as fast, significantly lower VRAM usage), `Low` (slowest, very low VRAM usage). The `Low` setting is applied automatically for GPUs with less than 4 GB of VRAM.
|
||||||
|
- **Find models in sub-folders** - This allows you to organize your models into sub-folders inside `models/stable-diffusion`, instead of keeping them all in a single folder. Thanks @patriceac and @ogmaresca.
|
||||||
|
- **Custom Modifier Categories** - Ability to create custom modifiers with thumbnails, and custom categories (and hierarchy of categories). Details: https://github.com/easydiffusion/easydiffusion/wiki/Custom-Modifiers . Thanks @ogmaresca.
|
||||||
|
- **Embed metadata, or save as TXT/JSON** - You can now embed the metadata directly into the images, or save them as text or json files (choose in the Settings tab). Thanks @patriceac.
|
||||||
|
- **Major rewrite of the code** - Most of the codebase has been reorganized and rewritten, to make it more manageable and easier for new developers to contribute features. We've separated our core engine into a new project called `sdkit`, which allows anyone to easily integrate Stable Diffusion (and related modules like GFPGAN etc) into their programming projects (via a simple `pip install sdkit`): https://github.com/easydiffusion/sdkit/
|
||||||
|
- **Name change** - Last, and probably the least, the UI is now called "Easy Diffusion". It indicates the focus of this project - an easy way for people to play with Stable Diffusion.
|
||||||
|
|
||||||
|
Our focus continues to remain on an easy installation experience, and an easy user-interface. While still remaining pretty powerful, in terms of features and speed.
|
||||||
|
|
||||||
|
### Detailed changelog
|
||||||
|
* 2.5.48 - 1 Aug 2023 - (beta-only) Full support for ControlNets. You can select a control image to guide the AI. You can pick a filter to pre-process the image, and one of the known (or custom) controlnet models. Supports `OpenPose`, `Canny`, `Straight Lines`, `Depth`, `Line Art`, `Scribble`, `Soft Edge`, `Shuffle` and `Segment`.
|
||||||
|
* 2.5.47 - 30 Jul 2023 - An option to use `Strict Mask Border` while inpainting, to avoid touching areas outside the mask. But this might show a slight outline of the mask, which you will have to touch up separately.
|
||||||
|
* 2.5.47 - 29 Jul 2023 - (beta-only) Fix long prompts with SDXL.
|
||||||
|
* 2.5.47 - 29 Jul 2023 - (beta-only) Fix red dots in some SDXL images.
|
||||||
|
* 2.5.47 - 29 Jul 2023 - Significantly faster `Fix Faces` and `Upscale` buttons (on the image). They no longer need to generate the image from scratch, instead they just upscale/fix the generated image in-place.
|
||||||
|
* 2.5.47 - 28 Jul 2023 - Lots of internal code reorganization, in preparation for supporting Controlnets. No user-facing changes.
|
||||||
|
* 2.5.46 - 27 Jul 2023 - (beta-only) Full support for SD-XL models (base and refiner)!
|
||||||
|
* 2.5.45 - 24 Jul 2023 - (beta-only) Hide the samplers that won't be supported in the new diffusers version.
|
||||||
|
* 2.5.45 - 22 Jul 2023 - (beta-only) Fix the recently-broken inpainting models.
|
||||||
|
* 2.5.45 - 16 Jul 2023 - (beta-only) Fix the image quality of LoRAs, which had degraded in v2.5.44.
|
||||||
|
* 2.5.44 - 15 Jul 2023 - (beta-only) Support for multiple LoRA files.
|
||||||
|
* 2.5.43 - 9 Jul 2023 - (beta-only) Support for loading Textual Inversion embeddings. You can find the option in the Image Settings panel. Thanks @JeLuf.
|
||||||
|
* 2.5.43 - 9 Jul 2023 - Improve the startup time of the UI.
|
||||||
|
* 2.5.42 - 4 Jul 2023 - Keyboard shortcuts for the Image Editor. Thanks @JeLuf.
|
||||||
|
* 2.5.42 - 28 Jun 2023 - Allow dropping images from folders to use as an Initial Image.
|
||||||
|
* 2.5.42 - 26 Jun 2023 - Show a popup for Image Modifiers, allowing a larger screen space, better UX on mobile screens, and more room for us to develop and improve the Image Modifiers panel. Thanks @Hakorr.
|
||||||
|
* 2.5.42 - 26 Jun 2023 - (beta-only) Show a welcome screen for users of the diffusers beta, with instructions on how to use the new prompt syntax, and known bugs. Thanks @JeLuf.
|
||||||
|
* 2.5.42 - 26 Jun 2023 - Use YAML files for config. You can now edit the `config.yaml` file (using a text editor, like Notepad). This file is present inside the Easy Diffusion folder, and is easier to read and edit (for humans) than JSON. Thanks @JeLuf.
|
||||||
|
* 2.5.41 - 24 Jun 2023 - (beta-only) Fix broken inpainting in low VRAM usage mode.
|
||||||
|
* 2.5.41 - 24 Jun 2023 - (beta-only) Fix a recent regression where the LoRA would not get applied when changing SD models.
|
||||||
|
* 2.5.41 - 23 Jun 2023 - Fix a regression where latent upscaler stopped working on PCs without a graphics card.
|
||||||
|
* 2.5.41 - 20 Jun 2023 - Automatically fix black images if fp32 attention precision is required in diffusers.
|
||||||
|
* 2.5.41 - 19 Jun 2023 - Another fix for multi-gpu rendering (in all VRAM usage modes).
|
||||||
|
* 2.5.41 - 13 Jun 2023 - Fix multi-gpu bug with "low" VRAM usage mode while generating images.
|
||||||
|
* 2.5.41 - 12 Jun 2023 - Fix multi-gpu bug with CodeFormer.
|
||||||
|
* 2.5.41 - 6 Jun 2023 - Allow changing the strength of CodeFormer, and slightly improved styling of the CodeFormer options.
|
||||||
|
* 2.5.41 - 5 Jun 2023 - Allow sharing an Easy Diffusion instance via https://try.cloudflare.com/ . You can find this option at the bottom of the Settings tab. Thanks @JeLuf.
|
||||||
|
* 2.5.41 - 5 Jun 2023 - Show an option to download for tiled images. Shows a button on the generated image. Creates larger images by tiling them with the image generated by Easy Diffusion. Thanks @JeLuf.
|
||||||
|
* 2.5.41 - 5 Jun 2023 - (beta-only) Allow LoRA strengths between -2 and 2. Thanks @ogmaresca.
|
||||||
|
* 2.5.40 - 5 Jun 2023 - Reduce the VRAM usage of Latent Upscaling when using "balanced" VRAM usage mode.
|
||||||
|
* 2.5.40 - 5 Jun 2023 - Fix the "realesrgan" key error when using CodeFormer with more than 1 image in a batch.
|
||||||
|
* 2.5.40 - 3 Jun 2023 - Added CodeFormer as another option for fixing faces and eyes. CodeFormer tends to perform better than GFPGAN for many images. Thanks @patriceac for the implementation, and for contacting the CodeFormer team (who were supportive of it being integrated into Easy Diffusion).
|
||||||
|
* 2.5.39 - 25 May 2023 - (beta-only) Seamless Tiling - make seamlessly tiled images, e.g. rock and grass textures. Thanks @JeLuf.
|
||||||
|
* 2.5.38 - 24 May 2023 - Better reporting of errors, and show an explanation if the user cannot disable the "Use CPU" setting.
|
||||||
|
* 2.5.38 - 23 May 2023 - Add Latent Upscaler as another option for upscaling images. Thanks @JeLuf for the implementation of the Latent Upscaler model.
|
||||||
|
* 2.5.37 - 19 May 2023 - (beta-only) Two more samplers: DDPM and DEIS. Also disables the samplers that aren't working yet in the Diffusers version. Thanks @ogmaresca.
|
||||||
|
* 2.5.37 - 19 May 2023 - (beta-only) Support CLIP-Skip. You can set this option under the models dropdown. Thanks @JeLuf.
|
||||||
|
* 2.5.37 - 19 May 2023 - (beta-only) More VRAM optimizations for all modes in diffusers. The VRAM usage for diffusers in "low" and "balanced" should now be equal or less than the non-diffusers version. Performs softmax in half precision, like sdkit does.
|
||||||
|
* 2.5.36 - 16 May 2023 - (beta-only) More VRAM optimizations for "balanced" VRAM usage mode.
|
||||||
|
* 2.5.36 - 11 May 2023 - (beta-only) More VRAM optimizations for "low" VRAM usage mode.
|
||||||
|
* 2.5.36 - 10 May 2023 - (beta-only) Bug fix for "meta" error when using a LoRA in 'low' VRAM usage mode.
|
||||||
|
* 2.5.35 - 8 May 2023 - Allow dragging a zoomed-in image (after opening an image with the "expand" button). Thanks @ogmaresca.
|
||||||
|
* 2.5.35 - 3 May 2023 - (beta-only) First round of VRAM Optimizations for the "Test Diffusers" version. This change significantly reduces the amount of VRAM used by the diffusers version during image generation. The VRAM usage is still not equal to the "non-diffusers" version, but more optimizations are coming soon.
|
||||||
|
* 2.5.34 - 22 Apr 2023 - Don't start the browser in an incognito new profile (on Windows). Thanks @JeLuf.
|
||||||
|
* 2.5.33 - 21 Apr 2023 - Install PyTorch 2.0 on new installations (on Windows and Linux).
|
||||||
|
* 2.5.32 - 19 Apr 2023 - Automatically check for black images, and set full-precision if necessary (for attn). This means custom models based on Stable Diffusion v2.1 will just work, without needing special command-line arguments or editing of yaml config files.
|
||||||
|
* 2.5.32 - 18 Apr 2023 - Automatic support for AMD graphics cards on Linux. Thanks @DianaNites and @JeLuf.
|
||||||
|
* 2.5.31 - 10 Apr 2023 - Reduce VRAM usage while upscaling.
|
||||||
|
* 2.5.31 - 6 Apr 2023 - Allow seeds upto `4,294,967,295`. Thanks @ogmaresca.
|
||||||
|
* 2.5.31 - 6 Apr 2023 - Buttons to show the previous/next image in the image popup. Thanks @ogmaresca.
|
||||||
|
* 2.5.30 - 5 Apr 2023 - Fix a bug where the JPEG image quality wasn't being respected when embedding the metadata into it. Thanks @JeLuf.
|
||||||
|
* 2.5.30 - 1 Apr 2023 - (beta-only) Slider to control the strength of the LoRA model.
|
||||||
|
* 2.5.30 - 28 Mar 2023 - Refactor task entry config to use a generating method. Added ability for plugins to easily add to this. Removed confusing sentence from `contributing.md`
|
||||||
|
* 2.5.30 - 28 Mar 2023 - Allow the user to undo the deletion of tasks or images, instead of showing a pop-up each time. The new `Undo` button will be present at the top of the UI. Thanks @JeLuf.
|
||||||
|
* 2.5.30 - 28 Mar 2023 - Support saving lossless WEBP images. Thanks @ogmaresca.
|
||||||
|
* 2.5.30 - 28 Mar 2023 - Lots of bug fixes for the UI (Read LoRA flag in metadata files, new prompt weight format with scrollwheel, fix overflow with lots of tabs, clear button in image editor, shorter filenames in download). Thanks @patriceac, @JeLuf and @ogmaresca.
|
||||||
|
* 2.5.29 - 27 Mar 2023 - (beta-only) Fix a bug where some non-square images would fail while inpainting with a `The size of tensor a must match size of tensor b` error.
|
||||||
|
* 2.5.29 - 27 Mar 2023 - (beta-only) Fix the `incorrect number of channels` error, when given a PNG image with an alpha channel in `Test Diffusers`.
|
||||||
|
* 2.5.29 - 27 Mar 2023 - (beta-only) Fix broken inpainting in `Test Diffusers`.
|
||||||
|
* 2.5.28 - 24 Mar 2023 - (beta-only) Support for weighted prompts and long prompt lengths (not limited to 77 tokens). This change requires enabling the `Test Diffusers` setting in beta (in the Settings tab), and restarting the program.
|
||||||
|
* 2.5.27 - 21 Mar 2023 - (beta-only) LoRA support, accessible by enabling the `Test Diffusers` setting (in the Settings tab in the UI). This change switches the internal engine to diffusers (if the `Test Diffusers` setting is enabled). If the `Test Diffusers` flag is disabled, it'll have no impact for the user.
|
||||||
|
* 2.5.26 - 15 Mar 2023 - Allow styling the buttons displayed on an image. Update the API to allow multiple buttons and text labels in a single row. Thanks @ogmaresca.
|
||||||
|
* 2.5.26 - 15 Mar 2023 - View images in full-screen, by either clicking on the image, or clicking the "Full screen" icon next to the Seed number on the image. Thanks @ogmaresca for the internal API.
|
||||||
|
* 2.5.25 - 14 Mar 2023 - Button to download all the images, and all the metadata as a zip file. This is available at the top of the UI, as well as on each image. Thanks @JeLuf.
|
||||||
|
* 2.5.25 - 14 Mar 2023 - Lots of UI tweaks and bug fixes. Thanks @patriceac and @JeLuf.
|
||||||
|
* 2.5.24 - 11 Mar 2023 - Button to load an image mask from a file.
|
||||||
|
* 2.5.24 - 10 Mar 2023 - Logo change. Image credit: @lazlo_vii.
|
||||||
|
* 2.5.23 - 8 Mar 2023 - Experimental support for Mac M1/M2. Thanks @michaelgallacher, @JeLuf and vishae!
|
||||||
|
* 2.5.23 - 8 Mar 2023 - Ability to create custom modifiers with thumbnails, and custom categories (and hierarchy of categories). More details - https://github.com/easydiffusion/easydiffusion/wiki/Custom-Modifiers . Thanks @ogmaresca.
|
||||||
|
* 2.5.22 - 28 Feb 2023 - Minor styling changes to UI buttons, and the models dropdown.
|
||||||
|
* 2.5.22 - 28 Feb 2023 - Lots of UI-related bug fixes. Thanks @patriceac.
|
||||||
|
* 2.5.21 - 22 Feb 2023 - An option to control the size of the image thumbnails. You can use the `Display options` in the top-right corner to change this. Thanks @JeLuf.
|
||||||
|
* 2.5.20 - 20 Feb 2023 - Support saving images in WEBP format (which consumes less disk space, with similar quality). Thanks @ogmaresca.
|
||||||
|
* 2.5.20 - 18 Feb 2023 - A setting to block NSFW images from being generated. You can enable this setting in the Settings tab.
|
||||||
|
* 2.5.19 - 17 Feb 2023 - Initial support for server-side plugins. Currently supports overriding the `get_cond_and_uncond()` function.
|
||||||
|
* 2.5.18 - 17 Feb 2023 - 5 new samplers! UniPC samplers, some of which produce images in less than 15 steps. Thanks @Schorny.
|
||||||
|
* 2.5.16 - 13 Feb 2023 - Searchable dropdown for models. This is useful if you have a LOT of models. You can type part of the model name, to auto-search through your models. Thanks @patriceac for the feature, and @AssassinJN for help in UI tweaks!
|
||||||
|
* 2.5.16 - 13 Feb 2023 - Lots of fixes and improvements to the installer. First round of changes to add Mac support. Thanks @JeLuf.
|
||||||
|
* 2.5.16 - 13 Feb 2023 - UI bug fixes for the inpainter editor. Thanks @patriceac.
|
||||||
|
* 2.5.16 - 13 Feb 2023 - Fix broken task reorder. Thanks @JeLuf.
|
||||||
|
* 2.5.16 - 13 Feb 2023 - Remove a task if all the images inside it have been removed. Thanks @AssassinJN.
|
||||||
|
* 2.5.16 - 10 Feb 2023 - Embed metadata into the JPG/PNG images, if selected in the "Settings" tab (under "Metadata format"). Thanks @patriceac.
|
||||||
|
* 2.5.16 - 10 Feb 2023 - Sort models alphabetically in the models dropdown. Thanks @ogmaresca.
|
||||||
|
* 2.5.16 - 10 Feb 2023 - Support multiple GFPGAN models. Download new GFPGAN models into the `models/gfpgan` folder, and refresh the UI to use it. Thanks @JeLuf.
|
||||||
|
* 2.5.16 - 10 Feb 2023 - Allow a server to enforce a fixed directory path to save images. This is useful if the server is exposed to a lot of users. This can be set in the `config.json` file as `force_save_path: "/path/to/fixed/save/dir"`. E.g. `force_save_path: "D:/user_images"`. Thanks @JeLuf.
|
||||||
|
* 2.5.16 - 10 Feb 2023 - The "Make Images" button now shows the correct amount of images it'll create when using operators like `{}` or `|`. For e.g. if the prompt is `Photo of a {woman, man}`, then the button will say `Make 2 Images`. Thanks @JeLuf.
|
||||||
|
* 2.5.16 - 10 Feb 2023 - A bunch of UI-related bug fixes. Thanks @patriceac.
|
||||||
|
* 2.5.15 - 8 Feb 2023 - Allow using 'balanced' VRAM usage mode on GPUs with 4 GB or less of VRAM. This mode used to be called 'Turbo' in the previous version.
|
||||||
|
* 2.5.14 - 8 Feb 2023 - Fix broken auto-save settings. We renamed `sampler` to `sampler_name`, which caused old settings to fail.
|
||||||
|
* 2.5.14 - 6 Feb 2023 - Simplify the UI for merging models, and some other minor UI tweaks. Better error reporting if a model failed to load.
|
||||||
|
* 2.5.14 - 3 Feb 2023 - Fix the 'Make Similar Images' button, which was producing incorrect images (weren't very similar).
|
||||||
|
* 2.5.13 - 1 Feb 2023 - Fix the remaining GPU memory leaks, including a better fix (more comprehensive) for the change in 2.5.12 (27 Jan).
|
||||||
|
* 2.5.12 - 27 Jan 2023 - Fix a memory leak, which made the UI unresponsive after an out-of-memory error. The allocated memory is now freed-up after an error.
|
||||||
|
* 2.5.11 - 25 Jan 2023 - UI for Merging Models. Thanks @JeLuf. More info: https://github.com/easydiffusion/easydiffusion/wiki/Model-Merging
|
||||||
|
* 2.5.10 - 24 Jan 2023 - Reduce the VRAM usage for img2img in 'balanced' mode (without reducing the rendering speed), to make it similar to v2.4 of this UI.
|
||||||
|
* 2.5.9 - 23 Jan 2023 - Fix a bug where img2img would produce poorer-quality images for the same settings, as compared to version 2.4 of this UI.
|
||||||
|
* 2.5.9 - 23 Jan 2023 - Reduce the VRAM usage for 'balanced' mode (without reducing the rendering speed), to make it similar to v2.4 of the UI.
|
||||||
|
* 2.5.8 - 17 Jan 2023 - Fix a bug where 'Low' VRAM usage would consume a LOT of VRAM (on higher-end GPUs). Also fixed a bug that caused out-of-memory errors on SD 2.1-768 models, on 'high' VRAM usage setting.
|
||||||
|
* 2.5.7 - 16 Jan 2023 - Fix a bug where VAE files ending with .vae.pt weren't getting displayed. Thanks Madrang, rbertus2000 and JeLuf.
|
||||||
|
* 2.5.6 - 10 Jan 2023 - `Fill` tool for the Image Editor, to allow filling areas with color (or the entire image). And some bug fixes to the Image Editor. Thanks @mdiller.
|
||||||
|
* 2.5.6 - 10 Jan 2023 - Find Stable Diffusion models in sub-folders inside `models/stable-diffusion`. This allows you to organize your models into sub-folders, instead of keeping them all in a single folder. Thanks @JeLuf.
|
||||||
|
* 2.5.5 - 9 Jan 2023 - Lots of bug fixes. Thanks @patriceac and @JeLuf.
|
||||||
|
* 2.5.4 - 29 Dec 2022 - Press Esc key on the keyboard to close the Image Editor. Thanks @patriceac.
|
||||||
|
* 2.5.4 - 29 Dec 2022 - Lots of bug fixes in the UI. Thanks @patriceac.
|
||||||
|
* 2.5.4 - 28 Dec 2022 - Full support for running tasks in parallel on multiple GPUs. Warning: 'Euler Ancestral', 'DPM2 Ancestral' and 'DPM++ 2s Ancestral' may produce slight variations in the image (if run in parallel), so we recommend using the other samplers.
|
||||||
|
* 2.5.3 - 27 Dec 2022 - Fix broken drag-and-drop for text metadata files (as well as paste in clipboard).
|
||||||
|
* 2.5.3 - 27 Dec 2022 - Allow upscaling by 2x as well as 4x.
|
||||||
|
* 2.5.3 - 27 Dec 2022 - Fix broken renders on a second GPU.
|
||||||
|
* 2.5.3 - 26 Dec 2022 - Add a `Remove` button on each image. Thanks @JeLuf.
|
||||||
|
* 2.5.2 - 26 Dec 2022 - Fix broken inpainting if using non-square target images.
|
||||||
|
* 2.5.2 - 26 Dec 2022 - Fix a bug where an incorrect model config would get used for some SD 2.1 models.
|
||||||
|
* 2.5.2 - 26 Dec 2022 - Slight performance and memory improvement while rendering using SD 2.1 models.
|
||||||
|
* 2.5.1 - 25 Dec 2022 - Allow custom config yaml files for models. You can put a config file (`.yaml`) next to the model file, with the same name as the model. For e.g. if you put `robo-diffusion-v2-base.yaml` next to `robo-diffusion-v2-base.ckpt`, it'll automatically use that config file.
|
||||||
|
* 2.5.1 - 25 Dec 2022 - Fix broken rendering for SD 2.1-768 models. Fix broken rendering SD 2.0 safetensor models.
|
||||||
|
* 2.5.0 - 25 Dec 2022 - Major new release! Nearly twice as fast, Full support for SD 2.1 (including low GPU RAM optimizations), 6 new samplers, Model Merging, Fast loading/unloading of VAEs, Database of known models, Color correction for img2img, Three GPU Memory Usage Settings, Save metadata as JSON, Major rewrite of the code, Name change.
|
||||||
|
|
||||||
## v2.4
|
## v2.4
|
||||||
### Major Changes
|
### Major Changes
|
||||||
- **Automatic scanning for malicious model files** - using `picklescan`. Thanks @JeLuf
|
- **Allow reordering the task queue** (by dragging and dropping tasks). Thanks @madrang
|
||||||
- **Support for custom VAE models**. You can place your VAE files in the `models/vae` folder, and refresh the browser page to use them. More info: https://github.com/cmdr2/stable-diffusion-ui/wiki/VAE-Variational-Auto-Encoder
|
- **Automatic scanning for malicious model files** - using `picklescan`, and support for `safetensor` model format. Thanks @JeLuf
|
||||||
- **Experimental support for multiple GPUs!** It should work automatically. Just open one browser tab per GPU, and spread your tasks across your GPUs. For e.g. open our UI in two browser tabs if you have two GPUs. You can customize which GPUs it should use in the "Settings" tab, otherwise let it automatically pick the best GPUs. Thanks @madrang . More info: https://github.com/cmdr2/stable-diffusion-ui/wiki/Run-on-Multiple-GPUs
|
- **Image Editor** - for drawing simple images for guiding the AI. Thanks @mdiller
|
||||||
|
- **Use pre-trained hypernetworks** - for improving the quality of images. Thanks @C0bra5
|
||||||
|
- **Support for custom VAE models**. You can place your VAE files in the `models/vae` folder, and refresh the browser page to use them. More info: https://github.com/easydiffusion/easydiffusion/wiki/VAE-Variational-Auto-Encoder
|
||||||
|
- **Experimental support for multiple GPUs!** It should work automatically. Just open one browser tab per GPU, and spread your tasks across your GPUs. For e.g. open our UI in two browser tabs if you have two GPUs. You can customize which GPUs it should use in the "Settings" tab, otherwise let it automatically pick the best GPUs. Thanks @madrang . More info: https://github.com/easydiffusion/easydiffusion/wiki/Run-on-Multiple-GPUs
|
||||||
- **Cleaner UI design** - Show settings and help in new tabs, instead of dropdown popups (which were buggy). Thanks @mdiller
|
- **Cleaner UI design** - Show settings and help in new tabs, instead of dropdown popups (which were buggy). Thanks @mdiller
|
||||||
- **Progress bar.** Thanks @mdiller
|
- **Progress bar.** Thanks @mdiller
|
||||||
- **Custom Image Modifiers** - You can now save your custom image modifiers! Your saved modifiers can include special characters like `{}, (), [], |`
|
- **Custom Image Modifiers** - You can now save your custom image modifiers! Your saved modifiers can include special characters like `{}, (), [], |`
|
||||||
@@ -19,8 +212,34 @@
|
|||||||
- Configuration to prevent the browser from opening on startup
|
- Configuration to prevent the browser from opening on startup
|
||||||
- Lots of minor bug fixes
|
- Lots of minor bug fixes
|
||||||
- A `What's New?` tab in the UI
|
- A `What's New?` tab in the UI
|
||||||
|
- Ask for a confimation before clearing the results pane or stopping a render task. The dialog can be skipped by holding down the shift key while clicking on the button.
|
||||||
|
- Show the network addresses of the server in the systems setting dialog
|
||||||
|
- Support loading models in the safetensor format, for improved safety
|
||||||
|
|
||||||
### Detailed changelog
|
### Detailed changelog
|
||||||
|
* 2.4.24 - 9 Jan 2022 - Urgent fix for failures on old/long-term-support browsers. Thanks @JeLuf.
|
||||||
|
* 2.4.23/22 - 29 Dec 2022 - Allow rolling back from the upcoming v2.5 change (in beta).
|
||||||
|
* 2.4.21 - 23 Dec 2022 - Speed up image creation, by removing a delay (regression) of 4-5 seconds between clicking the `Make Image` button and calling the server.
|
||||||
|
* 2.4.20 - 22 Dec 2022 - `Pause All` button to pause all the pending tasks. Thanks @JeLuf
|
||||||
|
* 2.4.20 - 22 Dec 2022 - `Undo`/`Redo` buttons in the image editor. Thanks @JeLuf
|
||||||
|
* 2.4.20 - 22 Dec 2022 - Drag handle to reorder the tasks. This fixed a bug where the metadata was no longer selectable (for copying). Thanks @JeLuf
|
||||||
|
* 2.4.19 - 17 Dec 2022 - Add Undo/Redo buttons in the Image Editor. Thanks @JeLuf
|
||||||
|
* 2.4.19 - 10 Dec 2022 - Show init img in task list
|
||||||
|
* 2.4.19 - 7 Dec 2022 - Use pre-trained hypernetworks while generating images. Thanks @C0bra5
|
||||||
|
* 2.4.19 - 6 Dec 2022 - Allow processing new tasks first. Thanks @madrang
|
||||||
|
* 2.4.19 - 6 Dec 2022 - Allow reordering the task queue (by dragging tasks). Thanks @madrang
|
||||||
|
* 2.4.19 - 6 Dec 2022 - Re-organize the code, to make it easier to write user plugins. Thanks @madrang
|
||||||
|
* 2.4.18 - 5 Dec 2022 - Make JPEG Output quality user controllable. Thanks @JeLuf
|
||||||
|
* 2.4.18 - 5 Dec 2022 - Support loading models in the safetensor format, for improved safety. Thanks @JeLuf
|
||||||
|
* 2.4.18 - 1 Dec 2022 - Image Editor, for drawing simple images for guiding the AI. Thanks @mdiller
|
||||||
|
* 2.4.18 - 1 Dec 2022 - Disable an image modifier temporarily by right-clicking it. Thanks @patriceac
|
||||||
|
* 2.4.17 - 30 Nov 2022 - Scroll to generated image. Thanks @patriceac
|
||||||
|
* 2.4.17 - 30 Nov 2022 - Show the network addresses of the server in the systems setting dialog. Thanks @JeLuf
|
||||||
|
* 2.4.17 - 30 Nov 2022 - Fix a bug where GFPGAN wouldn't work properly when multiple GPUs tried to run it at the same time. Thanks @madrang
|
||||||
|
* 2.4.17 - 30 Nov 2022 - Confirm before stopping or clearing all the tasks. Thanks @JeLuf
|
||||||
|
* 2.4.16 - 29 Nov 2022 - Bug fixes for SD 2.0 - remove the need for patching, default to SD 1.4 model if trying to load an SD2 model in SD1.4.
|
||||||
|
* 2.4.15 - 25 Nov 2022 - Experimental support for SD 2.0. Uses lots of memory, not optimized, probably GPU-only.
|
||||||
|
* 2.4.14 - 22 Nov 2022 - Change the backend to a custom fork of Stable Diffusion
|
||||||
* 2.4.13 - 21 Nov 2022 - Change the modifier weight via mouse wheel, drag to reorder selected modifiers, and some more modifier-related fixes. Thanks @patriceac
|
* 2.4.13 - 21 Nov 2022 - Change the modifier weight via mouse wheel, drag to reorder selected modifiers, and some more modifier-related fixes. Thanks @patriceac
|
||||||
* 2.4.12 - 21 Nov 2022 - Another fix for improving how long images take to generate. Reduces the time taken for an enqueued task to start processing.
|
* 2.4.12 - 21 Nov 2022 - Another fix for improving how long images take to generate. Reduces the time taken for an enqueued task to start processing.
|
||||||
* 2.4.11 - 21 Nov 2022 - Installer improvements: avoid crashing if the username contains a space or special characters, allow moving/renaming the folder after installation on Windows, whitespace fix on git apply
|
* 2.4.11 - 21 Nov 2022 - Installer improvements: avoid crashing if the username contains a space or special characters, allow moving/renaming the folder after installation on Windows, whitespace fix on git apply
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
Hi there, these instructions are meant for the developers of this project.
|
Hi there, these instructions are meant for the developers of this project.
|
||||||
|
|
||||||
If you only want to use the Stable Diffusion UI, you've downloaded the wrong file. In that case, please download and follow the instructions at https://github.com/cmdr2/stable-diffusion-ui#installation
|
If you only want to use the Stable Diffusion UI, you've downloaded the wrong file. In that case, please download and follow the instructions at https://github.com/easydiffusion/easydiffusion#installation
|
||||||
|
|
||||||
Thanks
|
Thanks
|
||||||
|
|
||||||
# For developers:
|
# For developers:
|
||||||
|
|
||||||
If you would like to contribute to this project, there is a discord for dicussion:
|
If you would like to contribute to this project, there is a discord for discussion:
|
||||||
[](https://discord.com/invite/u9yhsFmEkB)
|
[](https://discord.com/invite/u9yhsFmEkB)
|
||||||
|
|
||||||
## Development environment for UI (frontend and server) changes
|
## Development environment for UI (frontend and server) changes
|
||||||
This is in-flux, but one way to get a development environment running for editing the UI of this project is:
|
This is in-flux, but one way to get a development environment running for editing the UI of this project is:
|
||||||
(swap `.sh` or `.bat` in instructions depending on your environment, and be sure to adjust any paths to match where you're working)
|
(swap `.sh` or `.bat` in instructions depending on your environment, and be sure to adjust any paths to match where you're working)
|
||||||
|
|
||||||
1) Install the project to a new location using the [usual installation process](https://github.com/cmdr2/stable-diffusion-ui#installation), e.g. to `/projects/stable-diffusion-ui-archive`
|
1) Install the project to a new location using the [usual installation process](https://github.com/easydiffusion/easydiffusion#installation), e.g. to `/projects/stable-diffusion-ui-archive`
|
||||||
2) Start the newly installed project, and check that you can view and generate images on `localhost:9000`
|
2) Start the newly installed project, and check that you can view and generate images on `localhost:9000`
|
||||||
3) Next, please clone the project repository using `git clone` (e.g. to `/projects/stable-diffusion-ui-repo`)
|
3) Next, please clone the project repository using `git clone` (e.g. to `/projects/stable-diffusion-ui-repo`)
|
||||||
4) Close the server (started in step 2), and edit `/projects/stable-diffusion-ui-archive/scripts/on_env_start.sh` (or `on_env_start.bat`)
|
4) Close the server (started in step 2), and edit `/projects/stable-diffusion-ui-archive/scripts/on_env_start.sh` (or `on_env_start.bat`)
|
||||||
@@ -42,10 +42,10 @@ or for Windows
|
|||||||
10) Congrats, now any changes you make in your repo `ui` folder are linked to this running archive of the app and can be previewed in the browser.
|
10) Congrats, now any changes you make in your repo `ui` folder are linked to this running archive of the app and can be previewed in the browser.
|
||||||
11) Please update CHANGES.md in your pull requests.
|
11) Please update CHANGES.md in your pull requests.
|
||||||
|
|
||||||
Check the `ui/frontend/build/README.md` for instructions on running and building the React code.
|
|
||||||
|
|
||||||
## Development environment for Installer changes
|
## Development environment for Installer changes
|
||||||
Build the Windows installer using Windows, and the Linux installer using Linux. Don't mix the two, and don't use WSL. An Ubuntu VM is fine for building the Linux installer on a Windows host.
|
Build the Windows installer using Windows, and the Linux installer using Linux. Don't mix the two, and don't use WSL. An Ubuntu VM is fine for building the Linux installer on a Windows host.
|
||||||
|
|
||||||
1. Run `build.bat` or `./build.sh` depending on whether you're in Windows or Linux.
|
1. Run `build.bat` or `./build.sh` depending on whether you're in Windows or Linux.
|
||||||
2. Make a new GitHub release and upload the Windows and Linux installer builds created inside the `dist` folder.
|
2. Make a new GitHub release and upload the Windows and Linux installer builds created inside the `dist` folder.
|
||||||
|
|
||||||
|
For NSIS (on Windows), you need to have these plugins in the `nsis/Plugins` folder: `amd64-unicode`, `x86-ansi`, `x86-unicode`
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
Congrats on downloading Stable Diffusion UI, version 2!
|
Congrats on downloading Easy Diffusion, version 3!
|
||||||
|
|
||||||
If you haven't downloaded Stable Diffusion UI yet, please download from https://github.com/cmdr2/stable-diffusion-ui#installation
|
If you haven't downloaded Easy Diffusion yet, please download from https://github.com/easydiffusion/easydiffusion#installation
|
||||||
|
|
||||||
After downloading, to install please follow these instructions:
|
After downloading, to install please follow these instructions:
|
||||||
|
|
||||||
For Windows:
|
For Windows:
|
||||||
- Please double-click the "Start Stable Diffusion UI.cmd" file inside the "stable-diffusion-ui" folder.
|
- Please double-click the "Easy-Diffusion-Windows.exe" file and follow the instructions.
|
||||||
|
|
||||||
For Linux:
|
For Linux and Mac:
|
||||||
- Please open a terminal, and go to the "stable-diffusion-ui" directory. Then run ./start.sh
|
- Please open a terminal, and go to the "easy-diffusion" directory. Then run ./start.sh
|
||||||
|
|
||||||
That file will automatically install everything. After that it will start the Stable Diffusion interface in a web browser.
|
That file will automatically install everything. After that it will start the Easy Diffusion interface in a web browser.
|
||||||
|
|
||||||
To start the UI in the future, please run the same command mentioned above.
|
To start Easy Diffusion in the future, please run the same command mentioned above.
|
||||||
|
|
||||||
|
|
||||||
If you have any problems, please:
|
If you have any problems, please:
|
||||||
1. Try the troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting
|
1. Try the troubleshooting steps at https://github.com/easydiffusion/easydiffusion/wiki/Troubleshooting
|
||||||
2. Or, seek help from the community at https://discord.com/invite/u9yhsFmEkB
|
2. Or, seek help from the community at https://discord.com/invite/u9yhsFmEkB
|
||||||
3. Or, file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues
|
3. Or, file an issue at https://github.com/easydiffusion/easydiffusion/issues
|
||||||
|
|
||||||
Thanks
|
Thanks
|
||||||
cmdr2 (and contributors to the project)
|
cmdr2 (and contributors to the project)
|
||||||
1
NSIS/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.exe
|
||||||
BIN
NSIS/astro.bmp
|
Before Width: | Height: | Size: 288 KiB |
BIN
NSIS/cyborg_flower_girl.bmp
Normal file
|
After Width: | Height: | Size: 565 KiB |
BIN
NSIS/cyborg_flower_girl.ico
Normal file
|
After Width: | Height: | Size: 223 KiB |
BIN
NSIS/cyborg_flower_girl_icon.png
Normal file
|
After Width: | Height: | Size: 454 KiB |
BIN
NSIS/cyborg_flower_girl_orig.jpeg
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
NSIS/sd.ico
|
Before Width: | Height: | Size: 200 KiB |
@@ -1,20 +1,24 @@
|
|||||||
; Script generated by the HM NIS Edit Script Wizard.
|
; Script generated by the HM NIS Edit Script Wizard.
|
||||||
|
|
||||||
Target x86-unicode
|
Target amd64-unicode
|
||||||
Unicode True
|
Unicode True
|
||||||
!AddPluginDir /x86-unicode "."
|
SetCompressor /FINAL lzma
|
||||||
|
RequestExecutionLevel user
|
||||||
|
!AddPluginDir /amd64-unicode "."
|
||||||
; HM NIS Edit Wizard helper defines
|
; HM NIS Edit Wizard helper defines
|
||||||
!define PRODUCT_NAME "Stable Diffusion UI"
|
!define PRODUCT_NAME "Easy Diffusion"
|
||||||
!define PRODUCT_VERSION "Installer 2.35"
|
!define PRODUCT_VERSION "3.0"
|
||||||
!define PRODUCT_PUBLISHER "cmdr2 and contributors"
|
!define PRODUCT_PUBLISHER "cmdr2 and contributors"
|
||||||
!define PRODUCT_WEB_SITE "https://stable-diffusion-ui.github.io"
|
!define PRODUCT_WEB_SITE "https://easydiffusion.github.io"
|
||||||
!define PRODUCT_DIR_REGKEY "Software\Microsoft\Cmdr2\App Paths\installer.exe"
|
!define PRODUCT_DIR_REGKEY "Software\Microsoft\Easy Diffusion\App Paths\installer.exe"
|
||||||
|
|
||||||
; MUI 1.67 compatible ------
|
; MUI 1.67 compatible ------
|
||||||
!include "MUI.nsh"
|
!include "MUI.nsh"
|
||||||
!include "LogicLib.nsh"
|
!include "LogicLib.nsh"
|
||||||
!include "nsDialogs.nsh"
|
!include "nsDialogs.nsh"
|
||||||
|
|
||||||
|
!include "nsisconf.nsh"
|
||||||
|
|
||||||
Var Dialog
|
Var Dialog
|
||||||
Var Label
|
Var Label
|
||||||
Var Button
|
Var Button
|
||||||
@@ -106,7 +110,7 @@ Function DirectoryLeave
|
|||||||
StrCpy $5 $INSTDIR 3
|
StrCpy $5 $INSTDIR 3
|
||||||
System::Call 'Kernel32::GetVolumeInformation(t "$5",t,i ${NSIS_MAX_STRLEN},*i,*i,*i,t.r1,i ${NSIS_MAX_STRLEN})i.r0'
|
System::Call 'Kernel32::GetVolumeInformation(t "$5",t,i ${NSIS_MAX_STRLEN},*i,*i,*i,t.r1,i ${NSIS_MAX_STRLEN})i.r0'
|
||||||
${If} $0 <> 0
|
${If} $0 <> 0
|
||||||
${AndIf} $1 == "NTFS"
|
${AndIf} $1 != "NTFS"
|
||||||
MessageBox mb_ok "$5 has filesystem type '$1'.$\nOnly NTFS filesystems are supported.$\nPlease choose a different drive."
|
MessageBox mb_ok "$5 has filesystem type '$1'.$\nOnly NTFS filesystems are supported.$\nPlease choose a different drive."
|
||||||
Abort
|
Abort
|
||||||
${EndIf}
|
${EndIf}
|
||||||
@@ -140,7 +144,7 @@ Function MediaPackDialog
|
|||||||
Abort
|
Abort
|
||||||
${EndIf}
|
${EndIf}
|
||||||
|
|
||||||
${NSD_CreateLabel} 0 0 100% 48u "The Windows Media Feature Pack is missing on this computer. It is required for the Stable Diffusion UI.$\nYou can continue the installation after installing the Windows Media Feature Pack."
|
${NSD_CreateLabel} 0 0 100% 48u "The Windows Media Feature Pack is missing on this computer. It is required for Easy Diffusion.$\nYou can continue the installation after installing the Windows Media Feature Pack."
|
||||||
Pop $Label
|
Pop $Label
|
||||||
|
|
||||||
${NSD_CreateButton} 10% 49u 80% 12u "Download Meda Feature Pack from Microsoft"
|
${NSD_CreateButton} 10% 49u 80% 12u "Download Meda Feature Pack from Microsoft"
|
||||||
@@ -153,23 +157,27 @@ Function MediaPackDialog
|
|||||||
nsDialogs::Show
|
nsDialogs::Show
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
|
Function FinishPageAction
|
||||||
|
CreateShortCut "$DESKTOP\Easy Diffusion.lnk" "$INSTDIR\Start Stable Diffusion UI.cmd" "" "$INSTDIR\installer_files\cyborg_flower_girl.ico"
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
;---------------------------------------------------------------------------------------------------------
|
;---------------------------------------------------------------------------------------------------------
|
||||||
; MUI Settings
|
; MUI Settings
|
||||||
;---------------------------------------------------------------------------------------------------------
|
;---------------------------------------------------------------------------------------------------------
|
||||||
!define MUI_ABORTWARNING
|
!define MUI_ABORTWARNING
|
||||||
!define MUI_ICON "sd.ico"
|
!define MUI_ICON "${EXISTING_INSTALLATION_DIR}\installer_files\cyborg_flower_girl.ico"
|
||||||
|
|
||||||
!define MUI_WELCOMEFINISHPAGE_BITMAP "astro.bmp"
|
!define MUI_WELCOMEFINISHPAGE_BITMAP "${EXISTING_INSTALLATION_DIR}\installer_files\cyborg_flower_girl.bmp"
|
||||||
|
|
||||||
; Welcome page
|
; Welcome page
|
||||||
!define MUI_WELCOMEPAGE_TEXT "This installer will guide you through the installation of Stable Diffusion UI.$\n$\n\
|
!define MUI_WELCOMEPAGE_TEXT "This installer will guide you through the installation of Easy Diffusion.$\n$\n\
|
||||||
Click Next to continue."
|
Click Next to continue."
|
||||||
!insertmacro MUI_PAGE_WELCOME
|
!insertmacro MUI_PAGE_WELCOME
|
||||||
Page custom MediaPackDialog
|
Page custom MediaPackDialog
|
||||||
|
|
||||||
; License page
|
; License page
|
||||||
!insertmacro MUI_PAGE_LICENSE "..\LICENSE"
|
!insertmacro MUI_PAGE_LICENSE "${EXISTING_INSTALLATION_DIR}\LICENSE"
|
||||||
!insertmacro MUI_PAGE_LICENSE "..\CreativeML Open RAIL-M License"
|
!insertmacro MUI_PAGE_LICENSE "${EXISTING_INSTALLATION_DIR}\CreativeML Open RAIL-M License"
|
||||||
; Directory page
|
; Directory page
|
||||||
!define MUI_PAGE_CUSTOMFUNCTION_LEAVE "DirectoryLeave"
|
!define MUI_PAGE_CUSTOMFUNCTION_LEAVE "DirectoryLeave"
|
||||||
!insertmacro MUI_PAGE_DIRECTORY
|
!insertmacro MUI_PAGE_DIRECTORY
|
||||||
@@ -178,6 +186,11 @@ Page custom MediaPackDialog
|
|||||||
!insertmacro MUI_PAGE_INSTFILES
|
!insertmacro MUI_PAGE_INSTFILES
|
||||||
|
|
||||||
; Finish page
|
; Finish page
|
||||||
|
!define MUI_FINISHPAGE_SHOWREADME ""
|
||||||
|
!define MUI_FINISHPAGE_SHOWREADME_NOTCHECKED
|
||||||
|
!define MUI_FINISHPAGE_SHOWREADME_TEXT "Create Desktop Shortcut"
|
||||||
|
!define MUI_FINISHPAGE_SHOWREADME_FUNCTION FinishPageAction
|
||||||
|
|
||||||
!define MUI_FINISHPAGE_RUN "$INSTDIR\Start Stable Diffusion UI.cmd"
|
!define MUI_FINISHPAGE_RUN "$INSTDIR\Start Stable Diffusion UI.cmd"
|
||||||
!insertmacro MUI_PAGE_FINISH
|
!insertmacro MUI_PAGE_FINISH
|
||||||
|
|
||||||
@@ -188,8 +201,8 @@ Page custom MediaPackDialog
|
|||||||
;---------------------------------------------------------------------------------------------------------
|
;---------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"
|
Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"
|
||||||
OutFile "Install Stable Diffusion UI.exe"
|
OutFile "Install Easy Diffusion.exe"
|
||||||
InstallDir "C:\Stable-Diffusion-UI\"
|
InstallDir "C:\EasyDiffusion\"
|
||||||
InstallDirRegKey HKLM "${PRODUCT_DIR_REGKEY}" ""
|
InstallDirRegKey HKLM "${PRODUCT_DIR_REGKEY}" ""
|
||||||
ShowInstDetails show
|
ShowInstDetails show
|
||||||
|
|
||||||
@@ -197,18 +210,49 @@ ShowInstDetails show
|
|||||||
; List of files to be installed
|
; List of files to be installed
|
||||||
Section "MainSection" SEC01
|
Section "MainSection" SEC01
|
||||||
SetOutPath "$INSTDIR"
|
SetOutPath "$INSTDIR"
|
||||||
File "..\CreativeML Open RAIL-M License"
|
File "${EXISTING_INSTALLATION_DIR}\CreativeML Open RAIL-M License"
|
||||||
File "..\How to install and run.txt"
|
File "${EXISTING_INSTALLATION_DIR}\How to install and run.txt"
|
||||||
File "..\LICENSE"
|
File "${EXISTING_INSTALLATION_DIR}\LICENSE"
|
||||||
File "..\Start Stable Diffusion UI.cmd"
|
File "${EXISTING_INSTALLATION_DIR}\Start Stable Diffusion UI.cmd"
|
||||||
|
File /r "${EXISTING_INSTALLATION_DIR}\installer_files"
|
||||||
|
File /r "${EXISTING_INSTALLATION_DIR}\sd-ui-files"
|
||||||
|
|
||||||
SetOutPath "$INSTDIR\scripts"
|
SetOutPath "$INSTDIR\scripts"
|
||||||
File "..\scripts\bootstrap.bat"
|
File "${EXISTING_INSTALLATION_DIR}\scripts\install_status.txt"
|
||||||
File "..\scripts\install_status.txt"
|
File "${EXISTING_INSTALLATION_DIR}\scripts\on_env_start.bat"
|
||||||
File "..\scripts\on_env_start.bat"
|
|
||||||
File "C:\windows\system32\curl.exe"
|
File "C:\windows\system32\curl.exe"
|
||||||
CreateDirectory "$INSTDIR\profile"
|
File "${EXISTING_INSTALLATION_DIR}\scripts\config.yaml.sample"
|
||||||
CreateDirectory "$SMPROGRAMS\Stable Diffusion UI"
|
|
||||||
CreateShortCut "$SMPROGRAMS\Stable Diffusion UI\Start Stable Diffusion UI.lnk" "$INSTDIR\Start Stable Diffusion UI.cmd"
|
CreateDirectory "$INSTDIR\models\stable-diffusion"
|
||||||
|
CreateDirectory "$INSTDIR\models\gfpgan"
|
||||||
|
CreateDirectory "$INSTDIR\models\realesrgan"
|
||||||
|
CreateDirectory "$INSTDIR\models\vae"
|
||||||
|
|
||||||
|
CreateDirectory "$INSTDIR\profile\.cache\huggingface\hub"
|
||||||
|
SetOutPath "$INSTDIR\profile\.cache\huggingface\hub"
|
||||||
|
File /r /x pytorch_model.bin "${EXISTING_INSTALLATION_DIR}\profile\.cache\huggingface\hub\models--openai--clip-vit-large-patch14"
|
||||||
|
|
||||||
|
CreateDirectory "$SMPROGRAMS\Easy Diffusion"
|
||||||
|
CreateShortCut "$SMPROGRAMS\Easy Diffusion\Easy Diffusion.lnk" "$INSTDIR\Start Stable Diffusion UI.cmd" "" "$INSTDIR\installer_files\cyborg_flower_girl.ico"
|
||||||
|
|
||||||
|
DetailPrint 'Downloading the Stable Diffusion 1.5 model...'
|
||||||
|
NScurl::http get "https://github.com/easydiffusion/sdkit-test-data/releases/download/assets/sd-v1-5.safetensors" "$INSTDIR\models\stable-diffusion\sd-v1-5.safetensors" /CANCEL /INSIST /END
|
||||||
|
|
||||||
|
DetailPrint 'Downloading the GFPGAN model...'
|
||||||
|
NScurl::http get "https://github.com/TencentARC/GFPGAN/releases/download/v1.3.4/GFPGANv1.4.pth" "$INSTDIR\models\gfpgan\GFPGANv1.4.pth" /CANCEL /INSIST /END
|
||||||
|
|
||||||
|
DetailPrint 'Downloading the RealESRGAN_x4plus model...'
|
||||||
|
NScurl::http get "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth" "$INSTDIR\models\realesrgan\RealESRGAN_x4plus.pth" /CANCEL /INSIST /END
|
||||||
|
|
||||||
|
DetailPrint 'Downloading the RealESRGAN_x4plus_anime model...'
|
||||||
|
NScurl::http get "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth" "$INSTDIR\models\realesrgan\RealESRGAN_x4plus_anime_6B.pth" /CANCEL /INSIST /END
|
||||||
|
|
||||||
|
DetailPrint 'Downloading the default VAE (sd-vae-ft-mse-original) model...'
|
||||||
|
NScurl::http get "https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.ckpt" "$INSTDIR\models\vae\vae-ft-mse-840000-ema-pruned.ckpt" /CANCEL /INSIST /END
|
||||||
|
|
||||||
|
DetailPrint 'Downloading the CLIP model (clip-vit-large-patch14)...'
|
||||||
|
NScurl::http get "https://huggingface.co/openai/clip-vit-large-patch14/resolve/8d052a0f05efbaefbc9e8786ba291cfdf93e5bff/pytorch_model.bin" "$INSTDIR\profile\.cache\huggingface\hub\models--openai--clip-vit-large-patch14\snapshots\8d052a0f05efbaefbc9e8786ba291cfdf93e5bff\pytorch_model.bin" /CANCEL /INSIST /END
|
||||||
|
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
;---------------------------------------------------------------------------------------------------------
|
;---------------------------------------------------------------------------------------------------------
|
||||||
@@ -254,7 +298,7 @@ Function .onInit
|
|||||||
|
|
||||||
${If} $4 < "8000"
|
${If} $4 < "8000"
|
||||||
MessageBox MB_OK|MB_ICONEXCLAMATION "Warning!$\n$\nYour system has less than 8GB of memory (RAM).$\n$\n\
|
MessageBox MB_OK|MB_ICONEXCLAMATION "Warning!$\n$\nYour system has less than 8GB of memory (RAM).$\n$\n\
|
||||||
You can still try to install Stable Diffusion UI,$\nbut it might have problems to start, or run$\nvery slowly."
|
You can still try to install Easy Diffusion,$\nbut it might have problems to start, or run$\nvery slowly."
|
||||||
${EndIf}
|
${EndIf}
|
||||||
|
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|||||||
9
PRIVACY.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// placeholder until a more formal and legal-sounding privacy policy document is written. but the information below is true.
|
||||||
|
|
||||||
|
This is a summary of whether Easy Diffusion uses your data or tracks you:
|
||||||
|
* The short answer is - Easy Diffusion does *not* use your data, and does *not* track you.
|
||||||
|
* Easy Diffusion does not send your prompts or usage or analytics to anyone. There is no tracking. We don't even know how many people use Easy Diffusion, let alone their prompts.
|
||||||
|
* Easy Diffusion fetches updates to the code whenever it starts up. It does this by contacting GitHub directly, via SSL (secure connection). Only your computer and GitHub and [this repository](https://github.com/easydiffusion/easydiffusion) are involved, and no third party is involved. Some countries intercepts SSL connections, that's not something we can do much about. GitHub does *not* share statistics (even with me) about how many people fetched code updates.
|
||||||
|
* Easy Diffusion fetches the models from huggingface.co and github.com, if they don't exist on your PC. For e.g. if the safety checker (NSFW) model doesn't exist, it'll try to download it.
|
||||||
|
* Easy Diffusion fetches code packages from pypi.org, which is the standard hosting service for all Python projects. That's where packages installed via `pip install` are stored.
|
||||||
|
* Occasionally, antivirus software are known to *incorrectly* flag and delete some model files, which will result in Easy Diffusion re-downloading `pytorch_model.bin`. This *incorrect deletion* affects other Stable Diffusion UIs as well, like Invoke AI - https://itch.io/post/7509488
|
||||||
@@ -3,6 +3,6 @@ Hi there,
|
|||||||
What you have downloaded is meant for the developers of this project, not for users.
|
What you have downloaded is meant for the developers of this project, not for users.
|
||||||
|
|
||||||
If you only want to use the Stable Diffusion UI, you've downloaded the wrong file.
|
If you only want to use the Stable Diffusion UI, you've downloaded the wrong file.
|
||||||
Please download and follow the instructions at https://github.com/cmdr2/stable-diffusion-ui#installation
|
Please download and follow the instructions at https://github.com/easydiffusion/easydiffusion#installation
|
||||||
|
|
||||||
Thanks
|
Thanks
|
||||||
188
README.md
@@ -1,105 +1,149 @@
|
|||||||
# Stable Diffusion UI
|
# Easy Diffusion 3.0
|
||||||
### Easiest way to install and use [Stable Diffusion](https://github.com/CompVis/stable-diffusion) on your own computer. No dependencies or technical knowledge required. 1-click install, powerful features.
|
### The easiest way to install and use [Stable Diffusion](https://github.com/CompVis/stable-diffusion) on your computer.
|
||||||
|
|
||||||
[](https://discord.com/invite/u9yhsFmEkB) (for support, and development discussion) | [Troubleshooting guide for common problems](Troubleshooting.md)
|
Does not require technical knowledge, does not require pre-installed software. 1-click install, powerful features, friendly community.
|
||||||
|
|
||||||
----
|
️🔥🎉 **New!** Support for SDXL, ControlNet, multiple LoRA files, embeddings (and a lot more) have been added!
|
||||||
|
|
||||||
## Step 1: Download the installer
|
[Installation guide](#installation) | [Troubleshooting guide](https://github.com/easydiffusion/easydiffusion/wiki/Troubleshooting) | [User guide](https://github.com/easydiffusion/easydiffusion/wiki) | <sub>[](https://discord.com/invite/u9yhsFmEkB)</sub> <sup>(for support queries, and development discussions)</sup>
|
||||||
|
|
||||||
|
---
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
Click the download button for your operating system:
|
||||||
|
|
||||||
<p float="left">
|
<p float="left">
|
||||||
<a href="#installation"><img src="https://github.com/cmdr2/stable-diffusion-ui/raw/develop/media/download-win.png" width="200" /></a>
|
<a href="https://github.com/cmdr2/stable-diffusion-ui/releases/latest/download/Easy-Diffusion-Linux.zip"><img src="https://github.com/cmdr2/stable-diffusion-ui/raw/main/media/download-linux.png" width="200" /></a>
|
||||||
<a href="#installation"><img src="https://github.com/cmdr2/stable-diffusion-ui/raw/develop/media/download-linux.png" width="200" /></a>
|
<a href="https://github.com/cmdr2/stable-diffusion-ui/releases/latest/download/Easy-Diffusion-Mac.zip"><img src="https://github.com/cmdr2/stable-diffusion-ui/raw/main/media/download-mac.png" width="200" /></a>
|
||||||
|
<a href="https://github.com/cmdr2/stable-diffusion-ui/releases/latest/download/Easy-Diffusion-Windows.exe"><img src="https://github.com/cmdr2/stable-diffusion-ui/raw/main/media/download-win.png" width="200" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Step 2: Run the program
|
**Hardware requirements:**
|
||||||
- On Windows: Double-click `Start Stable Diffusion UI.cmd`
|
- **Windows:** NVIDIA graphics card¹ (minimum 2 GB RAM), or run on your CPU.
|
||||||
- On Linux: Run `./start.sh` in a terminal
|
- **Linux:** NVIDIA¹ or AMD² graphics card (minimum 2 GB RAM), or run on your CPU.
|
||||||
|
- **Mac:** M1 or M2, or run on your CPU.
|
||||||
|
- Minimum 8 GB of system RAM.
|
||||||
|
- Atleast 25 GB of space on the hard disk.
|
||||||
|
|
||||||
## Step 3: There is no step 3!
|
¹) [CUDA Compute capability](https://en.wikipedia.org/wiki/CUDA#GPUs_supported) level of 3.7 or higher required.
|
||||||
It's simple to get started. You don't need to install or struggle with Python, Anaconda, Docker etc.
|
|
||||||
|
|
||||||
The installer will take care of whatever is needed. A friendly [Discord community](https://discord.com/invite/u9yhsFmEkB) will help you if you face any problems.
|
²) ROCm 5.2 support required.
|
||||||
|
|
||||||
|
The installer will take care of whatever is needed. If you face any problems, you can join the friendly [Discord community](https://discord.com/invite/u9yhsFmEkB) and ask for assistance.
|
||||||
|
|
||||||
|
## On Windows:
|
||||||
|
1. Run the downloaded `Easy-Diffusion-Windows.exe` file.
|
||||||
|
2. Run `Easy Diffusion` once the installation finishes. You can also start from your Start Menu, or from your desktop (if you created a shortcut).
|
||||||
|
|
||||||
|
If Windows SmartScreen prevents you from running the program click `More info` and then `Run anyway`.
|
||||||
|
|
||||||
|
**Tip:** On Windows 10, please install at the top level in your drive, e.g. `C:\EasyDiffusion` or `D:\EasyDiffusion`. This will avoid a common problem with Windows 10 (file path length limits).
|
||||||
|
|
||||||
|
## On Linux/Mac:
|
||||||
|
1. Unzip/extract the folder `easy-diffusion` which should be in your downloads folder, unless you changed your default downloads destination.
|
||||||
|
2. Open a terminal window, and navigate to the `easy-diffusion` directory.
|
||||||
|
3. Run `./start.sh` (or `bash start.sh`) in a terminal.
|
||||||
|
|
||||||
|
# To remove/uninstall:
|
||||||
|
Just delete the `EasyDiffusion` folder to uninstall all the downloaded packages.
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
# Easy for new users, powerful features for advanced users
|
# Easy for new users, powerful features for advanced users
|
||||||
### Features:
|
## Features:
|
||||||
- **No Dependencies or Technical Knowledge Required**: 1-click install for Windows 10/11 and Linux. *No dependencies*, no need for WSL or Docker or Conda or technical setup. Just download and run!
|
|
||||||
- **Clutter-free UI**: a friendly and simple UI, while providing a lot of powerful features
|
### User experience
|
||||||
- Supports "*Text to Image*" and "*Image to Image*"
|
- **Hassle-free installation**: Does not require technical knowledge, does not require pre-installed software. Just download and run!
|
||||||
- **Custom Models**: Use your own `.ckpt` file, by placing it inside the `models/stable-diffusion` folder!
|
- **Clutter-free UI**: A friendly and simple UI, while providing a lot of powerful features.
|
||||||
- **Live Preview**: See the image as the AI is drawing it
|
- **Task Queue**: Queue up all your ideas, without waiting for the current task to finish.
|
||||||
- **Task Queue**: Queue up all your ideas, without waiting for the current task to finish
|
- **Intelligent Model Detection**: Automatically figures out the YAML config file to use for the chosen model (via a models database).
|
||||||
- **In-Painting**: Specify areas of your image to paint into
|
- **Live Preview**: See the image as the AI is drawing it.
|
||||||
- **Face Correction (GFPGAN) and Upscaling (RealESRGAN)**
|
|
||||||
- **Image Modifiers**: A library of *modifier tags* like *"Realistic"*, *"Pencil Sketch"*, *"ArtStation"* etc. Experiment with various styles quickly.
|
- **Image Modifiers**: A library of *modifier tags* like *"Realistic"*, *"Pencil Sketch"*, *"ArtStation"* etc. Experiment with various styles quickly.
|
||||||
- **Loopback**: Use the output image as the input image for the next img2img task
|
- **Multiple Prompts File**: Queue multiple prompts by entering one prompt per line, or by running a text file.
|
||||||
|
- **Save generated images to disk**: Save your images to your PC!
|
||||||
|
- **UI Themes**: Customize the program to your liking.
|
||||||
|
- **Searchable models dropdown**: organize your models into sub-folders, and search through them in the UI.
|
||||||
|
|
||||||
|
### Powerful image generation
|
||||||
|
- **Supports**: "*Text to Image*", "*Image to Image*" and "*InPainting*"
|
||||||
|
- **ControlNet**: For advanced control over the image, e.g. by setting the pose or drawing the outline for the AI to fill in.
|
||||||
|
- **16 Samplers**: `PLMS`, `DDIM`, `DEIS`, `Heun`, `Euler`, `Euler Ancestral`, `DPM2`, `DPM2 Ancestral`, `LMS`, `DPM Solver`, `DPM++ 2s Ancestral`, `DPM++ 2m`, `DPM++ 2m SDE`, `DPM++ SDE`, `DDPM`, `UniPC`.
|
||||||
|
- **Stable Diffusion XL and 2.1**: Generate higher-quality images using the latest Stable Diffusion XL models.
|
||||||
|
- **Textual Inversion Embeddings**: For guiding the AI strongly towards a particular concept.
|
||||||
|
- **Simple Drawing Tool**: Draw basic images to guide the AI, without needing an external drawing program.
|
||||||
|
- **Face Correction (GFPGAN)**
|
||||||
|
- **Upscaling (RealESRGAN)**
|
||||||
|
- **Loopback**: Use the output image as the input image for the next image task.
|
||||||
- **Negative Prompt**: Specify aspects of the image to *remove*.
|
- **Negative Prompt**: Specify aspects of the image to *remove*.
|
||||||
- **Attention/Emphasis:** () in the prompt increases the model's attention to enclosed words, and [] decreases it
|
- **Attention/Emphasis**: `+` in the prompt increases the model's attention to enclosed words, and `-` decreases it. E.g. `apple++ falling from a tree`.
|
||||||
- **Weighted Prompts:** Use weights for specific words in your prompt to change their importance, e.g. `red:2.4 dragon:1.2`
|
- **Weighted Prompts**: Use weights for specific words in your prompt to change their importance, e.g. `(red)2.4 (dragon)1.2`.
|
||||||
- **Prompt Matrix:** (in beta) Quickly create multiple variations of your prompt, e.g. `a photograph of an astronaut riding a horse | illustration | cinematic lighting`
|
- **Prompt Matrix**: Quickly create multiple variations of your prompt, e.g. `a photograph of an astronaut riding a horse | illustration | cinematic lighting`.
|
||||||
- **Lots of Samplers:** ddim, plms, heun, euler, euler_a, dpm2, dpm2_a, lms
|
- **Prompt Set**: Quickly create multiple variations of your prompt, e.g. `a photograph of an astronaut on the {moon,earth}`
|
||||||
- **Multiple Prompts File:** Queue multiple prompts by entering one prompt per line, or by running a text file
|
- **1-click Upscale/Face Correction**: Upscale or correct an image after it has been generated.
|
||||||
- **NSFW Setting**: A setting in the UI to control *NSFW content*
|
- **Make Similar Images**: Click to generate multiple variations of a generated image.
|
||||||
- **JPEG/PNG output**
|
- **NSFW Setting**: A setting in the UI to control *NSFW content*.
|
||||||
- **Save generated images to disk**
|
- **JPEG/PNG/WEBP output**: Multiple file formats.
|
||||||
|
|
||||||
|
### Advanced features
|
||||||
|
- **Custom Models**: Use your own `.ckpt` or `.safetensors` file, by placing it inside the `models/stable-diffusion` folder!
|
||||||
|
- **Stable Diffusion XL and 2.1 support**
|
||||||
|
- **Merge Models**
|
||||||
|
- **Use custom VAE models**
|
||||||
|
- **Textual Inversion Embeddings**
|
||||||
|
- **ControlNet**
|
||||||
|
- **Use custom GFPGAN models**
|
||||||
|
- **UI Plugins**: Choose from a growing list of [community-generated UI plugins](https://github.com/easydiffusion/easydiffusion/wiki/UI-Plugins), or write your own plugin to add features to the project!
|
||||||
|
|
||||||
|
### Performance and security
|
||||||
|
- **Fast**: Creates a 512x512 image with euler_a in 5 seconds, on an NVIDIA 3060 12GB.
|
||||||
|
- **Low Memory Usage**: Create 512x512 images with less than 2 GB of GPU RAM, and 768x768 images with less than 3 GB of GPU RAM!
|
||||||
- **Use CPU setting**: If you don't have a compatible graphics card, but still want to run it on your CPU.
|
- **Use CPU setting**: If you don't have a compatible graphics card, but still want to run it on your CPU.
|
||||||
|
- **Multi-GPU support**: Automatically spreads your tasks across multiple GPUs (if available), for faster performance!
|
||||||
|
- **Auto scan for malicious models**: Uses picklescan to prevent malicious models.
|
||||||
|
- **Safetensors support**: Support loading models in the safetensor format, for improved safety.
|
||||||
- **Auto-updater**: Gets you the latest improvements and bug-fixes to a rapidly evolving project.
|
- **Auto-updater**: Gets you the latest improvements and bug-fixes to a rapidly evolving project.
|
||||||
- **Low Memory Usage**: Creates 512x512 images with less than 4GB of VRAM!
|
- **Developer Console**: A developer-mode for those who want to modify their Stable Diffusion code, modify packages, and edit the conda environment.
|
||||||
- **Developer Console**: A developer-mode for those who want to modify their Stable Diffusion code, and edit the conda environment.
|
|
||||||
|
|
||||||
### Easy for new users:
|
**(and a lot more)**
|
||||||

|
|
||||||
|
|
||||||
### Powerful features for advanced users:
|
----
|
||||||

|
|
||||||
|
|
||||||
### Live Preview
|
## Easy for new users, powerful features for advanced users:
|
||||||
Useful for judging (and stopping) an image quickly, without waiting for it to finish rendering.
|

|
||||||
|
|
||||||

|
## Task Queue
|
||||||
|

|
||||||
|
|
||||||
### Task Queue
|
|
||||||

|
|
||||||
|
|
||||||
# System Requirements
|
----
|
||||||
1. Windows 10/11, or Linux. Experimental support for Mac is coming soon.
|
|
||||||
2. An NVIDIA graphics card, preferably with 4GB or more of VRAM. If you don't have a compatible graphics card, it'll automatically run in the slower "CPU Mode".
|
|
||||||
3. Minimum 8 GB of RAM and 25GB of disk space.
|
|
||||||
|
|
||||||
You don't need to install or struggle with Python, Anaconda, Docker etc. The installer will take care of whatever is needed.
|
|
||||||
|
|
||||||
# Installation
|
|
||||||
1. **Download** [for Windows](https://github.com/cmdr2/stable-diffusion-ui/releases/download/v2.3.5/stable-diffusion-ui-windows.zip) or [for Linux](https://github.com/cmdr2/stable-diffusion-ui/releases/download/v2.3.5/stable-diffusion-ui-linux.zip).
|
|
||||||
|
|
||||||
2. **Extract**:
|
|
||||||
- For Windows: After unzipping the file, please move the `stable-diffusion-ui` folder to your `C:` (or any drive like D:, at the top root level), e.g. `C:\stable-diffusion-ui`. This will avoid a common problem with Windows (file path length limits).
|
|
||||||
- For Linux: After extracting the .tar.xz file, please open a terminal, and go to the `stable-diffusion-ui` directory.
|
|
||||||
|
|
||||||
3. **Run**:
|
|
||||||
- For Windows: `Start Stable Diffusion UI.cmd` by double-clicking it.
|
|
||||||
- For Linux: In the terminal, run `./start.sh` (or `bash start.sh`)
|
|
||||||
|
|
||||||
This will automatically install Stable Diffusion, set it up, and start the interface. No additional steps are needed.
|
|
||||||
|
|
||||||
**To Uninstall:** Just delete the `stable-diffusion-ui` folder to uninstall all the downloaded packages.
|
|
||||||
|
|
||||||
# How to use?
|
# How to use?
|
||||||
Please use our [guide](https://github.com/cmdr2/stable-diffusion-ui/wiki/How-to-Use) to understand how to use the features in this UI.
|
Please refer to our [guide](https://github.com/easydiffusion/easydiffusion/wiki/How-to-Use) to understand how to use the features in this UI.
|
||||||
|
|
||||||
# Bugs reports and code contributions welcome
|
# Bugs reports and code contributions welcome
|
||||||
If there are any problems or suggestions, please feel free to ask on the [discord server](https://discord.com/invite/u9yhsFmEkB) or [file an issue](https://github.com/cmdr2/stable-diffusion-ui/issues).
|
If there are any problems or suggestions, please feel free to ask on the [discord server](https://discord.com/invite/u9yhsFmEkB) or [file an issue](https://github.com/easydiffusion/easydiffusion/issues).
|
||||||
|
|
||||||
We could really use help on these aspects (click to view tasks that need your help):
|
|
||||||
* [User Interface](https://github.com/users/cmdr2/projects/1/views/1)
|
|
||||||
* [Engine](https://github.com/users/cmdr2/projects/3/views/1)
|
|
||||||
* [Installer](https://github.com/users/cmdr2/projects/4/views/1)
|
|
||||||
* [Documentation](https://github.com/users/cmdr2/projects/5/views/1)
|
|
||||||
|
|
||||||
If you have any code contributions in mind, please feel free to say Hi to us on the [discord server](https://discord.com/invite/u9yhsFmEkB). We use the Discord server for development-related discussions, and for helping users.
|
If you have any code contributions in mind, please feel free to say Hi to us on the [discord server](https://discord.com/invite/u9yhsFmEkB). We use the Discord server for development-related discussions, and for helping users.
|
||||||
|
|
||||||
|
# Credits
|
||||||
|
* Stable Diffusion: https://github.com/Stability-AI/stablediffusion
|
||||||
|
* CodeFormer: https://github.com/sczhou/CodeFormer (license: https://github.com/sczhou/CodeFormer/blob/master/LICENSE)
|
||||||
|
* GFPGAN: https://github.com/TencentARC/GFPGAN
|
||||||
|
* RealESRGAN: https://github.com/xinntao/Real-ESRGAN
|
||||||
|
* k-diffusion: https://github.com/crowsonkb/k-diffusion
|
||||||
|
* Code contributors and artists on the cmdr2 UI: https://github.com/cmdr2/stable-diffusion-ui and Discord (https://discord.com/invite/u9yhsFmEkB)
|
||||||
|
* Lots of contributors on the internet
|
||||||
|
|
||||||
# Disclaimer
|
# Disclaimer
|
||||||
The authors of this project are not responsible for any content generated using this interface.
|
The authors of this project are not responsible for any content generated using this interface.
|
||||||
|
|
||||||
The license of this software forbids you from sharing any content that violates any laws, produce any harm to a person, disseminate any personal information that would be meant for harm, spread misinformation, or target vulnerable groups. For the full list of restrictions please read [the license](LICENSE). You agree to these terms by using this software.
|
The license of this software forbids you from sharing any content that:
|
||||||
|
- Violates any laws.
|
||||||
|
- Produces any harm to a person or persons.
|
||||||
|
- Disseminates (spreads) any personal information that would be meant for harm.
|
||||||
|
- Spreads misinformation.
|
||||||
|
- Target vulnerable groups.
|
||||||
|
|
||||||
|
For the full list of restrictions please read [the License](LICENSE). You agree to these terms by using this software.
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
Moved to https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting
|
|
||||||
95
build.bat
@@ -1,47 +1,78 @@
|
|||||||
@echo off
|
@echo off
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
@echo "Hi there, what you are running is meant for the developers of this project, not for users." & echo.
|
@echo "Hi there, what you are running is meant for the developers of this project, not for users." & echo.
|
||||||
@echo "If you only want to use the Stable Diffusion UI, you've downloaded the wrong file."
|
@echo "If you only want to use Easy Diffusion, you've downloaded the wrong file."
|
||||||
@echo "Please download and follow the instructions at https://github.com/cmdr2/stable-diffusion-ui#installation" & echo.
|
@echo "Please download and follow the instructions at https://github.com/easydiffusion/easydiffusion#installation" & echo.
|
||||||
@echo "If you are actually a developer of this project, please type Y and press enter" & echo.
|
@echo "If you are actually a developer of this project, please type Y and press enter" & echo.
|
||||||
|
|
||||||
set /p answer=Are you a developer of this project (Y/N)?
|
set /p answer=Are you a developer of this project (Y/N)?
|
||||||
if /i "%answer:~,1%" NEQ "Y" exit /b
|
if /i "%answer:~,1%" NEQ "Y" exit /b
|
||||||
|
|
||||||
mkdir dist\win\stable-diffusion-ui\scripts
|
@rem verify dependencies
|
||||||
@REM mkdir dist\linux-mac\stable-diffusion-ui\scripts
|
call makensis /VERSION >.tmp1 2>.tmp2
|
||||||
|
if "!ERRORLEVEL!" NEQ "0" (
|
||||||
|
echo makensis.exe not found! Download it from https://sourceforge.net/projects/nsisbi/files/ and set it on the PATH variable.
|
||||||
|
pause
|
||||||
|
exit
|
||||||
|
)
|
||||||
|
|
||||||
@rem copy the installer files for Windows
|
set /p OUT_DIR=Output folder path (will create the installer files inside this, e.g. F:\EasyDiffusion):
|
||||||
|
|
||||||
copy scripts\on_env_start.bat dist\win\stable-diffusion-ui\scripts\
|
mkdir "%OUT_DIR%\scripts"
|
||||||
copy scripts\bootstrap.bat dist\win\stable-diffusion-ui\scripts\
|
mkdir "%OUT_DIR%\installer_files"
|
||||||
copy "scripts\Start Stable Diffusion UI.cmd" dist\win\stable-diffusion-ui\
|
|
||||||
copy LICENSE dist\win\stable-diffusion-ui\
|
|
||||||
copy "CreativeML Open RAIL-M License" dist\win\stable-diffusion-ui\
|
|
||||||
copy "How to install and run.txt" dist\win\stable-diffusion-ui\
|
|
||||||
echo. > dist\win\stable-diffusion-ui\scripts\install_status.txt
|
|
||||||
|
|
||||||
@rem copy the installer files for Linux and Mac
|
set BASE_DIR=%cd%
|
||||||
|
|
||||||
@REM copy scripts\on_env_start.sh dist\linux-mac\stable-diffusion-ui\scripts\
|
@rem STEP 1: copy the installer files for Windows
|
||||||
@REM copy scripts\bootstrap.sh dist\linux-mac\stable-diffusion-ui\scripts\
|
|
||||||
@REM copy scripts\start.sh dist\linux-mac\stable-diffusion-ui\
|
|
||||||
@REM copy LICENSE dist\linux-mac\stable-diffusion-ui\
|
|
||||||
@REM copy "CreativeML Open RAIL-M License" dist\linux-mac\stable-diffusion-ui\
|
|
||||||
@REM copy "How to install and run.txt" dist\linux-mac\stable-diffusion-ui\
|
|
||||||
@REM echo. > dist\linux-mac\stable-diffusion-ui\scripts\install_status.txt
|
|
||||||
|
|
||||||
@rem make the zip
|
copy "%BASE_DIR%\scripts\on_env_start.bat" "%OUT_DIR%\scripts\"
|
||||||
|
copy "%BASE_DIR%\scripts\config.yaml.sample" "%OUT_DIR%\scripts\config.yaml.sample"
|
||||||
cd dist\win
|
copy "%BASE_DIR%\scripts\Start Stable Diffusion UI.cmd" "%OUT_DIR%\"
|
||||||
call powershell Compress-Archive -Path stable-diffusion-ui -DestinationPath ..\stable-diffusion-ui-windows.zip
|
copy "%BASE_DIR%\LICENSE" "%OUT_DIR%\"
|
||||||
cd ..\..
|
copy "%BASE_DIR%\CreativeML Open RAIL-M License" "%OUT_DIR%\"
|
||||||
|
copy "%BASE_DIR%\How to install and run.txt" "%OUT_DIR%\"
|
||||||
@REM cd dist\linux-mac
|
copy "%BASE_DIR%\NSIS\cyborg_flower_girl.ico" "%OUT_DIR%\installer_files\"
|
||||||
@REM call powershell Compress-Archive -Path stable-diffusion-ui -DestinationPath ..\stable-diffusion-ui-linux.zip
|
copy "%BASE_DIR%\NSIS\cyborg_flower_girl.bmp" "%OUT_DIR%\installer_files\"
|
||||||
@REM call powershell Compress-Archive -Path stable-diffusion-ui -DestinationPath ..\stable-diffusion-ui-mac.zip
|
echo. > "%OUT_DIR%\scripts\install_status.txt"
|
||||||
@REM cd ..\..
|
|
||||||
|
|
||||||
echo "Build ready. Upload the zip files inside the 'dist' folder."
|
|
||||||
|
|
||||||
|
echo ----
|
||||||
|
echo Basic files ready. Verify the files in %OUT_DIR%, then press Enter to initialize the environment, or close to quit.
|
||||||
|
echo ----
|
||||||
|
pause
|
||||||
|
|
||||||
|
@rem STEP 2: Initialize the environment with git, python and conda
|
||||||
|
|
||||||
|
cd /d "%OUT_DIR%\"
|
||||||
|
call "%BASE_DIR%\scripts\bootstrap.bat"
|
||||||
|
|
||||||
|
echo ----
|
||||||
|
echo Environment ready. Verify the environment, then press Enter to download the necessary packages, or close to quit.
|
||||||
|
echo ----
|
||||||
|
pause
|
||||||
|
|
||||||
|
@rem STEP 3: Download the packages and create a working installation
|
||||||
|
|
||||||
|
cd /d "%OUT_DIR%\"
|
||||||
|
start "Install Easy Diffusion" /D "%OUT_DIR%" "Start Stable Diffusion UI.cmd"
|
||||||
|
|
||||||
|
echo ----
|
||||||
|
echo Installation in progress (in a new window). Once complete, verify the installation, then press Enter to create an installer from these files, or close to quit.
|
||||||
|
echo ----
|
||||||
|
pause
|
||||||
|
|
||||||
|
@rem STEP 4: Build the installer from a working installation
|
||||||
|
|
||||||
|
cd /d "%OUT_DIR%\"
|
||||||
|
|
||||||
|
echo ^^!define EXISTING_INSTALLATION_DIR "%OUT_DIR%" > nsisconf.nsh
|
||||||
|
call makensis /NOCD /V4 "%BASE_DIR%\NSIS\sdui.nsi"
|
||||||
|
|
||||||
|
echo ----
|
||||||
|
if "!ERRORLEVEL!" EQU "0" (
|
||||||
|
echo Installer built successfully at %OUT_DIR%
|
||||||
|
) else (
|
||||||
|
echo Installer failed to build at %OUT_DIR%
|
||||||
|
)
|
||||||
|
echo ----
|
||||||
pause
|
pause
|
||||||
47
build.sh
@@ -1,8 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
printf "Hi there, what you are running is meant for the developers of this project, not for users.\n\n"
|
printf "Hi there, what you are running is meant for the developers of this project, not for users.\n\n"
|
||||||
printf "If you only want to use the Stable Diffusion UI, you've downloaded the wrong file.\n"
|
printf "If you only want to use Easy Diffusion, you've downloaded the wrong file.\n"
|
||||||
printf "Please download and follow the instructions at https://github.com/cmdr2/stable-diffusion-ui#installation\n\n"
|
printf "Please download and follow the instructions at https://github.com/easydiffusion/easydiffusion#installation \n\n"
|
||||||
printf "If you are actually a developer of this project, please type Y and press enter\n\n"
|
printf "If you are actually a developer of this project, please type Y and press enter\n\n"
|
||||||
|
|
||||||
read -p "Are you a developer of this project (Y/N) " yn
|
read -p "Are you a developer of this project (Y/N) " yn
|
||||||
@@ -11,39 +11,30 @@ case $yn in
|
|||||||
* ) exit;;
|
* ) exit;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# mkdir -p dist/win/stable-diffusion-ui/scripts
|
mkdir -p dist/linux-mac/easy-diffusion/scripts
|
||||||
mkdir -p dist/linux-mac/stable-diffusion-ui/scripts
|
|
||||||
|
|
||||||
# copy the installer files for Windows
|
|
||||||
|
|
||||||
# cp scripts/on_env_start.bat dist/win/stable-diffusion-ui/scripts/
|
|
||||||
# cp scripts/bootstrap.bat dist/win/stable-diffusion-ui/scripts/
|
|
||||||
# cp "scripts/Start Stable Diffusion UI.cmd" dist/win/stable-diffusion-ui/
|
|
||||||
# cp LICENSE dist/win/stable-diffusion-ui/
|
|
||||||
# cp "CreativeML Open RAIL-M License" dist/win/stable-diffusion-ui/
|
|
||||||
# cp "How to install and run.txt" dist/win/stable-diffusion-ui/
|
|
||||||
# echo "" > dist/win/stable-diffusion-ui/scripts/install_status.txt
|
|
||||||
|
|
||||||
# copy the installer files for Linux and Mac
|
# copy the installer files for Linux and Mac
|
||||||
|
|
||||||
cp scripts/on_env_start.sh dist/linux-mac/stable-diffusion-ui/scripts/
|
cp scripts/on_env_start.sh dist/linux-mac/easy-diffusion/scripts/
|
||||||
cp scripts/bootstrap.sh dist/linux-mac/stable-diffusion-ui/scripts/
|
cp scripts/bootstrap.sh dist/linux-mac/easy-diffusion/scripts/
|
||||||
cp scripts/functions.sh dist/linux-mac/stable-diffusion-ui/scripts/
|
cp scripts/functions.sh dist/linux-mac/easy-diffusion/scripts/
|
||||||
cp scripts/start.sh dist/linux-mac/stable-diffusion-ui/
|
cp scripts/config.yaml.sample dist/linux-mac/easy-diffusion/scripts/config.yaml.sample
|
||||||
cp LICENSE dist/linux-mac/stable-diffusion-ui/
|
cp scripts/start.sh dist/linux-mac/easy-diffusion/
|
||||||
cp "CreativeML Open RAIL-M License" dist/linux-mac/stable-diffusion-ui/
|
cp LICENSE dist/linux-mac/easy-diffusion/
|
||||||
cp "How to install and run.txt" dist/linux-mac/stable-diffusion-ui/
|
cp "CreativeML Open RAIL-M License" dist/linux-mac/easy-diffusion/
|
||||||
echo "" > dist/linux-mac/stable-diffusion-ui/scripts/install_status.txt
|
cp "How to install and run.txt" dist/linux-mac/easy-diffusion/
|
||||||
|
echo "" > dist/linux-mac/easy-diffusion/scripts/install_status.txt
|
||||||
|
|
||||||
|
# set the permissions
|
||||||
|
chmod u+x dist/linux-mac/easy-diffusion/scripts/on_env_start.sh
|
||||||
|
chmod u+x dist/linux-mac/easy-diffusion/scripts/bootstrap.sh
|
||||||
|
chmod u+x dist/linux-mac/easy-diffusion/start.sh
|
||||||
|
|
||||||
# make the zip
|
# make the zip
|
||||||
|
|
||||||
# cd dist/win
|
|
||||||
# zip -r ../stable-diffusion-ui-windows.zip stable-diffusion-ui
|
|
||||||
# cd ../..
|
|
||||||
|
|
||||||
cd dist/linux-mac
|
cd dist/linux-mac
|
||||||
zip -r ../stable-diffusion-ui-linux.zip stable-diffusion-ui
|
zip -r ../Easy-Diffusion-Linux.zip easy-diffusion
|
||||||
zip -r ../stable-diffusion-ui-mac.zip stable-diffusion-ui
|
zip -r ../Easy-Diffusion-Mac.zip easy-diffusion
|
||||||
cd ../..
|
cd ../..
|
||||||
|
|
||||||
echo "Build ready. Upload the zip files inside the 'dist' folder."
|
echo "Build ready. Upload the zip files inside the 'dist' folder."
|
||||||
|
|||||||
BIN
media/download-mac.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
9
package.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"prettier-fix": "npx prettier --write \"./**/*.js\"",
|
||||||
|
"prettier-check": "npx prettier --check \"./**/*.js\""
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "^1.19.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
echo "Opening Stable Diffusion UI - Developer Console.." & echo.
|
echo "Opening Stable Diffusion UI - Developer Console.." & echo.
|
||||||
|
|
||||||
|
cd /d %~dp0
|
||||||
|
|
||||||
set PATH=C:\Windows\System32;%PATH%
|
set PATH=C:\Windows\System32;%PATH%
|
||||||
|
|
||||||
@rem set legacy and new installer's PATH, if they exist
|
@rem set legacy and new installer's PATH, if they exist
|
||||||
@@ -21,14 +23,29 @@ call git --version
|
|||||||
call where conda
|
call where conda
|
||||||
call conda --version
|
call conda --version
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo COMSPEC=%COMSPEC%
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
@rem activate the environment
|
@rem activate the legacy environment (if present) and set PYTHONPATH
|
||||||
call conda activate .\stable-diffusion\env
|
if exist "installer_files\env" (
|
||||||
|
set PYTHONPATH=%cd%\installer_files\env\lib\site-packages
|
||||||
|
)
|
||||||
|
if exist "stable-diffusion\env" (
|
||||||
|
call conda activate .\stable-diffusion\env
|
||||||
|
set PYTHONPATH=%cd%\stable-diffusion\env\lib\site-packages
|
||||||
|
)
|
||||||
|
|
||||||
call where python
|
call where python
|
||||||
call python --version
|
call python --version
|
||||||
|
|
||||||
|
echo PYTHONPATH=%PYTHONPATH%
|
||||||
|
|
||||||
|
if exist "%cd%\profile" (
|
||||||
|
set HF_HOME=%cd%\profile\.cache\huggingface
|
||||||
|
)
|
||||||
|
|
||||||
|
@rem done
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
cmd /k
|
cmd /k
|
||||||
|
|||||||
@@ -1,27 +1,46 @@
|
|||||||
@echo off
|
@echo off
|
||||||
|
|
||||||
cd /d %~dp0
|
cd /d %~dp0
|
||||||
|
echo Install dir: %~dp0
|
||||||
|
|
||||||
set PATH=C:\Windows\System32;%PATH%
|
set PATH=C:\Windows\System32;%PATH%
|
||||||
|
set PYTHONHOME=
|
||||||
|
|
||||||
|
if exist "on_sd_start.bat" (
|
||||||
|
echo ================================================================================
|
||||||
|
echo.
|
||||||
|
echo !!!! WARNING !!!!
|
||||||
|
echo.
|
||||||
|
echo It looks like you're trying to run the installation script from a source code
|
||||||
|
echo download. This will not work.
|
||||||
|
echo.
|
||||||
|
echo Recommended: Please close this window and download the installer from
|
||||||
|
echo https://easydiffusion.github.io/docs/installation/
|
||||||
|
echo.
|
||||||
|
echo ================================================================================
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b
|
||||||
|
)
|
||||||
|
|
||||||
@rem set legacy installer's PATH, if it exists
|
@rem set legacy installer's PATH, if it exists
|
||||||
if exist "installer" set PATH=%cd%\installer;%cd%\installer\Library\bin;%cd%\installer\Scripts;%cd%\installer\Library\usr\bin;%PATH%
|
if exist "installer" set PATH=%cd%\installer;%cd%\installer\Library\bin;%cd%\installer\Scripts;%cd%\installer\Library\usr\bin;%PATH%
|
||||||
|
|
||||||
@rem Setup the packages required for the installer
|
|
||||||
call scripts\bootstrap.bat
|
|
||||||
|
|
||||||
@rem set new installer's PATH, if it downloaded any packages
|
@rem set new installer's PATH, if it downloaded any packages
|
||||||
if exist "installer_files\env" set PATH=%cd%\installer_files\env;%cd%\installer_files\env\Library\bin;%cd%\installer_files\env\Scripts;%cd%\installer_files\Library\usr\bin;%PATH%
|
if exist "installer_files\env" set PATH=%cd%\installer_files\env;%cd%\installer_files\env\Library\bin;%cd%\installer_files\env\Scripts;%cd%\installer_files\Library\usr\bin;%PATH%
|
||||||
|
|
||||||
set PYTHONPATH=%cd%\installer;%cd%\installer_files\env
|
set PYTHONPATH=%cd%\installer;%cd%\installer_files\env
|
||||||
|
|
||||||
@rem Test the bootstrap
|
@rem Test the core requirements
|
||||||
call where git
|
call where git
|
||||||
call git --version
|
call git --version
|
||||||
|
|
||||||
call where conda
|
call where conda
|
||||||
call conda --version
|
call conda --version
|
||||||
|
echo .
|
||||||
|
echo COMSPEC=%COMSPEC%
|
||||||
|
wmic path win32_VideoController get name,AdapterRAM,DriverDate,DriverVersion
|
||||||
|
|
||||||
@rem Download the rest of the installer and UI
|
@rem Download the rest of the installer and UI
|
||||||
call scripts\on_env_start.bat
|
call scripts\on_env_start.bat
|
||||||
|
|
||||||
@pause
|
@pause
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@echo off
|
@echo off
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
@rem This script will install git and conda (if not found on the PATH variable)
|
@rem This script will install git and conda (if not found on the PATH variable)
|
||||||
@rem using micromamba (an 8mb static-linked single-file binary, conda replacement).
|
@rem using micromamba (an 8mb static-linked single-file binary, conda replacement).
|
||||||
@@ -10,9 +11,11 @@
|
|||||||
set MAMBA_ROOT_PREFIX=%cd%\installer_files\mamba
|
set MAMBA_ROOT_PREFIX=%cd%\installer_files\mamba
|
||||||
set INSTALL_ENV_DIR=%cd%\installer_files\env
|
set INSTALL_ENV_DIR=%cd%\installer_files\env
|
||||||
set LEGACY_INSTALL_ENV_DIR=%cd%\installer
|
set LEGACY_INSTALL_ENV_DIR=%cd%\installer
|
||||||
set MICROMAMBA_DOWNLOAD_URL=https://github.com/cmdr2/stable-diffusion-ui/releases/download/v1.1/micromamba.exe
|
set MICROMAMBA_DOWNLOAD_URL=https://github.com/easydiffusion/easydiffusion/releases/download/v1.1/micromamba.exe
|
||||||
set umamba_exists=F
|
set umamba_exists=F
|
||||||
|
|
||||||
|
set PYTHONHOME=
|
||||||
|
|
||||||
set OLD_APPDATA=%APPDATA%
|
set OLD_APPDATA=%APPDATA%
|
||||||
set OLD_USERPROFILE=%USERPROFILE%
|
set OLD_USERPROFILE=%USERPROFILE%
|
||||||
set APPDATA=%cd%\installer_files\appdata
|
set APPDATA=%cd%\installer_files\appdata
|
||||||
@@ -21,17 +24,14 @@ set USERPROFILE=%cd%\profile
|
|||||||
@rem figure out whether git and conda needs to be installed
|
@rem figure out whether git and conda needs to be installed
|
||||||
if exist "%INSTALL_ENV_DIR%" set PATH=%INSTALL_ENV_DIR%;%INSTALL_ENV_DIR%\Library\bin;%INSTALL_ENV_DIR%\Scripts;%INSTALL_ENV_DIR%\Library\usr\bin;%PATH%
|
if exist "%INSTALL_ENV_DIR%" set PATH=%INSTALL_ENV_DIR%;%INSTALL_ENV_DIR%\Library\bin;%INSTALL_ENV_DIR%\Scripts;%INSTALL_ENV_DIR%\Library\usr\bin;%PATH%
|
||||||
|
|
||||||
set PACKAGES_TO_INSTALL=
|
set PACKAGES_TO_INSTALL=git python=3.8.5
|
||||||
|
|
||||||
if not exist "%LEGACY_INSTALL_ENV_DIR%\etc\profile.d\conda.sh" (
|
if not exist "%LEGACY_INSTALL_ENV_DIR%\etc\profile.d\conda.sh" (
|
||||||
if not exist "%INSTALL_ENV_DIR%\etc\profile.d\conda.sh" set PACKAGES_TO_INSTALL=%PACKAGES_TO_INSTALL% conda
|
if not exist "%INSTALL_ENV_DIR%\etc\profile.d\conda.sh" set PACKAGES_TO_INSTALL=%PACKAGES_TO_INSTALL% conda
|
||||||
)
|
)
|
||||||
|
|
||||||
call git --version >.tmp1 2>.tmp2
|
|
||||||
if "%ERRORLEVEL%" NEQ "0" set PACKAGES_TO_INSTALL=%PACKAGES_TO_INSTALL% git
|
|
||||||
|
|
||||||
call "%MAMBA_ROOT_PREFIX%\micromamba.exe" --version >.tmp1 2>.tmp2
|
call "%MAMBA_ROOT_PREFIX%\micromamba.exe" --version >.tmp1 2>.tmp2
|
||||||
if "%ERRORLEVEL%" EQU "0" set umamba_exists=T
|
if "!ERRORLEVEL!" EQU "0" set umamba_exists=T
|
||||||
|
|
||||||
@rem (if necessary) install git and conda into a contained environment
|
@rem (if necessary) install git and conda into a contained environment
|
||||||
if "%PACKAGES_TO_INSTALL%" NEQ "" (
|
if "%PACKAGES_TO_INSTALL%" NEQ "" (
|
||||||
@@ -42,11 +42,11 @@ if "%PACKAGES_TO_INSTALL%" NEQ "" (
|
|||||||
mkdir "%MAMBA_ROOT_PREFIX%"
|
mkdir "%MAMBA_ROOT_PREFIX%"
|
||||||
call curl -Lk "%MICROMAMBA_DOWNLOAD_URL%" > "%MAMBA_ROOT_PREFIX%\micromamba.exe"
|
call curl -Lk "%MICROMAMBA_DOWNLOAD_URL%" > "%MAMBA_ROOT_PREFIX%\micromamba.exe"
|
||||||
|
|
||||||
@REM if "%ERRORLEVEL%" NEQ "0" (
|
if "!ERRORLEVEL!" NEQ "0" (
|
||||||
@REM echo "There was a problem downloading micromamba. Cannot continue."
|
echo "There was a problem downloading micromamba. Cannot continue."
|
||||||
@REM pause
|
pause
|
||||||
@REM exit /b
|
exit /b
|
||||||
@REM )
|
)
|
||||||
|
|
||||||
mkdir "%APPDATA%"
|
mkdir "%APPDATA%"
|
||||||
mkdir "%USERPROFILE%"
|
mkdir "%USERPROFILE%"
|
||||||
|
|||||||
@@ -21,9 +21,16 @@ OS_ARCH=$(uname -m)
|
|||||||
case "${OS_ARCH}" in
|
case "${OS_ARCH}" in
|
||||||
x86_64*) OS_ARCH="64";;
|
x86_64*) OS_ARCH="64";;
|
||||||
arm64*) OS_ARCH="arm64";;
|
arm64*) OS_ARCH="arm64";;
|
||||||
|
aarch64*) OS_ARCH="arm64";;
|
||||||
*) echo "Unknown system architecture: $OS_ARCH! This script runs only on x86_64 or arm64" && exit
|
*) echo "Unknown system architecture: $OS_ARCH! This script runs only on x86_64 or arm64" && exit
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
if ! which curl; then fail "'curl' not found. Please install curl."; fi
|
||||||
|
if ! which tar; then fail "'tar' not found. Please install tar."; fi
|
||||||
|
if ! which bzip2; then fail "'bzip2' not found. Please install bzip2."; fi
|
||||||
|
|
||||||
|
if pwd | grep ' '; then fail "The installation directory's path contains a space character. Conda will fail to install. Please change the directory."; fi
|
||||||
|
|
||||||
# https://mamba.readthedocs.io/en/latest/installation.html
|
# https://mamba.readthedocs.io/en/latest/installation.html
|
||||||
if [ "$OS_NAME" == "linux" ] && [ "$OS_ARCH" == "arm64" ]; then OS_ARCH="aarch64"; fi
|
if [ "$OS_NAME" == "linux" ] && [ "$OS_ARCH" == "arm64" ]; then OS_ARCH="aarch64"; fi
|
||||||
|
|
||||||
@@ -39,7 +46,7 @@ if [ -e "$INSTALL_ENV_DIR" ]; then export PATH="$INSTALL_ENV_DIR/bin:$PATH"; fi
|
|||||||
|
|
||||||
PACKAGES_TO_INSTALL=""
|
PACKAGES_TO_INSTALL=""
|
||||||
|
|
||||||
if [ ! -e "$LEGACY_INSTALL_ENV_DIR/etc/profile.d/conda.sh" ] && [ ! -e "$INSTALL_ENV_DIR/etc/profile.d/conda.sh" ]; then PACKAGES_TO_INSTALL="$PACKAGES_TO_INSTALL conda"; fi
|
if [ ! -e "$LEGACY_INSTALL_ENV_DIR/etc/profile.d/conda.sh" ] && [ ! -e "$INSTALL_ENV_DIR/etc/profile.d/conda.sh" ]; then PACKAGES_TO_INSTALL="$PACKAGES_TO_INSTALL conda python=3.8.5"; fi
|
||||||
if ! hash "git" &>/dev/null; then PACKAGES_TO_INSTALL="$PACKAGES_TO_INSTALL git"; fi
|
if ! hash "git" &>/dev/null; then PACKAGES_TO_INSTALL="$PACKAGES_TO_INSTALL git"; fi
|
||||||
|
|
||||||
if "$MAMBA_ROOT_PREFIX/micromamba" --version &>/dev/null; then umamba_exists="T"; fi
|
if "$MAMBA_ROOT_PREFIX/micromamba" --version &>/dev/null; then umamba_exists="T"; fi
|
||||||
@@ -51,7 +58,7 @@ if [ "$PACKAGES_TO_INSTALL" != "" ]; then
|
|||||||
echo "Downloading micromamba from $MICROMAMBA_DOWNLOAD_URL to $MAMBA_ROOT_PREFIX/micromamba"
|
echo "Downloading micromamba from $MICROMAMBA_DOWNLOAD_URL to $MAMBA_ROOT_PREFIX/micromamba"
|
||||||
|
|
||||||
mkdir -p "$MAMBA_ROOT_PREFIX"
|
mkdir -p "$MAMBA_ROOT_PREFIX"
|
||||||
curl -L "$MICROMAMBA_DOWNLOAD_URL" | tar -xvj bin/micromamba -O > "$MAMBA_ROOT_PREFIX/micromamba"
|
curl -L "$MICROMAMBA_DOWNLOAD_URL" | tar -xvj -O bin/micromamba > "$MAMBA_ROOT_PREFIX/micromamba"
|
||||||
|
|
||||||
if [ "$?" != "0" ]; then
|
if [ "$?" != "0" ]; then
|
||||||
echo
|
echo
|
||||||
|
|||||||
1106
scripts/check_modules.py
Normal file
24
scripts/config.yaml.sample
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Change listen_port if port 9000 is already in use on your system
|
||||||
|
# Set listen_to_network to true to make Easy Diffusion accessibble on your local network
|
||||||
|
net:
|
||||||
|
listen_port: 9000
|
||||||
|
listen_to_network: false
|
||||||
|
|
||||||
|
# Multi GPU setup
|
||||||
|
render_devices: auto
|
||||||
|
|
||||||
|
# Set open_browser_on_start to false to disable opening a new browser tab on each restart
|
||||||
|
ui:
|
||||||
|
open_browser_on_start: true
|
||||||
|
|
||||||
|
# set update_branch to main to use the stable version, or to beta to use the experimental
|
||||||
|
# beta version.
|
||||||
|
update_branch: main
|
||||||
|
|
||||||
|
# Set force_save_path to enforce an auto save path. Clients will not be able to change or
|
||||||
|
# disable auto save when this option is set. Please adapt the path in the examples to your
|
||||||
|
# needs.
|
||||||
|
# Windows:
|
||||||
|
# force_save_path: C:\\Easy Diffusion Images\\
|
||||||
|
# Linux:
|
||||||
|
# force_save_path: /data/easy-diffusion-images/
|
||||||
@@ -26,15 +26,28 @@ if [ "$0" == "bash" ]; then
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# activate the environment
|
# activate the legacy environment (if present) and set PYTHONPATH
|
||||||
CONDA_BASEPATH=$(conda info --base)
|
if [ -e "installer_files/env" ]; then
|
||||||
source "$CONDA_BASEPATH/etc/profile.d/conda.sh" # otherwise conda complains about 'shell not initialized' (needed when running in a script)
|
export PYTHONPATH="$(pwd)/installer_files/env/lib/python3.8/site-packages"
|
||||||
|
fi
|
||||||
|
if [ -e "stable-diffusion/env" ]; then
|
||||||
|
CONDA_BASEPATH=$(conda info --base)
|
||||||
|
source "$CONDA_BASEPATH/etc/profile.d/conda.sh" # otherwise conda complains about 'shell not initialized' (needed when running in a script)
|
||||||
|
|
||||||
conda activate ./stable-diffusion/env
|
conda activate ./stable-diffusion/env
|
||||||
|
|
||||||
|
export PYTHONPATH="$(pwd)/stable-diffusion/env/lib/python3.8/site-packages"
|
||||||
|
fi
|
||||||
|
|
||||||
|
export PYTHONNOUSERSITE=y
|
||||||
|
|
||||||
which python
|
which python
|
||||||
python --version
|
python --version
|
||||||
|
|
||||||
|
echo "PYTHONPATH=$PYTHONPATH"
|
||||||
|
|
||||||
|
# done
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
else
|
else
|
||||||
file_name=$(basename "${BASH_SOURCE[0]}")
|
file_name=$(basename "${BASH_SOURCE[0]}")
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ fail() {
|
|||||||
|
|
||||||
Error downloading Stable Diffusion UI. Sorry about that, please try to:
|
Error downloading Stable Diffusion UI. Sorry about that, please try to:
|
||||||
1. Run this installer again.
|
1. Run this installer again.
|
||||||
2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting
|
2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/easydiffusion/easydiffusion/wiki/Troubleshooting
|
||||||
3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB
|
3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB
|
||||||
4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues
|
4. If that doesn't solve the problem, please file an issue at https://github.com/easydiffusion/easydiffusion/issues
|
||||||
|
|
||||||
Thanks!
|
Thanks!
|
||||||
|
|
||||||
@@ -28,5 +28,12 @@ EOF
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filesize() {
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Linux*) stat -c "%s" $1;;
|
||||||
|
Darwin*) /usr/bin/stat -f "%z" $1;;
|
||||||
|
*) echo "Unknown OS: $OS_NAME! This script runs only on Linux or Mac" && exit
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
53
scripts/get_config.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
# The config file is in the same directory as this script
|
||||||
|
config_directory = os.path.dirname(__file__)
|
||||||
|
config_yaml = os.path.join(config_directory, "..", "config.yaml")
|
||||||
|
config_json = os.path.join(config_directory, "config.json")
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description='Get values from config file')
|
||||||
|
parser.add_argument('--default', dest='default', action='store',
|
||||||
|
help='default value, to be used if the setting is not defined in the config file')
|
||||||
|
parser.add_argument('key', metavar='key', nargs='+',
|
||||||
|
help='config key to return')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
config = None
|
||||||
|
|
||||||
|
# migrate the old config yaml location
|
||||||
|
config_legacy_yaml = os.path.join(config_directory, "config.yaml")
|
||||||
|
if os.path.isfile(config_legacy_yaml):
|
||||||
|
shutil.move(config_legacy_yaml, config_yaml)
|
||||||
|
|
||||||
|
if os.path.isfile(config_yaml):
|
||||||
|
from ruamel.yaml import YAML
|
||||||
|
yaml = YAML(typ='safe')
|
||||||
|
with open(config_yaml, 'r') as configfile:
|
||||||
|
try:
|
||||||
|
config = yaml.load(configfile)
|
||||||
|
except Exception as e:
|
||||||
|
print(e, file=sys.stderr)
|
||||||
|
elif os.path.isfile(config_json):
|
||||||
|
import json
|
||||||
|
with open(config_json, 'r') as configfile:
|
||||||
|
try:
|
||||||
|
config = json.load(configfile)
|
||||||
|
except Exception as e:
|
||||||
|
print(e, file=sys.stderr)
|
||||||
|
|
||||||
|
if config is None:
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
for k in args.key:
|
||||||
|
if k in config:
|
||||||
|
config = config[k]
|
||||||
|
else:
|
||||||
|
if args.default != None:
|
||||||
|
print(args.default)
|
||||||
|
exit()
|
||||||
|
|
||||||
|
print(config)
|
||||||
0
scripts/install_status.txt
Normal file
@@ -1,6 +1,6 @@
|
|||||||
@echo off
|
@echo off
|
||||||
|
|
||||||
@echo. & echo "Stable Diffusion UI - v2" & echo.
|
@echo. & echo "Easy Diffusion - v3" & echo.
|
||||||
|
|
||||||
set PATH=C:\Windows\System32;%PATH%
|
set PATH=C:\Windows\System32;%PATH%
|
||||||
|
|
||||||
@@ -8,6 +8,20 @@ if exist "scripts\config.bat" (
|
|||||||
@call scripts\config.bat
|
@call scripts\config.bat
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if exist "scripts\user_config.bat" (
|
||||||
|
@call scripts\user_config.bat
|
||||||
|
)
|
||||||
|
|
||||||
|
if exist "stable-diffusion\env" (
|
||||||
|
@set PYTHONPATH=%PYTHONPATH%;%cd%\stable-diffusion\env\lib\site-packages
|
||||||
|
)
|
||||||
|
|
||||||
|
if exist "scripts\get_config.py" (
|
||||||
|
@FOR /F "tokens=* USEBACKQ" %%F IN (`python scripts\get_config.py --default=main update_branch`) DO (
|
||||||
|
@SET update_branch=%%F
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if "%update_branch%"=="" (
|
if "%update_branch%"=="" (
|
||||||
set update_branch=main
|
set update_branch=main
|
||||||
)
|
)
|
||||||
@@ -28,23 +42,25 @@ if "%update_branch%"=="" (
|
|||||||
|
|
||||||
@>nul findstr /m "sd_ui_git_cloned" scripts\install_status.txt
|
@>nul findstr /m "sd_ui_git_cloned" scripts\install_status.txt
|
||||||
@if "%ERRORLEVEL%" EQU "0" (
|
@if "%ERRORLEVEL%" EQU "0" (
|
||||||
@echo "Stable Diffusion UI's git repository was already installed. Updating from %update_branch%.."
|
@echo "Easy Diffusion's git repository was already installed. Updating from %update_branch%.."
|
||||||
|
|
||||||
@cd sd-ui-files
|
@cd sd-ui-files
|
||||||
|
|
||||||
|
@call git add -A .
|
||||||
|
@call git stash
|
||||||
@call git reset --hard
|
@call git reset --hard
|
||||||
@call git -c advice.detachedHead=false checkout "%update_branch%"
|
@call git -c advice.detachedHead=false checkout "%update_branch%"
|
||||||
@call git pull
|
@call git pull
|
||||||
|
|
||||||
@cd ..
|
@cd ..
|
||||||
) else (
|
) else (
|
||||||
@echo. & echo "Downloading Stable Diffusion UI.." & echo.
|
@echo. & echo "Downloading Easy Diffusion..." & echo.
|
||||||
@echo "Using the %update_branch% channel" & echo.
|
@echo "Using the %update_branch% channel" & echo.
|
||||||
|
|
||||||
@call git clone -b "%update_branch%" https://github.com/cmdr2/stable-diffusion-ui.git sd-ui-files && (
|
@call git clone -b "%update_branch%" https://github.com/easydiffusion/easydiffusion.git sd-ui-files && (
|
||||||
@echo sd_ui_git_cloned >> scripts\install_status.txt
|
@echo sd_ui_git_cloned >> scripts\install_status.txt
|
||||||
) || (
|
) || (
|
||||||
@echo "Error downloading Stable Diffusion UI. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!"
|
@echo "Error downloading Easy Diffusion. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/easydiffusion/easydiffusion/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/easydiffusion/easydiffusion/issues" & echo "Thanks!"
|
||||||
pause
|
pause
|
||||||
@exit /b
|
@exit /b
|
||||||
)
|
)
|
||||||
@@ -52,7 +68,9 @@ if "%update_branch%"=="" (
|
|||||||
|
|
||||||
@xcopy sd-ui-files\ui ui /s /i /Y /q
|
@xcopy sd-ui-files\ui ui /s /i /Y /q
|
||||||
@copy sd-ui-files\scripts\on_sd_start.bat scripts\ /Y
|
@copy sd-ui-files\scripts\on_sd_start.bat scripts\ /Y
|
||||||
@copy sd-ui-files\scripts\bootstrap.bat scripts\ /Y
|
@copy sd-ui-files\scripts\check_modules.py scripts\ /Y
|
||||||
|
@copy sd-ui-files\scripts\get_config.py scripts\ /Y
|
||||||
|
@copy sd-ui-files\scripts\config.yaml.sample scripts\ /Y
|
||||||
@copy "sd-ui-files\scripts\Start Stable Diffusion UI.cmd" . /Y
|
@copy "sd-ui-files\scripts\Start Stable Diffusion UI.cmd" . /Y
|
||||||
@copy "sd-ui-files\scripts\Developer Console.cmd" . /Y
|
@copy "sd-ui-files\scripts\Developer Console.cmd" . /Y
|
||||||
|
|
||||||
|
|||||||
@@ -2,31 +2,45 @@
|
|||||||
|
|
||||||
source ./scripts/functions.sh
|
source ./scripts/functions.sh
|
||||||
|
|
||||||
printf "\n\nStable Diffusion UI\n\n"
|
printf "\n\nEasy Diffusion - v3\n\n"
|
||||||
|
|
||||||
|
export PYTHONNOUSERSITE=y
|
||||||
|
|
||||||
if [ -f "scripts/config.sh" ]; then
|
if [ -f "scripts/config.sh" ]; then
|
||||||
source scripts/config.sh
|
source scripts/config.sh
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ -f "scripts/user_config.sh" ]; then
|
||||||
|
source scripts/user_config.sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
export PYTHONPATH=$(pwd)/installer_files/env/lib/python3.8/site-packages:$(pwd)/stable-diffusion/env/lib/python3.8/site-packages
|
||||||
|
|
||||||
|
if [ -f "scripts/get_config.py" ]; then
|
||||||
|
export update_branch="$( python scripts/get_config.py --default=main update_branch )"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$update_branch" == "" ]; then
|
if [ "$update_branch" == "" ]; then
|
||||||
export update_branch="main"
|
export update_branch="main"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f "scripts/install_status.txt" ] && [ `grep -c sd_ui_git_cloned scripts/install_status.txt` -gt "0" ]; then
|
if [ -f "scripts/install_status.txt" ] && [ `grep -c sd_ui_git_cloned scripts/install_status.txt` -gt "0" ]; then
|
||||||
echo "Stable Diffusion UI's git repository was already installed. Updating from $update_branch.."
|
echo "Easy Diffusion's git repository was already installed. Updating from $update_branch.."
|
||||||
|
|
||||||
cd sd-ui-files
|
cd sd-ui-files
|
||||||
|
|
||||||
|
git add -A .
|
||||||
|
git stash
|
||||||
git reset --hard
|
git reset --hard
|
||||||
git -c advice.detachedHead=false checkout "$update_branch"
|
git -c advice.detachedHead=false checkout "$update_branch"
|
||||||
git pull
|
git pull
|
||||||
|
|
||||||
cd ..
|
cd ..
|
||||||
else
|
else
|
||||||
printf "\n\nDownloading Stable Diffusion UI..\n\n"
|
printf "\n\nDownloading Easy Diffusion..\n\n"
|
||||||
printf "Using the $update_branch channel\n\n"
|
printf "Using the $update_branch channel\n\n"
|
||||||
|
|
||||||
if git clone -b "$update_branch" https://github.com/cmdr2/stable-diffusion-ui.git sd-ui-files ; then
|
if git clone -b "$update_branch" https://github.com/easydiffusion/easydiffusion.git sd-ui-files ; then
|
||||||
echo sd_ui_git_cloned >> scripts/install_status.txt
|
echo sd_ui_git_cloned >> scripts/install_status.txt
|
||||||
else
|
else
|
||||||
fail "git clone failed"
|
fail "git clone failed"
|
||||||
@@ -37,9 +51,11 @@ rm -rf ui
|
|||||||
cp -Rf sd-ui-files/ui .
|
cp -Rf sd-ui-files/ui .
|
||||||
cp sd-ui-files/scripts/on_sd_start.sh scripts/
|
cp sd-ui-files/scripts/on_sd_start.sh scripts/
|
||||||
cp sd-ui-files/scripts/bootstrap.sh scripts/
|
cp sd-ui-files/scripts/bootstrap.sh scripts/
|
||||||
|
cp sd-ui-files/scripts/check_modules.py scripts/
|
||||||
|
cp sd-ui-files/scripts/get_config.py scripts/
|
||||||
|
cp sd-ui-files/scripts/config.yaml.sample scripts/
|
||||||
cp sd-ui-files/scripts/start.sh .
|
cp sd-ui-files/scripts/start.sh .
|
||||||
cp sd-ui-files/scripts/developer_console.sh .
|
cp sd-ui-files/scripts/developer_console.sh .
|
||||||
|
cp sd-ui-files/scripts/functions.sh scripts/
|
||||||
|
|
||||||
./scripts/on_sd_start.sh
|
exec ./scripts/on_sd_start.sh
|
||||||
|
|
||||||
read -p "Press any key to continue"
|
|
||||||
|
|||||||
@@ -4,10 +4,20 @@
|
|||||||
@REM Note to self: Please rewrite this in Python. For the sake of your own sanity.
|
@REM Note to self: Please rewrite this in Python. For the sake of your own sanity.
|
||||||
|
|
||||||
@copy sd-ui-files\scripts\on_env_start.bat scripts\ /Y
|
@copy sd-ui-files\scripts\on_env_start.bat scripts\ /Y
|
||||||
@copy sd-ui-files\scripts\bootstrap.bat scripts\ /Y
|
@copy sd-ui-files\scripts\check_modules.py scripts\ /Y
|
||||||
|
@copy sd-ui-files\scripts\get_config.py scripts\ /Y
|
||||||
|
@copy sd-ui-files\scripts\config.yaml.sample scripts\ /Y
|
||||||
|
|
||||||
if exist "%cd%\profile" (
|
if exist "%cd%\profile" (
|
||||||
set USERPROFILE=%cd%\profile
|
set HF_HOME=%cd%\profile\.cache\huggingface
|
||||||
|
)
|
||||||
|
|
||||||
|
@rem set the correct installer path (current vs legacy)
|
||||||
|
if exist "%cd%\installer_files\env" (
|
||||||
|
set INSTALL_ENV_DIR=%cd%\installer_files\env
|
||||||
|
)
|
||||||
|
if exist "%cd%\stable-diffusion\env" (
|
||||||
|
set INSTALL_ENV_DIR=%cd%\stable-diffusion\env
|
||||||
)
|
)
|
||||||
|
|
||||||
@mkdir tmp
|
@mkdir tmp
|
||||||
@@ -16,395 +26,51 @@ if exist "%cd%\profile" (
|
|||||||
|
|
||||||
@rem activate the installer env
|
@rem activate the installer env
|
||||||
call conda activate
|
call conda activate
|
||||||
@rem @if "%ERRORLEVEL%" NEQ "0" (
|
@if "%ERRORLEVEL%" NEQ "0" (
|
||||||
@rem @echo. & echo "Error activating conda for Stable Diffusion. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo.
|
@echo. & echo "Error activating conda for Easy Diffusion. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/easydiffusion/easydiffusion/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/easydiffusion/easydiffusion/issues" & echo "Thanks!" & echo.
|
||||||
@rem pause
|
pause
|
||||||
@rem exit /b
|
exit /b
|
||||||
@rem )
|
)
|
||||||
|
|
||||||
@REM remove the old version of the dev console script, if it's still present
|
@REM remove the old version of the dev console script, if it's still present
|
||||||
if exist "Open Developer Console.cmd" del "Open Developer Console.cmd"
|
if exist "Open Developer Console.cmd" del "Open Developer Console.cmd"
|
||||||
|
if exist "ui\plugins\ui\merge.plugin.js" del "ui\plugins\ui\merge.plugin.js"
|
||||||
|
|
||||||
@call python -c "import os; import shutil; frm = 'sd-ui-files\\ui\\hotfix\\9c24e6cd9f499d02c4f21a033736dabd365962dc80fe3aeb57a8f85ea45a20a3.26fead7ea4f0f843f6eb4055dfd25693f1a71f3c6871b184042d4b126244e142'; dst = os.path.join(os.path.expanduser('~'), '.cache', 'huggingface', 'transformers', '9c24e6cd9f499d02c4f21a033736dabd365962dc80fe3aeb57a8f85ea45a20a3.26fead7ea4f0f843f6eb4055dfd25693f1a71f3c6871b184042d4b126244e142'); shutil.copyfile(frm, dst) if os.path.exists(dst) else print(''); print('Hotfixed broken JSON file from OpenAI');"
|
@rem create the stable-diffusion folder, to work with legacy installations
|
||||||
|
if not exist "stable-diffusion" mkdir stable-diffusion
|
||||||
|
cd stable-diffusion
|
||||||
|
|
||||||
@>nul findstr /m "sd_git_cloned" scripts\install_status.txt
|
@rem activate the old stable-diffusion env, if it exists
|
||||||
@if "%ERRORLEVEL%" EQU "0" (
|
if exist "env" (
|
||||||
@echo "Stable Diffusion's git repository was already installed. Updating.."
|
call conda activate .\env
|
||||||
|
|
||||||
@cd stable-diffusion
|
|
||||||
|
|
||||||
@call git reset --hard
|
|
||||||
@call git pull
|
|
||||||
@call git -c advice.detachedHead=false checkout f6cfebffa752ee11a7b07497b8529d5971de916c
|
|
||||||
|
|
||||||
@call git apply --whitespace=nowarn ..\ui\sd_internal\ddim_callback.patch
|
|
||||||
@call git apply --whitespace=nowarn ..\ui\sd_internal\env_yaml.patch
|
|
||||||
|
|
||||||
@cd ..
|
|
||||||
) else (
|
|
||||||
@echo. & echo "Downloading Stable Diffusion.." & echo.
|
|
||||||
|
|
||||||
@call git clone https://github.com/basujindal/stable-diffusion.git && (
|
|
||||||
@echo sd_git_cloned >> scripts\install_status.txt
|
|
||||||
) || (
|
|
||||||
@echo "Error downloading Stable Diffusion. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!"
|
|
||||||
pause
|
|
||||||
@exit /b
|
|
||||||
)
|
|
||||||
|
|
||||||
@cd stable-diffusion
|
|
||||||
@call git -c advice.detachedHead=false checkout f6cfebffa752ee11a7b07497b8529d5971de916c
|
|
||||||
|
|
||||||
@call git apply --whitespace=nowarn ..\ui\sd_internal\ddim_callback.patch
|
|
||||||
@call git apply --whitespace=nowarn ..\ui\sd_internal\env_yaml.patch
|
|
||||||
|
|
||||||
@cd ..
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@cd stable-diffusion
|
@rem disable the legacy src and ldm folder (otherwise this prevents installing gfpgan and realesrgan)
|
||||||
|
if exist src rename src src-old
|
||||||
|
if exist ldm rename ldm ldm-old
|
||||||
|
|
||||||
@>nul findstr /m "conda_sd_env_created" ..\scripts\install_status.txt
|
|
||||||
@if "%ERRORLEVEL%" EQU "0" (
|
|
||||||
@echo "Packages necessary for Stable Diffusion were already installed"
|
|
||||||
|
|
||||||
@call conda activate .\env
|
if not exist "%INSTALL_ENV_DIR%\DLLs\libssl-1_1-x64.dll" copy "%INSTALL_ENV_DIR%\Library\bin\libssl-1_1-x64.dll" "%INSTALL_ENV_DIR%\DLLs\"
|
||||||
) else (
|
if not exist "%INSTALL_ENV_DIR%\DLLs\libcrypto-1_1-x64.dll" copy "%INSTALL_ENV_DIR%\Library\bin\libcrypto-1_1-x64.dll" "%INSTALL_ENV_DIR%\DLLs\"
|
||||||
@echo. & echo "Downloading packages necessary for Stable Diffusion.." & echo. & echo "***** This will take some time (depending on the speed of the Internet connection) and may appear to be stuck, but please be patient ***** .." & echo.
|
|
||||||
|
|
||||||
@rmdir /s /q .\env
|
cd ..
|
||||||
|
|
||||||
@REM prevent conda from using packages from the user's home directory, to avoid conflicts
|
@rem set any overrides
|
||||||
@set PYTHONNOUSERSITE=1
|
set HF_HUB_DISABLE_SYMLINKS_WARNING=true
|
||||||
|
|
||||||
set USERPROFILE=%cd%\profile
|
|
||||||
|
|
||||||
set PYTHONPATH=%cd%;%cd%\env\lib\site-packages
|
|
||||||
|
|
||||||
@call conda env create --prefix env -f environment.yaml || (
|
|
||||||
@echo. & echo "Error installing the packages necessary for Stable Diffusion. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo.
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
|
|
||||||
@call conda activate .\env
|
|
||||||
|
|
||||||
@call conda install -c conda-forge -y --prefix env antlr4-python3-runtime=4.8 || (
|
|
||||||
@echo. & echo "Error installing antlr4-python3-runtime for Stable Diffusion. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo.
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
|
|
||||||
for /f "tokens=*" %%a in ('python -c "import torch; import ldm; import transformers; import numpy; import antlr4; print(42)"') do if "%%a" NEQ "42" (
|
|
||||||
@echo. & echo "Dependency test failed! Error installing the packages necessary for Stable Diffusion. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo.
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
|
|
||||||
@echo conda_sd_env_created >> ..\scripts\install_status.txt
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@rem install or upgrade the required modules
|
||||||
set PATH=C:\Windows\System32;%PATH%
|
set PATH=C:\Windows\System32;%PATH%
|
||||||
|
|
||||||
@>nul findstr /m "conda_sd_gfpgan_deps_installed" ..\scripts\install_status.txt
|
@REM prevent from using packages from the user's home directory, to avoid conflicts
|
||||||
@if "%ERRORLEVEL%" EQU "0" (
|
set PYTHONNOUSERSITE=1
|
||||||
@echo "Packages necessary for GFPGAN (Face Correction) were already installed"
|
set PYTHONPATH=%INSTALL_ENV_DIR%\lib\site-packages
|
||||||
) else (
|
echo PYTHONPATH=%PYTHONPATH%
|
||||||
@echo. & echo "Downloading packages necessary for GFPGAN (Face Correction).." & echo.
|
|
||||||
|
|
||||||
@set PYTHONNOUSERSITE=1
|
|
||||||
|
|
||||||
set USERPROFILE=%cd%\profile
|
|
||||||
|
|
||||||
set PYTHONPATH=%cd%;%cd%\env\lib\site-packages
|
|
||||||
|
|
||||||
@call pip install -e git+https://github.com/TencentARC/GFPGAN#egg=GFPGAN || (
|
|
||||||
@echo. & echo "Error installing the packages necessary for GFPGAN (Face Correction). Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo.
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
|
|
||||||
@call pip install basicsr==1.4.2 || (
|
|
||||||
@echo. & echo "Error installing the basicsr package necessary for GFPGAN (Face Correction). Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo.
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
|
|
||||||
for /f "tokens=*" %%a in ('python -c "from gfpgan import GFPGANer; print(42)"') do if "%%a" NEQ "42" (
|
|
||||||
@echo. & echo "Dependency test failed! Error installing the packages necessary for GFPGAN (Face Correction). Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo.
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
|
|
||||||
@echo conda_sd_gfpgan_deps_installed >> ..\scripts\install_status.txt
|
|
||||||
)
|
|
||||||
|
|
||||||
@>nul findstr /m "conda_sd_esrgan_deps_installed" ..\scripts\install_status.txt
|
|
||||||
@if "%ERRORLEVEL%" EQU "0" (
|
|
||||||
@echo "Packages necessary for ESRGAN (Resolution Upscaling) were already installed"
|
|
||||||
) else (
|
|
||||||
@echo. & echo "Downloading packages necessary for ESRGAN (Resolution Upscaling).." & echo.
|
|
||||||
|
|
||||||
@set PYTHONNOUSERSITE=1
|
|
||||||
|
|
||||||
set USERPROFILE=%cd%\profile
|
|
||||||
|
|
||||||
set PYTHONPATH=%cd%;%cd%\env\lib\site-packages
|
|
||||||
|
|
||||||
@call pip install -e git+https://github.com/xinntao/Real-ESRGAN#egg=realesrgan || (
|
|
||||||
@echo. & echo "Error installing the packages necessary for ESRGAN (Resolution Upscaling). Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo.
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
|
|
||||||
for /f "tokens=*" %%a in ('python -c "from basicsr.archs.rrdbnet_arch import RRDBNet; from realesrgan import RealESRGANer; print(42)"') do if "%%a" NEQ "42" (
|
|
||||||
@echo. & echo "Dependency test failed! Error installing the packages necessary for ESRGAN (Resolution Upscaling). Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo.
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
|
|
||||||
@echo conda_sd_esrgan_deps_installed >> ..\scripts\install_status.txt
|
|
||||||
)
|
|
||||||
|
|
||||||
@>nul findstr /m "conda_sd_ui_deps_installed" ..\scripts\install_status.txt
|
|
||||||
@if "%ERRORLEVEL%" EQU "0" (
|
|
||||||
echo "Packages necessary for Stable Diffusion UI were already installed"
|
|
||||||
) else (
|
|
||||||
@echo. & echo "Downloading packages necessary for Stable Diffusion UI.." & echo.
|
|
||||||
|
|
||||||
@set PYTHONNOUSERSITE=1
|
|
||||||
|
|
||||||
set USERPROFILE=%cd%\profile
|
|
||||||
|
|
||||||
set PYTHONPATH=%cd%;%cd%\env\lib\site-packages
|
|
||||||
|
|
||||||
@call conda install -c conda-forge -y --prefix env uvicorn fastapi || (
|
|
||||||
echo "Error installing the packages necessary for Stable Diffusion UI. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!"
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
call WHERE uvicorn > .tmp
|
|
||||||
@>nul findstr /m "uvicorn" .tmp
|
|
||||||
@if "%ERRORLEVEL%" NEQ "0" (
|
|
||||||
@echo. & echo "UI packages not found! Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo.
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
|
|
||||||
@>nul 2>nul call python -m picklescan --help
|
|
||||||
@if "%ERRORLEVEL%" NEQ "0" (
|
|
||||||
@echo. & echo Picklescan not found. Installing
|
|
||||||
@call pip install picklescan || (
|
|
||||||
echo "Error installing the picklescan package necessary for Stable Diffusion UI. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!"
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@>nul findstr /m "conda_sd_ui_deps_installed" ..\scripts\install_status.txt
|
|
||||||
@if "%ERRORLEVEL%" NEQ "0" (
|
|
||||||
@echo conda_sd_ui_deps_installed >> ..\scripts\install_status.txt
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if not exist "..\models\stable-diffusion" mkdir "..\models\stable-diffusion"
|
|
||||||
if not exist "..\models\vae" mkdir "..\models\vae"
|
|
||||||
echo. > "..\models\stable-diffusion\Put your custom ckpt files here.txt"
|
|
||||||
echo. > "..\models\vae\Put your VAE files here.txt"
|
|
||||||
|
|
||||||
@if exist "sd-v1-4.ckpt" (
|
|
||||||
for %%I in ("sd-v1-4.ckpt") do if "%%~zI" EQU "4265380512" (
|
|
||||||
echo "Data files (weights) necessary for Stable Diffusion were already downloaded. Using the HuggingFace 4 GB Model."
|
|
||||||
) else (
|
|
||||||
for %%J in ("sd-v1-4.ckpt") do if "%%~zJ" EQU "7703807346" (
|
|
||||||
echo "Data files (weights) necessary for Stable Diffusion were already downloaded. Using the HuggingFace 7 GB Model."
|
|
||||||
) else (
|
|
||||||
for %%K in ("sd-v1-4.ckpt") do if "%%~zK" EQU "7703810927" (
|
|
||||||
echo "Data files (weights) necessary for Stable Diffusion were already downloaded. Using the Waifu Model."
|
|
||||||
) else (
|
|
||||||
echo. & echo "The model file present at %cd%\sd-v1-4.ckpt is invalid. It is only %%~zK bytes in size. Re-downloading.." & echo.
|
|
||||||
del "sd-v1-4.ckpt"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@if not exist "sd-v1-4.ckpt" (
|
|
||||||
@echo. & echo "Downloading data files (weights) for Stable Diffusion.." & echo.
|
|
||||||
|
|
||||||
@call curl -L -k https://me.cmdr2.org/stable-diffusion-ui/sd-v1-4.ckpt > sd-v1-4.ckpt
|
|
||||||
|
|
||||||
@if exist "sd-v1-4.ckpt" (
|
|
||||||
for %%I in ("sd-v1-4.ckpt") do if "%%~zI" NEQ "4265380512" (
|
|
||||||
echo. & echo "Error: The downloaded model file was invalid! Bytes downloaded: %%~zI" & echo.
|
|
||||||
echo. & echo "Error downloading the data files (weights) for Stable Diffusion. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo.
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
) else (
|
|
||||||
@echo. & echo "Error downloading the data files (weights) for Stable Diffusion. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo.
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@if exist "GFPGANv1.3.pth" (
|
|
||||||
for %%I in ("GFPGANv1.3.pth") do if "%%~zI" EQU "348632874" (
|
|
||||||
echo "Data files (weights) necessary for GFPGAN (Face Correction) were already downloaded"
|
|
||||||
) else (
|
|
||||||
echo. & echo "The GFPGAN model file present at %cd%\GFPGANv1.3.pth is invalid. It is only %%~zI bytes in size. Re-downloading.." & echo.
|
|
||||||
del "GFPGANv1.3.pth"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@if not exist "GFPGANv1.3.pth" (
|
|
||||||
@echo. & echo "Downloading data files (weights) for GFPGAN (Face Correction).." & echo.
|
|
||||||
|
|
||||||
@call curl -L -k https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.3.pth > GFPGANv1.3.pth
|
|
||||||
|
|
||||||
@if exist "GFPGANv1.3.pth" (
|
|
||||||
for %%I in ("GFPGANv1.3.pth") do if "%%~zI" NEQ "348632874" (
|
|
||||||
echo. & echo "Error: The downloaded GFPGAN model file was invalid! Bytes downloaded: %%~zI" & echo.
|
|
||||||
echo. & echo "Error downloading the data files (weights) for GFPGAN (Face Correction). Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo.
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
) else (
|
|
||||||
@echo. & echo "Error downloading the data files (weights) for GFPGAN (Face Correction). Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo.
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@if exist "RealESRGAN_x4plus.pth" (
|
|
||||||
for %%I in ("RealESRGAN_x4plus.pth") do if "%%~zI" EQU "67040989" (
|
|
||||||
echo "Data files (weights) necessary for ESRGAN (Resolution Upscaling) x4plus were already downloaded"
|
|
||||||
) else (
|
|
||||||
echo. & echo "The RealESRGAN model file present at %cd%\RealESRGAN_x4plus.pth is invalid. It is only %%~zI bytes in size. Re-downloading.." & echo.
|
|
||||||
del "RealESRGAN_x4plus.pth"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@if not exist "RealESRGAN_x4plus.pth" (
|
|
||||||
@echo. & echo "Downloading data files (weights) for ESRGAN (Resolution Upscaling) x4plus.." & echo.
|
|
||||||
|
|
||||||
@call curl -L -k https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth > RealESRGAN_x4plus.pth
|
|
||||||
|
|
||||||
@if exist "RealESRGAN_x4plus.pth" (
|
|
||||||
for %%I in ("RealESRGAN_x4plus.pth") do if "%%~zI" NEQ "67040989" (
|
|
||||||
echo. & echo "Error: The downloaded ESRGAN x4plus model file was invalid! Bytes downloaded: %%~zI" & echo.
|
|
||||||
echo. & echo "Error downloading the data files (weights) for ESRGAN (Resolution Upscaling) x4plus. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo.
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
) else (
|
|
||||||
@echo. & echo "Error downloading the data files (weights) for ESRGAN (Resolution Upscaling) x4plus. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo.
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@if exist "RealESRGAN_x4plus_anime_6B.pth" (
|
|
||||||
for %%I in ("RealESRGAN_x4plus_anime_6B.pth") do if "%%~zI" EQU "17938799" (
|
|
||||||
echo "Data files (weights) necessary for ESRGAN (Resolution Upscaling) x4plus_anime were already downloaded"
|
|
||||||
) else (
|
|
||||||
echo. & echo "The RealESRGAN model file present at %cd%\RealESRGAN_x4plus_anime_6B.pth is invalid. It is only %%~zI bytes in size. Re-downloading.." & echo.
|
|
||||||
del "RealESRGAN_x4plus_anime_6B.pth"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@if not exist "RealESRGAN_x4plus_anime_6B.pth" (
|
|
||||||
@echo. & echo "Downloading data files (weights) for ESRGAN (Resolution Upscaling) x4plus_anime.." & echo.
|
|
||||||
|
|
||||||
@call curl -L -k https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth > RealESRGAN_x4plus_anime_6B.pth
|
|
||||||
|
|
||||||
@if exist "RealESRGAN_x4plus_anime_6B.pth" (
|
|
||||||
for %%I in ("RealESRGAN_x4plus_anime_6B.pth") do if "%%~zI" NEQ "17938799" (
|
|
||||||
echo. & echo "Error: The downloaded ESRGAN x4plus_anime model file was invalid! Bytes downloaded: %%~zI" & echo.
|
|
||||||
echo. & echo "Error downloading the data files (weights) for ESRGAN (Resolution Upscaling) x4plus_anime. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo.
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
) else (
|
|
||||||
@echo. & echo "Error downloading the data files (weights) for ESRGAN (Resolution Upscaling) x4plus_anime. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo.
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@if exist "..\models\vae\vae-ft-mse-840000-ema-pruned.ckpt" (
|
|
||||||
for %%I in ("..\models\vae\vae-ft-mse-840000-ema-pruned.ckpt") do if "%%~zI" EQU "334695179" (
|
|
||||||
echo "Data files (weights) necessary for the default VAE (sd-vae-ft-mse-original) were already downloaded"
|
|
||||||
) else (
|
|
||||||
echo. & echo "The default VAE (sd-vae-ft-mse-original) file present at models\vae\vae-ft-mse-840000-ema-pruned.ckpt is invalid. It is only %%~zI bytes in size. Re-downloading.." & echo.
|
|
||||||
del "..\models\vae\vae-ft-mse-840000-ema-pruned.ckpt"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@if not exist "..\models\vae\vae-ft-mse-840000-ema-pruned.ckpt" (
|
|
||||||
@echo. & echo "Downloading data files (weights) for the default VAE (sd-vae-ft-mse-original).." & echo.
|
|
||||||
|
|
||||||
@call curl -L -k https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.ckpt > ..\models\vae\vae-ft-mse-840000-ema-pruned.ckpt
|
|
||||||
|
|
||||||
@if exist "..\models\vae\vae-ft-mse-840000-ema-pruned.ckpt" (
|
|
||||||
for %%I in ("..\models\vae\vae-ft-mse-840000-ema-pruned.ckpt") do if "%%~zI" NEQ "334695179" (
|
|
||||||
echo. & echo "Error: The downloaded default VAE (sd-vae-ft-mse-original) file was invalid! Bytes downloaded: %%~zI" & echo.
|
|
||||||
echo. & echo "Error downloading the data files (weights) for the default VAE (sd-vae-ft-mse-original). Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo.
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
) else (
|
|
||||||
@echo. & echo "Error downloading the data files (weights) for the default VAE (sd-vae-ft-mse-original). Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo.
|
|
||||||
pause
|
|
||||||
exit /b
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@>nul findstr /m "sd_install_complete" ..\scripts\install_status.txt
|
|
||||||
@if "%ERRORLEVEL%" NEQ "0" (
|
|
||||||
@echo sd_weights_downloaded >> ..\scripts\install_status.txt
|
|
||||||
@echo sd_install_complete >> ..\scripts\install_status.txt
|
|
||||||
)
|
|
||||||
|
|
||||||
@echo. & echo "Stable Diffusion is ready!" & echo.
|
|
||||||
|
|
||||||
@set SD_DIR=%cd%
|
|
||||||
|
|
||||||
@cd env\lib\site-packages
|
|
||||||
@set PYTHONPATH=%SD_DIR%;%cd%
|
|
||||||
@cd ..\..\..
|
|
||||||
@echo PYTHONPATH=%PYTHONPATH%
|
|
||||||
|
|
||||||
|
@rem Download the required packages
|
||||||
call where python
|
call where python
|
||||||
call python --version
|
call python --version
|
||||||
|
|
||||||
@cd ..
|
call python scripts\check_modules.py --launch-uvicorn
|
||||||
@set SD_UI_PATH=%cd%\ui
|
pause
|
||||||
@cd stable-diffusion
|
exit /b
|
||||||
|
|
||||||
@rem
|
|
||||||
@rem Rewrite easy-install.pth. This fixes the installation if the user has relocated the SDUI installation
|
|
||||||
@rem
|
|
||||||
>env\Lib\site-packages\easy-install.pth echo %cd%\src\taming-transformers
|
|
||||||
>>env\Lib\site-packages\easy-install.pth echo %cd%\src\clip
|
|
||||||
>>env\Lib\site-packages\easy-install.pth echo %cd%\src\gfpgan
|
|
||||||
>>env\Lib\site-packages\easy-install.pth echo %cd%\src\realesrgan
|
|
||||||
|
|
||||||
@if NOT DEFINED SD_UI_BIND_PORT set SD_UI_BIND_PORT=9000
|
|
||||||
@if NOT DEFINED SD_UI_BIND_IP set SD_UI_BIND_IP=0.0.0.0
|
|
||||||
@uvicorn server:app --app-dir "%SD_UI_PATH%" --port %SD_UI_BIND_PORT% --host %SD_UI_BIND_IP%
|
|
||||||
|
|
||||||
|
|
||||||
@pause
|
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
source ./scripts/functions.sh
|
cp sd-ui-files/scripts/functions.sh scripts/
|
||||||
|
|
||||||
cp sd-ui-files/scripts/on_env_start.sh scripts/
|
cp sd-ui-files/scripts/on_env_start.sh scripts/
|
||||||
cp sd-ui-files/scripts/bootstrap.sh scripts/
|
cp sd-ui-files/scripts/bootstrap.sh scripts/
|
||||||
|
cp sd-ui-files/scripts/check_modules.py scripts/
|
||||||
|
cp sd-ui-files/scripts/get_config.py scripts/
|
||||||
|
cp sd-ui-files/scripts/config.yaml.sample scripts/
|
||||||
|
|
||||||
|
|
||||||
|
source ./scripts/functions.sh
|
||||||
|
|
||||||
# activate the installer env
|
# activate the installer env
|
||||||
CONDA_BASEPATH=$(conda info --base)
|
CONDA_BASEPATH=$(conda info --base)
|
||||||
@@ -16,318 +21,33 @@ if [ -e "open_dev_console.sh" ]; then
|
|||||||
rm "open_dev_console.sh"
|
rm "open_dev_console.sh"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
python -c "import os; import shutil; frm = 'sd-ui-files/ui/hotfix/9c24e6cd9f499d02c4f21a033736dabd365962dc80fe3aeb57a8f85ea45a20a3.26fead7ea4f0f843f6eb4055dfd25693f1a71f3c6871b184042d4b126244e142'; dst = os.path.join(os.path.expanduser('~'), '.cache', 'huggingface', 'transformers', '9c24e6cd9f499d02c4f21a033736dabd365962dc80fe3aeb57a8f85ea45a20a3.26fead7ea4f0f843f6eb4055dfd25693f1a71f3c6871b184042d4b126244e142'); shutil.copyfile(frm, dst) if os.path.exists(dst) else print(''); print('Hotfixed broken JSON file from OpenAI');"
|
if [ -e "ui/plugins/ui/merge.plugin.js" ]; then
|
||||||
|
rm "ui/plugins/ui/merge.plugin.js"
|
||||||
# Caution, this file will make your eyes and brain bleed. It's such an unholy mess.
|
|
||||||
# Note to self: Please rewrite this in Python. For the sake of your own sanity.
|
|
||||||
|
|
||||||
if [ -e "scripts/install_status.txt" ] && [ `grep -c sd_git_cloned scripts/install_status.txt` -gt "0" ]; then
|
|
||||||
echo "Stable Diffusion's git repository was already installed. Updating.."
|
|
||||||
|
|
||||||
cd stable-diffusion
|
|
||||||
|
|
||||||
git reset --hard
|
|
||||||
git pull
|
|
||||||
git -c advice.detachedHead=false checkout f6cfebffa752ee11a7b07497b8529d5971de916c
|
|
||||||
|
|
||||||
git apply --whitespace=nowarn ../ui/sd_internal/ddim_callback.patch || fail "ddim patch failed"
|
|
||||||
git apply --whitespace=nowarn ../ui/sd_internal/env_yaml.patch || fail "yaml patch failed"
|
|
||||||
|
|
||||||
cd ..
|
|
||||||
else
|
|
||||||
printf "\n\nDownloading Stable Diffusion..\n\n"
|
|
||||||
|
|
||||||
if git clone https://github.com/basujindal/stable-diffusion.git ; then
|
|
||||||
echo sd_git_cloned >> scripts/install_status.txt
|
|
||||||
else
|
|
||||||
fail "git clone of basujindal/stable-diffusion.git failed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd stable-diffusion
|
|
||||||
git -c advice.detachedHead=false checkout f6cfebffa752ee11a7b07497b8529d5971de916c
|
|
||||||
|
|
||||||
git apply --whitespace=nowarn ../ui/sd_internal/ddim_callback.patch || fail "ddim patch failed"
|
|
||||||
git apply --whitespace=nowarn ../ui/sd_internal/env_yaml.patch || fail "yaml patch failed"
|
|
||||||
|
|
||||||
cd ..
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# set the correct installer path (current vs legacy)
|
||||||
|
if [ -e "installer_files/env" ]; then
|
||||||
|
export INSTALL_ENV_DIR="$(pwd)/installer_files/env"
|
||||||
|
fi
|
||||||
|
if [ -e "stable-diffusion/env" ]; then
|
||||||
|
export INSTALL_ENV_DIR="$(pwd)/stable-diffusion/env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# create the stable-diffusion folder, to work with legacy installations
|
||||||
|
if [ ! -e "stable-diffusion" ]; then mkdir stable-diffusion; fi
|
||||||
cd stable-diffusion
|
cd stable-diffusion
|
||||||
|
|
||||||
if [ `grep -c conda_sd_env_created ../scripts/install_status.txt` -gt "0" ]; then
|
# activate the old stable-diffusion env, if it exists
|
||||||
echo "Packages necessary for Stable Diffusion were already installed"
|
if [ -e "env" ]; then
|
||||||
|
|
||||||
conda activate ./env || fail "conda activate failed"
|
conda activate ./env || fail "conda activate failed"
|
||||||
else
|
|
||||||
printf "\n\nDownloading packages necessary for Stable Diffusion..\n"
|
|
||||||
printf "\n\n***** This will take some time (depending on the speed of the Internet connection) and may appear to be stuck, but please be patient ***** ..\n\n"
|
|
||||||
|
|
||||||
# prevent conda from using packages from the user's home directory, to avoid conflicts
|
|
||||||
export PYTHONNOUSERSITE=1
|
|
||||||
export PYTHONPATH="$(pwd):$(pwd)/env/lib/site-packages"
|
|
||||||
|
|
||||||
if conda env create --prefix env --force -f environment.yaml ; then
|
|
||||||
echo "Installed. Testing.."
|
|
||||||
else
|
|
||||||
fail "'conda env create' failed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
conda activate ./env || fail "conda activate failed"
|
|
||||||
|
|
||||||
if conda install -c conda-forge --prefix ./env -y antlr4-python3-runtime=4.8 ; then
|
|
||||||
echo "Installed. Testing.."
|
|
||||||
else
|
|
||||||
fail "Error installing antlr4-python3-runtime"
|
|
||||||
fi
|
|
||||||
|
|
||||||
out_test=`python -c "import torch; import ldm; import transformers; import numpy; import antlr4; print(42)"`
|
|
||||||
if [ "$out_test" != "42" ]; then
|
|
||||||
fail "Dependency test failed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo conda_sd_env_created >> ../scripts/install_status.txt
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ `grep -c conda_sd_gfpgan_deps_installed ../scripts/install_status.txt` -gt "0" ]; then
|
# disable the legacy src and ldm folder (otherwise this prevents installing gfpgan and realesrgan)
|
||||||
echo "Packages necessary for GFPGAN (Face Correction) were already installed"
|
if [ -e "src" ]; then mv src src-old; fi
|
||||||
else
|
if [ -e "ldm" ]; then mv ldm ldm-old; fi
|
||||||
printf "\n\nDownloading packages necessary for GFPGAN (Face Correction)..\n"
|
|
||||||
|
|
||||||
export PYTHONNOUSERSITE=1
|
|
||||||
export PYTHONPATH="$(pwd):$(pwd)/env/lib/site-packages"
|
|
||||||
|
|
||||||
if pip install -e git+https://github.com/TencentARC/GFPGAN#egg=GFPGAN ; then
|
|
||||||
echo "Installed. Testing.."
|
|
||||||
else
|
|
||||||
fail "Error installing the packages necessary for GFPGAN (Face Correction)."
|
|
||||||
fi
|
|
||||||
|
|
||||||
out_test=`python -c "from gfpgan import GFPGANer; print(42)"`
|
|
||||||
if [ "$out_test" != "42" ]; then
|
|
||||||
echo "EE The dependency check has failed. This usually means that some system libraries are missing."
|
|
||||||
echo "EE On Debian/Ubuntu systems, this are often these packages: libsm6 libxext6 libxrender-dev"
|
|
||||||
echo "EE Other Linux distributions might have different package names for these libraries."
|
|
||||||
fail "GFPGAN dependency test failed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo conda_sd_gfpgan_deps_installed >> ../scripts/install_status.txt
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ `grep -c conda_sd_esrgan_deps_installed ../scripts/install_status.txt` -gt "0" ]; then
|
|
||||||
echo "Packages necessary for ESRGAN (Resolution Upscaling) were already installed"
|
|
||||||
else
|
|
||||||
printf "\n\nDownloading packages necessary for ESRGAN (Resolution Upscaling)..\n"
|
|
||||||
|
|
||||||
export PYTHONNOUSERSITE=1
|
|
||||||
export PYTHONPATH="$(pwd):$(pwd)/env/lib/site-packages"
|
|
||||||
|
|
||||||
if pip install -e git+https://github.com/xinntao/Real-ESRGAN#egg=realesrgan ; then
|
|
||||||
echo "Installed. Testing.."
|
|
||||||
else
|
|
||||||
fail "Error installing the packages necessary for ESRGAN"
|
|
||||||
fi
|
|
||||||
|
|
||||||
out_test=`python -c "from basicsr.archs.rrdbnet_arch import RRDBNet; from realesrgan import RealESRGANer; print(42)"`
|
|
||||||
if [ "$out_test" != "42" ]; then
|
|
||||||
fail "ESRGAN dependency test failed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo conda_sd_esrgan_deps_installed >> ../scripts/install_status.txt
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ `grep -c conda_sd_ui_deps_installed ../scripts/install_status.txt` -gt "0" ]; then
|
|
||||||
echo "Packages necessary for Stable Diffusion UI were already installed"
|
|
||||||
else
|
|
||||||
printf "\n\nDownloading packages necessary for Stable Diffusion UI..\n\n"
|
|
||||||
|
|
||||||
export PYTHONNOUSERSITE=1
|
|
||||||
export PYTHONPATH="$(pwd):$(pwd)/env/lib/site-packages"
|
|
||||||
|
|
||||||
if conda install -c conda-forge --prefix ./env -y uvicorn fastapi ; then
|
|
||||||
echo "Installed. Testing.."
|
|
||||||
else
|
|
||||||
fail "'conda install uvicorn' failed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! command -v uvicorn &> /dev/null; then
|
|
||||||
fail "UI packages not found!"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo conda_sd_ui_deps_installed >> ../scripts/install_status.txt
|
|
||||||
fi
|
|
||||||
|
|
||||||
if python -m picklescan --help >/dev/null 2>&1; then
|
|
||||||
echo "Picklescan is already installed."
|
|
||||||
else
|
|
||||||
echo "Picklescan not found, installing."
|
|
||||||
pip install picklescan || fail "Picklescan installation failed."
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
mkdir -p "../models/stable-diffusion"
|
|
||||||
mkdir -p "../models/vae"
|
|
||||||
echo "" > "../models/stable-diffusion/Put your custom ckpt files here.txt"
|
|
||||||
echo "" > "../models/vae/Put your VAE files here.txt"
|
|
||||||
|
|
||||||
if [ -f "sd-v1-4.ckpt" ]; then
|
|
||||||
model_size=`find "sd-v1-4.ckpt" -printf "%s"`
|
|
||||||
|
|
||||||
if [ "$model_size" -eq "4265380512" ] || [ "$model_size" -eq "7703807346" ] || [ "$model_size" -eq "7703810927" ]; then
|
|
||||||
echo "Data files (weights) necessary for Stable Diffusion were already downloaded"
|
|
||||||
else
|
|
||||||
printf "\n\nThe model file present at $PWD/sd-v1-4.ckpt is invalid. It is only $model_size bytes in size. Re-downloading.."
|
|
||||||
rm sd-v1-4.ckpt
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "sd-v1-4.ckpt" ]; then
|
|
||||||
echo "Downloading data files (weights) for Stable Diffusion.."
|
|
||||||
|
|
||||||
curl -L -k https://me.cmdr2.org/stable-diffusion-ui/sd-v1-4.ckpt > sd-v1-4.ckpt
|
|
||||||
|
|
||||||
if [ -f "sd-v1-4.ckpt" ]; then
|
|
||||||
model_size=`find "sd-v1-4.ckpt" -printf "%s"`
|
|
||||||
if [ ! "$model_size" == "4265380512" ]; then
|
|
||||||
fail "The downloaded model file was invalid! Bytes downloaded: $model_size"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
fail "Error downloading the data files (weights) for Stable Diffusion"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
if [ -f "GFPGANv1.3.pth" ]; then
|
|
||||||
model_size=`find "GFPGANv1.3.pth" -printf "%s"`
|
|
||||||
|
|
||||||
if [ "$model_size" -eq "348632874" ]; then
|
|
||||||
echo "Data files (weights) necessary for GFPGAN (Face Correction) were already downloaded"
|
|
||||||
else
|
|
||||||
printf "\n\nThe model file present at $PWD/GFPGANv1.3.pth is invalid. It is only $model_size bytes in size. Re-downloading.."
|
|
||||||
rm GFPGANv1.3.pth
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "GFPGANv1.3.pth" ]; then
|
|
||||||
echo "Downloading data files (weights) for GFPGAN (Face Correction).."
|
|
||||||
|
|
||||||
curl -L -k https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.3.pth > GFPGANv1.3.pth
|
|
||||||
|
|
||||||
if [ -f "GFPGANv1.3.pth" ]; then
|
|
||||||
model_size=`find "GFPGANv1.3.pth" -printf "%s"`
|
|
||||||
if [ ! "$model_size" -eq "348632874" ]; then
|
|
||||||
fail "The downloaded GFPGAN model file was invalid! Bytes downloaded: $model_size"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
fail "Error downloading the data files (weights) for GFPGAN (Face Correction)."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
if [ -f "RealESRGAN_x4plus.pth" ]; then
|
|
||||||
model_size=`find "RealESRGAN_x4plus.pth" -printf "%s"`
|
|
||||||
|
|
||||||
if [ "$model_size" -eq "67040989" ]; then
|
|
||||||
echo "Data files (weights) necessary for ESRGAN (Resolution Upscaling) x4plus were already downloaded"
|
|
||||||
else
|
|
||||||
printf "\n\nThe model file present at $PWD/RealESRGAN_x4plus.pth is invalid. It is only $model_size bytes in size. Re-downloading.."
|
|
||||||
rm RealESRGAN_x4plus.pth
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "RealESRGAN_x4plus.pth" ]; then
|
|
||||||
echo "Downloading data files (weights) for ESRGAN (Resolution Upscaling) x4plus.."
|
|
||||||
|
|
||||||
curl -L -k https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth > RealESRGAN_x4plus.pth
|
|
||||||
|
|
||||||
if [ -f "RealESRGAN_x4plus.pth" ]; then
|
|
||||||
model_size=`find "RealESRGAN_x4plus.pth" -printf "%s"`
|
|
||||||
if [ ! "$model_size" -eq "67040989" ]; then
|
|
||||||
fail "The downloaded ESRGAN x4plus model file was invalid! Bytes downloaded: $model_size"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
fail "Error downloading the data files (weights) for ESRGAN (Resolution Upscaling) x4plus"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
if [ -f "RealESRGAN_x4plus_anime_6B.pth" ]; then
|
|
||||||
model_size=`find "RealESRGAN_x4plus_anime_6B.pth" -printf "%s"`
|
|
||||||
|
|
||||||
if [ "$model_size" -eq "17938799" ]; then
|
|
||||||
echo "Data files (weights) necessary for ESRGAN (Resolution Upscaling) x4plus_anime were already downloaded"
|
|
||||||
else
|
|
||||||
printf "\n\nThe model file present at $PWD/RealESRGAN_x4plus_anime_6B.pth is invalid. It is only $model_size bytes in size. Re-downloading.."
|
|
||||||
rm RealESRGAN_x4plus_anime_6B.pth
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "RealESRGAN_x4plus_anime_6B.pth" ]; then
|
|
||||||
echo "Downloading data files (weights) for ESRGAN (Resolution Upscaling) x4plus_anime.."
|
|
||||||
|
|
||||||
curl -L -k https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth > RealESRGAN_x4plus_anime_6B.pth
|
|
||||||
|
|
||||||
if [ -f "RealESRGAN_x4plus_anime_6B.pth" ]; then
|
|
||||||
model_size=`find "RealESRGAN_x4plus_anime_6B.pth" -printf "%s"`
|
|
||||||
if [ ! "$model_size" -eq "17938799" ]; then
|
|
||||||
fail "The downloaded ESRGAN x4plus_anime model file was invalid! Bytes downloaded: $model_size"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
fail "Error downloading the data files (weights) for ESRGAN (Resolution Upscaling) x4plus_anime."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
if [ -f "../models/vae/vae-ft-mse-840000-ema-pruned.ckpt" ]; then
|
|
||||||
model_size=`find ../models/vae/vae-ft-mse-840000-ema-pruned.ckpt -printf "%s"`
|
|
||||||
|
|
||||||
if [ "$model_size" -eq "334695179" ]; then
|
|
||||||
echo "Data files (weights) necessary for the default VAE (sd-vae-ft-mse-original) were already downloaded"
|
|
||||||
else
|
|
||||||
printf "\n\nThe model file present at models/vae/vae-ft-mse-840000-ema-pruned.ckpt is invalid. It is only $model_size bytes in size. Re-downloading.."
|
|
||||||
rm ../models/vae/vae-ft-mse-840000-ema-pruned.ckpt
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "../models/vae/vae-ft-mse-840000-ema-pruned.ckpt" ]; then
|
|
||||||
echo "Downloading data files (weights) for the default VAE (sd-vae-ft-mse-original).."
|
|
||||||
|
|
||||||
curl -L -k https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.ckpt > ../models/vae/vae-ft-mse-840000-ema-pruned.ckpt
|
|
||||||
|
|
||||||
if [ -f "../models/vae/vae-ft-mse-840000-ema-pruned.ckpt" ]; then
|
|
||||||
model_size=`find ../models/vae/vae-ft-mse-840000-ema-pruned.ckpt -printf "%s"`
|
|
||||||
if [ ! "$model_size" -eq "334695179" ]; then
|
|
||||||
printf "\n\nError: The downloaded default VAE (sd-vae-ft-mse-original) file was invalid! Bytes downloaded: $model_size\n\n"
|
|
||||||
printf "\n\nError downloading the data files (weights) for the default VAE (sd-vae-ft-mse-original). Sorry about that, please try to:\n 1. Run this installer again.\n 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting\n 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB\n 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues\nThanks!\n\n"
|
|
||||||
read -p "Press any key to continue"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
printf "\n\nError downloading the data files (weights) for the default VAE (sd-vae-ft-mse-original). Sorry about that, please try to:\n 1. Run this installer again.\n 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting\n 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB\n 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues\nThanks!\n\n"
|
|
||||||
read -p "Press any key to continue"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
if [ `grep -c sd_install_complete ../scripts/install_status.txt` -gt "0" ]; then
|
|
||||||
echo sd_weights_downloaded >> ../scripts/install_status.txt
|
|
||||||
echo sd_install_complete >> ../scripts/install_status.txt
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf "\n\nStable Diffusion is ready!\n\n"
|
|
||||||
|
|
||||||
SD_PATH=`pwd`
|
|
||||||
export PYTHONPATH="$SD_PATH:$SD_PATH/env/lib/python3.8/site-packages"
|
|
||||||
echo "PYTHONPATH=$PYTHONPATH"
|
|
||||||
|
|
||||||
which python
|
|
||||||
python --version
|
|
||||||
|
|
||||||
cd ..
|
cd ..
|
||||||
export SD_UI_PATH=`pwd`/ui
|
# Download the required packages
|
||||||
cd stable-diffusion
|
python scripts/check_modules.py --launch-uvicorn
|
||||||
|
|
||||||
uvicorn server:app --app-dir "$SD_UI_PATH" --port ${SD_UI_BIND_PORT:-9000} --host ${SD_UI_BIND_IP:-0.0.0.0}
|
|
||||||
|
|
||||||
read -p "Press any key to continue"
|
read -p "Press any key to continue"
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
@call conda --version
|
|
||||||
@call git --version
|
|
||||||
|
|
||||||
cd %CONDA_PREFIX%\..\scripts
|
|
||||||
|
|
||||||
on_env_start.bat
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
conda-unpack
|
|
||||||
|
|
||||||
source $CONDA_PREFIX/etc/profile.d/conda.sh
|
|
||||||
|
|
||||||
conda --version
|
|
||||||
git --version
|
|
||||||
|
|
||||||
cd $CONDA_PREFIX/../scripts
|
|
||||||
|
|
||||||
./on_env_start.sh
|
|
||||||
@@ -2,6 +2,25 @@
|
|||||||
|
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
|
||||||
|
if [ -f "on_sd_start.bat" ]; then
|
||||||
|
echo ================================================================================
|
||||||
|
echo
|
||||||
|
echo !!!! WARNING !!!!
|
||||||
|
echo
|
||||||
|
echo It looks like you\'re trying to run the installation script from a source code
|
||||||
|
echo download. This will not work.
|
||||||
|
echo
|
||||||
|
echo Recommended: Please close this window and download the installer from
|
||||||
|
echo https://easydiffusion.github.io/docs/installation/
|
||||||
|
echo
|
||||||
|
echo ================================================================================
|
||||||
|
echo
|
||||||
|
read
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
unset PYTHONHOME
|
||||||
|
|
||||||
# set legacy installer's PATH, if it exists
|
# set legacy installer's PATH, if it exists
|
||||||
if [ -e "installer" ]; then export PATH="$(pwd)/installer/bin:$PATH"; fi
|
if [ -e "installer" ]; then export PATH="$(pwd)/installer/bin:$PATH"; fi
|
||||||
|
|
||||||
@@ -19,4 +38,5 @@ which conda
|
|||||||
conda --version || exit 1
|
conda --version || exit 1
|
||||||
|
|
||||||
# Download the rest of the installer and UI
|
# Download the rest of the installer and UI
|
||||||
|
chmod +x scripts/*.sh
|
||||||
scripts/on_env_start.sh
|
scripts/on_env_start.sh
|
||||||
|
|||||||
0
ui/easydiffusion/__init__.py
Normal file
459
ui/easydiffusion/app.py
Normal file
@@ -0,0 +1,459 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
import copy
|
||||||
|
from ruamel.yaml import YAML
|
||||||
|
|
||||||
|
import urllib
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from easydiffusion import task_manager
|
||||||
|
from easydiffusion.utils import log
|
||||||
|
from rich.logging import RichHandler
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.panel import Panel
|
||||||
|
from sdkit.utils import log as sdkit_log # hack, so we can overwrite the log config
|
||||||
|
|
||||||
|
# Remove all handlers associated with the root logger object.
|
||||||
|
for handler in logging.root.handlers[:]:
|
||||||
|
logging.root.removeHandler(handler)
|
||||||
|
|
||||||
|
LOG_FORMAT = "%(asctime)s.%(msecs)03d %(levelname)s %(threadName)s %(message)s"
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format=LOG_FORMAT,
|
||||||
|
datefmt="%X",
|
||||||
|
handlers=[RichHandler(markup=True, rich_tracebacks=False, show_time=False, show_level=False)],
|
||||||
|
)
|
||||||
|
|
||||||
|
SD_DIR = os.getcwd()
|
||||||
|
|
||||||
|
ROOT_DIR = os.path.abspath(os.path.join(SD_DIR, ".."))
|
||||||
|
|
||||||
|
SD_UI_DIR = os.getenv("SD_UI_PATH", None)
|
||||||
|
|
||||||
|
CONFIG_DIR = os.path.abspath(os.path.join(SD_UI_DIR, "..", "scripts"))
|
||||||
|
BUCKET_DIR = os.path.abspath(os.path.join(SD_DIR, "..", "bucket"))
|
||||||
|
|
||||||
|
USER_PLUGINS_DIR = os.path.abspath(os.path.join(SD_DIR, "..", "plugins"))
|
||||||
|
CORE_PLUGINS_DIR = os.path.abspath(os.path.join(SD_UI_DIR, "plugins"))
|
||||||
|
|
||||||
|
USER_UI_PLUGINS_DIR = os.path.join(USER_PLUGINS_DIR, "ui")
|
||||||
|
CORE_UI_PLUGINS_DIR = os.path.join(CORE_PLUGINS_DIR, "ui")
|
||||||
|
USER_SERVER_PLUGINS_DIR = os.path.join(USER_PLUGINS_DIR, "server")
|
||||||
|
UI_PLUGINS_SOURCES = ((CORE_UI_PLUGINS_DIR, "core"), (USER_UI_PLUGINS_DIR, "user"))
|
||||||
|
|
||||||
|
sys.path.append(os.path.dirname(SD_UI_DIR))
|
||||||
|
sys.path.append(USER_SERVER_PLUGINS_DIR)
|
||||||
|
|
||||||
|
OUTPUT_DIRNAME = "Stable Diffusion UI" # in the user's home folder
|
||||||
|
PRESERVE_CONFIG_VARS = ["FORCE_FULL_PRECISION"]
|
||||||
|
TASK_TTL = 15 * 60 # Discard last session's task timeout
|
||||||
|
APP_CONFIG_DEFAULTS = {
|
||||||
|
# auto: selects the cuda device with the most free memory, cuda: use the currently active cuda device.
|
||||||
|
"render_devices": "auto", # valid entries: 'auto', 'cpu' or 'cuda:N' (where N is a GPU index)
|
||||||
|
"update_branch": "main",
|
||||||
|
"ui": {
|
||||||
|
"open_browser_on_start": True,
|
||||||
|
},
|
||||||
|
"use_v3_engine": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
IMAGE_EXTENSIONS = [
|
||||||
|
".png",
|
||||||
|
".apng",
|
||||||
|
".jpg",
|
||||||
|
".jpeg",
|
||||||
|
".jfif",
|
||||||
|
".pjpeg",
|
||||||
|
".pjp",
|
||||||
|
".jxl",
|
||||||
|
".gif",
|
||||||
|
".webp",
|
||||||
|
".avif",
|
||||||
|
".svg",
|
||||||
|
]
|
||||||
|
CUSTOM_MODIFIERS_DIR = os.path.abspath(os.path.join(SD_DIR, "..", "modifiers"))
|
||||||
|
CUSTOM_MODIFIERS_PORTRAIT_EXTENSIONS = [
|
||||||
|
".portrait",
|
||||||
|
"_portrait",
|
||||||
|
" portrait",
|
||||||
|
"-portrait",
|
||||||
|
]
|
||||||
|
CUSTOM_MODIFIERS_LANDSCAPE_EXTENSIONS = [
|
||||||
|
".landscape",
|
||||||
|
"_landscape",
|
||||||
|
" landscape",
|
||||||
|
"-landscape",
|
||||||
|
]
|
||||||
|
|
||||||
|
MODELS_DIR = os.path.abspath(os.path.join(SD_DIR, "..", "models"))
|
||||||
|
|
||||||
|
|
||||||
|
def init():
|
||||||
|
global MODELS_DIR
|
||||||
|
|
||||||
|
os.makedirs(USER_UI_PLUGINS_DIR, exist_ok=True)
|
||||||
|
os.makedirs(USER_SERVER_PLUGINS_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# https://pytorch.org/docs/stable/storage.html
|
||||||
|
warnings.filterwarnings("ignore", category=UserWarning, message="TypedStorage is deprecated")
|
||||||
|
|
||||||
|
config = getConfig()
|
||||||
|
config_models_dir = config.get("models_dir", None)
|
||||||
|
if (config_models_dir is not None and config_models_dir != ""):
|
||||||
|
MODELS_DIR = config_models_dir
|
||||||
|
|
||||||
|
|
||||||
|
def init_render_threads():
|
||||||
|
load_server_plugins()
|
||||||
|
|
||||||
|
update_render_threads()
|
||||||
|
|
||||||
|
|
||||||
|
def getConfig(default_val=APP_CONFIG_DEFAULTS):
|
||||||
|
config_yaml_path = os.path.join(CONFIG_DIR, "..", "config.yaml")
|
||||||
|
|
||||||
|
# migrate the old config yaml location
|
||||||
|
config_legacy_yaml = os.path.join(CONFIG_DIR, "config.yaml")
|
||||||
|
if os.path.isfile(config_legacy_yaml):
|
||||||
|
shutil.move(config_legacy_yaml, config_yaml_path)
|
||||||
|
|
||||||
|
def set_config_on_startup(config: dict):
|
||||||
|
if getConfig.__use_v3_engine_on_startup is None:
|
||||||
|
getConfig.__use_v3_engine_on_startup = config.get("use_v3_engine", True)
|
||||||
|
config["config_on_startup"] = {"use_v3_engine": getConfig.__use_v3_engine_on_startup}
|
||||||
|
|
||||||
|
if os.path.isfile(config_yaml_path):
|
||||||
|
try:
|
||||||
|
yaml = YAML()
|
||||||
|
with open(config_yaml_path, "r", encoding="utf-8") as f:
|
||||||
|
config = yaml.load(f)
|
||||||
|
if "net" not in config:
|
||||||
|
config["net"] = {}
|
||||||
|
if os.getenv("SD_UI_BIND_PORT") is not None:
|
||||||
|
config["net"]["listen_port"] = int(os.getenv("SD_UI_BIND_PORT"))
|
||||||
|
else:
|
||||||
|
config["net"]["listen_port"] = 9000
|
||||||
|
if os.getenv("SD_UI_BIND_IP") is not None:
|
||||||
|
config["net"]["listen_to_network"] = os.getenv("SD_UI_BIND_IP") == "0.0.0.0"
|
||||||
|
else:
|
||||||
|
config["net"]["listen_to_network"] = True
|
||||||
|
|
||||||
|
set_config_on_startup(config)
|
||||||
|
|
||||||
|
return config
|
||||||
|
except Exception as e:
|
||||||
|
log.warn(traceback.format_exc())
|
||||||
|
set_config_on_startup(default_val)
|
||||||
|
return default_val
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
config_json_path = os.path.join(CONFIG_DIR, "config.json")
|
||||||
|
if not os.path.exists(config_json_path):
|
||||||
|
return default_val
|
||||||
|
|
||||||
|
log.info("Converting old json config file to yaml")
|
||||||
|
with open(config_json_path, "r", encoding="utf-8") as f:
|
||||||
|
config = json.load(f)
|
||||||
|
# Save config in new format
|
||||||
|
setConfig(config)
|
||||||
|
|
||||||
|
with open(config_json_path + ".txt", "w") as f:
|
||||||
|
f.write("Moved to config.yaml inside the Easy Diffusion folder. You can open it in any text editor.")
|
||||||
|
os.remove(config_json_path)
|
||||||
|
|
||||||
|
return getConfig(default_val)
|
||||||
|
except Exception as e:
|
||||||
|
log.warn(traceback.format_exc())
|
||||||
|
set_config_on_startup(default_val)
|
||||||
|
return default_val
|
||||||
|
|
||||||
|
|
||||||
|
getConfig.__use_v3_engine_on_startup = None
|
||||||
|
|
||||||
|
|
||||||
|
def setConfig(config):
|
||||||
|
global MODELS_DIR
|
||||||
|
|
||||||
|
try: # config.yaml
|
||||||
|
config_yaml_path = os.path.join(CONFIG_DIR, "..", "config.yaml")
|
||||||
|
config_yaml_path = os.path.abspath(config_yaml_path)
|
||||||
|
yaml = YAML()
|
||||||
|
|
||||||
|
if not hasattr(config, "_yaml_comment"):
|
||||||
|
config_yaml_sample_path = os.path.join(CONFIG_DIR, "config.yaml.sample")
|
||||||
|
|
||||||
|
if os.path.exists(config_yaml_sample_path):
|
||||||
|
with open(config_yaml_sample_path, "r", encoding="utf-8") as f:
|
||||||
|
commented_config = yaml.load(f)
|
||||||
|
|
||||||
|
for k in config:
|
||||||
|
commented_config[k] = config[k]
|
||||||
|
|
||||||
|
config = commented_config
|
||||||
|
yaml.indent(mapping=2, sequence=4, offset=2)
|
||||||
|
|
||||||
|
if "config_on_startup" in config:
|
||||||
|
del config["config_on_startup"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
f = open(config_yaml_path + ".tmp", "w", encoding="utf-8")
|
||||||
|
yaml.dump(config, f)
|
||||||
|
finally:
|
||||||
|
f.close() # do this explicitly to avoid NUL bytes (possible rare bug when using 'with')
|
||||||
|
|
||||||
|
# verify that the new file is valid, and only then overwrite the old config file
|
||||||
|
# helps prevent the rare NUL bytes error from corrupting the config file
|
||||||
|
yaml = YAML()
|
||||||
|
with open(config_yaml_path + ".tmp", "r", encoding="utf-8") as f:
|
||||||
|
yaml.load(f)
|
||||||
|
shutil.move(config_yaml_path + ".tmp", config_yaml_path)
|
||||||
|
except:
|
||||||
|
log.error(traceback.format_exc())
|
||||||
|
|
||||||
|
if config.get("models_dir"):
|
||||||
|
MODELS_DIR = config["models_dir"]
|
||||||
|
|
||||||
|
|
||||||
|
def save_to_config(ckpt_model_name, vae_model_name, hypernetwork_model_name, vram_usage_level):
|
||||||
|
config = getConfig()
|
||||||
|
if "model" not in config:
|
||||||
|
config["model"] = {}
|
||||||
|
|
||||||
|
config["model"]["stable-diffusion"] = ckpt_model_name
|
||||||
|
config["model"]["vae"] = vae_model_name
|
||||||
|
config["model"]["hypernetwork"] = hypernetwork_model_name
|
||||||
|
|
||||||
|
if vae_model_name is None or vae_model_name == "":
|
||||||
|
del config["model"]["vae"]
|
||||||
|
if hypernetwork_model_name is None or hypernetwork_model_name == "":
|
||||||
|
del config["model"]["hypernetwork"]
|
||||||
|
|
||||||
|
config["vram_usage_level"] = vram_usage_level
|
||||||
|
|
||||||
|
setConfig(config)
|
||||||
|
|
||||||
|
|
||||||
|
def update_render_threads():
|
||||||
|
config = getConfig()
|
||||||
|
render_devices = config.get("render_devices", "auto")
|
||||||
|
active_devices = task_manager.get_devices()["active"].keys()
|
||||||
|
|
||||||
|
log.debug(f"requesting for render_devices: {render_devices}")
|
||||||
|
task_manager.update_render_threads(render_devices, active_devices)
|
||||||
|
|
||||||
|
|
||||||
|
def getUIPlugins():
|
||||||
|
plugins = []
|
||||||
|
|
||||||
|
file_names = set()
|
||||||
|
for plugins_dir, dir_prefix in UI_PLUGINS_SOURCES:
|
||||||
|
for file in os.listdir(plugins_dir):
|
||||||
|
if file.endswith(".plugin.js") and file not in file_names:
|
||||||
|
plugins.append(f"/plugins/{dir_prefix}/{file}")
|
||||||
|
file_names.add(file)
|
||||||
|
|
||||||
|
return plugins
|
||||||
|
|
||||||
|
|
||||||
|
def load_server_plugins():
|
||||||
|
if not os.path.exists(USER_SERVER_PLUGINS_DIR):
|
||||||
|
return
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
def load_plugin(file):
|
||||||
|
mod_path = file.replace(".py", "")
|
||||||
|
return importlib.import_module(mod_path)
|
||||||
|
|
||||||
|
def apply_plugin(file, plugin):
|
||||||
|
if hasattr(plugin, "get_cond_and_uncond"):
|
||||||
|
import sdkit.generate.image_generator
|
||||||
|
|
||||||
|
sdkit.generate.image_generator.get_cond_and_uncond = plugin.get_cond_and_uncond
|
||||||
|
log.info(f"Overridden get_cond_and_uncond with the one in the server plugin: {file}")
|
||||||
|
|
||||||
|
for file in os.listdir(USER_SERVER_PLUGINS_DIR):
|
||||||
|
file_path = os.path.join(USER_SERVER_PLUGINS_DIR, file)
|
||||||
|
if (not os.path.isdir(file_path) and not file_path.endswith("_plugin.py")) or (
|
||||||
|
os.path.isdir(file_path) and not file_path.endswith("_plugin")
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
log.info(f"Loading server plugin: {file}")
|
||||||
|
mod = load_plugin(file)
|
||||||
|
|
||||||
|
log.info(f"Applying server plugin: {file}")
|
||||||
|
apply_plugin(file, mod)
|
||||||
|
except:
|
||||||
|
log.warn(f"Error while loading a server plugin")
|
||||||
|
log.warn(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
def getIPConfig():
|
||||||
|
try:
|
||||||
|
ips = socket.gethostbyname_ex(socket.gethostname())
|
||||||
|
ips[2].append(ips[0])
|
||||||
|
return ips[2]
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def open_browser():
|
||||||
|
config = getConfig()
|
||||||
|
ui = config.get("ui", {})
|
||||||
|
net = config.get("net", {})
|
||||||
|
port = net.get("listen_port", 9000)
|
||||||
|
|
||||||
|
if ui.get("open_browser_on_start", True):
|
||||||
|
import webbrowser
|
||||||
|
|
||||||
|
log.info("Opening browser..")
|
||||||
|
|
||||||
|
webbrowser.open(f"http://localhost:{port}")
|
||||||
|
|
||||||
|
Console().print(
|
||||||
|
Panel(
|
||||||
|
"\n"
|
||||||
|
+ "[white]Easy Diffusion is ready to serve requests.\n\n"
|
||||||
|
+ "A new browser tab should have been opened by now.\n"
|
||||||
|
+ f"If not, please open your web browser and navigate to [bold yellow underline]http://localhost:{port}/\n",
|
||||||
|
title="Easy Diffusion is ready",
|
||||||
|
style="bold yellow on blue",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def fail_and_die(fail_type: str, data: str):
|
||||||
|
suggestions = [
|
||||||
|
"Run this installer again.",
|
||||||
|
"If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB",
|
||||||
|
"If that doesn't solve the problem, please file an issue at https://github.com/easydiffusion/easydiffusion/issues",
|
||||||
|
]
|
||||||
|
|
||||||
|
if fail_type == "model_download":
|
||||||
|
fail_label = f"Error downloading the {data} model"
|
||||||
|
suggestions.insert(
|
||||||
|
1,
|
||||||
|
"If that doesn't fix it, please try to download the file manually. The address to download from, and the destination to save to are printed above this message.",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
fail_label = "Error while installing Easy Diffusion"
|
||||||
|
|
||||||
|
msg = [f"{fail_label}. Sorry about that, please try to:"]
|
||||||
|
for i, suggestion in enumerate(suggestions):
|
||||||
|
msg.append(f"{i+1}. {suggestion}")
|
||||||
|
msg.append("Thanks!")
|
||||||
|
|
||||||
|
print("\n".join(msg))
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def get_image_modifiers():
|
||||||
|
modifiers_json_path = os.path.join(SD_UI_DIR, "modifiers.json")
|
||||||
|
|
||||||
|
modifier_categories = {}
|
||||||
|
original_category_order = []
|
||||||
|
with open(modifiers_json_path, "r", encoding="utf-8") as f:
|
||||||
|
modifiers_file = json.load(f)
|
||||||
|
|
||||||
|
# The trailing slash is needed to support symlinks
|
||||||
|
if not os.path.isdir(f"{CUSTOM_MODIFIERS_DIR}/"):
|
||||||
|
return modifiers_file
|
||||||
|
|
||||||
|
# convert modifiers from a list of objects to a dict of dicts
|
||||||
|
for category_item in modifiers_file:
|
||||||
|
category_name = category_item["category"]
|
||||||
|
original_category_order.append(category_name)
|
||||||
|
category = {}
|
||||||
|
for modifier_item in category_item["modifiers"]:
|
||||||
|
modifier = {}
|
||||||
|
for preview_item in modifier_item["previews"]:
|
||||||
|
modifier[preview_item["name"]] = preview_item["path"]
|
||||||
|
category[modifier_item["modifier"]] = modifier
|
||||||
|
modifier_categories[category_name] = category
|
||||||
|
|
||||||
|
def scan_directory(directory_path: str, category_name="Modifiers"):
|
||||||
|
for entry in os.scandir(directory_path):
|
||||||
|
if entry.is_file():
|
||||||
|
file_extension = list(filter(lambda e: entry.name.endswith(e), IMAGE_EXTENSIONS))
|
||||||
|
if len(file_extension) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
modifier_name = entry.name[: -len(file_extension[0])]
|
||||||
|
modifier_path = f"custom/{entry.path[len(CUSTOM_MODIFIERS_DIR) + 1:]}"
|
||||||
|
# URL encode path segments
|
||||||
|
modifier_path = "/".join(
|
||||||
|
map(
|
||||||
|
lambda segment: urllib.parse.quote(segment),
|
||||||
|
modifier_path.split("/"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
is_portrait = True
|
||||||
|
is_landscape = True
|
||||||
|
|
||||||
|
portrait_extension = list(
|
||||||
|
filter(
|
||||||
|
lambda e: modifier_name.lower().endswith(e),
|
||||||
|
CUSTOM_MODIFIERS_PORTRAIT_EXTENSIONS,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
landscape_extension = list(
|
||||||
|
filter(
|
||||||
|
lambda e: modifier_name.lower().endswith(e),
|
||||||
|
CUSTOM_MODIFIERS_LANDSCAPE_EXTENSIONS,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(portrait_extension) > 0:
|
||||||
|
is_landscape = False
|
||||||
|
modifier_name = modifier_name[: -len(portrait_extension[0])]
|
||||||
|
elif len(landscape_extension) > 0:
|
||||||
|
is_portrait = False
|
||||||
|
modifier_name = modifier_name[: -len(landscape_extension[0])]
|
||||||
|
|
||||||
|
if category_name not in modifier_categories:
|
||||||
|
modifier_categories[category_name] = {}
|
||||||
|
|
||||||
|
category = modifier_categories[category_name]
|
||||||
|
|
||||||
|
if modifier_name not in category:
|
||||||
|
category[modifier_name] = {}
|
||||||
|
|
||||||
|
if is_portrait or "portrait" not in category[modifier_name]:
|
||||||
|
category[modifier_name]["portrait"] = modifier_path
|
||||||
|
|
||||||
|
if is_landscape or "landscape" not in category[modifier_name]:
|
||||||
|
category[modifier_name]["landscape"] = modifier_path
|
||||||
|
elif entry.is_dir():
|
||||||
|
scan_directory(
|
||||||
|
entry.path,
|
||||||
|
entry.name if directory_path == CUSTOM_MODIFIERS_DIR else f"{category_name}/{entry.name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
scan_directory(CUSTOM_MODIFIERS_DIR)
|
||||||
|
|
||||||
|
custom_categories = sorted(
|
||||||
|
[cn for cn in modifier_categories.keys() if cn not in original_category_order],
|
||||||
|
key=str.casefold,
|
||||||
|
)
|
||||||
|
|
||||||
|
# convert the modifiers back into a list of objects
|
||||||
|
modifier_categories_list = []
|
||||||
|
for category_name in [*original_category_order, *custom_categories]:
|
||||||
|
category = {"category": category_name, "modifiers": []}
|
||||||
|
for modifier_name in sorted(modifier_categories[category_name].keys(), key=str.casefold):
|
||||||
|
modifier = {"modifier": modifier_name, "previews": []}
|
||||||
|
for preview_name, preview_path in modifier_categories[category_name][modifier_name].items():
|
||||||
|
modifier["previews"].append({"name": preview_name, "path": preview_path})
|
||||||
|
category["modifiers"].append(modifier)
|
||||||
|
modifier_categories_list.append(category)
|
||||||
|
|
||||||
|
return modifier_categories_list
|
||||||
107
ui/easydiffusion/bucket_manager.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
|
from fastapi import Depends, FastAPI, HTTPException, Response, File
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from easydiffusion.easydb import crud, models, schemas
|
||||||
|
from easydiffusion.easydb.database import SessionLocal, engine
|
||||||
|
|
||||||
|
from requests.compat import urlparse
|
||||||
|
|
||||||
|
import base64, json
|
||||||
|
|
||||||
|
MIME_TYPES = {
|
||||||
|
"jpg": "image/jpeg",
|
||||||
|
"jpeg": "image/jpeg",
|
||||||
|
"gif": "image/gif",
|
||||||
|
"png": "image/png",
|
||||||
|
"webp": "image/webp",
|
||||||
|
"js": "text/javascript",
|
||||||
|
"htm": "text/html",
|
||||||
|
"html": "text/html",
|
||||||
|
"css": "text/css",
|
||||||
|
"json": "application/json",
|
||||||
|
"mjs": "application/json",
|
||||||
|
"yaml": "application/yaml",
|
||||||
|
"svg": "image/svg+xml",
|
||||||
|
"txt": "text/plain",
|
||||||
|
}
|
||||||
|
|
||||||
|
def init():
|
||||||
|
from easydiffusion.server import server_api
|
||||||
|
|
||||||
|
models.BucketBase.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
|
||||||
|
# Dependency
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
@server_api.get("/bucket/{obj_path:path}")
|
||||||
|
def bucket_get_object(obj_path: str, db: Session = Depends(get_db)):
|
||||||
|
filename = get_filename_from_url(obj_path)
|
||||||
|
path = get_path_from_url(obj_path)
|
||||||
|
|
||||||
|
if filename==None:
|
||||||
|
bucket = crud.get_bucket_by_path(db, path=path)
|
||||||
|
if bucket == None:
|
||||||
|
raise HTTPException(status_code=404, detail="Bucket not found")
|
||||||
|
bucketfiles = db.query(models.BucketFile).with_entities(models.BucketFile.filename).filter(models.BucketFile.bucket_id == bucket.id).all()
|
||||||
|
bucketfiles = [ x.filename for x in bucketfiles ]
|
||||||
|
return bucketfiles
|
||||||
|
|
||||||
|
else:
|
||||||
|
bucket = crud.get_bucket_by_path(db, path)
|
||||||
|
if bucket == None:
|
||||||
|
raise HTTPException(status_code=404, detail="Bucket not found")
|
||||||
|
bucket_id = bucket.id
|
||||||
|
bucketfile = db.query(models.BucketFile).filter(models.BucketFile.bucket_id == bucket_id, models.BucketFile.filename == filename).first()
|
||||||
|
if bucketfile == None:
|
||||||
|
raise HTTPException(status_code=404, detail="File not found")
|
||||||
|
|
||||||
|
suffix = get_suffix_from_filename(filename)
|
||||||
|
|
||||||
|
return Response(content=bucketfile.data, media_type=MIME_TYPES.get(suffix, "application/octet-stream"))
|
||||||
|
|
||||||
|
@server_api.post("/bucket/{obj_path:path}")
|
||||||
|
def bucket_post_object(obj_path: str, file: bytes = File(), db: Session = Depends(get_db)):
|
||||||
|
filename = get_filename_from_url(obj_path)
|
||||||
|
path = get_path_from_url(obj_path)
|
||||||
|
bucket = crud.get_bucket_by_path(db, path)
|
||||||
|
|
||||||
|
if bucket == None:
|
||||||
|
bucket = crud.create_bucket(db=db, bucket=schemas.BucketCreate(path=path))
|
||||||
|
bucket_id = bucket.id
|
||||||
|
|
||||||
|
bucketfile = schemas.BucketFileCreate(filename=filename, data=file)
|
||||||
|
result = crud.create_bucketfile(db=db, bucketfile=bucketfile, bucket_id=bucket_id)
|
||||||
|
result.data = base64.encodestring(result.data)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@server_api.post("/buckets/{bucket_id}/items/", response_model=schemas.BucketFile)
|
||||||
|
def create_bucketfile_in_bucket(
|
||||||
|
bucket_id: int, bucketfile: schemas.BucketFileCreate, db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
bucketfile.data = base64.decodestring(bucketfile.data)
|
||||||
|
result = crud.create_bucketfile(db=db, bucketfile=bucketfile, bucket_id=bucket_id)
|
||||||
|
result.data = base64.encodestring(result.data)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_filename_from_url(url):
|
||||||
|
path = urlparse(url).path
|
||||||
|
name = path[path.rfind('/')+1:]
|
||||||
|
return name or None
|
||||||
|
|
||||||
|
def get_path_from_url(url):
|
||||||
|
path = urlparse(url).path
|
||||||
|
path = path[0:path.rfind('/')]
|
||||||
|
return path or None
|
||||||
|
|
||||||
|
def get_suffix_from_filename(filename):
|
||||||
|
return filename[filename.rfind('.')+1:]
|
||||||
257
ui/easydiffusion/device_manager.py
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import re
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from easydiffusion.utils import log
|
||||||
|
|
||||||
|
"""
|
||||||
|
Set `FORCE_FULL_PRECISION` in the environment variables, or in `config.bat`/`config.sh` to set full precision (i.e. float32).
|
||||||
|
Otherwise the models will load at half-precision (i.e. float16).
|
||||||
|
|
||||||
|
Half-precision is fine most of the time. Full precision is only needed for working around GPU bugs (like NVIDIA 16xx GPUs).
|
||||||
|
"""
|
||||||
|
|
||||||
|
COMPARABLE_GPU_PERCENTILE = (
|
||||||
|
0.65 # if a GPU's free_mem is within this % of the GPU with the most free_mem, it will be picked
|
||||||
|
)
|
||||||
|
|
||||||
|
mem_free_threshold = 0
|
||||||
|
|
||||||
|
|
||||||
|
def get_device_delta(render_devices, active_devices):
|
||||||
|
"""
|
||||||
|
render_devices: 'cpu', or 'auto', or 'mps' or ['cuda:N'...]
|
||||||
|
active_devices: ['cpu', 'mps', 'cuda:N'...]
|
||||||
|
"""
|
||||||
|
|
||||||
|
if render_devices in ("cpu", "auto", "mps"):
|
||||||
|
render_devices = [render_devices]
|
||||||
|
elif render_devices is not None:
|
||||||
|
if isinstance(render_devices, str):
|
||||||
|
render_devices = [render_devices]
|
||||||
|
if isinstance(render_devices, list) and len(render_devices) > 0:
|
||||||
|
render_devices = list(filter(lambda x: x.startswith("cuda:") or x == "mps", render_devices))
|
||||||
|
if len(render_devices) == 0:
|
||||||
|
raise Exception(
|
||||||
|
'Invalid render_devices value in config.json. Valid: {"render_devices": ["cuda:0", "cuda:1"...]}, or {"render_devices": "cpu"} or {"render_devices": "mps"} or {"render_devices": "auto"}'
|
||||||
|
)
|
||||||
|
|
||||||
|
render_devices = list(filter(lambda x: is_device_compatible(x), render_devices))
|
||||||
|
if len(render_devices) == 0:
|
||||||
|
raise Exception(
|
||||||
|
"Sorry, none of the render_devices configured in config.json are compatible with Stable Diffusion"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise Exception(
|
||||||
|
'Invalid render_devices value in config.json. Valid: {"render_devices": ["cuda:0", "cuda:1"...]}, or {"render_devices": "cpu"} or {"render_devices": "auto"}'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
render_devices = ["auto"]
|
||||||
|
|
||||||
|
if "auto" in render_devices:
|
||||||
|
render_devices = auto_pick_devices(active_devices)
|
||||||
|
if "cpu" in render_devices:
|
||||||
|
log.warn("WARNING: Could not find a compatible GPU. Using the CPU, but this will be very slow!")
|
||||||
|
|
||||||
|
active_devices = set(active_devices)
|
||||||
|
render_devices = set(render_devices)
|
||||||
|
|
||||||
|
devices_to_start = render_devices - active_devices
|
||||||
|
devices_to_stop = active_devices - render_devices
|
||||||
|
|
||||||
|
return devices_to_start, devices_to_stop
|
||||||
|
|
||||||
|
|
||||||
|
def is_mps_available():
|
||||||
|
return (
|
||||||
|
platform.system() == "Darwin"
|
||||||
|
and hasattr(torch.backends, "mps")
|
||||||
|
and torch.backends.mps.is_available()
|
||||||
|
and torch.backends.mps.is_built()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_cuda_available():
|
||||||
|
return torch.cuda.is_available()
|
||||||
|
|
||||||
|
|
||||||
|
def auto_pick_devices(currently_active_devices):
|
||||||
|
global mem_free_threshold
|
||||||
|
|
||||||
|
if is_mps_available():
|
||||||
|
return ["mps"]
|
||||||
|
|
||||||
|
if not is_cuda_available():
|
||||||
|
return ["cpu"]
|
||||||
|
|
||||||
|
device_count = torch.cuda.device_count()
|
||||||
|
if device_count == 1:
|
||||||
|
return ["cuda:0"] if is_device_compatible("cuda:0") else ["cpu"]
|
||||||
|
|
||||||
|
log.debug("Autoselecting GPU. Using most free memory.")
|
||||||
|
devices = []
|
||||||
|
for device in range(device_count):
|
||||||
|
device = f"cuda:{device}"
|
||||||
|
if not is_device_compatible(device):
|
||||||
|
continue
|
||||||
|
|
||||||
|
mem_free, mem_total = torch.cuda.mem_get_info(device)
|
||||||
|
mem_free /= float(10**9)
|
||||||
|
mem_total /= float(10**9)
|
||||||
|
device_name = torch.cuda.get_device_name(device)
|
||||||
|
log.debug(
|
||||||
|
f"{device} detected: {device_name} - Memory (free/total): {round(mem_free, 2)}Gb / {round(mem_total, 2)}Gb"
|
||||||
|
)
|
||||||
|
devices.append({"device": device, "device_name": device_name, "mem_free": mem_free})
|
||||||
|
|
||||||
|
devices.sort(key=lambda x: x["mem_free"], reverse=True)
|
||||||
|
max_mem_free = devices[0]["mem_free"]
|
||||||
|
curr_mem_free_threshold = COMPARABLE_GPU_PERCENTILE * max_mem_free
|
||||||
|
mem_free_threshold = max(curr_mem_free_threshold, mem_free_threshold)
|
||||||
|
|
||||||
|
# Auto-pick algorithm:
|
||||||
|
# 1. Pick the top 75 percentile of the GPUs, sorted by free_mem.
|
||||||
|
# 2. Also include already-running devices (GPU-only), otherwise their free_mem will
|
||||||
|
# always be very low (since their VRAM contains the model).
|
||||||
|
# These already-running devices probably aren't terrible, since they were picked in the past.
|
||||||
|
# Worst case, the user can restart the program and that'll get rid of them.
|
||||||
|
devices = list(
|
||||||
|
filter(
|
||||||
|
(lambda x: x["mem_free"] > mem_free_threshold or x["device"] in currently_active_devices),
|
||||||
|
devices,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
devices = list(map(lambda x: x["device"], devices))
|
||||||
|
return devices
|
||||||
|
|
||||||
|
|
||||||
|
def device_init(context, device):
|
||||||
|
"""
|
||||||
|
This function assumes the 'device' has already been verified to be compatible.
|
||||||
|
`get_device_delta()` has already filtered out incompatible devices.
|
||||||
|
"""
|
||||||
|
|
||||||
|
validate_device_id(device, log_prefix="device_init")
|
||||||
|
|
||||||
|
if "cuda" not in device:
|
||||||
|
context.device = device
|
||||||
|
context.device_name = get_processor_name()
|
||||||
|
context.half_precision = False
|
||||||
|
log.debug(f"Render device available as {context.device_name}")
|
||||||
|
return
|
||||||
|
|
||||||
|
context.device_name = torch.cuda.get_device_name(device)
|
||||||
|
context.device = device
|
||||||
|
|
||||||
|
# Force full precision on 1660 and 1650 NVIDIA cards to avoid creating green images
|
||||||
|
if needs_to_force_full_precision(context):
|
||||||
|
log.warn(f"forcing full precision on this GPU, to avoid green images. GPU detected: {context.device_name}")
|
||||||
|
# Apply force_full_precision now before models are loaded.
|
||||||
|
context.half_precision = False
|
||||||
|
|
||||||
|
log.info(f'Setting {device} as active, with precision: {"half" if context.half_precision else "full"}')
|
||||||
|
torch.cuda.device(device)
|
||||||
|
|
||||||
|
|
||||||
|
def needs_to_force_full_precision(context):
|
||||||
|
if "FORCE_FULL_PRECISION" in os.environ:
|
||||||
|
return True
|
||||||
|
|
||||||
|
device_name = context.device_name.lower()
|
||||||
|
return (
|
||||||
|
("nvidia" in device_name or "geforce" in device_name or "quadro" in device_name)
|
||||||
|
and (
|
||||||
|
" 1660" in device_name
|
||||||
|
or " 1650" in device_name
|
||||||
|
or " 1630" in device_name
|
||||||
|
or " t400" in device_name
|
||||||
|
or " t550" in device_name
|
||||||
|
or " t600" in device_name
|
||||||
|
or " t1000" in device_name
|
||||||
|
or " t1200" in device_name
|
||||||
|
or " t2000" in device_name
|
||||||
|
)
|
||||||
|
) or ("tesla k40m" in device_name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_max_vram_usage_level(device):
|
||||||
|
if "cuda" in device:
|
||||||
|
_, mem_total = torch.cuda.mem_get_info(device)
|
||||||
|
else:
|
||||||
|
return "high"
|
||||||
|
|
||||||
|
mem_total /= float(10**9)
|
||||||
|
if mem_total < 4.5:
|
||||||
|
return "low"
|
||||||
|
elif mem_total < 6.5:
|
||||||
|
return "balanced"
|
||||||
|
|
||||||
|
return "high"
|
||||||
|
|
||||||
|
|
||||||
|
def validate_device_id(device, log_prefix=""):
|
||||||
|
def is_valid():
|
||||||
|
if not isinstance(device, str):
|
||||||
|
return False
|
||||||
|
if device == "cpu" or device == "mps":
|
||||||
|
return True
|
||||||
|
if not device.startswith("cuda:") or not device[5:].isnumeric():
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
if not is_valid():
|
||||||
|
raise EnvironmentError(
|
||||||
|
f"{log_prefix}: device id should be 'cpu', 'mps', or 'cuda:N' (where N is an integer index for the GPU). Got: {device}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_device_compatible(device):
|
||||||
|
"""
|
||||||
|
Returns True/False, and prints any compatibility errors
|
||||||
|
"""
|
||||||
|
# static variable "history".
|
||||||
|
is_device_compatible.history = getattr(is_device_compatible, "history", {})
|
||||||
|
try:
|
||||||
|
validate_device_id(device, log_prefix="is_device_compatible")
|
||||||
|
except:
|
||||||
|
log.error(str(e))
|
||||||
|
return False
|
||||||
|
|
||||||
|
if device in ("cpu", "mps"):
|
||||||
|
return True
|
||||||
|
# Memory check
|
||||||
|
try:
|
||||||
|
_, mem_total = torch.cuda.mem_get_info(device)
|
||||||
|
mem_total /= float(10**9)
|
||||||
|
if mem_total < 1.9:
|
||||||
|
if is_device_compatible.history.get(device) == None:
|
||||||
|
log.warn(f"GPU {device} with less than 2 GB of VRAM is not compatible with Stable Diffusion")
|
||||||
|
is_device_compatible.history[device] = 1
|
||||||
|
return False
|
||||||
|
except RuntimeError as e:
|
||||||
|
log.error(str(e))
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_processor_name():
|
||||||
|
try:
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
return platform.processor()
|
||||||
|
elif platform.system() == "Darwin":
|
||||||
|
os.environ["PATH"] = os.environ["PATH"] + os.pathsep + "/usr/sbin"
|
||||||
|
command = "sysctl -n machdep.cpu.brand_string"
|
||||||
|
return subprocess.check_output(command, shell=True).decode().strip()
|
||||||
|
elif platform.system() == "Linux":
|
||||||
|
command = "cat /proc/cpuinfo"
|
||||||
|
all_info = subprocess.check_output(command, shell=True).decode().strip()
|
||||||
|
for line in all_info.split("\n"):
|
||||||
|
if "model name" in line:
|
||||||
|
return re.sub(".*model name.*:", "", line, 1).strip()
|
||||||
|
except:
|
||||||
|
log.error(traceback.format_exc())
|
||||||
|
return "cpu"
|
||||||
24
ui/easydiffusion/easydb/crud.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from easydiffusion.easydb import models, schemas
|
||||||
|
|
||||||
|
|
||||||
|
def get_bucket_by_path(db: Session, path: str):
|
||||||
|
return db.query(models.Bucket).filter(models.Bucket.path == path).first()
|
||||||
|
|
||||||
|
|
||||||
|
def create_bucket(db: Session, bucket: schemas.BucketCreate):
|
||||||
|
db_bucket = models.Bucket(path=bucket.path)
|
||||||
|
db.add(db_bucket)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_bucket)
|
||||||
|
return db_bucket
|
||||||
|
|
||||||
|
|
||||||
|
def create_bucketfile(db: Session, bucketfile: schemas.BucketFileCreate, bucket_id: int):
|
||||||
|
db_bucketfile = models.BucketFile(**bucketfile.dict(), bucket_id=bucket_id)
|
||||||
|
db.merge(db_bucketfile)
|
||||||
|
db.commit()
|
||||||
|
db_bucketfile = db.query(models.BucketFile).filter(models.BucketFile.bucket_id==bucket_id, models.BucketFile.filename==bucketfile.filename).first()
|
||||||
|
return db_bucketfile
|
||||||
|
|
||||||
14
ui/easydiffusion/easydb/database.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import os
|
||||||
|
from easydiffusion import app
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
os.makedirs(app.BUCKET_DIR, exist_ok=True)
|
||||||
|
SQLALCHEMY_DATABASE_URL = "sqlite:///"+os.path.join(app.BUCKET_DIR, "bucket.db")
|
||||||
|
|
||||||
|
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
BucketBase = declarative_base()
|
||||||
25
ui/easydiffusion/easydb/models.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, BLOB
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from easydiffusion.easydb.database import BucketBase
|
||||||
|
|
||||||
|
|
||||||
|
class Bucket(BucketBase):
|
||||||
|
__tablename__ = "bucket"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
path = Column(String, unique=True, index=True)
|
||||||
|
|
||||||
|
bucketfiles = relationship("BucketFile", back_populates="bucket")
|
||||||
|
|
||||||
|
|
||||||
|
class BucketFile(BucketBase):
|
||||||
|
__tablename__ = "bucketfile"
|
||||||
|
|
||||||
|
filename = Column(String, index=True, primary_key=True)
|
||||||
|
bucket_id = Column(Integer, ForeignKey("bucket.id"), primary_key=True)
|
||||||
|
|
||||||
|
data = Column(BLOB, index=False)
|
||||||
|
|
||||||
|
bucket = relationship("Bucket", back_populates="bucketfiles")
|
||||||
|
|
||||||
36
ui/easydiffusion/easydb/schemas.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class BucketFileBase(BaseModel):
|
||||||
|
filename: str
|
||||||
|
data: bytes
|
||||||
|
|
||||||
|
|
||||||
|
class BucketFileCreate(BucketFileBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BucketFile(BucketFileBase):
|
||||||
|
bucket_id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class BucketBase(BaseModel):
|
||||||
|
path: str
|
||||||
|
|
||||||
|
|
||||||
|
class BucketCreate(BucketBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Bucket(BucketBase):
|
||||||
|
id: int
|
||||||
|
bucketfiles: List[BucketFile] = []
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
427
ui/easydiffusion/model_manager.py
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from glob import glob
|
||||||
|
import traceback
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from easydiffusion import app
|
||||||
|
from easydiffusion.types import ModelsData
|
||||||
|
from easydiffusion.utils import log
|
||||||
|
from sdkit import Context
|
||||||
|
from sdkit.models import load_model, scan_model, unload_model, download_model, get_model_info_from_db
|
||||||
|
from sdkit.models.model_loader.controlnet_filters import filters as cn_filters
|
||||||
|
from sdkit.utils import hash_file_quick
|
||||||
|
from sdkit.models.model_loader.embeddings import get_embedding_token
|
||||||
|
|
||||||
|
KNOWN_MODEL_TYPES = [
|
||||||
|
"stable-diffusion",
|
||||||
|
"vae",
|
||||||
|
"hypernetwork",
|
||||||
|
"gfpgan",
|
||||||
|
"realesrgan",
|
||||||
|
"lora",
|
||||||
|
"codeformer",
|
||||||
|
"embeddings",
|
||||||
|
"controlnet",
|
||||||
|
]
|
||||||
|
MODEL_EXTENSIONS = {
|
||||||
|
"stable-diffusion": [".ckpt", ".safetensors"],
|
||||||
|
"vae": [".vae.pt", ".ckpt", ".safetensors"],
|
||||||
|
"hypernetwork": [".pt", ".safetensors"],
|
||||||
|
"gfpgan": [".pth"],
|
||||||
|
"realesrgan": [".pth"],
|
||||||
|
"lora": [".ckpt", ".safetensors", ".pt"],
|
||||||
|
"codeformer": [".pth"],
|
||||||
|
"embeddings": [".pt", ".bin", ".safetensors"],
|
||||||
|
"controlnet": [".pth", ".safetensors"],
|
||||||
|
}
|
||||||
|
DEFAULT_MODELS = {
|
||||||
|
"stable-diffusion": [
|
||||||
|
{"file_name": "sd-v1-5.safetensors", "model_id": "1.5-pruned-emaonly-fp16"},
|
||||||
|
],
|
||||||
|
"gfpgan": [
|
||||||
|
{"file_name": "GFPGANv1.4.pth", "model_id": "1.4"},
|
||||||
|
],
|
||||||
|
"realesrgan": [
|
||||||
|
{"file_name": "RealESRGAN_x4plus.pth", "model_id": "x4plus"},
|
||||||
|
{"file_name": "RealESRGAN_x4plus_anime_6B.pth", "model_id": "x4plus_anime_6"},
|
||||||
|
],
|
||||||
|
"vae": [
|
||||||
|
{"file_name": "vae-ft-mse-840000-ema-pruned.ckpt", "model_id": "vae-ft-mse-840000-ema-pruned"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
MODELS_TO_LOAD_ON_START = ["stable-diffusion", "vae", "hypernetwork", "lora"]
|
||||||
|
|
||||||
|
known_models = {}
|
||||||
|
|
||||||
|
|
||||||
|
def init():
|
||||||
|
make_model_folders()
|
||||||
|
migrate_legacy_model_location() # if necessary
|
||||||
|
download_default_models_if_necessary()
|
||||||
|
|
||||||
|
|
||||||
|
def load_default_models(context: Context):
|
||||||
|
from easydiffusion import runtime
|
||||||
|
|
||||||
|
runtime.set_vram_optimizations(context)
|
||||||
|
|
||||||
|
# init default model paths
|
||||||
|
for model_type in MODELS_TO_LOAD_ON_START:
|
||||||
|
context.model_paths[model_type] = resolve_model_to_use(model_type=model_type, fail_if_not_found=False)
|
||||||
|
try:
|
||||||
|
load_model(
|
||||||
|
context,
|
||||||
|
model_type,
|
||||||
|
scan_model=context.model_paths[model_type] != None
|
||||||
|
and not context.model_paths[model_type].endswith(".safetensors"),
|
||||||
|
)
|
||||||
|
if model_type in context.model_load_errors:
|
||||||
|
del context.model_load_errors[model_type]
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"[red]Error while loading {model_type} model: {context.model_paths[model_type]}[/red]")
|
||||||
|
if "DefaultCPUAllocator: not enough memory" in str(e):
|
||||||
|
log.error(
|
||||||
|
f"[red]Your PC is low on system RAM. Please add some virtual memory (or swap space) by following the instructions at this link: https://www.ibm.com/docs/en/opw/8.2.0?topic=tuning-optional-increasing-paging-file-size-windows-computers[/red]"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log.exception(e)
|
||||||
|
del context.model_paths[model_type]
|
||||||
|
|
||||||
|
context.model_load_errors[model_type] = str(e) # storing the entire Exception can lead to memory leaks
|
||||||
|
|
||||||
|
|
||||||
|
def unload_all(context: Context):
|
||||||
|
for model_type in KNOWN_MODEL_TYPES:
|
||||||
|
unload_model(context, model_type)
|
||||||
|
if model_type in context.model_load_errors:
|
||||||
|
del context.model_load_errors[model_type]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_model_to_use(model_name: Union[str, list] = None, model_type: str = None, fail_if_not_found: bool = True):
|
||||||
|
model_names = model_name if isinstance(model_name, list) else [model_name]
|
||||||
|
model_paths = []
|
||||||
|
for m in model_names:
|
||||||
|
if model_type == "embeddings":
|
||||||
|
try:
|
||||||
|
resolve_model_to_use_single(m, model_type)
|
||||||
|
except FileNotFoundError: # try with spaces
|
||||||
|
m = m.replace("_", " ")
|
||||||
|
|
||||||
|
path = resolve_model_to_use_single(m, model_type, fail_if_not_found)
|
||||||
|
model_paths.append(path)
|
||||||
|
|
||||||
|
return model_paths[0] if len(model_paths) == 1 else model_paths
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_model_to_use_single(model_name: str = None, model_type: str = None, fail_if_not_found: bool = True):
|
||||||
|
model_extensions = MODEL_EXTENSIONS.get(model_type, [])
|
||||||
|
default_models = DEFAULT_MODELS.get(model_type, [])
|
||||||
|
config = app.getConfig()
|
||||||
|
|
||||||
|
model_dir = os.path.join(app.MODELS_DIR, model_type)
|
||||||
|
if not model_name: # When None try user configured model.
|
||||||
|
# config = getConfig()
|
||||||
|
if "model" in config and model_type in config["model"]:
|
||||||
|
model_name = config["model"][model_type]
|
||||||
|
|
||||||
|
if model_name:
|
||||||
|
# Check models directory
|
||||||
|
model_path = os.path.join(model_dir, model_name)
|
||||||
|
if os.path.exists(model_path):
|
||||||
|
return model_path
|
||||||
|
for model_extension in model_extensions:
|
||||||
|
if os.path.exists(model_path + model_extension):
|
||||||
|
return model_path + model_extension
|
||||||
|
if os.path.exists(model_name + model_extension):
|
||||||
|
return os.path.abspath(model_name + model_extension)
|
||||||
|
|
||||||
|
# Can't find requested model, check the default paths.
|
||||||
|
if model_type == "stable-diffusion" and not fail_if_not_found:
|
||||||
|
for default_model in default_models:
|
||||||
|
default_model_path = os.path.join(model_dir, default_model["file_name"])
|
||||||
|
if os.path.exists(default_model_path):
|
||||||
|
if model_name is not None:
|
||||||
|
log.warn(
|
||||||
|
f"Could not find the configured custom model {model_name}. Using the default one: {default_model_path}"
|
||||||
|
)
|
||||||
|
return default_model_path
|
||||||
|
|
||||||
|
if model_name and fail_if_not_found:
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Could not find the desired model {model_name}! Is it present in the {model_dir} folder?"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def reload_models_if_necessary(context: Context, models_data: ModelsData, models_to_force_reload: list = []):
|
||||||
|
models_to_reload = {
|
||||||
|
model_type: path
|
||||||
|
for model_type, path in models_data.model_paths.items()
|
||||||
|
if context.model_paths.get(model_type) != path or (path is not None and context.models.get(model_type) is None)
|
||||||
|
}
|
||||||
|
|
||||||
|
if models_data.model_paths.get("codeformer"):
|
||||||
|
if "realesrgan" not in models_to_reload and "realesrgan" not in context.models:
|
||||||
|
default_realesrgan = DEFAULT_MODELS["realesrgan"][0]["file_name"]
|
||||||
|
models_to_reload["realesrgan"] = resolve_model_to_use(default_realesrgan, "realesrgan")
|
||||||
|
elif "realesrgan" in models_to_reload and models_to_reload["realesrgan"] is None:
|
||||||
|
del models_to_reload["realesrgan"] # don't unload realesrgan
|
||||||
|
|
||||||
|
for model_type in models_to_force_reload:
|
||||||
|
if model_type not in models_data.model_paths:
|
||||||
|
continue
|
||||||
|
models_to_reload[model_type] = models_data.model_paths[model_type]
|
||||||
|
|
||||||
|
for model_type, model_path_in_req in models_to_reload.items():
|
||||||
|
context.model_paths[model_type] = model_path_in_req
|
||||||
|
|
||||||
|
action_fn = unload_model if context.model_paths[model_type] is None else load_model
|
||||||
|
extra_params = models_data.model_params.get(model_type, {})
|
||||||
|
try:
|
||||||
|
action_fn(context, model_type, scan_model=False, **extra_params) # we've scanned them already
|
||||||
|
if model_type in context.model_load_errors:
|
||||||
|
del context.model_load_errors[model_type]
|
||||||
|
except Exception as e:
|
||||||
|
log.exception(e)
|
||||||
|
if action_fn == load_model:
|
||||||
|
context.model_load_errors[model_type] = str(e) # storing the entire Exception can lead to memory leaks
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_model_paths(models_data: ModelsData):
|
||||||
|
model_paths = models_data.model_paths
|
||||||
|
for model_type in model_paths:
|
||||||
|
skip_models = cn_filters + ["latent_upscaler", "nsfw_checker"]
|
||||||
|
if model_type in skip_models: # doesn't use model paths
|
||||||
|
continue
|
||||||
|
if model_type == "codeformer" and model_paths[model_type]:
|
||||||
|
download_if_necessary("codeformer", "codeformer.pth", "codeformer-0.1.0")
|
||||||
|
elif model_type == "controlnet" and model_paths[model_type]:
|
||||||
|
model_id = model_paths[model_type]
|
||||||
|
model_info = get_model_info_from_db(model_type=model_type, model_id=model_id)
|
||||||
|
if model_info:
|
||||||
|
filename = model_info.get("url", "").split("/")[-1]
|
||||||
|
download_if_necessary("controlnet", filename, model_id, skip_if_others_exist=False)
|
||||||
|
|
||||||
|
model_paths[model_type] = resolve_model_to_use(model_paths[model_type], model_type=model_type)
|
||||||
|
|
||||||
|
|
||||||
|
def fail_if_models_did_not_load(context: Context):
|
||||||
|
for model_type in KNOWN_MODEL_TYPES:
|
||||||
|
if model_type in context.model_load_errors:
|
||||||
|
e = context.model_load_errors[model_type]
|
||||||
|
raise Exception(f"Could not load the {model_type} model! Reason: " + e)
|
||||||
|
|
||||||
|
|
||||||
|
def download_default_models_if_necessary():
|
||||||
|
for model_type, models in DEFAULT_MODELS.items():
|
||||||
|
for model in models:
|
||||||
|
try:
|
||||||
|
download_if_necessary(model_type, model["file_name"], model["model_id"])
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
app.fail_and_die(fail_type="model_download", data=model_type)
|
||||||
|
|
||||||
|
print(model_type, "model(s) found.")
|
||||||
|
|
||||||
|
|
||||||
|
def download_if_necessary(model_type: str, file_name: str, model_id: str, skip_if_others_exist=True):
|
||||||
|
model_path = os.path.join(app.MODELS_DIR, model_type, file_name)
|
||||||
|
expected_hash = get_model_info_from_db(model_type=model_type, model_id=model_id)["quick_hash"]
|
||||||
|
|
||||||
|
other_models_exist = any_model_exists(model_type) and skip_if_others_exist
|
||||||
|
known_model_exists = os.path.exists(model_path)
|
||||||
|
known_model_is_corrupt = known_model_exists and hash_file_quick(model_path) != expected_hash
|
||||||
|
|
||||||
|
if known_model_is_corrupt or (not other_models_exist and not known_model_exists):
|
||||||
|
print("> download", model_type, model_id)
|
||||||
|
download_model(model_type, model_id, download_base_dir=app.MODELS_DIR, download_config_if_available=False)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_legacy_model_location():
|
||||||
|
'Move the models inside the legacy "stable-diffusion" folder, to their respective folders'
|
||||||
|
|
||||||
|
for model_type, models in DEFAULT_MODELS.items():
|
||||||
|
for model in models:
|
||||||
|
file_name = model["file_name"]
|
||||||
|
legacy_path = os.path.join(app.SD_DIR, file_name)
|
||||||
|
if os.path.exists(legacy_path):
|
||||||
|
shutil.move(legacy_path, os.path.join(app.MODELS_DIR, model_type, file_name))
|
||||||
|
|
||||||
|
|
||||||
|
def any_model_exists(model_type: str) -> bool:
|
||||||
|
extensions = MODEL_EXTENSIONS.get(model_type, [])
|
||||||
|
for ext in extensions:
|
||||||
|
if any(glob(f"{app.MODELS_DIR}/{model_type}/**/*{ext}", recursive=True)):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def make_model_folders():
|
||||||
|
for model_type in KNOWN_MODEL_TYPES:
|
||||||
|
model_dir_path = os.path.join(app.MODELS_DIR, model_type)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(model_dir_path, exist_ok=True)
|
||||||
|
except Exception as e:
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.panel import Panel
|
||||||
|
|
||||||
|
Console().print(
|
||||||
|
Panel(
|
||||||
|
"\n"
|
||||||
|
+ f"Error while creating the models directory: '{model_dir_path}'\n"
|
||||||
|
+ f"Error: {e}\n\n"
|
||||||
|
+ f"[white]Check the 'models_dir:' line in the file '{os.path.join(app.ROOT_DIR, 'config.yaml')}'.[/white]\n",
|
||||||
|
title="Fatal Error starting Easy Diffusion",
|
||||||
|
style="bold yellow on red",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
input("Press Enter to terminate...")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
help_file_name = f"Place your {model_type} model files here.txt"
|
||||||
|
help_file_contents = f'Supported extensions: {" or ".join(MODEL_EXTENSIONS.get(model_type))}'
|
||||||
|
|
||||||
|
with open(os.path.join(model_dir_path, help_file_name), "w", encoding="utf-8") as f:
|
||||||
|
f.write(help_file_contents)
|
||||||
|
|
||||||
|
|
||||||
|
def is_malicious_model(file_path):
|
||||||
|
try:
|
||||||
|
if file_path.endswith(".safetensors"):
|
||||||
|
return False
|
||||||
|
scan_result = scan_model(file_path)
|
||||||
|
if scan_result.issues_count > 0 or scan_result.infected_files > 0:
|
||||||
|
log.warn(
|
||||||
|
":warning: [bold red]Scan %s: %d scanned, %d issue, %d infected.[/bold red]"
|
||||||
|
% (
|
||||||
|
file_path,
|
||||||
|
scan_result.scanned_files,
|
||||||
|
scan_result.issues_count,
|
||||||
|
scan_result.infected_files,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
log.debug(
|
||||||
|
"Scan %s: [green]%d scanned, %d issue, %d infected.[/green]"
|
||||||
|
% (
|
||||||
|
file_path,
|
||||||
|
scan_result.scanned_files,
|
||||||
|
scan_result.issues_count,
|
||||||
|
scan_result.infected_files,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"error while scanning: {file_path}, error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def getModels(scan_for_malicious: bool = True):
|
||||||
|
models = {
|
||||||
|
"options": {
|
||||||
|
"stable-diffusion": [],
|
||||||
|
"vae": [],
|
||||||
|
"hypernetwork": [],
|
||||||
|
"lora": [],
|
||||||
|
"codeformer": [{"codeformer": "CodeFormer"}],
|
||||||
|
"embeddings": [],
|
||||||
|
"controlnet": [
|
||||||
|
{"control_v11p_sd15_canny": "Canny (*)"},
|
||||||
|
{"control_v11p_sd15_openpose": "OpenPose (*)"},
|
||||||
|
{"control_v11p_sd15_normalbae": "Normal BAE (*)"},
|
||||||
|
{"control_v11f1p_sd15_depth": "Depth (*)"},
|
||||||
|
{"control_v11p_sd15_scribble": "Scribble"},
|
||||||
|
{"control_v11p_sd15_softedge": "Soft Edge"},
|
||||||
|
{"control_v11p_sd15_inpaint": "Inpaint"},
|
||||||
|
{"control_v11p_sd15_lineart": "Line Art"},
|
||||||
|
{"control_v11p_sd15s2_lineart_anime": "Line Art Anime"},
|
||||||
|
{"control_v11p_sd15_mlsd": "Straight Lines"},
|
||||||
|
{"control_v11p_sd15_seg": "Segment"},
|
||||||
|
{"control_v11e_sd15_shuffle": "Shuffle"},
|
||||||
|
{"control_v11f1e_sd15_tile": "Tile"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
models_scanned = 0
|
||||||
|
|
||||||
|
class MaliciousModelException(Exception):
|
||||||
|
"Raised when picklescan reports a problem with a model"
|
||||||
|
|
||||||
|
def scan_directory(directory, suffixes, directoriesFirst: bool = True, default_entries=[], nameFilter=None):
|
||||||
|
nonlocal models_scanned
|
||||||
|
|
||||||
|
tree = list(default_entries)
|
||||||
|
|
||||||
|
for entry in sorted(
|
||||||
|
os.scandir(directory),
|
||||||
|
key=lambda entry: (entry.is_file() == directoriesFirst, entry.name.lower()),
|
||||||
|
):
|
||||||
|
if entry.is_file():
|
||||||
|
matching_suffix = list(filter(lambda s: entry.name.endswith(s), suffixes))
|
||||||
|
if len(matching_suffix) == 0:
|
||||||
|
continue
|
||||||
|
matching_suffix = matching_suffix[0]
|
||||||
|
|
||||||
|
mtime = entry.stat().st_mtime
|
||||||
|
mod_time = known_models[entry.path] if entry.path in known_models else -1
|
||||||
|
if mod_time != mtime:
|
||||||
|
models_scanned += 1
|
||||||
|
if scan_for_malicious and is_malicious_model(entry.path):
|
||||||
|
raise MaliciousModelException(entry.path)
|
||||||
|
if scan_for_malicious:
|
||||||
|
known_models[entry.path] = mtime
|
||||||
|
|
||||||
|
model_id = entry.name[: -len(matching_suffix)]
|
||||||
|
if callable(nameFilter):
|
||||||
|
model_id = nameFilter(model_id)
|
||||||
|
|
||||||
|
model_exists = False
|
||||||
|
for m in tree: # allows default "named" models, like CodeFormer and known ControlNet models
|
||||||
|
if (isinstance(m, str) and model_id == m) or (isinstance(m, dict) and model_id in m):
|
||||||
|
model_exists = True
|
||||||
|
break
|
||||||
|
if not model_exists:
|
||||||
|
tree.append(model_id)
|
||||||
|
|
||||||
|
elif entry.is_dir():
|
||||||
|
scan = scan_directory(entry.path, suffixes, directoriesFirst=False, nameFilter=nameFilter)
|
||||||
|
|
||||||
|
if len(scan) != 0:
|
||||||
|
tree.append((entry.name, scan))
|
||||||
|
return tree
|
||||||
|
|
||||||
|
def listModels(model_type, nameFilter=None):
|
||||||
|
nonlocal models_scanned
|
||||||
|
|
||||||
|
model_extensions = MODEL_EXTENSIONS.get(model_type, [])
|
||||||
|
models_dir = os.path.join(app.MODELS_DIR, model_type)
|
||||||
|
if not os.path.exists(models_dir):
|
||||||
|
os.makedirs(models_dir)
|
||||||
|
|
||||||
|
try:
|
||||||
|
default_tree = models["options"].get(model_type, [])
|
||||||
|
models["options"][model_type] = scan_directory(
|
||||||
|
models_dir, model_extensions, default_entries=default_tree, nameFilter=nameFilter
|
||||||
|
)
|
||||||
|
except MaliciousModelException as e:
|
||||||
|
models["scan-error"] = str(e)
|
||||||
|
|
||||||
|
if scan_for_malicious:
|
||||||
|
log.info(f"[green]Scanning all model folders for models...[/]")
|
||||||
|
# custom models
|
||||||
|
listModels(model_type="stable-diffusion")
|
||||||
|
listModels(model_type="vae")
|
||||||
|
listModels(model_type="hypernetwork")
|
||||||
|
listModels(model_type="gfpgan")
|
||||||
|
listModels(model_type="lora")
|
||||||
|
listModels(model_type="embeddings", nameFilter=get_embedding_token)
|
||||||
|
listModels(model_type="controlnet")
|
||||||
|
|
||||||
|
if scan_for_malicious and models_scanned > 0:
|
||||||
|
log.info(f"[green]Scanned {models_scanned} models. Nothing infected[/]")
|
||||||
|
|
||||||
|
return models
|
||||||
102
ui/easydiffusion/package_manager.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
from importlib.metadata import version as pkg_version
|
||||||
|
|
||||||
|
from sdkit.utils import log
|
||||||
|
|
||||||
|
from easydiffusion import app
|
||||||
|
|
||||||
|
# future home of scripts/check_modules.py
|
||||||
|
|
||||||
|
manifest = {
|
||||||
|
"tensorrt": {
|
||||||
|
"install": [
|
||||||
|
"wheel",
|
||||||
|
"nvidia-cudnn-cu11==8.9.4.25",
|
||||||
|
"tensorrt==9.0.0.post11.dev1 --pre --extra-index-url=https://pypi.nvidia.com --trusted-host pypi.nvidia.com",
|
||||||
|
],
|
||||||
|
"uninstall": ["tensorrt"],
|
||||||
|
# TODO also uninstall tensorrt-libs and nvidia-cudnn, but do it upon restarting (avoid 'file in use' error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
installing = []
|
||||||
|
|
||||||
|
# remove this once TRT releases on pypi
|
||||||
|
if platform.system() == "Windows":
|
||||||
|
trt_dir = os.path.join(app.ROOT_DIR, "tensorrt")
|
||||||
|
if os.path.exists(trt_dir) and os.path.isdir(trt_dir) and len(os.listdir(trt_dir)) > 0:
|
||||||
|
files = os.listdir(trt_dir)
|
||||||
|
|
||||||
|
packages = manifest["tensorrt"]["install"]
|
||||||
|
packages = tuple(p.replace("-", "_") for p in packages)
|
||||||
|
|
||||||
|
wheels = []
|
||||||
|
for p in packages:
|
||||||
|
p = p.split(" ")[0]
|
||||||
|
f = next((f for f in files if f.startswith(p) and f.endswith((".whl", ".tar.gz"))), None)
|
||||||
|
if f:
|
||||||
|
wheels.append(os.path.join(trt_dir, f))
|
||||||
|
|
||||||
|
manifest["tensorrt"]["install"] = wheels
|
||||||
|
|
||||||
|
|
||||||
|
def get_installed_packages() -> list:
|
||||||
|
return {module_name: version(module_name) for module_name in manifest if is_installed(module_name)}
|
||||||
|
|
||||||
|
|
||||||
|
def is_installed(module_name) -> bool:
|
||||||
|
return version(module_name) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def install(module_name):
|
||||||
|
if is_installed(module_name):
|
||||||
|
log.info(f"{module_name} has already been installed!")
|
||||||
|
return
|
||||||
|
if module_name in installing:
|
||||||
|
log.info(f"{module_name} is already installing!")
|
||||||
|
return
|
||||||
|
|
||||||
|
if module_name not in manifest:
|
||||||
|
raise RuntimeError(f"Can't install unknown package: {module_name}!")
|
||||||
|
|
||||||
|
commands = manifest[module_name]["install"]
|
||||||
|
if module_name == "tensorrt":
|
||||||
|
commands += [
|
||||||
|
"protobuf==3.20.3 polygraphy==0.47.1 onnx==1.14.0 --extra-index-url=https://pypi.ngc.nvidia.com --trusted-host pypi.ngc.nvidia.com"
|
||||||
|
]
|
||||||
|
commands = [f"python -m pip install --upgrade {cmd}" for cmd in commands]
|
||||||
|
|
||||||
|
installing.append(module_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
for cmd in commands:
|
||||||
|
print(">", cmd)
|
||||||
|
if os.system(cmd) != 0:
|
||||||
|
raise RuntimeError(f"Error while running {cmd}. Please check the logs in the command-line.")
|
||||||
|
finally:
|
||||||
|
installing.remove(module_name)
|
||||||
|
|
||||||
|
|
||||||
|
def uninstall(module_name):
|
||||||
|
if not is_installed(module_name):
|
||||||
|
log.info(f"{module_name} hasn't been installed!")
|
||||||
|
return
|
||||||
|
|
||||||
|
if module_name not in manifest:
|
||||||
|
raise RuntimeError(f"Can't uninstall unknown package: {module_name}!")
|
||||||
|
|
||||||
|
commands = manifest[module_name]["uninstall"]
|
||||||
|
commands = [f"python -m pip uninstall -y {cmd}" for cmd in commands]
|
||||||
|
|
||||||
|
for cmd in commands:
|
||||||
|
print(">", cmd)
|
||||||
|
if os.system(cmd) != 0:
|
||||||
|
raise RuntimeError(f"Error while running {cmd}. Please check the logs in the command-line.")
|
||||||
|
|
||||||
|
|
||||||
|
def version(module_name: str) -> str:
|
||||||
|
try:
|
||||||
|
return pkg_version(module_name)
|
||||||
|
except:
|
||||||
|
return None
|
||||||
51
ui/easydiffusion/runtime.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
"""
|
||||||
|
A runtime that runs on a specific device (in a thread).
|
||||||
|
|
||||||
|
It can run various tasks like image generation, image filtering, model merge etc by using that thread-local context.
|
||||||
|
|
||||||
|
This creates an `sdkit.Context` that's bound to the device specified while calling the `init()` function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from easydiffusion import device_manager
|
||||||
|
from easydiffusion.utils import log
|
||||||
|
from sdkit import Context
|
||||||
|
from sdkit.utils import get_device_usage
|
||||||
|
|
||||||
|
context = Context() # thread-local
|
||||||
|
"""
|
||||||
|
runtime data (bound locally to this thread), for e.g. device, references to loaded models, optimization flags etc
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def init(device):
|
||||||
|
"""
|
||||||
|
Initializes the fields that will be bound to this runtime's context, and sets the current torch device
|
||||||
|
"""
|
||||||
|
context.stop_processing = False
|
||||||
|
context.temp_images = {}
|
||||||
|
context.partial_x_samples = None
|
||||||
|
context.model_load_errors = {}
|
||||||
|
context.enable_codeformer = True
|
||||||
|
|
||||||
|
from easydiffusion import app
|
||||||
|
|
||||||
|
app_config = app.getConfig()
|
||||||
|
context.test_diffusers = app_config.get("use_v3_engine", True)
|
||||||
|
|
||||||
|
log.info("Device usage during initialization:")
|
||||||
|
get_device_usage(device, log_info=True, process_usage_only=False)
|
||||||
|
|
||||||
|
device_manager.device_init(context, device)
|
||||||
|
|
||||||
|
|
||||||
|
def set_vram_optimizations(context: Context):
|
||||||
|
from easydiffusion import app
|
||||||
|
|
||||||
|
config = app.getConfig()
|
||||||
|
vram_usage_level = config.get("vram_usage_level", "balanced")
|
||||||
|
|
||||||
|
if vram_usage_level != context.vram_usage_level:
|
||||||
|
context.vram_usage_level = vram_usage_level
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
492
ui/easydiffusion/server.py
Normal file
@@ -0,0 +1,492 @@
|
|||||||
|
"""server.py: FastAPI SD-UI Web Host.
|
||||||
|
Notes:
|
||||||
|
async endpoints always run on the main thread. Without they run on the thread pool.
|
||||||
|
"""
|
||||||
|
import datetime
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
from typing import List, Union
|
||||||
|
|
||||||
|
from easydiffusion import app, model_manager, task_manager, package_manager
|
||||||
|
from easydiffusion.tasks import RenderTask, FilterTask
|
||||||
|
from easydiffusion.types import (
|
||||||
|
GenerateImageRequest,
|
||||||
|
FilterImageRequest,
|
||||||
|
MergeRequest,
|
||||||
|
TaskData,
|
||||||
|
RenderTaskData,
|
||||||
|
ModelsData,
|
||||||
|
OutputFormatData,
|
||||||
|
SaveToDiskData,
|
||||||
|
convert_legacy_render_req_to_new,
|
||||||
|
)
|
||||||
|
from easydiffusion.utils import log
|
||||||
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from pydantic import BaseModel, Extra
|
||||||
|
from starlette.responses import FileResponse, JSONResponse, StreamingResponse
|
||||||
|
from pycloudflared import try_cloudflare
|
||||||
|
|
||||||
|
log.info(f"started in {app.SD_DIR}")
|
||||||
|
log.info(f"started at {datetime.datetime.now():%x %X}")
|
||||||
|
|
||||||
|
server_api = FastAPI()
|
||||||
|
|
||||||
|
NOCACHE_HEADERS = {
|
||||||
|
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||||
|
"Pragma": "no-cache",
|
||||||
|
"Expires": "0",
|
||||||
|
}
|
||||||
|
PROTECTED_CONFIG_KEYS = ("block_nsfw",) # can't change these via the HTTP API
|
||||||
|
|
||||||
|
|
||||||
|
class NoCacheStaticFiles(StaticFiles):
|
||||||
|
def __init__(self, directory: str):
|
||||||
|
# follow_symlink is only available on fastapi >= 0.92.0
|
||||||
|
if os.path.islink(directory):
|
||||||
|
super().__init__(directory=os.path.realpath(directory))
|
||||||
|
else:
|
||||||
|
super().__init__(directory=directory)
|
||||||
|
|
||||||
|
def is_not_modified(self, response_headers, request_headers) -> bool:
|
||||||
|
if "content-type" in response_headers and (
|
||||||
|
"javascript" in response_headers["content-type"] or "css" in response_headers["content-type"]
|
||||||
|
):
|
||||||
|
response_headers.update(NOCACHE_HEADERS)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return super().is_not_modified(response_headers, request_headers)
|
||||||
|
|
||||||
|
|
||||||
|
class SetAppConfigRequest(BaseModel, extra=Extra.allow):
|
||||||
|
update_branch: str = None
|
||||||
|
render_devices: Union[List[str], List[int], str, int] = None
|
||||||
|
model_vae: str = None
|
||||||
|
ui_open_browser_on_start: bool = None
|
||||||
|
listen_to_network: bool = None
|
||||||
|
listen_port: int = None
|
||||||
|
use_v3_engine: bool = True
|
||||||
|
models_dir: str = None
|
||||||
|
|
||||||
|
|
||||||
|
def init():
|
||||||
|
mimetypes.init()
|
||||||
|
mimetypes.add_type("text/css", ".css")
|
||||||
|
|
||||||
|
if os.path.isdir(app.CUSTOM_MODIFIERS_DIR):
|
||||||
|
server_api.mount(
|
||||||
|
"/media/modifier-thumbnails/custom",
|
||||||
|
NoCacheStaticFiles(directory=app.CUSTOM_MODIFIERS_DIR),
|
||||||
|
name="custom-thumbnails",
|
||||||
|
)
|
||||||
|
|
||||||
|
server_api.mount(
|
||||||
|
"/media",
|
||||||
|
NoCacheStaticFiles(directory=os.path.join(app.SD_UI_DIR, "media")),
|
||||||
|
name="media",
|
||||||
|
)
|
||||||
|
|
||||||
|
for plugins_dir, dir_prefix in app.UI_PLUGINS_SOURCES:
|
||||||
|
server_api.mount(
|
||||||
|
f"/plugins/{dir_prefix}",
|
||||||
|
NoCacheStaticFiles(directory=plugins_dir),
|
||||||
|
name=f"plugins-{dir_prefix}",
|
||||||
|
)
|
||||||
|
|
||||||
|
@server_api.post("/app_config")
|
||||||
|
async def set_app_config(req: SetAppConfigRequest):
|
||||||
|
return set_app_config_internal(req)
|
||||||
|
|
||||||
|
@server_api.get("/get/{key:path}")
|
||||||
|
def read_web_data(key: str = None, scan_for_malicious: bool = True):
|
||||||
|
return read_web_data_internal(key, scan_for_malicious=scan_for_malicious)
|
||||||
|
|
||||||
|
@server_api.get("/ping") # Get server and optionally session status.
|
||||||
|
def ping(session_id: str = None):
|
||||||
|
return ping_internal(session_id)
|
||||||
|
|
||||||
|
@server_api.post("/render")
|
||||||
|
def render(req: dict):
|
||||||
|
return render_internal(req)
|
||||||
|
|
||||||
|
@server_api.post("/filter")
|
||||||
|
def render(req: dict):
|
||||||
|
return filter_internal(req)
|
||||||
|
|
||||||
|
@server_api.post("/model/merge")
|
||||||
|
def model_merge(req: dict):
|
||||||
|
print(req)
|
||||||
|
return model_merge_internal(req)
|
||||||
|
|
||||||
|
@server_api.get("/image/stream/{task_id:int}")
|
||||||
|
def stream(task_id: int):
|
||||||
|
return stream_internal(task_id)
|
||||||
|
|
||||||
|
@server_api.get("/image/stop")
|
||||||
|
def stop(task: int):
|
||||||
|
return stop_internal(task)
|
||||||
|
|
||||||
|
@server_api.get("/image/tmp/{task_id:int}/{img_id:int}")
|
||||||
|
def get_image(task_id: int, img_id: int):
|
||||||
|
return get_image_internal(task_id, img_id)
|
||||||
|
|
||||||
|
@server_api.post("/tunnel/cloudflare/start")
|
||||||
|
def start_cloudflare_tunnel(req: dict):
|
||||||
|
return start_cloudflare_tunnel_internal(req)
|
||||||
|
|
||||||
|
@server_api.post("/tunnel/cloudflare/stop")
|
||||||
|
def stop_cloudflare_tunnel(req: dict):
|
||||||
|
return stop_cloudflare_tunnel_internal(req)
|
||||||
|
|
||||||
|
@server_api.post("/package/{package_name:str}")
|
||||||
|
def modify_package(package_name: str, req: dict):
|
||||||
|
return modify_package_internal(package_name, req)
|
||||||
|
|
||||||
|
@server_api.get("/sha256/{obj_path:path}")
|
||||||
|
def get_sha256(obj_path: str):
|
||||||
|
return get_sha256_internal(obj_path)
|
||||||
|
|
||||||
|
@server_api.get("/")
|
||||||
|
def read_root():
|
||||||
|
return FileResponse(os.path.join(app.SD_UI_DIR, "index.html"), headers=NOCACHE_HEADERS)
|
||||||
|
|
||||||
|
@server_api.on_event("shutdown")
|
||||||
|
def shutdown_event(): # Signal render thread to close on shutdown
|
||||||
|
task_manager.current_state_error = SystemExit("Application shutting down.")
|
||||||
|
|
||||||
|
|
||||||
|
# API implementations
|
||||||
|
def set_app_config_internal(req: SetAppConfigRequest):
|
||||||
|
config = app.getConfig()
|
||||||
|
if req.update_branch is not None:
|
||||||
|
config["update_branch"] = req.update_branch
|
||||||
|
if req.render_devices is not None:
|
||||||
|
update_render_devices_in_config(config, req.render_devices)
|
||||||
|
if req.ui_open_browser_on_start is not None:
|
||||||
|
if "ui" not in config:
|
||||||
|
config["ui"] = {}
|
||||||
|
config["ui"]["open_browser_on_start"] = req.ui_open_browser_on_start
|
||||||
|
if req.listen_to_network is not None:
|
||||||
|
if "net" not in config:
|
||||||
|
config["net"] = {}
|
||||||
|
config["net"]["listen_to_network"] = bool(req.listen_to_network)
|
||||||
|
if req.listen_port is not None:
|
||||||
|
if "net" not in config:
|
||||||
|
config["net"] = {}
|
||||||
|
config["net"]["listen_port"] = int(req.listen_port)
|
||||||
|
|
||||||
|
config["use_v3_engine"] = req.use_v3_engine
|
||||||
|
config["models_dir"] = req.models_dir
|
||||||
|
|
||||||
|
for property, property_value in req.dict().items():
|
||||||
|
if property_value is not None and property not in req.__fields__ and property not in PROTECTED_CONFIG_KEYS:
|
||||||
|
config[property] = property_value
|
||||||
|
|
||||||
|
try:
|
||||||
|
app.setConfig(config)
|
||||||
|
|
||||||
|
if req.render_devices:
|
||||||
|
app.update_render_threads()
|
||||||
|
|
||||||
|
return JSONResponse({"status": "OK"}, headers=NOCACHE_HEADERS)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(traceback.format_exc())
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
def update_render_devices_in_config(config, render_devices):
|
||||||
|
if render_devices not in ("cpu", "auto") and not render_devices.startswith("cuda:"):
|
||||||
|
raise HTTPException(status_code=400, detail=f"Invalid render device requested: {render_devices}")
|
||||||
|
|
||||||
|
if render_devices.startswith("cuda:"):
|
||||||
|
render_devices = render_devices.split(",")
|
||||||
|
|
||||||
|
config["render_devices"] = render_devices
|
||||||
|
|
||||||
|
|
||||||
|
def read_web_data_internal(key: str = None, **kwargs):
|
||||||
|
if not key: # /get without parameters, stable-diffusion easter egg.
|
||||||
|
raise HTTPException(status_code=418, detail="StableDiffusion is drawing a teapot!") # HTTP418 I'm a teapot
|
||||||
|
elif key == "app_config":
|
||||||
|
config = app.getConfig()
|
||||||
|
|
||||||
|
if "models_dir" not in config:
|
||||||
|
config["models_dir"] = app.MODELS_DIR
|
||||||
|
|
||||||
|
return JSONResponse(config, headers=NOCACHE_HEADERS)
|
||||||
|
elif key == "system_info":
|
||||||
|
config = app.getConfig()
|
||||||
|
|
||||||
|
output_dir = config.get("force_save_path", os.path.join(os.path.expanduser("~"), app.OUTPUT_DIRNAME))
|
||||||
|
|
||||||
|
system_info = {
|
||||||
|
"devices": task_manager.get_devices(),
|
||||||
|
"hosts": app.getIPConfig(),
|
||||||
|
"default_output_dir": output_dir,
|
||||||
|
"enforce_output_dir": ("force_save_path" in config),
|
||||||
|
"enforce_output_metadata": ("force_save_metadata" in config),
|
||||||
|
}
|
||||||
|
system_info["devices"]["config"] = config.get("render_devices", "auto")
|
||||||
|
return JSONResponse(system_info, headers=NOCACHE_HEADERS)
|
||||||
|
elif key == "models":
|
||||||
|
scan_for_malicious = kwargs.get("scan_for_malicious", True)
|
||||||
|
return JSONResponse(model_manager.getModels(scan_for_malicious), headers=NOCACHE_HEADERS)
|
||||||
|
elif key == "modifiers":
|
||||||
|
return JSONResponse(app.get_image_modifiers(), headers=NOCACHE_HEADERS)
|
||||||
|
elif key == "ui_plugins":
|
||||||
|
return JSONResponse(app.getUIPlugins(), headers=NOCACHE_HEADERS)
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Request for unknown {key}") # HTTP404 Not Found
|
||||||
|
|
||||||
|
|
||||||
|
def ping_internal(session_id: str = None):
|
||||||
|
if task_manager.is_alive() <= 0: # Check that render threads are alive.
|
||||||
|
if task_manager.current_state_error:
|
||||||
|
raise HTTPException(status_code=500, detail=str(task_manager.current_state_error))
|
||||||
|
raise HTTPException(status_code=500, detail="Render thread is dead.")
|
||||||
|
|
||||||
|
if task_manager.current_state_error and not isinstance(task_manager.current_state_error, StopAsyncIteration):
|
||||||
|
raise HTTPException(status_code=500, detail=str(task_manager.current_state_error))
|
||||||
|
|
||||||
|
# Alive
|
||||||
|
response = {"status": str(task_manager.current_state)}
|
||||||
|
|
||||||
|
if session_id:
|
||||||
|
session = task_manager.get_cached_session(session_id, update_ttl=True)
|
||||||
|
response["tasks"] = {id(t): t.status for t in session.tasks}
|
||||||
|
|
||||||
|
response["devices"] = task_manager.get_devices()
|
||||||
|
response["packages_installed"] = package_manager.get_installed_packages()
|
||||||
|
response["packages_installing"] = package_manager.installing
|
||||||
|
|
||||||
|
if cloudflare.address != None:
|
||||||
|
response["cloudflare"] = cloudflare.address
|
||||||
|
|
||||||
|
return JSONResponse(response, headers=NOCACHE_HEADERS)
|
||||||
|
|
||||||
|
|
||||||
|
def render_internal(req: dict):
|
||||||
|
try:
|
||||||
|
req = convert_legacy_render_req_to_new(req)
|
||||||
|
|
||||||
|
# separate out the request data into rendering and task-specific data
|
||||||
|
render_req: GenerateImageRequest = GenerateImageRequest.parse_obj(req)
|
||||||
|
task_data: RenderTaskData = RenderTaskData.parse_obj(req)
|
||||||
|
models_data: ModelsData = ModelsData.parse_obj(req)
|
||||||
|
output_format: OutputFormatData = OutputFormatData.parse_obj(req)
|
||||||
|
save_data: SaveToDiskData = SaveToDiskData.parse_obj(req)
|
||||||
|
|
||||||
|
# Overwrite user specified save path
|
||||||
|
config = app.getConfig()
|
||||||
|
if "force_save_path" in config:
|
||||||
|
save_data.save_to_disk_path = config["force_save_path"]
|
||||||
|
|
||||||
|
render_req.init_image_mask = req.get("mask") # hack: will rename this in the HTTP API in a future revision
|
||||||
|
|
||||||
|
app.save_to_config(
|
||||||
|
models_data.model_paths.get("stable-diffusion"),
|
||||||
|
models_data.model_paths.get("vae"),
|
||||||
|
models_data.model_paths.get("hypernetwork"),
|
||||||
|
task_data.vram_usage_level,
|
||||||
|
)
|
||||||
|
|
||||||
|
# enqueue the task
|
||||||
|
task = RenderTask(render_req, task_data, models_data, output_format, save_data)
|
||||||
|
return enqueue_task(task)
|
||||||
|
except HTTPException as e:
|
||||||
|
raise e
|
||||||
|
except Exception as e:
|
||||||
|
log.error(traceback.format_exc())
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
def filter_internal(req: dict):
|
||||||
|
try:
|
||||||
|
filter_req: FilterImageRequest = FilterImageRequest.parse_obj(req)
|
||||||
|
task_data: TaskData = TaskData.parse_obj(req)
|
||||||
|
models_data: ModelsData = ModelsData.parse_obj(req)
|
||||||
|
output_format: OutputFormatData = OutputFormatData.parse_obj(req)
|
||||||
|
save_data: SaveToDiskData = SaveToDiskData.parse_obj(req)
|
||||||
|
|
||||||
|
# enqueue the task
|
||||||
|
task = FilterTask(filter_req, task_data, models_data, output_format, save_data)
|
||||||
|
return enqueue_task(task)
|
||||||
|
except HTTPException as e:
|
||||||
|
raise e
|
||||||
|
except Exception as e:
|
||||||
|
log.error(traceback.format_exc())
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
def enqueue_task(task):
|
||||||
|
try:
|
||||||
|
task_manager.enqueue_task(task)
|
||||||
|
response = {
|
||||||
|
"status": str(task_manager.current_state),
|
||||||
|
"queue": len(task_manager.tasks_queue),
|
||||||
|
"stream": f"/image/stream/{task.id}",
|
||||||
|
"task": task.id,
|
||||||
|
}
|
||||||
|
return JSONResponse(response, headers=NOCACHE_HEADERS)
|
||||||
|
except ChildProcessError as e: # Render thread is dead
|
||||||
|
raise HTTPException(status_code=500, detail=f"Rendering thread has died.") # HTTP500 Internal Server Error
|
||||||
|
except ConnectionRefusedError as e: # Unstarted task pending limit reached, deny queueing too many.
|
||||||
|
raise HTTPException(status_code=503, detail=str(e)) # HTTP503 Service Unavailable
|
||||||
|
|
||||||
|
|
||||||
|
def model_merge_internal(req: dict):
|
||||||
|
try:
|
||||||
|
from easydiffusion.utils.save_utils import filename_regex
|
||||||
|
from sdkit.train import merge_models
|
||||||
|
|
||||||
|
mergeReq: MergeRequest = MergeRequest.parse_obj(req)
|
||||||
|
|
||||||
|
merge_models(
|
||||||
|
model_manager.resolve_model_to_use(mergeReq.model0, "stable-diffusion"),
|
||||||
|
model_manager.resolve_model_to_use(mergeReq.model1, "stable-diffusion"),
|
||||||
|
mergeReq.ratio,
|
||||||
|
os.path.join(
|
||||||
|
app.MODELS_DIR,
|
||||||
|
"stable-diffusion",
|
||||||
|
filename_regex.sub("_", mergeReq.out_path),
|
||||||
|
),
|
||||||
|
mergeReq.use_fp16,
|
||||||
|
)
|
||||||
|
return JSONResponse({"status": "OK"}, headers=NOCACHE_HEADERS)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(traceback.format_exc())
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
def stream_internal(task_id: int):
|
||||||
|
# TODO Move to WebSockets ??
|
||||||
|
task = task_manager.get_cached_task(task_id, update_ttl=True)
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Request {task_id} not found.") # HTTP404 NotFound
|
||||||
|
# if (id(task) != task_id): raise HTTPException(status_code=409, detail=f'Wrong task id received. Expected:{id(task)}, Received:{task_id}') # HTTP409 Conflict
|
||||||
|
if task.buffer_queue.empty() and not task.lock.locked():
|
||||||
|
if task.response:
|
||||||
|
# log.info(f'Session {session_id} sending cached response')
|
||||||
|
return JSONResponse(task.response, headers=NOCACHE_HEADERS)
|
||||||
|
raise HTTPException(status_code=425, detail="Too Early, task not started yet.") # HTTP425 Too Early
|
||||||
|
# log.info(f'Session {session_id} opened live render stream {id(task.buffer_queue)}')
|
||||||
|
return StreamingResponse(task.read_buffer_generator(), media_type="application/json")
|
||||||
|
|
||||||
|
|
||||||
|
def stop_internal(task: int):
|
||||||
|
if not task:
|
||||||
|
if (
|
||||||
|
task_manager.current_state == task_manager.ServerStates.Online
|
||||||
|
or task_manager.current_state == task_manager.ServerStates.Unavailable
|
||||||
|
):
|
||||||
|
raise HTTPException(status_code=409, detail="Not currently running any tasks.") # HTTP409 Conflict
|
||||||
|
task_manager.current_state_error = StopAsyncIteration("")
|
||||||
|
return {"OK"}
|
||||||
|
task_id = task
|
||||||
|
task = task_manager.get_cached_task(task_id, update_ttl=False)
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Task {task_id} was not found.") # HTTP404 Not Found
|
||||||
|
if isinstance(task.error, StopAsyncIteration):
|
||||||
|
raise HTTPException(status_code=409, detail=f"Task {task_id} is already stopped.") # HTTP409 Conflict
|
||||||
|
task.error = StopAsyncIteration(f"Task {task_id} stop requested.")
|
||||||
|
return {"OK"}
|
||||||
|
|
||||||
|
|
||||||
|
def get_image_internal(task_id: int, img_id: int):
|
||||||
|
task = task_manager.get_cached_task(task_id, update_ttl=True)
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(status_code=410, detail=f"Task {task_id} could not be found.") # HTTP404 NotFound
|
||||||
|
if not task.temp_images[img_id]:
|
||||||
|
raise HTTPException(status_code=425, detail="Too Early, task data is not available yet.") # HTTP425 Too Early
|
||||||
|
try:
|
||||||
|
img_data = task.temp_images[img_id]
|
||||||
|
img_data.seek(0)
|
||||||
|
return StreamingResponse(img_data, media_type="image/jpeg")
|
||||||
|
except KeyError as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
# ---- Cloudflare Tunnel ----
|
||||||
|
class CloudflareTunnel:
|
||||||
|
def __init__(self):
|
||||||
|
config = app.getConfig()
|
||||||
|
self.urls = None
|
||||||
|
self.port = config.get("net", {}).get("listen_port")
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
if self.port:
|
||||||
|
self.urls = try_cloudflare(self.port)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if self.urls:
|
||||||
|
try_cloudflare.terminate(self.port)
|
||||||
|
self.urls = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def address(self):
|
||||||
|
if self.urls:
|
||||||
|
return self.urls.tunnel
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
cloudflare = CloudflareTunnel()
|
||||||
|
|
||||||
|
|
||||||
|
def start_cloudflare_tunnel_internal(req: dict):
|
||||||
|
try:
|
||||||
|
cloudflare.start()
|
||||||
|
log.info(f"- Started cloudflare tunnel. Using address: {cloudflare.address}")
|
||||||
|
return JSONResponse({"address": cloudflare.address})
|
||||||
|
except Exception as e:
|
||||||
|
log.error(str(e))
|
||||||
|
log.error(traceback.format_exc())
|
||||||
|
return HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
def stop_cloudflare_tunnel_internal(req: dict):
|
||||||
|
try:
|
||||||
|
cloudflare.stop()
|
||||||
|
except Exception as e:
|
||||||
|
log.error(str(e))
|
||||||
|
log.error(traceback.format_exc())
|
||||||
|
return HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
def modify_package_internal(package_name: str, req: dict):
|
||||||
|
try:
|
||||||
|
cmd = req["command"]
|
||||||
|
if cmd not in ("install", "uninstall"):
|
||||||
|
raise RuntimeError(f"Unknown command: {cmd}")
|
||||||
|
|
||||||
|
cmd = getattr(package_manager, cmd)
|
||||||
|
cmd(package_name)
|
||||||
|
|
||||||
|
return JSONResponse({"status": "OK"}, headers=NOCACHE_HEADERS)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(str(e))
|
||||||
|
log.error(traceback.format_exc())
|
||||||
|
return HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
def get_sha256_internal(obj_path):
|
||||||
|
from easydiffusion.utils import sha256sum
|
||||||
|
|
||||||
|
path = obj_path.split("/")
|
||||||
|
type = path.pop(0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
model_path = model_manager.resolve_model_to_use("/".join(path), type)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(str(e))
|
||||||
|
log.error(traceback.format_exc())
|
||||||
|
|
||||||
|
return HTTPException(status_code=404)
|
||||||
|
try:
|
||||||
|
digest = sha256sum(model_path)
|
||||||
|
return {"digest": digest}
|
||||||
|
except Exception as e:
|
||||||
|
log.error(str(e))
|
||||||
|
log.error(traceback.format_exc())
|
||||||
|
return HTTPException(status_code=500, detail=str(e))
|
||||||
515
ui/easydiffusion/task_manager.py
Normal file
@@ -0,0 +1,515 @@
|
|||||||
|
"""task_manager.py: manage tasks dispatching and render threads.
|
||||||
|
Notes:
|
||||||
|
render_threads should be the only hard reference held by the manager to the threads.
|
||||||
|
Use weak_thread_data to store all other data using weak keys.
|
||||||
|
This will allow for garbage collection after the thread dies.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
TASK_TTL = 30 * 60 # seconds, Discard last session's task timeout
|
||||||
|
|
||||||
|
import queue
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import weakref
|
||||||
|
from typing import Any, Hashable
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from easydiffusion import device_manager
|
||||||
|
from easydiffusion.tasks import Task
|
||||||
|
from easydiffusion.utils import log
|
||||||
|
from sdkit.utils import gc
|
||||||
|
|
||||||
|
THREAD_NAME_PREFIX = ""
|
||||||
|
ERR_LOCK_FAILED = " failed to acquire lock within timeout."
|
||||||
|
LOCK_TIMEOUT = 15 # Maximum locking time in seconds before failing a task.
|
||||||
|
# It's better to get an exception than a deadlock... ALWAYS use timeout in critical paths.
|
||||||
|
|
||||||
|
DEVICE_START_TIMEOUT = 60 # seconds - Maximum time to wait for a render device to init.
|
||||||
|
MAX_OVERLOAD_ALLOWED_RATIO = 2 # i.e. 2x pending tasks compared to the number of render threads
|
||||||
|
|
||||||
|
|
||||||
|
class SymbolClass(type): # Print nicely formatted Symbol names.
|
||||||
|
def __repr__(self):
|
||||||
|
return self.__qualname__
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.__name__
|
||||||
|
|
||||||
|
|
||||||
|
class Symbol(metaclass=SymbolClass):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ServerStates:
|
||||||
|
class Init(Symbol):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class LoadingModel(Symbol):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Online(Symbol):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Rendering(Symbol):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Unavailable(Symbol):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Temporary cache to allow to query tasks results for a short time after they are completed.
|
||||||
|
class DataCache:
|
||||||
|
def __init__(self):
|
||||||
|
self._base = dict()
|
||||||
|
self._lock: threading.Lock = threading.Lock()
|
||||||
|
|
||||||
|
def _get_ttl_time(self, ttl: int) -> int:
|
||||||
|
return int(time.time()) + ttl
|
||||||
|
|
||||||
|
def _is_expired(self, timestamp: int) -> bool:
|
||||||
|
return int(time.time()) >= timestamp
|
||||||
|
|
||||||
|
def clean(self) -> None:
|
||||||
|
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
|
||||||
|
raise Exception("DataCache.clean" + ERR_LOCK_FAILED)
|
||||||
|
try:
|
||||||
|
# Create a list of expired keys to delete
|
||||||
|
to_delete = []
|
||||||
|
for key in self._base:
|
||||||
|
ttl, _ = self._base[key]
|
||||||
|
if self._is_expired(ttl):
|
||||||
|
to_delete.append(key)
|
||||||
|
# Remove Items
|
||||||
|
for key in to_delete:
|
||||||
|
(_, val) = self._base[key]
|
||||||
|
if isinstance(val, Task):
|
||||||
|
log.debug(f"Task {key} expired. Data removed.")
|
||||||
|
elif isinstance(val, SessionState):
|
||||||
|
log.debug(f"Session {key} expired. Data removed.")
|
||||||
|
else:
|
||||||
|
log.debug(f"Key {key} expired. Data removed.")
|
||||||
|
del self._base[key]
|
||||||
|
finally:
|
||||||
|
self._lock.release()
|
||||||
|
|
||||||
|
def clear(self) -> None:
|
||||||
|
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
|
||||||
|
raise Exception("DataCache.clear" + ERR_LOCK_FAILED)
|
||||||
|
try:
|
||||||
|
self._base.clear()
|
||||||
|
finally:
|
||||||
|
self._lock.release()
|
||||||
|
|
||||||
|
def delete(self, key: Hashable) -> bool:
|
||||||
|
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
|
||||||
|
raise Exception("DataCache.delete" + ERR_LOCK_FAILED)
|
||||||
|
try:
|
||||||
|
if key not in self._base:
|
||||||
|
return False
|
||||||
|
del self._base[key]
|
||||||
|
return True
|
||||||
|
finally:
|
||||||
|
self._lock.release()
|
||||||
|
|
||||||
|
def keep(self, key: Hashable, ttl: int) -> bool:
|
||||||
|
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
|
||||||
|
raise Exception("DataCache.keep" + ERR_LOCK_FAILED)
|
||||||
|
try:
|
||||||
|
if key in self._base:
|
||||||
|
_, value = self._base.get(key)
|
||||||
|
self._base[key] = (self._get_ttl_time(ttl), value)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
self._lock.release()
|
||||||
|
|
||||||
|
def put(self, key: Hashable, value: Any, ttl: int) -> bool:
|
||||||
|
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
|
||||||
|
raise Exception("DataCache.put" + ERR_LOCK_FAILED)
|
||||||
|
try:
|
||||||
|
self._base[key] = (self._get_ttl_time(ttl), value)
|
||||||
|
except Exception:
|
||||||
|
log.error(traceback.format_exc())
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
finally:
|
||||||
|
self._lock.release()
|
||||||
|
|
||||||
|
def tryGet(self, key: Hashable) -> Any:
|
||||||
|
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
|
||||||
|
raise Exception("DataCache.tryGet" + ERR_LOCK_FAILED)
|
||||||
|
try:
|
||||||
|
ttl, value = self._base.get(key, (None, None))
|
||||||
|
if ttl is not None and self._is_expired(ttl):
|
||||||
|
log.debug(f"Session {key} expired. Discarding data.")
|
||||||
|
del self._base[key]
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
finally:
|
||||||
|
self._lock.release()
|
||||||
|
|
||||||
|
|
||||||
|
manager_lock = threading.RLock()
|
||||||
|
render_threads = []
|
||||||
|
current_state = ServerStates.Init
|
||||||
|
current_state_error: Exception = None
|
||||||
|
tasks_queue = []
|
||||||
|
session_cache = DataCache()
|
||||||
|
task_cache = DataCache()
|
||||||
|
weak_thread_data = weakref.WeakKeyDictionary()
|
||||||
|
idle_event: threading.Event = threading.Event()
|
||||||
|
|
||||||
|
|
||||||
|
class SessionState:
|
||||||
|
def __init__(self, id: str):
|
||||||
|
self._id = id
|
||||||
|
self._tasks_ids = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self):
|
||||||
|
return self._id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tasks(self):
|
||||||
|
tasks = []
|
||||||
|
for task_id in self._tasks_ids:
|
||||||
|
task = task_cache.tryGet(task_id)
|
||||||
|
if task:
|
||||||
|
tasks.append(task)
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
def put(self, task: Task, ttl=TASK_TTL):
|
||||||
|
task_id = task.id
|
||||||
|
self._tasks_ids.append(task_id)
|
||||||
|
if not task_cache.put(task_id, task, ttl):
|
||||||
|
return False
|
||||||
|
while len(self._tasks_ids) > len(render_threads) * 2:
|
||||||
|
self._tasks_ids.pop(0)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def keep_task_alive(task: Task):
|
||||||
|
task_cache.keep(task.id, TASK_TTL)
|
||||||
|
session_cache.keep(task.session_id, TASK_TTL)
|
||||||
|
|
||||||
|
|
||||||
|
def thread_get_next_task():
|
||||||
|
from easydiffusion import runtime
|
||||||
|
|
||||||
|
if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
|
||||||
|
log.warn(f"Render thread on device: {runtime.context.device} failed to acquire manager lock.")
|
||||||
|
return None
|
||||||
|
if len(tasks_queue) <= 0:
|
||||||
|
manager_lock.release()
|
||||||
|
return None
|
||||||
|
task = None
|
||||||
|
try: # Select a render task.
|
||||||
|
for queued_task in tasks_queue:
|
||||||
|
if queued_task.render_device and runtime.context.device != queued_task.render_device:
|
||||||
|
# Is asking for a specific render device.
|
||||||
|
if is_alive(queued_task.render_device) > 0:
|
||||||
|
continue # requested device alive, skip current one.
|
||||||
|
else:
|
||||||
|
# Requested device is not active, return error to UI.
|
||||||
|
queued_task.error = Exception(queued_task.render_device + " is not currently active.")
|
||||||
|
task = queued_task
|
||||||
|
break
|
||||||
|
if not queued_task.render_device and runtime.context.device == "cpu" and is_alive() > 1:
|
||||||
|
# not asking for any specific devices, cpu want to grab task but other render devices are alive.
|
||||||
|
continue # Skip Tasks, don't run on CPU unless there is nothing else or user asked for it.
|
||||||
|
task = queued_task
|
||||||
|
break
|
||||||
|
if task is not None:
|
||||||
|
del tasks_queue[tasks_queue.index(task)]
|
||||||
|
return task
|
||||||
|
finally:
|
||||||
|
manager_lock.release()
|
||||||
|
|
||||||
|
|
||||||
|
def thread_render(device):
|
||||||
|
global current_state, current_state_error
|
||||||
|
|
||||||
|
from easydiffusion import model_manager, runtime
|
||||||
|
|
||||||
|
try:
|
||||||
|
runtime.init(device)
|
||||||
|
|
||||||
|
weak_thread_data[threading.current_thread()] = {
|
||||||
|
"device": runtime.context.device,
|
||||||
|
"device_name": runtime.context.device_name,
|
||||||
|
"alive": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
current_state = ServerStates.LoadingModel
|
||||||
|
model_manager.load_default_models(runtime.context)
|
||||||
|
|
||||||
|
current_state = ServerStates.Online
|
||||||
|
except Exception as e:
|
||||||
|
log.error(traceback.format_exc())
|
||||||
|
weak_thread_data[threading.current_thread()] = {"error": e, "alive": False}
|
||||||
|
return
|
||||||
|
|
||||||
|
while True:
|
||||||
|
session_cache.clean()
|
||||||
|
task_cache.clean()
|
||||||
|
if not weak_thread_data[threading.current_thread()]["alive"]:
|
||||||
|
log.info(f"Shutting down thread for device {runtime.context.device}")
|
||||||
|
model_manager.unload_all(runtime.context)
|
||||||
|
return
|
||||||
|
if isinstance(current_state_error, SystemExit):
|
||||||
|
current_state = ServerStates.Unavailable
|
||||||
|
return
|
||||||
|
task = thread_get_next_task()
|
||||||
|
if task is None:
|
||||||
|
idle_event.clear()
|
||||||
|
idle_event.wait(timeout=1)
|
||||||
|
continue
|
||||||
|
if task.error is not None:
|
||||||
|
log.error(task.error)
|
||||||
|
task.response = {"status": "failed", "detail": str(task.error)}
|
||||||
|
task.buffer_queue.put(json.dumps(task.response))
|
||||||
|
continue
|
||||||
|
if current_state_error:
|
||||||
|
task.error = current_state_error
|
||||||
|
task.response = {"status": "failed", "detail": str(task.error)}
|
||||||
|
task.buffer_queue.put(json.dumps(task.response))
|
||||||
|
continue
|
||||||
|
log.info(f"Session {task.session_id} starting task {task.id} on {runtime.context.device_name}")
|
||||||
|
if not task.lock.acquire(blocking=False):
|
||||||
|
raise Exception("Got locked task from queue.")
|
||||||
|
try:
|
||||||
|
task.run()
|
||||||
|
|
||||||
|
# Before looping back to the generator, mark cache as still alive.
|
||||||
|
keep_task_alive(task)
|
||||||
|
except Exception as e:
|
||||||
|
task.error = str(e)
|
||||||
|
task.response = {"status": "failed", "detail": str(task.error)}
|
||||||
|
task.buffer_queue.put(json.dumps(task.response))
|
||||||
|
log.error(traceback.format_exc())
|
||||||
|
finally:
|
||||||
|
gc(runtime.context)
|
||||||
|
task.lock.release()
|
||||||
|
|
||||||
|
keep_task_alive(task)
|
||||||
|
|
||||||
|
if isinstance(task.error, StopAsyncIteration):
|
||||||
|
log.info(f"Session {task.session_id} task {task.id} cancelled!")
|
||||||
|
elif task.error is not None:
|
||||||
|
log.info(f"Session {task.session_id} task {task.id} failed!")
|
||||||
|
else:
|
||||||
|
log.info(f"Session {task.session_id} task {task.id} completed by {runtime.context.device_name}.")
|
||||||
|
current_state = ServerStates.Online
|
||||||
|
|
||||||
|
|
||||||
|
def get_cached_task(task_id: str, update_ttl: bool = False):
|
||||||
|
# By calling keep before tryGet, wont discard if was expired.
|
||||||
|
if update_ttl and not task_cache.keep(task_id, TASK_TTL):
|
||||||
|
# Failed to keep task, already gone.
|
||||||
|
return None
|
||||||
|
return task_cache.tryGet(task_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_cached_session(session_id: str, update_ttl: bool = False):
|
||||||
|
if update_ttl:
|
||||||
|
session_cache.keep(session_id, TASK_TTL)
|
||||||
|
session = session_cache.tryGet(session_id)
|
||||||
|
if not session:
|
||||||
|
session = SessionState(session_id)
|
||||||
|
session_cache.put(session_id, session, TASK_TTL)
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
def get_devices():
|
||||||
|
devices = {
|
||||||
|
"all": {},
|
||||||
|
"active": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_device_info(device):
|
||||||
|
if device in ("cpu", "mps"):
|
||||||
|
return {"name": device_manager.get_processor_name()}
|
||||||
|
|
||||||
|
mem_free, mem_total = torch.cuda.mem_get_info(device)
|
||||||
|
mem_free /= float(10**9)
|
||||||
|
mem_total /= float(10**9)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": torch.cuda.get_device_name(device),
|
||||||
|
"mem_free": mem_free,
|
||||||
|
"mem_total": mem_total,
|
||||||
|
"max_vram_usage_level": device_manager.get_max_vram_usage_level(device),
|
||||||
|
}
|
||||||
|
|
||||||
|
# list the compatible devices
|
||||||
|
cuda_count = torch.cuda.device_count()
|
||||||
|
for device in range(cuda_count):
|
||||||
|
device = f"cuda:{device}"
|
||||||
|
if not device_manager.is_device_compatible(device):
|
||||||
|
continue
|
||||||
|
|
||||||
|
devices["all"].update({device: get_device_info(device)})
|
||||||
|
|
||||||
|
if device_manager.is_mps_available():
|
||||||
|
devices["all"].update({"mps": get_device_info("mps")})
|
||||||
|
|
||||||
|
devices["all"].update({"cpu": get_device_info("cpu")})
|
||||||
|
|
||||||
|
# list the activated devices
|
||||||
|
if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
|
||||||
|
raise Exception("get_devices" + ERR_LOCK_FAILED)
|
||||||
|
try:
|
||||||
|
for rthread in render_threads:
|
||||||
|
if not rthread.is_alive():
|
||||||
|
continue
|
||||||
|
weak_data = weak_thread_data.get(rthread)
|
||||||
|
if not weak_data or not "device" in weak_data or not "device_name" in weak_data:
|
||||||
|
continue
|
||||||
|
device = weak_data["device"]
|
||||||
|
devices["active"].update({device: get_device_info(device)})
|
||||||
|
finally:
|
||||||
|
manager_lock.release()
|
||||||
|
|
||||||
|
# temp until TRT releases
|
||||||
|
import os
|
||||||
|
from easydiffusion import app
|
||||||
|
|
||||||
|
devices["enable_trt"] = os.path.exists(os.path.join(app.ROOT_DIR, "tensorrt"))
|
||||||
|
|
||||||
|
return devices
|
||||||
|
|
||||||
|
|
||||||
|
def is_alive(device=None):
|
||||||
|
if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
|
||||||
|
raise Exception("is_alive" + ERR_LOCK_FAILED)
|
||||||
|
nbr_alive = 0
|
||||||
|
try:
|
||||||
|
for rthread in render_threads:
|
||||||
|
if device is not None:
|
||||||
|
weak_data = weak_thread_data.get(rthread)
|
||||||
|
if weak_data is None or not "device" in weak_data or weak_data["device"] is None:
|
||||||
|
continue
|
||||||
|
thread_device = weak_data["device"]
|
||||||
|
if thread_device != device:
|
||||||
|
continue
|
||||||
|
if rthread.is_alive():
|
||||||
|
nbr_alive += 1
|
||||||
|
return nbr_alive
|
||||||
|
finally:
|
||||||
|
manager_lock.release()
|
||||||
|
|
||||||
|
|
||||||
|
def start_render_thread(device):
|
||||||
|
if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
|
||||||
|
raise Exception("start_render_thread" + ERR_LOCK_FAILED)
|
||||||
|
log.info(f"Start new Rendering Thread on device: {device}")
|
||||||
|
try:
|
||||||
|
rthread = threading.Thread(target=thread_render, kwargs={"device": device})
|
||||||
|
rthread.daemon = True
|
||||||
|
rthread.name = THREAD_NAME_PREFIX + device
|
||||||
|
rthread.start()
|
||||||
|
render_threads.append(rthread)
|
||||||
|
finally:
|
||||||
|
manager_lock.release()
|
||||||
|
timeout = DEVICE_START_TIMEOUT
|
||||||
|
while not rthread.is_alive() or not rthread in weak_thread_data or not "device" in weak_thread_data[rthread]:
|
||||||
|
if rthread in weak_thread_data and "error" in weak_thread_data[rthread]:
|
||||||
|
log.error(f"{rthread}, {device}, error: {weak_thread_data[rthread]['error']}")
|
||||||
|
return False
|
||||||
|
if timeout <= 0:
|
||||||
|
return False
|
||||||
|
timeout -= 1
|
||||||
|
time.sleep(1)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def stop_render_thread(device):
|
||||||
|
try:
|
||||||
|
device_manager.validate_device_id(device, log_prefix="stop_render_thread")
|
||||||
|
except:
|
||||||
|
log.error(traceback.format_exc())
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
|
||||||
|
raise Exception("stop_render_thread" + ERR_LOCK_FAILED)
|
||||||
|
log.info(f"Stopping Rendering Thread on device: {device}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
thread_to_remove = None
|
||||||
|
for rthread in render_threads:
|
||||||
|
weak_data = weak_thread_data.get(rthread)
|
||||||
|
if weak_data is None or not "device" in weak_data or weak_data["device"] is None:
|
||||||
|
continue
|
||||||
|
thread_device = weak_data["device"]
|
||||||
|
if thread_device == device:
|
||||||
|
weak_data["alive"] = False
|
||||||
|
thread_to_remove = rthread
|
||||||
|
break
|
||||||
|
if thread_to_remove is not None:
|
||||||
|
render_threads.remove(rthread)
|
||||||
|
return True
|
||||||
|
finally:
|
||||||
|
manager_lock.release()
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def update_render_threads(render_devices, active_devices):
|
||||||
|
devices_to_start, devices_to_stop = device_manager.get_device_delta(render_devices, active_devices)
|
||||||
|
log.debug(f"devices_to_start: {devices_to_start}")
|
||||||
|
log.debug(f"devices_to_stop: {devices_to_stop}")
|
||||||
|
|
||||||
|
for device in devices_to_stop:
|
||||||
|
if is_alive(device) <= 0:
|
||||||
|
log.debug(f"{device} is not alive")
|
||||||
|
continue
|
||||||
|
if not stop_render_thread(device):
|
||||||
|
log.warn(f"{device} could not stop render thread")
|
||||||
|
|
||||||
|
for device in devices_to_start:
|
||||||
|
if is_alive(device) >= 1:
|
||||||
|
log.debug(f"{device} already registered.")
|
||||||
|
continue
|
||||||
|
if not start_render_thread(device):
|
||||||
|
log.warn(f"{device} failed to start.")
|
||||||
|
|
||||||
|
if is_alive() <= 0: # No running devices, probably invalid user config.
|
||||||
|
raise EnvironmentError(
|
||||||
|
'ERROR: No active render devices! Please verify the "render_devices" value in config.json'
|
||||||
|
)
|
||||||
|
|
||||||
|
log.debug(f"active devices: {get_devices()['active']}")
|
||||||
|
|
||||||
|
|
||||||
|
def shutdown_event(): # Signal render thread to close on shutdown
|
||||||
|
global current_state_error
|
||||||
|
current_state_error = SystemExit("Application shutting down.")
|
||||||
|
|
||||||
|
|
||||||
|
def enqueue_task(task: Task):
|
||||||
|
current_thread_count = is_alive()
|
||||||
|
if current_thread_count <= 0: # Render thread is dead
|
||||||
|
raise ChildProcessError("Rendering thread has died.")
|
||||||
|
|
||||||
|
# Alive, check if task in cache
|
||||||
|
session = get_cached_session(task.session_id, update_ttl=True)
|
||||||
|
pending_tasks = list(filter(lambda t: t.is_pending, session.tasks))
|
||||||
|
if len(pending_tasks) > current_thread_count * MAX_OVERLOAD_ALLOWED_RATIO:
|
||||||
|
raise ConnectionRefusedError(
|
||||||
|
f"Session {task.session_id} already has {len(pending_tasks)} pending tasks, with {current_thread_count} workers."
|
||||||
|
)
|
||||||
|
|
||||||
|
if session.put(task, TASK_TTL):
|
||||||
|
# Use twice the normal timeout for adding user requests.
|
||||||
|
# Tries to force session.put to fail before tasks_queue.put would.
|
||||||
|
if manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT * 2):
|
||||||
|
try:
|
||||||
|
tasks_queue.append(task)
|
||||||
|
idle_event.set()
|
||||||
|
return task
|
||||||
|
finally:
|
||||||
|
manager_lock.release()
|
||||||
|
raise RuntimeError("Failed to add task to cache.")
|
||||||
3
ui/easydiffusion/tasks/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .task import Task
|
||||||
|
from .render_images import RenderTask
|
||||||
|
from .filter_images import FilterTask
|
||||||
164
ui/easydiffusion/tasks/filter_images.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import pprint
|
||||||
|
import time
|
||||||
|
|
||||||
|
from numpy import base_repr
|
||||||
|
|
||||||
|
from sdkit.filter import apply_filters
|
||||||
|
from sdkit.models import load_model
|
||||||
|
from sdkit.utils import img_to_base64_str, get_image, log, save_images
|
||||||
|
|
||||||
|
from easydiffusion import model_manager, runtime
|
||||||
|
from easydiffusion.types import (
|
||||||
|
FilterImageRequest,
|
||||||
|
FilterImageResponse,
|
||||||
|
ModelsData,
|
||||||
|
OutputFormatData,
|
||||||
|
SaveToDiskData,
|
||||||
|
TaskData,
|
||||||
|
GenerateImageRequest,
|
||||||
|
)
|
||||||
|
from easydiffusion.utils.save_utils import format_folder_name
|
||||||
|
|
||||||
|
from .task import Task
|
||||||
|
|
||||||
|
|
||||||
|
class FilterTask(Task):
|
||||||
|
"For applying filters to input images"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
req: FilterImageRequest,
|
||||||
|
task_data: TaskData,
|
||||||
|
models_data: ModelsData,
|
||||||
|
output_format: OutputFormatData,
|
||||||
|
save_data: SaveToDiskData,
|
||||||
|
):
|
||||||
|
super().__init__(task_data.session_id)
|
||||||
|
|
||||||
|
task_data.request_id = self.id
|
||||||
|
|
||||||
|
self.request = req
|
||||||
|
self.task_data = task_data
|
||||||
|
self.models_data = models_data
|
||||||
|
self.output_format = output_format
|
||||||
|
self.save_data = save_data
|
||||||
|
|
||||||
|
# convert to multi-filter format, if necessary
|
||||||
|
if isinstance(req.filter, str):
|
||||||
|
req.filter_params = {req.filter: req.filter_params}
|
||||||
|
req.filter = [req.filter]
|
||||||
|
|
||||||
|
if not isinstance(req.image, list):
|
||||||
|
req.image = [req.image]
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"Runs the image filtering task on the assigned thread"
|
||||||
|
|
||||||
|
from easydiffusion import app
|
||||||
|
|
||||||
|
context = runtime.context
|
||||||
|
|
||||||
|
model_manager.resolve_model_paths(self.models_data)
|
||||||
|
model_manager.reload_models_if_necessary(context, self.models_data)
|
||||||
|
model_manager.fail_if_models_did_not_load(context)
|
||||||
|
|
||||||
|
print_task_info(self.request, self.models_data, self.output_format, self.save_data)
|
||||||
|
|
||||||
|
if isinstance(self.request.image, list):
|
||||||
|
images = [get_image(img) for img in self.request.image]
|
||||||
|
else:
|
||||||
|
images = get_image(self.request.image)
|
||||||
|
|
||||||
|
images = filter_images(context, images, self.request.filter, self.request.filter_params)
|
||||||
|
|
||||||
|
output_format = self.output_format
|
||||||
|
|
||||||
|
if self.save_data.save_to_disk_path is not None:
|
||||||
|
app_config = app.getConfig()
|
||||||
|
folder_format = app_config.get("folder_format", "$id")
|
||||||
|
|
||||||
|
dummy_req = GenerateImageRequest()
|
||||||
|
img_id = base_repr(int(time.time() * 10000), 36)[-7:] # Base 36 conversion, 0-9, A-Z
|
||||||
|
|
||||||
|
save_dir_path = os.path.join(
|
||||||
|
self.save_data.save_to_disk_path, format_folder_name(folder_format, dummy_req, self.task_data)
|
||||||
|
)
|
||||||
|
save_images(
|
||||||
|
images,
|
||||||
|
save_dir_path,
|
||||||
|
file_name=img_id,
|
||||||
|
output_format=output_format.output_format,
|
||||||
|
output_quality=output_format.output_quality,
|
||||||
|
output_lossless=output_format.output_lossless,
|
||||||
|
)
|
||||||
|
|
||||||
|
images = [
|
||||||
|
img_to_base64_str(
|
||||||
|
img, output_format.output_format, output_format.output_quality, output_format.output_lossless
|
||||||
|
)
|
||||||
|
for img in images
|
||||||
|
]
|
||||||
|
|
||||||
|
res = FilterImageResponse(self.request, self.models_data, images=images)
|
||||||
|
res = res.json()
|
||||||
|
self.buffer_queue.put(json.dumps(res))
|
||||||
|
|
||||||
|
log.info("Filter task completed")
|
||||||
|
|
||||||
|
self.response = res
|
||||||
|
|
||||||
|
|
||||||
|
def filter_images(context, images, filters, filter_params={}):
|
||||||
|
filters = filters if isinstance(filters, list) else [filters]
|
||||||
|
|
||||||
|
for filter_name in filters:
|
||||||
|
params = filter_params.get(filter_name, {})
|
||||||
|
|
||||||
|
previous_state = before_filter(context, filter_name, params)
|
||||||
|
|
||||||
|
try:
|
||||||
|
images = apply_filters(context, filter_name, images, **params)
|
||||||
|
finally:
|
||||||
|
after_filter(context, filter_name, params, previous_state)
|
||||||
|
|
||||||
|
return images
|
||||||
|
|
||||||
|
|
||||||
|
def before_filter(context, filter_name, filter_params):
|
||||||
|
if filter_name == "codeformer":
|
||||||
|
from easydiffusion.model_manager import DEFAULT_MODELS, resolve_model_to_use
|
||||||
|
|
||||||
|
default_realesrgan = DEFAULT_MODELS["realesrgan"][0]["file_name"]
|
||||||
|
prev_realesrgan_path = None
|
||||||
|
|
||||||
|
upscale_faces = filter_params.get("upscale_faces", False)
|
||||||
|
if upscale_faces and default_realesrgan not in context.model_paths["realesrgan"]:
|
||||||
|
prev_realesrgan_path = context.model_paths.get("realesrgan")
|
||||||
|
context.model_paths["realesrgan"] = resolve_model_to_use(default_realesrgan, "realesrgan")
|
||||||
|
load_model(context, "realesrgan")
|
||||||
|
|
||||||
|
return prev_realesrgan_path
|
||||||
|
|
||||||
|
|
||||||
|
def after_filter(context, filter_name, filter_params, previous_state):
|
||||||
|
if filter_name == "codeformer":
|
||||||
|
prev_realesrgan_path = previous_state
|
||||||
|
if prev_realesrgan_path:
|
||||||
|
context.model_paths["realesrgan"] = prev_realesrgan_path
|
||||||
|
load_model(context, "realesrgan")
|
||||||
|
|
||||||
|
|
||||||
|
def print_task_info(
|
||||||
|
req: FilterImageRequest, models_data: ModelsData, output_format: OutputFormatData, save_data: SaveToDiskData
|
||||||
|
):
|
||||||
|
req_str = pprint.pformat({"filter": req.filter, "filter_params": req.filter_params}).replace("[", "\[")
|
||||||
|
models_data = pprint.pformat(models_data.dict()).replace("[", "\[")
|
||||||
|
output_format = pprint.pformat(output_format.dict()).replace("[", "\[")
|
||||||
|
save_data = pprint.pformat(save_data.dict()).replace("[", "\[")
|
||||||
|
|
||||||
|
log.info(f"request: {req_str}")
|
||||||
|
log.info(f"models data: {models_data}")
|
||||||
|
log.info(f"output format: {output_format}")
|
||||||
|
log.info(f"save data: {save_data}")
|
||||||
378
ui/easydiffusion/tasks/render_images.py
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
import json
|
||||||
|
import pprint
|
||||||
|
import queue
|
||||||
|
import time
|
||||||
|
|
||||||
|
from easydiffusion import model_manager, runtime
|
||||||
|
from easydiffusion.types import GenerateImageRequest, ModelsData, OutputFormatData, SaveToDiskData
|
||||||
|
from easydiffusion.types import Image as ResponseImage
|
||||||
|
from easydiffusion.types import GenerateImageResponse, RenderTaskData, UserInitiatedStop
|
||||||
|
from easydiffusion.utils import get_printable_request, log, save_images_to_disk
|
||||||
|
from sdkit.generate import generate_images
|
||||||
|
from sdkit.utils import (
|
||||||
|
diffusers_latent_samples_to_images,
|
||||||
|
gc,
|
||||||
|
img_to_base64_str,
|
||||||
|
img_to_buffer,
|
||||||
|
latent_samples_to_images,
|
||||||
|
resize_img,
|
||||||
|
get_image,
|
||||||
|
log,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .task import Task
|
||||||
|
from .filter_images import filter_images
|
||||||
|
|
||||||
|
|
||||||
|
class RenderTask(Task):
|
||||||
|
"For image generation"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
req: GenerateImageRequest,
|
||||||
|
task_data: RenderTaskData,
|
||||||
|
models_data: ModelsData,
|
||||||
|
output_format: OutputFormatData,
|
||||||
|
save_data: SaveToDiskData,
|
||||||
|
):
|
||||||
|
super().__init__(task_data.session_id)
|
||||||
|
|
||||||
|
task_data.request_id = self.id
|
||||||
|
|
||||||
|
self.render_request = req # Initial Request
|
||||||
|
self.task_data = task_data
|
||||||
|
self.models_data = models_data
|
||||||
|
self.output_format = output_format
|
||||||
|
self.save_data = save_data
|
||||||
|
|
||||||
|
self.temp_images: list = [None] * req.num_outputs * (1 if task_data.show_only_filtered_image else 2)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"Runs the image generation task on the assigned thread"
|
||||||
|
|
||||||
|
from easydiffusion import task_manager, app
|
||||||
|
|
||||||
|
context = runtime.context
|
||||||
|
config = app.getConfig()
|
||||||
|
|
||||||
|
if config.get("block_nsfw", False): # override if set on the server
|
||||||
|
self.task_data.block_nsfw = True
|
||||||
|
if "nsfw_checker" not in self.task_data.filters:
|
||||||
|
self.task_data.filters.append("nsfw_checker")
|
||||||
|
self.models_data.model_paths["nsfw_checker"] = "nsfw_checker"
|
||||||
|
|
||||||
|
def step_callback():
|
||||||
|
task_manager.keep_task_alive(self)
|
||||||
|
task_manager.current_state = task_manager.ServerStates.Rendering
|
||||||
|
|
||||||
|
if isinstance(task_manager.current_state_error, (SystemExit, StopAsyncIteration)) or isinstance(
|
||||||
|
self.error, StopAsyncIteration
|
||||||
|
):
|
||||||
|
context.stop_processing = True
|
||||||
|
if isinstance(task_manager.current_state_error, StopAsyncIteration):
|
||||||
|
self.error = task_manager.current_state_error
|
||||||
|
task_manager.current_state_error = None
|
||||||
|
log.info(f"Session {self.session_id} sent cancel signal for task {self.id}")
|
||||||
|
|
||||||
|
task_manager.current_state = task_manager.ServerStates.LoadingModel
|
||||||
|
model_manager.resolve_model_paths(self.models_data)
|
||||||
|
|
||||||
|
models_to_force_reload = []
|
||||||
|
if (
|
||||||
|
runtime.set_vram_optimizations(context)
|
||||||
|
or self.has_param_changed(context, "clip_skip")
|
||||||
|
or self.trt_needs_reload(context)
|
||||||
|
):
|
||||||
|
models_to_force_reload.append("stable-diffusion")
|
||||||
|
|
||||||
|
model_manager.reload_models_if_necessary(context, self.models_data, models_to_force_reload)
|
||||||
|
model_manager.fail_if_models_did_not_load(context)
|
||||||
|
|
||||||
|
task_manager.current_state = task_manager.ServerStates.Rendering
|
||||||
|
self.response = make_images(
|
||||||
|
context,
|
||||||
|
self.render_request,
|
||||||
|
self.task_data,
|
||||||
|
self.models_data,
|
||||||
|
self.output_format,
|
||||||
|
self.save_data,
|
||||||
|
self.buffer_queue,
|
||||||
|
self.temp_images,
|
||||||
|
step_callback,
|
||||||
|
)
|
||||||
|
|
||||||
|
def has_param_changed(self, context, param_name):
|
||||||
|
if not context.test_diffusers:
|
||||||
|
return False
|
||||||
|
if "stable-diffusion" not in context.models or "params" not in context.models["stable-diffusion"]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
model = context.models["stable-diffusion"]
|
||||||
|
new_val = self.models_data.model_params.get("stable-diffusion", {}).get(param_name, False)
|
||||||
|
return model["params"].get(param_name) != new_val
|
||||||
|
|
||||||
|
def trt_needs_reload(self, context):
|
||||||
|
if not context.test_diffusers:
|
||||||
|
return False
|
||||||
|
if "stable-diffusion" not in context.models or "params" not in context.models["stable-diffusion"]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
model = context.models["stable-diffusion"]
|
||||||
|
|
||||||
|
# curr_convert_to_trt = model["params"].get("convert_to_tensorrt")
|
||||||
|
new_convert_to_trt = self.models_data.model_params.get("stable-diffusion", {}).get("convert_to_tensorrt", False)
|
||||||
|
|
||||||
|
pipe = model["default"]
|
||||||
|
is_trt_loaded = hasattr(pipe.unet, "_allocate_trt_buffers") or hasattr(
|
||||||
|
pipe.unet, "_allocate_trt_buffers_backup"
|
||||||
|
)
|
||||||
|
if new_convert_to_trt and not is_trt_loaded:
|
||||||
|
return True
|
||||||
|
|
||||||
|
curr_build_config = model["params"].get("trt_build_config")
|
||||||
|
new_build_config = self.models_data.model_params.get("stable-diffusion", {}).get("trt_build_config", {})
|
||||||
|
|
||||||
|
return new_convert_to_trt and curr_build_config != new_build_config
|
||||||
|
|
||||||
|
|
||||||
|
def make_images(
|
||||||
|
context,
|
||||||
|
req: GenerateImageRequest,
|
||||||
|
task_data: RenderTaskData,
|
||||||
|
models_data: ModelsData,
|
||||||
|
output_format: OutputFormatData,
|
||||||
|
save_data: SaveToDiskData,
|
||||||
|
data_queue: queue.Queue,
|
||||||
|
task_temp_images: list,
|
||||||
|
step_callback,
|
||||||
|
):
|
||||||
|
context.stop_processing = False
|
||||||
|
print_task_info(req, task_data, models_data, output_format, save_data)
|
||||||
|
|
||||||
|
images, seeds = make_images_internal(
|
||||||
|
context, req, task_data, models_data, output_format, save_data, data_queue, task_temp_images, step_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
res = GenerateImageResponse(
|
||||||
|
req, task_data, models_data, output_format, save_data, images=construct_response(images, seeds, output_format)
|
||||||
|
)
|
||||||
|
res = res.json()
|
||||||
|
data_queue.put(json.dumps(res))
|
||||||
|
log.info("Task completed")
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def print_task_info(
|
||||||
|
req: GenerateImageRequest,
|
||||||
|
task_data: RenderTaskData,
|
||||||
|
models_data: ModelsData,
|
||||||
|
output_format: OutputFormatData,
|
||||||
|
save_data: SaveToDiskData,
|
||||||
|
):
|
||||||
|
req_str = pprint.pformat(get_printable_request(req, task_data, models_data, output_format, save_data)).replace("[", "\[")
|
||||||
|
task_str = pprint.pformat(task_data.dict()).replace("[", "\[")
|
||||||
|
models_data = pprint.pformat(models_data.dict()).replace("[", "\[")
|
||||||
|
output_format = pprint.pformat(output_format.dict()).replace("[", "\[")
|
||||||
|
save_data = pprint.pformat(save_data.dict()).replace("[", "\[")
|
||||||
|
|
||||||
|
log.info(f"request: {req_str}")
|
||||||
|
log.info(f"task data: {task_str}")
|
||||||
|
# log.info(f"models data: {models_data}")
|
||||||
|
log.info(f"output format: {output_format}")
|
||||||
|
log.info(f"save data: {save_data}")
|
||||||
|
|
||||||
|
|
||||||
|
def make_images_internal(
|
||||||
|
context,
|
||||||
|
req: GenerateImageRequest,
|
||||||
|
task_data: RenderTaskData,
|
||||||
|
models_data: ModelsData,
|
||||||
|
output_format: OutputFormatData,
|
||||||
|
save_data: SaveToDiskData,
|
||||||
|
data_queue: queue.Queue,
|
||||||
|
task_temp_images: list,
|
||||||
|
step_callback,
|
||||||
|
):
|
||||||
|
images, user_stopped = generate_images_internal(
|
||||||
|
context,
|
||||||
|
req,
|
||||||
|
task_data,
|
||||||
|
models_data,
|
||||||
|
data_queue,
|
||||||
|
task_temp_images,
|
||||||
|
step_callback,
|
||||||
|
task_data.stream_image_progress,
|
||||||
|
task_data.stream_image_progress_interval,
|
||||||
|
)
|
||||||
|
|
||||||
|
gc(context)
|
||||||
|
|
||||||
|
filters, filter_params = task_data.filters, task_data.filter_params
|
||||||
|
filtered_images = filter_images(context, images, filters, filter_params) if not user_stopped else images
|
||||||
|
|
||||||
|
if save_data.save_to_disk_path is not None:
|
||||||
|
save_images_to_disk(images, filtered_images, req, task_data, models_data, output_format, save_data)
|
||||||
|
|
||||||
|
seeds = [*range(req.seed, req.seed + len(images))]
|
||||||
|
if task_data.show_only_filtered_image or filtered_images is images:
|
||||||
|
return filtered_images, seeds
|
||||||
|
else:
|
||||||
|
return images + filtered_images, seeds + seeds
|
||||||
|
|
||||||
|
|
||||||
|
def generate_images_internal(
|
||||||
|
context,
|
||||||
|
req: GenerateImageRequest,
|
||||||
|
task_data: RenderTaskData,
|
||||||
|
models_data: ModelsData,
|
||||||
|
data_queue: queue.Queue,
|
||||||
|
task_temp_images: list,
|
||||||
|
step_callback,
|
||||||
|
stream_image_progress: bool,
|
||||||
|
stream_image_progress_interval: int,
|
||||||
|
):
|
||||||
|
context.temp_images.clear()
|
||||||
|
|
||||||
|
callback = make_step_callback(
|
||||||
|
context,
|
||||||
|
req,
|
||||||
|
task_data,
|
||||||
|
data_queue,
|
||||||
|
task_temp_images,
|
||||||
|
step_callback,
|
||||||
|
stream_image_progress,
|
||||||
|
stream_image_progress_interval,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if req.init_image is not None and not context.test_diffusers:
|
||||||
|
req.sampler_name = "ddim"
|
||||||
|
|
||||||
|
req.width, req.height = map(lambda x: x - x % 8, (req.width, req.height)) # clamp to 8
|
||||||
|
|
||||||
|
if req.control_image and task_data.control_filter_to_apply:
|
||||||
|
req.control_image = get_image(req.control_image)
|
||||||
|
req.control_image = resize_img(req.control_image.convert("RGB"), req.width, req.height, clamp_to_8=True)
|
||||||
|
req.control_image = filter_images(context, req.control_image, task_data.control_filter_to_apply)[0]
|
||||||
|
|
||||||
|
if req.init_image is not None and int(req.num_inference_steps * req.prompt_strength) == 0:
|
||||||
|
req.prompt_strength = 1 / req.num_inference_steps if req.num_inference_steps > 0 else 1
|
||||||
|
|
||||||
|
if context.test_diffusers:
|
||||||
|
pipe = context.models["stable-diffusion"]["default"]
|
||||||
|
if hasattr(pipe.unet, "_allocate_trt_buffers_backup"):
|
||||||
|
setattr(pipe.unet, "_allocate_trt_buffers", pipe.unet._allocate_trt_buffers_backup)
|
||||||
|
delattr(pipe.unet, "_allocate_trt_buffers_backup")
|
||||||
|
|
||||||
|
if hasattr(pipe.unet, "_allocate_trt_buffers"):
|
||||||
|
convert_to_trt = models_data.model_params["stable-diffusion"].get("convert_to_tensorrt", False)
|
||||||
|
if convert_to_trt:
|
||||||
|
pipe.unet.forward = pipe.unet._trt_forward
|
||||||
|
# pipe.vae.decoder.forward = pipe.vae.decoder._trt_forward
|
||||||
|
log.info(f"Setting unet.forward to TensorRT")
|
||||||
|
else:
|
||||||
|
log.info(f"Not using TensorRT for unet.forward")
|
||||||
|
pipe.unet.forward = pipe.unet._non_trt_forward
|
||||||
|
# pipe.vae.decoder.forward = pipe.vae.decoder._non_trt_forward
|
||||||
|
setattr(pipe.unet, "_allocate_trt_buffers_backup", pipe.unet._allocate_trt_buffers)
|
||||||
|
delattr(pipe.unet, "_allocate_trt_buffers")
|
||||||
|
|
||||||
|
if task_data.enable_vae_tiling:
|
||||||
|
if hasattr(pipe, "enable_vae_tiling"):
|
||||||
|
pipe.enable_vae_tiling()
|
||||||
|
else:
|
||||||
|
if hasattr(pipe, "disable_vae_tiling"):
|
||||||
|
pipe.disable_vae_tiling()
|
||||||
|
|
||||||
|
images = generate_images(context, callback=callback, **req.dict())
|
||||||
|
user_stopped = False
|
||||||
|
except UserInitiatedStop:
|
||||||
|
images = []
|
||||||
|
user_stopped = True
|
||||||
|
if context.partial_x_samples is not None:
|
||||||
|
if context.test_diffusers:
|
||||||
|
images = diffusers_latent_samples_to_images(context, context.partial_x_samples)
|
||||||
|
else:
|
||||||
|
images = latent_samples_to_images(context, context.partial_x_samples)
|
||||||
|
finally:
|
||||||
|
if hasattr(context, "partial_x_samples") and context.partial_x_samples is not None:
|
||||||
|
if not context.test_diffusers:
|
||||||
|
del context.partial_x_samples
|
||||||
|
context.partial_x_samples = None
|
||||||
|
|
||||||
|
return images, user_stopped
|
||||||
|
|
||||||
|
|
||||||
|
def construct_response(images: list, seeds: list, output_format: OutputFormatData):
|
||||||
|
return [
|
||||||
|
ResponseImage(
|
||||||
|
data=img_to_base64_str(
|
||||||
|
img,
|
||||||
|
output_format.output_format,
|
||||||
|
output_format.output_quality,
|
||||||
|
output_format.output_lossless,
|
||||||
|
),
|
||||||
|
seed=seed,
|
||||||
|
)
|
||||||
|
for img, seed in zip(images, seeds)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def make_step_callback(
|
||||||
|
context,
|
||||||
|
req: GenerateImageRequest,
|
||||||
|
task_data: RenderTaskData,
|
||||||
|
data_queue: queue.Queue,
|
||||||
|
task_temp_images: list,
|
||||||
|
step_callback,
|
||||||
|
stream_image_progress: bool,
|
||||||
|
stream_image_progress_interval: int,
|
||||||
|
):
|
||||||
|
n_steps = req.num_inference_steps if req.init_image is None else int(req.num_inference_steps * req.prompt_strength)
|
||||||
|
last_callback_time = -1
|
||||||
|
|
||||||
|
def update_temp_img(x_samples, task_temp_images: list):
|
||||||
|
partial_images = []
|
||||||
|
|
||||||
|
if context.test_diffusers:
|
||||||
|
images = diffusers_latent_samples_to_images(context, x_samples)
|
||||||
|
else:
|
||||||
|
images = latent_samples_to_images(context, x_samples)
|
||||||
|
|
||||||
|
if task_data.block_nsfw:
|
||||||
|
images = filter_images(context, images, "nsfw_checker")
|
||||||
|
|
||||||
|
for i, img in enumerate(images):
|
||||||
|
buf = img_to_buffer(img, output_format="JPEG")
|
||||||
|
|
||||||
|
context.temp_images[f"{task_data.request_id}/{i}"] = buf
|
||||||
|
task_temp_images[i] = buf
|
||||||
|
partial_images.append({"path": f"/image/tmp/{task_data.request_id}/{i}"})
|
||||||
|
del images
|
||||||
|
return partial_images
|
||||||
|
|
||||||
|
def on_image_step(x_samples, i, *args):
|
||||||
|
nonlocal last_callback_time
|
||||||
|
|
||||||
|
if context.test_diffusers:
|
||||||
|
context.partial_x_samples = (x_samples, args[0])
|
||||||
|
else:
|
||||||
|
context.partial_x_samples = x_samples
|
||||||
|
|
||||||
|
step_time = time.time() - last_callback_time if last_callback_time != -1 else -1
|
||||||
|
last_callback_time = time.time()
|
||||||
|
|
||||||
|
progress = {"step": i, "step_time": step_time, "total_steps": n_steps}
|
||||||
|
|
||||||
|
if stream_image_progress and stream_image_progress_interval > 0 and i % stream_image_progress_interval == 0:
|
||||||
|
progress["output"] = update_temp_img(context.partial_x_samples, task_temp_images)
|
||||||
|
|
||||||
|
data_queue.put(json.dumps(progress))
|
||||||
|
|
||||||
|
step_callback()
|
||||||
|
|
||||||
|
if context.stop_processing:
|
||||||
|
raise UserInitiatedStop("User requested that we stop processing")
|
||||||
|
|
||||||
|
return on_image_step
|
||||||
47
ui/easydiffusion/tasks/task.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from threading import Lock
|
||||||
|
from queue import Queue, Empty as EmptyQueueException
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class Task:
|
||||||
|
"Task with output queue and completion lock"
|
||||||
|
|
||||||
|
def __init__(self, session_id):
|
||||||
|
self.id = id(self)
|
||||||
|
self.session_id = session_id
|
||||||
|
self.render_device = None # Select the task affinity. (Not used to change active devices).
|
||||||
|
self.error: Exception = None
|
||||||
|
self.lock: Lock = Lock() # Locks at task start and unlocks when task is completed
|
||||||
|
self.buffer_queue: Queue = Queue() # Queue of JSON string segments
|
||||||
|
self.response: Any = None # Copy of the last reponse
|
||||||
|
|
||||||
|
async def read_buffer_generator(self):
|
||||||
|
try:
|
||||||
|
while not self.buffer_queue.empty():
|
||||||
|
res = self.buffer_queue.get(block=False)
|
||||||
|
self.buffer_queue.task_done()
|
||||||
|
yield res
|
||||||
|
except EmptyQueueException as e:
|
||||||
|
yield
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self):
|
||||||
|
if self.lock.locked():
|
||||||
|
return "running"
|
||||||
|
if isinstance(self.error, StopAsyncIteration):
|
||||||
|
return "stopped"
|
||||||
|
if self.error:
|
||||||
|
return "error"
|
||||||
|
if not self.buffer_queue.empty():
|
||||||
|
return "buffer"
|
||||||
|
if self.response:
|
||||||
|
return "completed"
|
||||||
|
return "pending"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_pending(self):
|
||||||
|
return bool(not self.response and not self.error)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"Override this to implement the task's behavior"
|
||||||
|
pass
|
||||||
277
ui/easydiffusion/types.py
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
from typing import Any, List, Dict, Union
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class GenerateImageRequest(BaseModel):
|
||||||
|
prompt: str = ""
|
||||||
|
negative_prompt: str = ""
|
||||||
|
|
||||||
|
seed: int = 42
|
||||||
|
width: int = 512
|
||||||
|
height: int = 512
|
||||||
|
|
||||||
|
num_outputs: int = 1
|
||||||
|
num_inference_steps: int = 50
|
||||||
|
guidance_scale: float = 7.5
|
||||||
|
|
||||||
|
init_image: Any = None
|
||||||
|
init_image_mask: Any = None
|
||||||
|
control_image: Any = None
|
||||||
|
control_alpha: Union[float, List[float]] = None
|
||||||
|
prompt_strength: float = 0.8
|
||||||
|
preserve_init_image_color_profile = False
|
||||||
|
strict_mask_border = False
|
||||||
|
|
||||||
|
sampler_name: str = None # "ddim", "plms", "heun", "euler", "euler_a", "dpm2", "dpm2_a", "lms"
|
||||||
|
hypernetwork_strength: float = 0
|
||||||
|
lora_alpha: Union[float, List[float]] = 0
|
||||||
|
tiling: str = None # None, "x", "y", "xy"
|
||||||
|
|
||||||
|
|
||||||
|
class FilterImageRequest(BaseModel):
|
||||||
|
image: Any = None
|
||||||
|
filter: Union[str, List[str]] = None
|
||||||
|
filter_params: dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
class ModelsData(BaseModel):
|
||||||
|
"""
|
||||||
|
Contains the information related to the models involved in a request.
|
||||||
|
|
||||||
|
- To load a model: set the relative path(s) to the model in `model_paths`. No effect if already loaded.
|
||||||
|
- To unload a model: set the model to `None` in `model_paths`. No effect if already unloaded.
|
||||||
|
|
||||||
|
Models that aren't present in `model_paths` will not be changed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model_paths: Dict[str, Union[str, None, List[str]]] = None
|
||||||
|
"model_type to string path, or list of string paths"
|
||||||
|
|
||||||
|
model_params: Dict[str, Dict[str, Any]] = {}
|
||||||
|
"model_type to dict of parameters"
|
||||||
|
|
||||||
|
|
||||||
|
class OutputFormatData(BaseModel):
|
||||||
|
output_format: str = "jpeg" # or "png" or "webp"
|
||||||
|
output_quality: int = 75
|
||||||
|
output_lossless: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class SaveToDiskData(BaseModel):
|
||||||
|
save_to_disk_path: str = None
|
||||||
|
metadata_output_format: str = "txt" # or "json"
|
||||||
|
|
||||||
|
|
||||||
|
class TaskData(BaseModel):
|
||||||
|
request_id: str = None
|
||||||
|
session_id: str = "session"
|
||||||
|
|
||||||
|
|
||||||
|
class RenderTaskData(TaskData):
|
||||||
|
vram_usage_level: str = "balanced" # or "low" or "medium"
|
||||||
|
|
||||||
|
use_face_correction: Union[str, List[str]] = None # or "GFPGANv1.3"
|
||||||
|
use_upscale: Union[str, List[str]] = None
|
||||||
|
upscale_amount: int = 4 # or 2
|
||||||
|
latent_upscaler_steps: int = 10
|
||||||
|
use_stable_diffusion_model: Union[str, List[str]] = "sd-v1-4"
|
||||||
|
use_vae_model: Union[str, List[str]] = None
|
||||||
|
use_hypernetwork_model: Union[str, List[str]] = None
|
||||||
|
use_lora_model: Union[str, List[str]] = None
|
||||||
|
use_controlnet_model: Union[str, List[str]] = None
|
||||||
|
use_embeddings_model: Union[str, List[str]] = None
|
||||||
|
filters: List[str] = []
|
||||||
|
filter_params: Dict[str, Dict[str, Any]] = {}
|
||||||
|
control_filter_to_apply: Union[str, List[str]] = None
|
||||||
|
enable_vae_tiling: bool = True
|
||||||
|
|
||||||
|
show_only_filtered_image: bool = False
|
||||||
|
block_nsfw: bool = False
|
||||||
|
stream_image_progress: bool = False
|
||||||
|
stream_image_progress_interval: int = 5
|
||||||
|
clip_skip: bool = False
|
||||||
|
codeformer_upscale_faces: bool = False
|
||||||
|
codeformer_fidelity: float = 0.5
|
||||||
|
|
||||||
|
|
||||||
|
class MergeRequest(BaseModel):
|
||||||
|
model0: str = None
|
||||||
|
model1: str = None
|
||||||
|
ratio: float = None
|
||||||
|
out_path: str = "mix"
|
||||||
|
use_fp16 = True
|
||||||
|
|
||||||
|
|
||||||
|
class Image:
|
||||||
|
data: str # base64
|
||||||
|
seed: int
|
||||||
|
is_nsfw: bool
|
||||||
|
path_abs: str = None
|
||||||
|
|
||||||
|
def __init__(self, data, seed):
|
||||||
|
self.data = data
|
||||||
|
self.seed = seed
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return {
|
||||||
|
"data": self.data,
|
||||||
|
"seed": self.seed,
|
||||||
|
"path_abs": self.path_abs,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class GenerateImageResponse:
|
||||||
|
render_request: GenerateImageRequest
|
||||||
|
task_data: TaskData
|
||||||
|
models_data: ModelsData
|
||||||
|
images: list
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
render_request: GenerateImageRequest,
|
||||||
|
task_data: TaskData,
|
||||||
|
models_data: ModelsData,
|
||||||
|
output_format: OutputFormatData,
|
||||||
|
save_data: SaveToDiskData,
|
||||||
|
images: list,
|
||||||
|
):
|
||||||
|
self.render_request = render_request
|
||||||
|
self.task_data = task_data
|
||||||
|
self.models_data = models_data
|
||||||
|
self.output_format = output_format
|
||||||
|
self.save_data = save_data
|
||||||
|
self.images = images
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
del self.render_request.init_image
|
||||||
|
del self.render_request.init_image_mask
|
||||||
|
del self.render_request.control_image
|
||||||
|
|
||||||
|
task_data = self.task_data.dict()
|
||||||
|
task_data.update(self.output_format.dict())
|
||||||
|
task_data.update(self.save_data.dict())
|
||||||
|
|
||||||
|
res = {
|
||||||
|
"status": "succeeded",
|
||||||
|
"render_request": self.render_request.dict(),
|
||||||
|
"task_data": task_data,
|
||||||
|
# "models_data": self.models_data.dict(), # haven't migrated the UI to the new format (yet)
|
||||||
|
"output": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
for image in self.images:
|
||||||
|
res["output"].append(image.json())
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class FilterImageResponse:
|
||||||
|
request: FilterImageRequest
|
||||||
|
models_data: ModelsData
|
||||||
|
images: list
|
||||||
|
|
||||||
|
def __init__(self, request: FilterImageRequest, models_data: ModelsData, images: list):
|
||||||
|
self.request = request
|
||||||
|
self.models_data = models_data
|
||||||
|
self.images = images
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
del self.request.image
|
||||||
|
|
||||||
|
res = {
|
||||||
|
"status": "succeeded",
|
||||||
|
"request": self.request.dict(),
|
||||||
|
"models_data": self.models_data.dict(),
|
||||||
|
"output": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
for image in self.images:
|
||||||
|
res["output"].append(image)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class UserInitiatedStop(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def convert_legacy_render_req_to_new(old_req: dict):
|
||||||
|
new_req = dict(old_req)
|
||||||
|
|
||||||
|
# new keys
|
||||||
|
model_paths = new_req["model_paths"] = {}
|
||||||
|
model_params = new_req["model_params"] = {}
|
||||||
|
filters = new_req["filters"] = []
|
||||||
|
filter_params = new_req["filter_params"] = {}
|
||||||
|
|
||||||
|
# move the model info
|
||||||
|
model_paths["stable-diffusion"] = old_req.get("use_stable_diffusion_model")
|
||||||
|
model_paths["vae"] = old_req.get("use_vae_model")
|
||||||
|
model_paths["hypernetwork"] = old_req.get("use_hypernetwork_model")
|
||||||
|
model_paths["lora"] = old_req.get("use_lora_model")
|
||||||
|
model_paths["controlnet"] = old_req.get("use_controlnet_model")
|
||||||
|
model_paths["embeddings"] = old_req.get("use_embeddings_model")
|
||||||
|
|
||||||
|
model_paths["gfpgan"] = old_req.get("use_face_correction", "")
|
||||||
|
model_paths["gfpgan"] = model_paths["gfpgan"] if "gfpgan" in model_paths["gfpgan"].lower() else None
|
||||||
|
|
||||||
|
model_paths["codeformer"] = old_req.get("use_face_correction", "")
|
||||||
|
model_paths["codeformer"] = model_paths["codeformer"] if "codeformer" in model_paths["codeformer"].lower() else None
|
||||||
|
|
||||||
|
model_paths["realesrgan"] = old_req.get("use_upscale", "")
|
||||||
|
model_paths["realesrgan"] = model_paths["realesrgan"] if "realesrgan" in model_paths["realesrgan"].lower() else None
|
||||||
|
|
||||||
|
model_paths["latent_upscaler"] = old_req.get("use_upscale", "")
|
||||||
|
model_paths["latent_upscaler"] = (
|
||||||
|
model_paths["latent_upscaler"] if "latent_upscaler" in model_paths["latent_upscaler"].lower() else None
|
||||||
|
)
|
||||||
|
if "control_filter_to_apply" in old_req:
|
||||||
|
filter_model = old_req["control_filter_to_apply"]
|
||||||
|
model_paths[filter_model] = filter_model
|
||||||
|
|
||||||
|
if old_req.get("block_nsfw"):
|
||||||
|
model_paths["nsfw_checker"] = "nsfw_checker"
|
||||||
|
|
||||||
|
# move the model params
|
||||||
|
if model_paths["stable-diffusion"]:
|
||||||
|
model_params["stable-diffusion"] = {
|
||||||
|
"clip_skip": bool(old_req.get("clip_skip", False)),
|
||||||
|
"convert_to_tensorrt": bool(old_req.get("convert_to_tensorrt", False)),
|
||||||
|
"trt_build_config": old_req.get(
|
||||||
|
"trt_build_config", {"batch_size_range": (1, 1), "dimensions_range": [(768, 1024)]}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# move the filter params
|
||||||
|
if model_paths["realesrgan"]:
|
||||||
|
filter_params["realesrgan"] = {"scale": int(old_req.get("upscale_amount", 4))}
|
||||||
|
if model_paths["latent_upscaler"]:
|
||||||
|
filter_params["latent_upscaler"] = {
|
||||||
|
"prompt": old_req["prompt"],
|
||||||
|
"negative_prompt": old_req.get("negative_prompt"),
|
||||||
|
"seed": int(old_req.get("seed", 42)),
|
||||||
|
"num_inference_steps": int(old_req.get("latent_upscaler_steps", 10)),
|
||||||
|
"guidance_scale": 0,
|
||||||
|
}
|
||||||
|
if model_paths["codeformer"]:
|
||||||
|
filter_params["codeformer"] = {
|
||||||
|
"upscale_faces": bool(old_req.get("codeformer_upscale_faces", True)),
|
||||||
|
"codeformer_fidelity": float(old_req.get("codeformer_fidelity", 0.5)),
|
||||||
|
}
|
||||||
|
|
||||||
|
# set the filters
|
||||||
|
if old_req.get("block_nsfw"):
|
||||||
|
filters.append("nsfw_checker")
|
||||||
|
|
||||||
|
if model_paths["codeformer"]:
|
||||||
|
filters.append("codeformer")
|
||||||
|
elif model_paths["gfpgan"]:
|
||||||
|
filters.append("gfpgan")
|
||||||
|
|
||||||
|
if model_paths["realesrgan"]:
|
||||||
|
filters.append("realesrgan")
|
||||||
|
elif model_paths["latent_upscaler"]:
|
||||||
|
filters.append("latent_upscaler")
|
||||||
|
|
||||||
|
return new_req
|
||||||
21
ui/easydiffusion/utils/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import logging
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
log = logging.getLogger("easydiffusion")
|
||||||
|
|
||||||
|
from .save_utils import (
|
||||||
|
save_images_to_disk,
|
||||||
|
get_printable_request,
|
||||||
|
)
|
||||||
|
|
||||||
|
def sha256sum(filename):
|
||||||
|
sha256 = hashlib.sha256()
|
||||||
|
with open(filename, "rb") as f:
|
||||||
|
while True:
|
||||||
|
data = f.read(8192) # Read in chunks of 8192 bytes
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
sha256.update(data)
|
||||||
|
|
||||||
|
return sha256.hexdigest()
|
||||||
|
|
||||||
366
ui/easydiffusion/utils/save_utils.py
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import regex
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
|
from easydiffusion import app
|
||||||
|
from easydiffusion.types import (
|
||||||
|
GenerateImageRequest,
|
||||||
|
TaskData,
|
||||||
|
RenderTaskData,
|
||||||
|
OutputFormatData,
|
||||||
|
SaveToDiskData,
|
||||||
|
ModelsData,
|
||||||
|
)
|
||||||
|
from numpy import base_repr
|
||||||
|
from sdkit.utils import save_dicts, save_images
|
||||||
|
from sdkit.models.model_loader.embeddings import get_embedding_token
|
||||||
|
|
||||||
|
filename_regex = re.compile("[^a-zA-Z0-9._-]")
|
||||||
|
img_number_regex = re.compile("([0-9]{5,})")
|
||||||
|
|
||||||
|
# keep in sync with `ui/media/js/dnd.js`
|
||||||
|
TASK_TEXT_MAPPING = {
|
||||||
|
"prompt": "Prompt",
|
||||||
|
"negative_prompt": "Negative Prompt",
|
||||||
|
"seed": "Seed",
|
||||||
|
"use_stable_diffusion_model": "Stable Diffusion model",
|
||||||
|
"clip_skip": "Clip Skip",
|
||||||
|
"use_controlnet_model": "ControlNet model",
|
||||||
|
"control_filter_to_apply": "ControlNet Filter",
|
||||||
|
"use_vae_model": "VAE model",
|
||||||
|
"sampler_name": "Sampler",
|
||||||
|
"width": "Width",
|
||||||
|
"height": "Height",
|
||||||
|
"num_inference_steps": "Steps",
|
||||||
|
"guidance_scale": "Guidance Scale",
|
||||||
|
"prompt_strength": "Prompt Strength",
|
||||||
|
"use_lora_model": "LoRA model",
|
||||||
|
"lora_alpha": "LoRA Strength",
|
||||||
|
"use_hypernetwork_model": "Hypernetwork model",
|
||||||
|
"hypernetwork_strength": "Hypernetwork Strength",
|
||||||
|
"use_embeddings_model": "Embedding models",
|
||||||
|
"tiling": "Seamless Tiling",
|
||||||
|
"use_face_correction": "Use Face Correction",
|
||||||
|
"use_upscale": "Use Upscaling",
|
||||||
|
"upscale_amount": "Upscale By",
|
||||||
|
"latent_upscaler_steps": "Latent Upscaler Steps",
|
||||||
|
}
|
||||||
|
|
||||||
|
time_placeholders = {
|
||||||
|
"$yyyy": "%Y",
|
||||||
|
"$MM": "%m",
|
||||||
|
"$dd": "%d",
|
||||||
|
"$HH": "%H",
|
||||||
|
"$mm": "%M",
|
||||||
|
"$ss": "%S",
|
||||||
|
}
|
||||||
|
|
||||||
|
other_placeholders = {
|
||||||
|
"$id": lambda req, task_data: filename_regex.sub("_", task_data.session_id),
|
||||||
|
"$p": lambda req, task_data: filename_regex.sub("_", req.prompt)[:50],
|
||||||
|
"$s": lambda req, task_data: str(req.seed),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ImageNumber:
|
||||||
|
_factory = None
|
||||||
|
_evaluated = False
|
||||||
|
|
||||||
|
def __init__(self, factory):
|
||||||
|
self._factory = factory
|
||||||
|
self._evaluated = None
|
||||||
|
|
||||||
|
def __call__(self) -> int:
|
||||||
|
if self._evaluated is None:
|
||||||
|
self._evaluated = self._factory()
|
||||||
|
return self._evaluated
|
||||||
|
|
||||||
|
|
||||||
|
def format_placeholders(format: str, req: GenerateImageRequest, task_data: TaskData, now=None):
|
||||||
|
if now is None:
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
|
for placeholder, time_format in time_placeholders.items():
|
||||||
|
if placeholder in format:
|
||||||
|
format = format.replace(placeholder, datetime.fromtimestamp(now).strftime(time_format))
|
||||||
|
for placeholder, replace_func in other_placeholders.items():
|
||||||
|
if placeholder in format:
|
||||||
|
format = format.replace(placeholder, replace_func(req, task_data))
|
||||||
|
|
||||||
|
return format
|
||||||
|
|
||||||
|
|
||||||
|
def format_folder_name(format: str, req: GenerateImageRequest, task_data: TaskData):
|
||||||
|
format = format_placeholders(format, req, task_data)
|
||||||
|
return filename_regex.sub("_", format)
|
||||||
|
|
||||||
|
|
||||||
|
def format_file_name(
|
||||||
|
format: str,
|
||||||
|
req: GenerateImageRequest,
|
||||||
|
task_data: RenderTaskData,
|
||||||
|
now: float,
|
||||||
|
batch_file_number: int,
|
||||||
|
folder_img_number: ImageNumber,
|
||||||
|
):
|
||||||
|
format = format_placeholders(format, req, task_data, now)
|
||||||
|
|
||||||
|
if "$n" in format:
|
||||||
|
format = format.replace("$n", f"{folder_img_number():05}")
|
||||||
|
|
||||||
|
if "$tsb64" in format:
|
||||||
|
img_id = base_repr(int(now * 10000), 36)[-7:] + base_repr(
|
||||||
|
int(batch_file_number), 36
|
||||||
|
) # Base 36 conversion, 0-9, A-Z
|
||||||
|
format = format.replace("$tsb64", img_id)
|
||||||
|
|
||||||
|
if "$ts" in format:
|
||||||
|
format = format.replace("$ts", str(int(now * 1000) + batch_file_number))
|
||||||
|
|
||||||
|
return filename_regex.sub("_", format)
|
||||||
|
|
||||||
|
|
||||||
|
def save_images_to_disk(
|
||||||
|
images: list,
|
||||||
|
filtered_images: list,
|
||||||
|
req: GenerateImageRequest,
|
||||||
|
task_data: RenderTaskData,
|
||||||
|
models_data: ModelsData,
|
||||||
|
output_format: OutputFormatData,
|
||||||
|
save_data: SaveToDiskData,
|
||||||
|
):
|
||||||
|
now = time.time()
|
||||||
|
app_config = app.getConfig()
|
||||||
|
folder_format = app_config.get("folder_format", "$id")
|
||||||
|
save_dir_path = os.path.join(save_data.save_to_disk_path, format_folder_name(folder_format, req, task_data))
|
||||||
|
metadata_entries = get_metadata_entries_for_request(req, task_data, models_data, output_format, save_data)
|
||||||
|
file_number = calculate_img_number(save_dir_path, task_data)
|
||||||
|
make_filename = make_filename_callback(
|
||||||
|
app_config.get("filename_format", "$p_$tsb64"),
|
||||||
|
req,
|
||||||
|
task_data,
|
||||||
|
file_number,
|
||||||
|
now=now,
|
||||||
|
)
|
||||||
|
|
||||||
|
if task_data.show_only_filtered_image or filtered_images is images:
|
||||||
|
save_images(
|
||||||
|
filtered_images,
|
||||||
|
save_dir_path,
|
||||||
|
file_name=make_filename,
|
||||||
|
output_format=output_format.output_format,
|
||||||
|
output_quality=output_format.output_quality,
|
||||||
|
output_lossless=output_format.output_lossless,
|
||||||
|
)
|
||||||
|
if save_data.metadata_output_format:
|
||||||
|
for metadata_output_format in save_data.metadata_output_format.split(","):
|
||||||
|
if metadata_output_format.lower() in ["json", "txt", "embed"]:
|
||||||
|
save_dicts(
|
||||||
|
metadata_entries,
|
||||||
|
save_dir_path,
|
||||||
|
file_name=make_filename,
|
||||||
|
output_format=metadata_output_format,
|
||||||
|
file_format=output_format.output_format,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
make_filter_filename = make_filename_callback(
|
||||||
|
app_config.get("filename_format", "$p_$tsb64"),
|
||||||
|
req,
|
||||||
|
task_data,
|
||||||
|
file_number,
|
||||||
|
now=now,
|
||||||
|
suffix="filtered",
|
||||||
|
)
|
||||||
|
|
||||||
|
save_images(
|
||||||
|
images,
|
||||||
|
save_dir_path,
|
||||||
|
file_name=make_filename,
|
||||||
|
output_format=output_format.output_format,
|
||||||
|
output_quality=output_format.output_quality,
|
||||||
|
output_lossless=output_format.output_lossless,
|
||||||
|
)
|
||||||
|
save_images(
|
||||||
|
filtered_images,
|
||||||
|
save_dir_path,
|
||||||
|
file_name=make_filter_filename,
|
||||||
|
output_format=output_format.output_format,
|
||||||
|
output_quality=output_format.output_quality,
|
||||||
|
output_lossless=output_format.output_lossless,
|
||||||
|
)
|
||||||
|
if save_data.metadata_output_format:
|
||||||
|
for metadata_output_format in save_data.metadata_output_format.split(","):
|
||||||
|
if metadata_output_format.lower() in ["json", "txt", "embed"]:
|
||||||
|
save_dicts(
|
||||||
|
metadata_entries,
|
||||||
|
save_dir_path,
|
||||||
|
file_name=make_filter_filename,
|
||||||
|
output_format=metadata_output_format,
|
||||||
|
file_format=output_format.output_format,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_metadata_entries_for_request(
|
||||||
|
req: GenerateImageRequest,
|
||||||
|
task_data: RenderTaskData,
|
||||||
|
models_data: ModelsData,
|
||||||
|
output_format: OutputFormatData,
|
||||||
|
save_data: SaveToDiskData,
|
||||||
|
):
|
||||||
|
metadata = get_printable_request(req, task_data, models_data, output_format, save_data)
|
||||||
|
|
||||||
|
# if text, format it in the text format expected by the UI
|
||||||
|
is_txt_format = save_data.metadata_output_format and "txt" in save_data.metadata_output_format.lower().split(",")
|
||||||
|
if is_txt_format:
|
||||||
|
|
||||||
|
def format_value(value):
|
||||||
|
if isinstance(value, list):
|
||||||
|
return ", ".join([str(it) for it in value])
|
||||||
|
return value
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
TASK_TEXT_MAPPING[key]: format_value(val) for key, val in metadata.items() if key in TASK_TEXT_MAPPING
|
||||||
|
}
|
||||||
|
|
||||||
|
entries = [metadata.copy() for _ in range(req.num_outputs)]
|
||||||
|
for i, entry in enumerate(entries):
|
||||||
|
entry["Seed" if is_txt_format else "seed"] = req.seed + i
|
||||||
|
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def get_printable_request(
|
||||||
|
req: GenerateImageRequest,
|
||||||
|
task_data: RenderTaskData,
|
||||||
|
models_data: ModelsData,
|
||||||
|
output_format: OutputFormatData,
|
||||||
|
save_data: SaveToDiskData,
|
||||||
|
):
|
||||||
|
req_metadata = req.dict()
|
||||||
|
task_data_metadata = task_data.dict()
|
||||||
|
task_data_metadata.update(output_format.dict())
|
||||||
|
task_data_metadata.update(save_data.dict())
|
||||||
|
|
||||||
|
app_config = app.getConfig()
|
||||||
|
using_diffusers = app_config.get("use_v3_engine", True)
|
||||||
|
|
||||||
|
# Save the metadata in the order defined in TASK_TEXT_MAPPING
|
||||||
|
metadata = {}
|
||||||
|
for key in TASK_TEXT_MAPPING.keys():
|
||||||
|
if key in req_metadata:
|
||||||
|
metadata[key] = req_metadata[key]
|
||||||
|
elif key in task_data_metadata:
|
||||||
|
metadata[key] = task_data_metadata[key]
|
||||||
|
|
||||||
|
if key == "use_embeddings_model" and task_data_metadata[key] and using_diffusers:
|
||||||
|
embeddings_used = models_data.model_paths["embeddings"]
|
||||||
|
embeddings_used = embeddings_used if isinstance(embeddings_used, list) else [embeddings_used]
|
||||||
|
|
||||||
|
metadata["use_embeddings_model"] = embeddings_used if len(embeddings_used) > 0 else None
|
||||||
|
|
||||||
|
# Clean up the metadata
|
||||||
|
if req.init_image is None and "prompt_strength" in metadata:
|
||||||
|
del metadata["prompt_strength"]
|
||||||
|
if task_data.use_upscale is None and "upscale_amount" in metadata:
|
||||||
|
del metadata["upscale_amount"]
|
||||||
|
if task_data.use_hypernetwork_model is None and "hypernetwork_strength" in metadata:
|
||||||
|
del metadata["hypernetwork_strength"]
|
||||||
|
if task_data.use_lora_model is None and "lora_alpha" in metadata:
|
||||||
|
del metadata["lora_alpha"]
|
||||||
|
if task_data.use_upscale != "latent_upscaler" and "latent_upscaler_steps" in metadata:
|
||||||
|
del metadata["latent_upscaler_steps"]
|
||||||
|
if task_data.use_controlnet_model is None and "control_filter_to_apply" in metadata:
|
||||||
|
del metadata["control_filter_to_apply"]
|
||||||
|
|
||||||
|
if using_diffusers:
|
||||||
|
for key in (x for x in ["use_hypernetwork_model", "hypernetwork_strength"] if x in metadata):
|
||||||
|
del metadata[key]
|
||||||
|
else:
|
||||||
|
for key in (
|
||||||
|
x
|
||||||
|
for x in [
|
||||||
|
"use_lora_model",
|
||||||
|
"lora_alpha",
|
||||||
|
"clip_skip",
|
||||||
|
"tiling",
|
||||||
|
"latent_upscaler_steps",
|
||||||
|
"use_controlnet_model",
|
||||||
|
"control_filter_to_apply",
|
||||||
|
]
|
||||||
|
if x in metadata
|
||||||
|
):
|
||||||
|
del metadata[key]
|
||||||
|
|
||||||
|
return metadata
|
||||||
|
|
||||||
|
|
||||||
|
def make_filename_callback(
|
||||||
|
filename_format: str,
|
||||||
|
req: GenerateImageRequest,
|
||||||
|
task_data: RenderTaskData,
|
||||||
|
folder_img_number: int,
|
||||||
|
suffix=None,
|
||||||
|
now=None,
|
||||||
|
):
|
||||||
|
if now is None:
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
|
def make_filename(i):
|
||||||
|
name = format_file_name(filename_format, req, task_data, now, i, folder_img_number)
|
||||||
|
name = name if suffix is None else f"{name}_{suffix}"
|
||||||
|
|
||||||
|
return name
|
||||||
|
|
||||||
|
return make_filename
|
||||||
|
|
||||||
|
|
||||||
|
def _calculate_img_number(save_dir_path: str, task_data: RenderTaskData):
|
||||||
|
def get_highest_img_number(accumulator: int, file: os.DirEntry) -> int:
|
||||||
|
if not file.is_file:
|
||||||
|
return accumulator
|
||||||
|
|
||||||
|
if len(list(filter(lambda e: file.name.endswith(e), app.IMAGE_EXTENSIONS))) == 0:
|
||||||
|
return accumulator
|
||||||
|
|
||||||
|
get_highest_img_number.number_of_images = get_highest_img_number.number_of_images + 1
|
||||||
|
|
||||||
|
number_match = img_number_regex.match(file.name)
|
||||||
|
if not number_match:
|
||||||
|
return accumulator
|
||||||
|
|
||||||
|
file_number = number_match.group().lstrip("0")
|
||||||
|
|
||||||
|
# Handle 00000
|
||||||
|
return int(file_number) if file_number else 0
|
||||||
|
|
||||||
|
get_highest_img_number.number_of_images = 0
|
||||||
|
|
||||||
|
highest_file_number = -1
|
||||||
|
|
||||||
|
if os.path.isdir(save_dir_path):
|
||||||
|
existing_files = list(os.scandir(save_dir_path))
|
||||||
|
highest_file_number = reduce(get_highest_img_number, existing_files, -1)
|
||||||
|
|
||||||
|
calculated_img_number = max(highest_file_number, get_highest_img_number.number_of_images - 1)
|
||||||
|
|
||||||
|
if task_data.session_id in _calculate_img_number.session_img_numbers:
|
||||||
|
calculated_img_number = max(
|
||||||
|
_calculate_img_number.session_img_numbers[task_data.session_id],
|
||||||
|
calculated_img_number,
|
||||||
|
)
|
||||||
|
|
||||||
|
calculated_img_number = calculated_img_number + 1
|
||||||
|
|
||||||
|
_calculate_img_number.session_img_numbers[task_data.session_id] = calculated_img_number
|
||||||
|
return calculated_img_number
|
||||||
|
|
||||||
|
|
||||||
|
_calculate_img_number.session_img_numbers = {}
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_img_number(save_dir_path: str, task_data: RenderTaskData):
|
||||||
|
return ImageNumber(lambda: _calculate_img_number(save_dir_path, task_data))
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
{
|
|
||||||
"_name_or_path": "clip-vit-large-patch14/",
|
|
||||||
"architectures": [
|
|
||||||
"CLIPModel"
|
|
||||||
],
|
|
||||||
"initializer_factor": 1.0,
|
|
||||||
"logit_scale_init_value": 2.6592,
|
|
||||||
"model_type": "clip",
|
|
||||||
"projection_dim": 768,
|
|
||||||
"text_config": {
|
|
||||||
"_name_or_path": "",
|
|
||||||
"add_cross_attention": false,
|
|
||||||
"architectures": null,
|
|
||||||
"attention_dropout": 0.0,
|
|
||||||
"bad_words_ids": null,
|
|
||||||
"bos_token_id": 0,
|
|
||||||
"chunk_size_feed_forward": 0,
|
|
||||||
"cross_attention_hidden_size": null,
|
|
||||||
"decoder_start_token_id": null,
|
|
||||||
"diversity_penalty": 0.0,
|
|
||||||
"do_sample": false,
|
|
||||||
"dropout": 0.0,
|
|
||||||
"early_stopping": false,
|
|
||||||
"encoder_no_repeat_ngram_size": 0,
|
|
||||||
"eos_token_id": 2,
|
|
||||||
"finetuning_task": null,
|
|
||||||
"forced_bos_token_id": null,
|
|
||||||
"forced_eos_token_id": null,
|
|
||||||
"hidden_act": "quick_gelu",
|
|
||||||
"hidden_size": 768,
|
|
||||||
"id2label": {
|
|
||||||
"0": "LABEL_0",
|
|
||||||
"1": "LABEL_1"
|
|
||||||
},
|
|
||||||
"initializer_factor": 1.0,
|
|
||||||
"initializer_range": 0.02,
|
|
||||||
"intermediate_size": 3072,
|
|
||||||
"is_decoder": false,
|
|
||||||
"is_encoder_decoder": false,
|
|
||||||
"label2id": {
|
|
||||||
"LABEL_0": 0,
|
|
||||||
"LABEL_1": 1
|
|
||||||
},
|
|
||||||
"layer_norm_eps": 1e-05,
|
|
||||||
"length_penalty": 1.0,
|
|
||||||
"max_length": 20,
|
|
||||||
"max_position_embeddings": 77,
|
|
||||||
"min_length": 0,
|
|
||||||
"model_type": "clip_text_model",
|
|
||||||
"no_repeat_ngram_size": 0,
|
|
||||||
"num_attention_heads": 12,
|
|
||||||
"num_beam_groups": 1,
|
|
||||||
"num_beams": 1,
|
|
||||||
"num_hidden_layers": 12,
|
|
||||||
"num_return_sequences": 1,
|
|
||||||
"output_attentions": false,
|
|
||||||
"output_hidden_states": false,
|
|
||||||
"output_scores": false,
|
|
||||||
"pad_token_id": 1,
|
|
||||||
"prefix": null,
|
|
||||||
"problem_type": null,
|
|
||||||
"projection_dim" : 768,
|
|
||||||
"pruned_heads": {},
|
|
||||||
"remove_invalid_values": false,
|
|
||||||
"repetition_penalty": 1.0,
|
|
||||||
"return_dict": true,
|
|
||||||
"return_dict_in_generate": false,
|
|
||||||
"sep_token_id": null,
|
|
||||||
"task_specific_params": null,
|
|
||||||
"temperature": 1.0,
|
|
||||||
"tie_encoder_decoder": false,
|
|
||||||
"tie_word_embeddings": true,
|
|
||||||
"tokenizer_class": null,
|
|
||||||
"top_k": 50,
|
|
||||||
"top_p": 1.0,
|
|
||||||
"torch_dtype": null,
|
|
||||||
"torchscript": false,
|
|
||||||
"transformers_version": "4.16.0.dev0",
|
|
||||||
"use_bfloat16": false,
|
|
||||||
"vocab_size": 49408
|
|
||||||
},
|
|
||||||
"text_config_dict": {
|
|
||||||
"hidden_size": 768,
|
|
||||||
"intermediate_size": 3072,
|
|
||||||
"num_attention_heads": 12,
|
|
||||||
"num_hidden_layers": 12,
|
|
||||||
"projection_dim": 768
|
|
||||||
},
|
|
||||||
"torch_dtype": "float32",
|
|
||||||
"transformers_version": null,
|
|
||||||
"vision_config": {
|
|
||||||
"_name_or_path": "",
|
|
||||||
"add_cross_attention": false,
|
|
||||||
"architectures": null,
|
|
||||||
"attention_dropout": 0.0,
|
|
||||||
"bad_words_ids": null,
|
|
||||||
"bos_token_id": null,
|
|
||||||
"chunk_size_feed_forward": 0,
|
|
||||||
"cross_attention_hidden_size": null,
|
|
||||||
"decoder_start_token_id": null,
|
|
||||||
"diversity_penalty": 0.0,
|
|
||||||
"do_sample": false,
|
|
||||||
"dropout": 0.0,
|
|
||||||
"early_stopping": false,
|
|
||||||
"encoder_no_repeat_ngram_size": 0,
|
|
||||||
"eos_token_id": null,
|
|
||||||
"finetuning_task": null,
|
|
||||||
"forced_bos_token_id": null,
|
|
||||||
"forced_eos_token_id": null,
|
|
||||||
"hidden_act": "quick_gelu",
|
|
||||||
"hidden_size": 1024,
|
|
||||||
"id2label": {
|
|
||||||
"0": "LABEL_0",
|
|
||||||
"1": "LABEL_1"
|
|
||||||
},
|
|
||||||
"image_size": 224,
|
|
||||||
"initializer_factor": 1.0,
|
|
||||||
"initializer_range": 0.02,
|
|
||||||
"intermediate_size": 4096,
|
|
||||||
"is_decoder": false,
|
|
||||||
"is_encoder_decoder": false,
|
|
||||||
"label2id": {
|
|
||||||
"LABEL_0": 0,
|
|
||||||
"LABEL_1": 1
|
|
||||||
},
|
|
||||||
"layer_norm_eps": 1e-05,
|
|
||||||
"length_penalty": 1.0,
|
|
||||||
"max_length": 20,
|
|
||||||
"min_length": 0,
|
|
||||||
"model_type": "clip_vision_model",
|
|
||||||
"no_repeat_ngram_size": 0,
|
|
||||||
"num_attention_heads": 16,
|
|
||||||
"num_beam_groups": 1,
|
|
||||||
"num_beams": 1,
|
|
||||||
"num_hidden_layers": 24,
|
|
||||||
"num_return_sequences": 1,
|
|
||||||
"output_attentions": false,
|
|
||||||
"output_hidden_states": false,
|
|
||||||
"output_scores": false,
|
|
||||||
"pad_token_id": null,
|
|
||||||
"patch_size": 14,
|
|
||||||
"prefix": null,
|
|
||||||
"problem_type": null,
|
|
||||||
"projection_dim" : 768,
|
|
||||||
"pruned_heads": {},
|
|
||||||
"remove_invalid_values": false,
|
|
||||||
"repetition_penalty": 1.0,
|
|
||||||
"return_dict": true,
|
|
||||||
"return_dict_in_generate": false,
|
|
||||||
"sep_token_id": null,
|
|
||||||
"task_specific_params": null,
|
|
||||||
"temperature": 1.0,
|
|
||||||
"tie_encoder_decoder": false,
|
|
||||||
"tie_word_embeddings": true,
|
|
||||||
"tokenizer_class": null,
|
|
||||||
"top_k": 50,
|
|
||||||
"top_p": 1.0,
|
|
||||||
"torch_dtype": null,
|
|
||||||
"torchscript": false,
|
|
||||||
"transformers_version": "4.16.0.dev0",
|
|
||||||
"use_bfloat16": false
|
|
||||||
},
|
|
||||||
"vision_config_dict": {
|
|
||||||
"hidden_size": 1024,
|
|
||||||
"intermediate_size": 4096,
|
|
||||||
"num_attention_heads": 16,
|
|
||||||
"num_hidden_layers": 24,
|
|
||||||
"patch_size": 14,
|
|
||||||
"projection_dim": 768
|
|
||||||
}
|
|
||||||
}
|
|
||||||
757
ui/index.html
@@ -1,32 +1,48 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Stable Diffusion UI</title>
|
<title>Easy Diffusion</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="theme-color" content="#673AB6">
|
||||||
<link rel="icon" type="image/png" href="/media/images/favicon-16x16.png" sizes="16x16">
|
<link rel="icon" type="image/png" href="/media/images/favicon-16x16.png" sizes="16x16">
|
||||||
<link rel="icon" type="image/png" href="/media/images/favicon-32x32.png" sizes="32x32">
|
<link rel="icon" type="image/png" href="/media/images/favicon-32x32.png" sizes="32x32">
|
||||||
|
<link rel="stylesheet" href="/media/css/jquery-confirm.min.css">
|
||||||
<link rel="stylesheet" href="/media/css/fonts.css">
|
<link rel="stylesheet" href="/media/css/fonts.css">
|
||||||
<link rel="stylesheet" href="/media/css/themes.css">
|
<link rel="stylesheet" href="/media/css/themes.css">
|
||||||
<link rel="stylesheet" href="/media/css/main.css">
|
<link rel="stylesheet" href="/media/css/main.css">
|
||||||
<link rel="stylesheet" href="/media/css/auto-save.css">
|
<link rel="stylesheet" href="/media/css/auto-save.css">
|
||||||
<link rel="stylesheet" href="/media/css/modifier-thumbnails.css">
|
<link rel="stylesheet" href="/media/css/modifier-thumbnails.css">
|
||||||
<link rel="stylesheet" href="/media/css/fontawesome-all.min.css">
|
<link rel="stylesheet" href="/media/css/fontawesome-all.min.css">
|
||||||
<link rel="stylesheet" href="/media/css/drawingboard.min.css">
|
<link rel="stylesheet" href="/media/css/image-editor.css">
|
||||||
|
<link rel="stylesheet" href="/media/css/searchable-models.css">
|
||||||
|
<link rel="stylesheet" href="/media/css/image-modal.css">
|
||||||
|
<link rel="stylesheet" href="/media/css/plugins.css">
|
||||||
|
<link rel="stylesheet" href="/media/css/animations.css">
|
||||||
|
<link rel="stylesheet" href="/media/css/croppr.css" rel="stylesheet"/>
|
||||||
|
<link rel="manifest" href="/media/manifest.webmanifest">
|
||||||
<script src="/media/js/jquery-3.6.1.min.js"></script>
|
<script src="/media/js/jquery-3.6.1.min.js"></script>
|
||||||
<script src="/media/js/drawingboard.min.js"></script>
|
<script src="/media/js/jquery-confirm.min.js"></script>
|
||||||
|
<script src="/media/js/jszip.min.js"></script>
|
||||||
|
<script src="/media/js/FileSaver.min.js"></script>
|
||||||
<script src="/media/js/marked.min.js"></script>
|
<script src="/media/js/marked.min.js"></script>
|
||||||
|
<script src="/media/js/croppr.js"></script>
|
||||||
|
<script src="/media/js/exif-reader.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="container">
|
<div id="container">
|
||||||
<div id="top-nav">
|
<div id="top-nav">
|
||||||
<div id="logo">
|
<div id="logo">
|
||||||
<h1>Stable Diffusion UI <small>v2.4.13 <span id="updateBranchLabel"></span></small></h1>
|
<h1>
|
||||||
|
<img id="logo_img" src="/media/images/icon-512x512.png" >
|
||||||
|
Easy Diffusion
|
||||||
|
<small><span id="version">v3.0.8</span> <span id="updateBranchLabel"></span></small>
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div id="server-status">
|
<div id="server-status">
|
||||||
<div id="server-status-color">●</div>
|
<div id="server-status-color">●</div>
|
||||||
<span id="server-status-msg">Stable Diffusion is starting..</span>
|
<span id="server-status-msg">Stable Diffusion is starting..</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="tab-container">
|
<div id="tab-container" class="tab-container">
|
||||||
<span id="tab-main" class="tab active">
|
<span id="tab-main" class="tab active">
|
||||||
<span><i class="fa fa-image icon"></i> Generate</span>
|
<span><i class="fa fa-image icon"></i> Generate</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -44,47 +60,85 @@
|
|||||||
<div id="editor">
|
<div id="editor">
|
||||||
<div id="editor-inputs">
|
<div id="editor-inputs">
|
||||||
<div id="editor-inputs-prompt" class="row">
|
<div id="editor-inputs-prompt" class="row">
|
||||||
<label for="prompt"><b>Enter Prompt</b></label> <small>or</small> <button id="promptsFromFileBtn">Load from a file</button>
|
<div id="prompt-toolbar" class="split-toolbar">
|
||||||
|
<div id="prompt-toolbar-left" class="toolbar-left">
|
||||||
|
<label for="prompt"><b>Enter Prompt</b>
|
||||||
|
<i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip right">
|
||||||
|
You can type your prompts in the below textbox or load them from a file. You can also
|
||||||
|
reload tasks from metadata embedded in PNG, WEBP and JPEG images (enable embedding from the Settings).
|
||||||
|
</span></i>
|
||||||
|
</label>
|
||||||
|
<small>or</small>
|
||||||
|
<button id="promptsFromFileBtn" class="tertiaryButton smallButton">Load from a file</button>
|
||||||
|
</div>
|
||||||
|
<div id="prompt-toolbar-right" class="toolbar-right">
|
||||||
|
<button id="image-modifier-dropdown" class="tertiaryButton smallButton">+ Image Modifiers</button>
|
||||||
|
<button id="embeddings-button" class="tertiaryButton smallButton displayNone">+ Embedding</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<textarea id="prompt" class="col-free">a photograph of an astronaut riding a horse</textarea>
|
<textarea id="prompt" class="col-free">a photograph of an astronaut riding a horse</textarea>
|
||||||
<input id="prompt_from_file" name="prompt_from_file" type="file" /> <!-- hidden -->
|
<input id="prompt_from_file" name="prompt_from_file" type="file" /> <!-- hidden -->
|
||||||
<label for="negative_prompt" class="collapsible" id="negative_prompt_handle">
|
<label for="negative_prompt" class="collapsible" id="negative_prompt_handle">
|
||||||
Negative Prompt
|
Negative Prompt
|
||||||
<a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Writing-prompts#negative-prompts" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip right">Click to learn more about Negative Prompts</span></i></a>
|
<a href="https://github.com/easydiffusion/easydiffusion/wiki/Writing-prompts#negative-prompts" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top">Click to learn more about Negative Prompts</span></i></a>
|
||||||
<small>(optional)</small>
|
<small>(optional)</small>
|
||||||
</label>
|
</label>
|
||||||
|
<button id="negative-embeddings-button" class="tertiaryButton smallButton displayNone">+ Negative Embedding</button>
|
||||||
<div class="collapsible-content">
|
<div class="collapsible-content">
|
||||||
<textarea id="negative_prompt" name="negative_prompt" placeholder="list the things to remove from the image (e.g. fog, green)"></textarea>
|
<textarea id="negative_prompt" name="negative_prompt" placeholder="list the things to remove from the image (e.g. fog, green)"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="editor-inputs-init-image" class="row">
|
<div id="editor-inputs-init-image" class="row">
|
||||||
<label for="init_image">Initial Image (img2img) <small>(optional)</small> </label> <input id="init_image" name="init_image" type="file" /><br/>
|
<label for="init_image">Initial Image (img2img) <small>(optional)</small> </label>
|
||||||
|
<i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top">
|
||||||
|
Add img2img source image using the Browse button, via drag & drop from external file or browser image (incl.
|
||||||
|
rendered image) or by pasting an image from the clipboard using Ctrl+V.<br /><br />
|
||||||
|
You may also reload the metadata embedded in a PNG, WEBP or JPEG image (enable embedding from the Settings).
|
||||||
|
</span></i>
|
||||||
|
|
||||||
<div id="init_image_preview_container" class="image_preview_container">
|
<div id="init_image_preview_container" class="image_preview_container">
|
||||||
<div id="init_image_wrapper">
|
<div id="init_image_wrapper" class="preview_image_wrapper">
|
||||||
<img id="init_image_preview" src="" />
|
<img id="init_image_preview" class="image_preview" src="" crossorigin="anonymous" />
|
||||||
<span id="init_image_size_box"></span>
|
<span id="init_image_size_box" class="img_bottom_label"></span>
|
||||||
<button class="init_image_clear image_clear_btn">X</button>
|
<button class="init_image_clear image_clear_btn"><i class="fa-solid fa-xmark"></i></button>
|
||||||
|
</div>
|
||||||
|
<div id="init_image_buttons">
|
||||||
|
<div class="button">
|
||||||
|
<i class="fa-regular fa-folder-open"></i>
|
||||||
|
Browse
|
||||||
|
<input id="init_image" name="init_image" type="file" />
|
||||||
|
</div>
|
||||||
|
<div id="init_image_button_draw" class="button">
|
||||||
|
<i class="fa-solid fa-pencil"></i>
|
||||||
|
Draw
|
||||||
|
</div>
|
||||||
|
<div id="inpaint_button_container">
|
||||||
|
<div id="init_image_button_inpaint" class="button">
|
||||||
|
<i class="fa-solid fa-paintbrush"></i>
|
||||||
|
Inpaint
|
||||||
|
</div>
|
||||||
|
<input id="enable_mask" name="enable_mask" type="checkbox">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br/>
|
|
||||||
<input id="enable_mask" name="enable_mask" type="checkbox">
|
|
||||||
<label for="enable_mask">
|
|
||||||
In-Painting (beta)
|
|
||||||
<a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Inpainting" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip right">Click to learn more about InPainting</span></i></a>
|
|
||||||
<small>(select the area which the AI will paint into)</small>
|
|
||||||
</label>
|
|
||||||
<div id="inpaintingEditor"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="apply_color_correction_setting" class="pl-5"><input id="apply_color_correction" name="apply_color_correction" type="checkbox"> <label for="apply_color_correction">Preserve color profile <small>(helps during inpainting)</small></label></div>
|
||||||
|
<div id="strict_mask_border_setting" class="pl-5"><input id="strict_mask_border" name="strict_mask_border" type="checkbox"> <label for="strict_mask_border">Strict Mask Border <small>(won't modify outside the mask, but the mask border might be visible)</small></label></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="editor-inputs-tags-container" class="row">
|
<div id="editor-inputs-tags-container" class="row">
|
||||||
<label>Image Modifiers: <small>(click an Image Modifier to remove it)</small></label>
|
<label>Image Modifiers <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip right">Click an Image Modifier to remove it, right-click to temporarily disable it, use Ctrl+Mouse Wheel to adjust its weight</span></i></label>
|
||||||
<div id="editor-inputs-tags-list"></div>
|
<div id="editor-inputs-tags-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button id="makeImage" class="primaryButton">Make Image</button>
|
<button id="makeImage" class="primaryButton">Make Image</button>
|
||||||
<button id="stopImage" class="secondaryButton">Stop All</button>
|
<div id="render-buttons">
|
||||||
|
<button id="stopImage" class="secondaryButton">Stop All</button>
|
||||||
|
<button id="pause"><i class="fa-solid fa-pause"></i> Pause All</button>
|
||||||
|
<button id="resume"><i class="fa-solid fa-play"></i> Resume</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span class="line-separator"></span>
|
<span class="line-separator"></span>
|
||||||
@@ -93,7 +147,7 @@
|
|||||||
<h4 class="collapsible">
|
<h4 class="collapsible">
|
||||||
Image Settings
|
Image Settings
|
||||||
<i id="reset-image-settings" class="fa-solid fa-arrow-rotate-left section-button">
|
<i id="reset-image-settings" class="fa-solid fa-arrow-rotate-left section-button">
|
||||||
<span class="simple-tooltip right">
|
<span class="simple-tooltip top-left">
|
||||||
Reset Image Settings
|
Reset Image Settings
|
||||||
</span>
|
</span>
|
||||||
</i>
|
</i>
|
||||||
@@ -101,42 +155,128 @@
|
|||||||
<div id="editor-settings-entries" class="collapsible-content">
|
<div id="editor-settings-entries" class="collapsible-content">
|
||||||
<div><table>
|
<div><table>
|
||||||
<tr><b class="settings-subheader">Image Settings</b></tr>
|
<tr><b class="settings-subheader">Image Settings</b></tr>
|
||||||
<tr class="pl-5"><td><label for="seed">Seed:</label></td><td><input id="seed" name="seed" size="10" value="30000" onkeypress="preventNonNumericalInput(event)"> <input id="random_seed" name="random_seed" type="checkbox" checked><label for="random_seed">Random</label></td></tr>
|
<tr class="pl-5"><td><label for="seed">Seed:</label></td><td><input id="seed" name="seed" size="10" value="0" onkeypress="preventNonNumericalInput(event)" inputmode="numeric"> <input id="random_seed" name="random_seed" type="checkbox" checked><label for="random_seed">Random</label></td></tr>
|
||||||
<tr class="pl-5"><td><label for="num_outputs_total">Number of Images:</label></td><td><input id="num_outputs_total" name="num_outputs_total" value="1" size="1" onkeypress="preventNonNumericalInput(event)"> <label><small>(total)</small></label> <input id="num_outputs_parallel" name="num_outputs_parallel" value="1" size="1" onkeypress="preventNonNumericalInput(event)"> <label for="num_outputs_parallel"><small>(in parallel)</small></label></td></tr>
|
<tr class="pl-5"><td><label for="num_outputs_total">Number of Images:</label></td>
|
||||||
<tr class="pl-5"><td><label for="stable_diffusion_model">Model:</label></td><td>
|
<td><input id="num_outputs_total" name="num_outputs_total" value="1" type="number" value="1" min="1" step="1" onkeypres"="preventNonNumericalInput(event)" inputmode="numeric">
|
||||||
<select id="stable_diffusion_model" name="stable_diffusion_model">
|
<label><small>(total)</small></label>
|
||||||
<!-- <option value="sd-v1-4" selected>sd-v1-4</option> -->
|
<input id="num_outputs_parallel" name="num_outputs_parallel" value="1" type="number" value="1" min="1" step="1" onkeypress="preventNonNumericalInput(event)" inputmode="numeric">
|
||||||
</select>
|
<label id="num_outputs_parallel_label" for="num_outputs_parallel"><small>(in parallel)</small></label></td>
|
||||||
<a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Custom-Models" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip right">Click to learn more about custom models</span></i></a>
|
</tr>
|
||||||
|
<tr class="pl-5"><td><label for="stable_diffusion_model">Model:</label></td><td class="model-input">
|
||||||
|
<input id="stable_diffusion_model" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
|
||||||
|
<button id="reload-models" class="secondaryButton reloadModels"><i class='fa-solid fa-rotate'></i></button>
|
||||||
|
<a href="https://github.com/easydiffusion/easydiffusion/wiki/Custom-Models" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Click to learn more about custom models</span></i></a>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
<tr class="pl-5"><td><label for="vae_model">Custom VAE:</i></label></td><td>
|
<tr class="pl-5 displayNone" id="enable_trt_config">
|
||||||
<select id="vae_model" name="vae_model">
|
<td><label for="convert_to_tensorrt">Enable TensorRT:</label></td>
|
||||||
<!-- <option value="" selected>None</option> -->
|
<td class="diffusers-restart-needed">
|
||||||
</select>
|
<input id="convert_to_tensorrt" name="convert_to_tensorrt" type="checkbox">
|
||||||
<a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/VAE-Variational-Auto-Encoder" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip right">Click to learn more about VAEs</span></i></a>
|
<!-- <label><small>Takes upto 20 mins the first time</small></label> -->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="pl-5 displayNone" id="clip_skip_config">
|
||||||
|
<td><label for="clip_skip">Clip Skip:</label></td>
|
||||||
|
<td class="diffusers-restart-needed">
|
||||||
|
<input id="clip_skip" name="clip_skip" type="checkbox">
|
||||||
|
<a href="https://github.com/easydiffusion/easydiffusion/wiki/Clip-Skip" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Click to learn more about Clip Skip</span></i></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr id="controlnet_model_container" class="pl-5">
|
||||||
|
<td><label for="controlnet_model">ControlNet Image:</label></td>
|
||||||
|
<td class="diffusers-restart-needed">
|
||||||
|
<div id="control_image_wrapper" class="preview_image_wrapper">
|
||||||
|
<img id="control_image_preview" class="image_preview" src="" crossorigin="anonymous" />
|
||||||
|
<span id="control_image_size_box" class="img_bottom_label"></span>
|
||||||
|
<button class="control_image_clear image_clear_btn"><i class="fa-solid fa-xmark"></i></button>
|
||||||
|
</div>
|
||||||
|
<input id="control_image" name="control_image" type="file" />
|
||||||
|
<a href="https://github.com/easydiffusion/easydiffusion/wiki/ControlNet" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Click to learn more about ControlNets</span></i></a>
|
||||||
|
<div id="controlnet_config" class="displayNone">
|
||||||
|
<label><small>Filter to apply:</small></label>
|
||||||
|
<select id="control_image_filter">
|
||||||
|
<option value="">None</option>
|
||||||
|
<optgroup label="Pose">
|
||||||
|
<option value="openpose">OpenPose (*)</option>
|
||||||
|
<option value="openpose_face">OpenPose face</option>
|
||||||
|
<option value="openpose_faceonly">OpenPose face-only</option>
|
||||||
|
<option value="openpose_hand">OpenPose hand</option>
|
||||||
|
<option value="openpose_full">OpenPose full</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Outline">
|
||||||
|
<option value="canny">Canny (*)</option>
|
||||||
|
<option value="mlsd">Straight lines</option>
|
||||||
|
<option value="scribble_hed">Scribble hed (*)</option>
|
||||||
|
<option value="scribble_hedsafe">Scribble hedsafe</option>
|
||||||
|
<option value="scribble_pidinet">Scribble pidinet</option>
|
||||||
|
<option value="scribble_pidsafe">Scribble pidsafe</option>
|
||||||
|
<option value="softedge_hed">Softedge hed</option>
|
||||||
|
<option value="softedge_hedsafe">Softedge hedsafe</option>
|
||||||
|
<option value="softedge_pidinet">Softedge pidinet</option>
|
||||||
|
<option value="softedge_pidsafe">Softedge pidsafe</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Depth">
|
||||||
|
<option value="normal_bae">Normal bae (*)</option>
|
||||||
|
<option value="depth_midas">Depth midas</option>
|
||||||
|
<option value="depth_zoe">Depth zoe</option>
|
||||||
|
<option value="depth_leres">Depth leres</option>
|
||||||
|
<option value="depth_leres++">Depth leres++</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Line art">
|
||||||
|
<option value="lineart_coarse">Lineart coarse</option>
|
||||||
|
<option value="lineart_realistic">Lineart realistic</option>
|
||||||
|
<option value="lineart_anime">Lineart anime</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Misc">
|
||||||
|
<option value="shuffle">Shuffle</option>
|
||||||
|
<option value="segment">Segment</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
<br/>
|
||||||
|
<label for="controlnet_model"><small>Model:</small></label> <input id="controlnet_model" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
|
||||||
|
<br/>
|
||||||
|
<label><small>Will download the necessary models, the first time.</small></label>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="pl-5"><td><label for="vae_model">Custom VAE:</label></td><td>
|
||||||
|
<input id="vae_model" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
|
||||||
|
<a href="https://github.com/easydiffusion/easydiffusion/wiki/VAE-Variational-Auto-Encoder" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Click to learn more about VAEs</span></i></a>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
<tr id="samplerSelection" class="pl-5"><td><label for="sampler">Sampler:</label></td><td>
|
<tr id="samplerSelection" class="pl-5"><td><label for="sampler_name">Sampler:</label></td><td>
|
||||||
<select id="sampler" name="sampler">
|
<select id="sampler_name" name="sampler_name">
|
||||||
<option value="plms">plms</option>
|
<option value="plms">PLMS</option>
|
||||||
<option value="ddim">ddim</option>
|
<option value="ddim">DDIM</option>
|
||||||
<option value="heun">heun</option>
|
<option value="heun">Heun</option>
|
||||||
<option value="euler">euler</option>
|
<option value="euler">Euler</option>
|
||||||
<option value="euler_a" selected>euler_a</option>
|
<option value="euler_a" selected>Euler Ancestral</option>
|
||||||
<option value="dpm2">dpm2</option>
|
<option value="dpm2">DPM2</option>
|
||||||
<option value="dpm2_a">dpm2_a</option>
|
<option value="dpm2_a">DPM2 Ancestral</option>
|
||||||
<option value="lms">lms</option>
|
<option value="lms">LMS</option>
|
||||||
|
<option value="dpm_solver_stability">DPM Solver (Stability AI)</option>
|
||||||
|
<option value="dpmpp_2s_a">DPM++ 2s Ancestral (Karras)</option>
|
||||||
|
<option value="dpmpp_2m">DPM++ 2m (Karras)</option>
|
||||||
|
<option value="dpmpp_2m_sde" class="diffusers-only">DPM++ 2m SDE (Karras)</option>
|
||||||
|
<option value="dpmpp_sde">DPM++ SDE (Karras)</option>
|
||||||
|
<option value="dpm_adaptive" class="k_diffusion-only">DPM Adaptive (Karras)</option>
|
||||||
|
<option value="ddpm" class="diffusers-only">DDPM</option>
|
||||||
|
<option value="deis" class="diffusers-only">DEIS</option>
|
||||||
|
<option value="unipc_snr" class="k_diffusion-only">UniPC SNR</option>
|
||||||
|
<option value="unipc_tu">UniPC TU</option>
|
||||||
|
<option value="unipc_snr_2" class="k_diffusion-only">UniPC SNR 2</option>
|
||||||
|
<option value="unipc_tu_2" class="k_diffusion-only">UniPC TU 2</option>
|
||||||
|
<option value="unipc_tq" class="k_diffusion-only">UniPC TQ</option>
|
||||||
</select>
|
</select>
|
||||||
<a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/How-to-Use#samplers" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip right">Click to learn more about samplers</span></i></a>
|
<a href="https://github.com/easydiffusion/easydiffusion/wiki/How-to-Use#samplers" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Click to learn more about samplers</span></i></a>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
<tr class="pl-5"><td><label>Image Size: </label></td><td>
|
<tr class="pl-5"><td><label>Image Size: </label></td><td id="image-size-options">
|
||||||
<select id="width" name="width" value="512">
|
<select id="width" name="width" value="512">
|
||||||
<option value="128">128 (*)</option>
|
<option value="128">128</option>
|
||||||
<option value="192">192</option>
|
<option value="192">192</option>
|
||||||
<option value="256">256 (*)</option>
|
<option value="256">256</option>
|
||||||
<option value="320">320</option>
|
<option value="320">320</option>
|
||||||
<option value="384">384</option>
|
<option value="384">384</option>
|
||||||
<option value="448">448</option>
|
<option value="448">448</option>
|
||||||
<option value="512" selected>512 (*)</option>
|
<option value="512" selected="">512 (*)</option>
|
||||||
<option value="576">576</option>
|
<option value="576">576</option>
|
||||||
<option value="640">640</option>
|
<option value="640">640</option>
|
||||||
<option value="704">704</option>
|
<option value="704">704</option>
|
||||||
@@ -150,15 +290,18 @@
|
|||||||
<option value="1792">1792</option>
|
<option value="1792">1792</option>
|
||||||
<option value="2048">2048</option>
|
<option value="2048">2048</option>
|
||||||
</select>
|
</select>
|
||||||
<label for="width"><small>(width)</small></label>
|
<label id="widthLabel" for="width"><small><span>(width)</span></small></label>
|
||||||
|
<div class="tooltip-container">
|
||||||
|
<span id="swap-width-height" class="clickable smallButton" style="margin-left: 2px; margin-right:2px;"><i class="fa-solid fa-right-left"><span class="simple-tooltip top-left"> Swap width and height </span></i></span>
|
||||||
|
</div>
|
||||||
<select id="height" name="height" value="512">
|
<select id="height" name="height" value="512">
|
||||||
<option value="128">128 (*)</option>
|
<option value="128">128</option>
|
||||||
<option value="192">192</option>
|
<option value="192">192</option>
|
||||||
<option value="256">256 (*)</option>
|
<option value="256">256</option>
|
||||||
<option value="320">320</option>
|
<option value="320">320</option>
|
||||||
<option value="384">384</option>
|
<option value="384">384</option>
|
||||||
<option value="448">448</option>
|
<option value="448">448</option>
|
||||||
<option value="512" selected>512 (*)</option>
|
<option value="512" selected="">512 (*)</option>
|
||||||
<option value="576">576</option>
|
<option value="576">576</option>
|
||||||
<option value="640">640</option>
|
<option value="640">640</option>
|
||||||
<option value="704">704</option>
|
<option value="704">704</option>
|
||||||
@@ -172,68 +315,164 @@
|
|||||||
<option value="1792">1792</option>
|
<option value="1792">1792</option>
|
||||||
<option value="2048">2048</option>
|
<option value="2048">2048</option>
|
||||||
</select>
|
</select>
|
||||||
<label for="height"><small>(height)</small></label>
|
<label id="heightLabel" for="height"><small><span>(height)</span></small></label>
|
||||||
|
<div id="recent-resolutions-container">
|
||||||
|
<span id="recent-resolutions-button" class="clickable"><i class="fa-solid fa-sliders"><span class="simple-tooltip top-left"> Advanced sizes </span></i></span>
|
||||||
|
<div id="recent-resolutions-popup" class="displayNone">
|
||||||
|
<small>Custom size:</small><br>
|
||||||
|
<input id="custom-width" name="custom-width" type="number" min="128" value="512" onkeypress="preventNonNumericalInput(event)" inputmode="numeric">
|
||||||
|
×
|
||||||
|
<input id="custom-height" name="custom-height" type="number" min="128" value="512" onkeypress="preventNonNumericalInput(event)" inputmode="numeric"><br>
|
||||||
|
<small>Resize:</small><br>
|
||||||
|
<input id="resize-slider" name="resize-slider" class="editor-slider" value="1" type="range" min="0.4" max="2" step="0.005" style="width:100%;"><br>
|
||||||
|
<div id="enlarge-buttons"><button data-factor="0.5" class="tertiaryButton smallButton">×0.5</button> <button data-factor="1.2" class="tertiaryButton smallButton">×1.2</button> <button data-factor="1.5" class="tertiaryButton smallButton">×1.5</button> <button data-factor="2" class="tertiaryButton smallButton">×2</button> <button data-factor="3" class="tertiaryButton smallButton">×3</button></div>
|
||||||
|
|
||||||
|
<div class="two-column">
|
||||||
|
<div class="left-column">
|
||||||
|
<small>Recently used:</small><br>
|
||||||
|
<div id="recent-resolution-list">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right-column">
|
||||||
|
<small>Common sizes:</small><br>
|
||||||
|
<div id="common-resolution-list">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="small_image_warning" class="displayNone">Small image sizes can cause bad image quality</div>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
<tr class="pl-5"><td><label for="num_inference_steps">Inference Steps:</label></td><td> <input id="num_inference_steps" name="num_inference_steps" size="4" value="25" onkeypress="preventNonNumericalInput(event)"></td></tr>
|
<tr class="pl-5"><td><label for="num_inference_steps">Inference Steps:</label></td><td> <input id="num_inference_steps" name="num_inference_steps" type="number" min="1" step="1" style="width: 42pt" value="25" onkeypress="preventNonNumericalInput(event)" inputmode="numeric"></td></tr>
|
||||||
<tr class="pl-5"><td><label for="guidance_scale_slider">Guidance Scale:</label></td><td> <input id="guidance_scale_slider" name="guidance_scale_slider" class="editor-slider" value="75" type="range" min="10" max="500"> <input id="guidance_scale" name="guidance_scale" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)"></td></tr>
|
<tr class="pl-5"><td><label for="guidance_scale_slider">Guidance Scale:</label></td><td> <input id="guidance_scale_slider" name="guidance_scale_slider" class="editor-slider" value="75" type="range" min="11" max="500"> <input id="guidance_scale" name="guidance_scale" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)" inputmode="decimal"></td></tr>
|
||||||
<tr id="prompt_strength_container" class="pl-5"><td><label for="prompt_strength_slider">Prompt Strength:</label></td><td> <input id="prompt_strength_slider" name="prompt_strength_slider" class="editor-slider" value="80" type="range" min="0" max="99"> <input id="prompt_strength" name="prompt_strength" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)"><br/></td></tr></span>
|
<tr id="prompt_strength_container" class="pl-5"><td><label for="prompt_strength_slider">Prompt Strength:</label></td><td> <input id="prompt_strength_slider" name="prompt_strength_slider" class="editor-slider" value="80" type="range" min="0" max="99"> <input id="prompt_strength" name="prompt_strength" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)" inputmode="decimal"><br/></td></tr>
|
||||||
|
<tr id="lora_model_container" class="pl-5">
|
||||||
|
<td>
|
||||||
|
<label for="lora_model">LoRA:</label>
|
||||||
|
</td>
|
||||||
|
<td class="diffusers-restart-needed">
|
||||||
|
<div id="lora_model" data-path=""></div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr id="hypernetwork_model_container" class="pl-5"><td><label for="hypernetwork_model">Hypernetwork:</label></td><td>
|
||||||
|
<input id="hypernetwork_model" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
|
||||||
|
</td></tr>
|
||||||
|
<tr id="hypernetwork_strength_container" class="pl-5">
|
||||||
|
<td><label for="hypernetwork_strength_slider">Hypernetwork Strength:</label></td>
|
||||||
|
<td> <input id="hypernetwork_strength_slider" name="hypernetwork_strength_slider" class="editor-slider" value="100" type="range" min="0" max="100"> <input id="hypernetwork_strength" name="hypernetwork_strength" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)" inputmode="decimal"><br/></td>
|
||||||
|
</tr>
|
||||||
|
<tr id="tiling_container" class="pl-5">
|
||||||
|
<td><label for="tiling">Seamless Tiling:</label></td>
|
||||||
|
<td class="diffusers-restart-needed">
|
||||||
|
<select id="tiling" name="tiling">
|
||||||
|
<option value="none" selected>None</option>
|
||||||
|
<option value="x">Horizontal</option>
|
||||||
|
<option value="y">Vertical</option>
|
||||||
|
<option value="xy">Both</option>
|
||||||
|
</select>
|
||||||
|
<a href="https://github.com/easydiffusion/easydiffusion/wiki/Seamless-Tiling" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Click to learn more about Seamless Tiling</span></i></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr class="pl-5"><td><label for="output_format">Output Format:</label></td><td>
|
<tr class="pl-5"><td><label for="output_format">Output Format:</label></td><td>
|
||||||
<select id="output_format" name="output_format">
|
<select id="output_format" name="output_format">
|
||||||
<option value="jpeg" selected>jpeg</option>
|
<option value="jpeg" selected>jpeg</option>
|
||||||
<option value="png">png</option>
|
<option value="png">png</option>
|
||||||
|
<option value="webp">webp</option>
|
||||||
</select>
|
</select>
|
||||||
|
<span id="output_lossless_container" class="displayNone">
|
||||||
|
<input id="output_lossless" name="output_lossless" type="checkbox"><label for="output_lossless">Lossless</label>
|
||||||
|
</span>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
|
<tr class="pl-5" id="output_quality_row"><td><label for="output_quality">Image Quality:</label></td><td>
|
||||||
|
<input id="output_quality_slider" name="output_quality" class="editor-slider" value="75" type="range" min="10" max="95"> <input id="output_quality" name="output_quality" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)" inputmode="numeric">
|
||||||
|
</td></tr>
|
||||||
|
<tr class="pl-5">
|
||||||
|
<td><label for="tiling">Enable VAE Tiling:</label></td>
|
||||||
|
<td class="diffusers-restart-needed">
|
||||||
|
<input id="enable_vae_tiling" name="enable_vae_tiling" type="checkbox" checked>
|
||||||
|
<label><small>Optimizes memory for larger images</small></label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table></div>
|
</table></div>
|
||||||
|
|
||||||
<div><ul>
|
<div><ul>
|
||||||
<li><b class="settings-subheader">Render Settings</b></li>
|
<li><b class="settings-subheader">Render Settings</b></li>
|
||||||
<li class="pl-5"><input id="stream_image_progress" name="stream_image_progress" type="checkbox"> <label for="stream_image_progress">Show a live preview <small>(uses more VRAM, and slower image creation)</small></label></li>
|
<li class="pl-5"><input id="stream_image_progress" name="stream_image_progress" type="checkbox"> <label for="stream_image_progress">Show a live preview <small>(uses more VRAM, slower images)</small></label></li>
|
||||||
<li class="pl-5"><input id="use_face_correction" name="use_face_correction" type="checkbox"> <label for="use_face_correction">Fix incorrect faces and eyes <small>(uses GFPGAN)</small></label></li>
|
<li class="pl-5" id="use_face_correction_container">
|
||||||
|
<input id="use_face_correction" name="use_face_correction" type="checkbox"> <label for="use_face_correction">Fix incorrect faces and eyes</label> <div style="display:inline-block;"><input id="gfpgan_model" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" /></div>
|
||||||
|
<table id="codeformer_settings" class="displayNone sub-settings">
|
||||||
|
<tr class="pl-5"><td><label for="codeformer_fidelity_slider">Strength:</label></td><td><input id="codeformer_fidelity_slider" name="codeformer_fidelity_slider" class="editor-slider" value="5" type="range" min="0" max="10"> <input id="codeformer_fidelity" name="codeformer_fidelity" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)" inputmode="decimal"></td></tr>
|
||||||
|
<tr class="pl-5"><td><label for="codeformer_upscale_faces">Upscale Faces:</label></td><td><input id="codeformer_upscale_faces" name="codeformer_upscale_faces" type="checkbox" checked> <label><small>(improves the resolution of faces)</small></label></td></tr>
|
||||||
|
</table>
|
||||||
|
</li>
|
||||||
<li class="pl-5">
|
<li class="pl-5">
|
||||||
<input id="use_upscale" name="use_upscale" type="checkbox"> <label for="use_upscale">Upscale image by 4x with </label>
|
<input id="use_upscale" name="use_upscale" type="checkbox"> <label for="use_upscale">Scale up by</label>
|
||||||
|
<select id="upscale_amount" name="upscale_amount">
|
||||||
|
<option id="upscale_amount_2x" value="2">2x</option>
|
||||||
|
<option id="upscale_amount_4x" value="4" selected>4x</option>
|
||||||
|
</select>
|
||||||
|
with
|
||||||
<select id="upscale_model" name="upscale_model">
|
<select id="upscale_model" name="upscale_model">
|
||||||
<option value="RealESRGAN_x4plus" selected>RealESRGAN_x4plus</option>
|
<option value="RealESRGAN_x4plus" selected>RealESRGAN_x4plus</option>
|
||||||
<option value="RealESRGAN_x4plus_anime_6B">RealESRGAN_x4plus_anime_6B</option>
|
<option value="RealESRGAN_x4plus_anime_6B">RealESRGAN_x4plus_anime_6B</option>
|
||||||
|
<option value="latent_upscaler">Latent Upscaler 2x</option>
|
||||||
</select>
|
</select>
|
||||||
|
<table id="latent_upscaler_settings" class="displayNone sub-settings">
|
||||||
|
<tr class="pl-5"><td><label for="latent_upscaler_steps_slider">Upscaling Steps:</label></td><td><input id="latent_upscaler_steps_slider" name="latent_upscaler_steps_slider" class="editor-slider" value="10" type="range" min="1" max="50"> <input id="latent_upscaler_steps" name="latent_upscaler_steps" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)" inputmode="numeric"></td></tr>
|
||||||
|
</table>
|
||||||
</li>
|
</li>
|
||||||
<li class="pl-5"><input id="show_only_filtered_image" name="show_only_filtered_image" type="checkbox" checked> <label for="show_only_filtered_image">Show only the corrected/upscaled image</label></li>
|
<li class="pl-5"><input id="show_only_filtered_image" name="show_only_filtered_image" type="checkbox" checked> <label for="show_only_filtered_image">Show only the corrected/upscaled image</label></li>
|
||||||
</ul></div>
|
</ul></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="editor-modifiers" class="panel-box">
|
<label><small><b>Note:</b> The Image Modifiers section has moved to the <code>+ Image Modifiers</code> button at the top, just above the Prompt textbox.</small></label>
|
||||||
<h4 class="collapsible">
|
|
||||||
Image Modifiers (art styles, tags etc)
|
|
||||||
<i id="modifier-settings-btn" class="fa-solid fa-gear section-button">
|
|
||||||
<span class="simple-tooltip left">
|
|
||||||
Add Custom Modifiers
|
|
||||||
</span>
|
|
||||||
</i>
|
|
||||||
</h4>
|
|
||||||
<div id="editor-modifiers-entries" class="collapsible-content">
|
|
||||||
<div id="editor-modifiers-entries-toolbar">
|
|
||||||
<label for="preview-image">Image Style:</label>
|
|
||||||
<select id="preview-image" name="preview-image" value="portrait">
|
|
||||||
<option value="portrait" selected="">Face</option>
|
|
||||||
<option value="landscape">Landscape</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<label for="modifier-card-size-slider">Thumbnail Size:</label>
|
|
||||||
<input id="modifier-card-size-slider" name="modifier-card-size-slider" value="0" type="range" min="-3" max="5">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="preview" class="col-free">
|
<div id="preview" class="col-free">
|
||||||
|
|
||||||
<div id="initial-text">
|
<div id="initial-text">
|
||||||
Type a prompt and press the "Make Image" button.<br/><br/>You can set an "Initial Image" if you want to guide the AI.<br/><br/>
|
Type a prompt and press the "Make Image" button.<br/><br/>You can set an "Initial Image" if you want to guide the AI.<br/><br/>
|
||||||
You can also add modifiers like "Realistic", "Pencil Sketch", "ArtStation" etc by browsing through the "Image Modifiers" section
|
You can also add modifiers like "Realistic", "Pencil Sketch", "ArtStation" etc by browsing through the "Image Modifiers" section
|
||||||
and selecting the desired modifiers.<br/><br/>
|
and selecting the desired modifiers.<br/><br/>
|
||||||
Click "Image Settings" for additional settings like seed, image size, number of images to generate etc.<br/><br/>Enjoy! :)
|
Click "Image Settings" for additional settings like seed, image size, number of images to generate etc.<br/><br/>Enjoy! :)
|
||||||
</div>
|
</div>
|
||||||
<div id="preview-tools">
|
<div id="preview-content">
|
||||||
<button id="clear-all-previews" class="secondaryButton"><i class="fa-solid fa-trash-can"></i> Clear All</button>
|
<div id="preview-tools" class="displayNone">
|
||||||
|
<button id="clear-all-previews" class="secondaryButton"><i class="fa-solid fa-trash-can icon"></i> Clear All</button>
|
||||||
|
<button class="tertiaryButton" id="show-download-popup"><i class="fa-solid fa-download"></i><span> Download images</span></button>
|
||||||
|
<div class="display-settings">
|
||||||
|
<button id="undo" class="displayNone primaryButton">
|
||||||
|
Undo <i class="fa-solid fa-rotate-left icon"></i>
|
||||||
|
<span class="simple-tooltip left">Undo last remove</span>
|
||||||
|
</button>
|
||||||
|
<span class="auto-scroll"></span> <!-- hack for Rabbit Hole update -->
|
||||||
|
<button id="auto_scroll_btn" class="tertiaryButton">
|
||||||
|
<i class="fa-solid fa-arrows-up-to-line icon"></i>
|
||||||
|
<input id="auto_scroll" name="auto_scroll" type="checkbox" style="display: none">
|
||||||
|
<span class="simple-tooltip left">
|
||||||
|
Scroll to generated image (<span class="state">OFF</span>)
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button class="dropdown tertiaryButton">
|
||||||
|
<i class="fa-solid fa-magnifying-glass-plus icon dropbtn"></i>
|
||||||
|
<span class="simple-tooltip left">
|
||||||
|
Image Size
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<div class="dropdown-item">
|
||||||
|
<input id="thumbnail_size" name="thumbnail_size" class="editor-slider" type="range" value="70" min="5" max="200" oninput="sliderUpdate(event)">
|
||||||
|
<input id="thumbnail_size-input" name="thumbnail_size-input" size="3" value="70" pattern="^[0-9.]+$" onkeypress="preventNonNumericalInput(event)" oninput="sliderUpdate(event)" inputmode="numeric"> %
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="clearfix" style="clear: both;"></div>
|
||||||
|
</div>
|
||||||
|
<div id="supportBanner" class="displayNone">
|
||||||
|
If you found this project useful and want to help keep it alive, please consider <a href="https://ko-fi.com/easydiffusion" target="_blank">buying me a coffee</a> to help cover the cost of development and maintenance! Thanks for your support!
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -241,14 +480,36 @@
|
|||||||
<div id="tab-content-settings" class="tab-content">
|
<div id="tab-content-settings" class="tab-content">
|
||||||
<div id="system-settings" class="tab-content-inner">
|
<div id="system-settings" class="tab-content-inner">
|
||||||
<h1>System Settings</h1>
|
<h1>System Settings</h1>
|
||||||
<div class="parameters-table"></div>
|
<div class="parameters-table" id="system-settings-table"></div>
|
||||||
<br/>
|
<br/>
|
||||||
<button id="save-system-settings-btn" class="primaryButton">Save</button>
|
<button id="save-system-settings-btn" class="primaryButton">Save</button>
|
||||||
|
<div id="install-extras-container" class="displayNone">
|
||||||
|
<br/>
|
||||||
|
<div id="install-extras">
|
||||||
|
<h3><i class="fa fa-cubes-stacked"></i> Optional Packages</h3>
|
||||||
|
<div class="parameters-table" id="system-settings-install-extras-table"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br/><br/>
|
||||||
|
<div id="share-easy-diffusion">
|
||||||
|
<h3><i class="fa fa-user-group"></i> Share Easy Diffusion</h3>
|
||||||
|
<div class="parameters-table" id="system-settings-network-table">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<br/><br/>
|
<br/><br/>
|
||||||
<div>
|
<div>
|
||||||
<h3><i class="fa fa-microchip icon"></i> System Info</h3>
|
<h3><i class="fa fa-microchip icon"></i> System Info</h3>
|
||||||
<div id="system-info"></div>
|
<div id="system-info">
|
||||||
|
<table>
|
||||||
|
<tr><td><label>Processor:</label></td><td id="system-info-cpu" class="value"></td></tr>
|
||||||
|
<tr><td><label>Compatible Graphics Cards (all):</label></td><td id="system-info-gpus-all" class="value"></td></tr>
|
||||||
|
<tr><td></td><td> </td></tr>
|
||||||
|
<tr><td><label>Used for rendering 🔥:</label></td><td id="system-info-rendering-devices" class="value"></td></tr>
|
||||||
|
<tr><td><label>Server Addresses <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">You can access Stable Diffusion UI from other devices using these addresses</span></i> :</label></td><td id="system-info-server-hosts" class="value"></td></tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="tab-content-about" class="tab-content">
|
<div id="tab-content-about" class="tab-content">
|
||||||
@@ -256,28 +517,44 @@
|
|||||||
<div class="float-container">
|
<div class="float-container">
|
||||||
<div class="float-child">
|
<div class="float-child">
|
||||||
<h1>Help</h1>
|
<h1>Help</h1>
|
||||||
<ul id="help-links">
|
<div id="help-links">
|
||||||
<li><span class="help-section">Using the software</span>
|
<h4><span class="help-section"><b>Basics</b></span></h4>
|
||||||
<ul>
|
<ul>
|
||||||
<li> <a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/How-To-Use" target="_blank"><i class="fa-solid fa-book fa-fw"></i> How to use</a>
|
<li> <a href="https://github.com/easydiffusion/easydiffusion/wiki/How-To-Use" target="_blank">How to use</a></li>
|
||||||
<li> <a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/UI-Overview" target="_blank"><i class="fa-solid fa-list fa-fw"></i> UI Overview</a>
|
<li> <a href="https://github.com/easydiffusion/easydiffusion/wiki/Writing-Prompts" target="_blank">Writing prompts</a></li>
|
||||||
<li> <a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Writing-Prompts" target="_blank"><i class="fa-solid fa-pen-to-square fa-fw"></i> Writing prompts</a>
|
<li> <a href="https://github.com/easydiffusion/easydiffusion/wiki/Image-Modifiers" target="_blank">Image Modifiers</a></li>
|
||||||
<li> <a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Inpainting" target="_blank"><i class="fa-solid fa-paintbrush fa-fw"></i> Inpainting</a>
|
<li> <a href="https://github.com/easydiffusion/easydiffusion/wiki/Inpainting" target="_blank">Inpainting</a></li>
|
||||||
<li> <a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Run-on-Multiple-GPUs" target="_blank"><i class="fa-solid fa-paintbrush fa-fw"></i> Run on Multiple GPUs</a>
|
<li> <a href="https://github.com/easydiffusion/easydiffusion/wiki/Samplers" target="_blank">Samplers</a></li>
|
||||||
|
<li> <a href="https://github.com/easydiffusion/easydiffusion/wiki/UI-Overview" target="_blank">Summary of every UI option</a></li>
|
||||||
|
<li> <a href="https://github.com/easydiffusion/easydiffusion/wiki/Troubleshooting" target="_blank">Common error messages (and solutions)</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<li><span class="help-section">Installation</span>
|
<h4><span class="help-section"><b>Intermediate</b></span></h4>
|
||||||
<ul>
|
<ul>
|
||||||
<li> <a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" target="_blank"><i class="fa-solid fa-circle-question fa-fw"></i> Troubleshooting</a>
|
<li> <a href="https://github.com/easydiffusion/easydiffusion/wiki/Custom-Models" target="_blank">Custom Models</a></li>
|
||||||
|
<li> <a href="https://github.com/easydiffusion/easydiffusion/wiki/Prompt-Syntax" target="_blank">Prompt Syntax (weights, emphasis etc)</a></li>
|
||||||
|
<li> <a href="https://github.com/easydiffusion/easydiffusion/wiki/UI-Plugins" target="_blank">UI Plugins</a></li>
|
||||||
|
<li> <a href="https://github.com/easydiffusion/easydiffusion/wiki/Embeddings" target="_blank">Embeddings</a></li>
|
||||||
|
<li> <a href="https://github.com/easydiffusion/easydiffusion/wiki/LoRA" target="_blank">LoRA</a></li>
|
||||||
|
<li> <a href="https://github.com/easydiffusion/easydiffusion/wiki/SDXL" target="_blank">SDXL</a></li>
|
||||||
|
<li> <a href="https://github.com/easydiffusion/easydiffusion/wiki/ControlNet" target="_blank">ControlNet</a></li>
|
||||||
|
<li> <a href="https://github.com/easydiffusion/easydiffusion/wiki/Seamless-Tiling" target="_blank">Seamless Tiling</a></li>
|
||||||
|
<li> <a href="https://github.com/easydiffusion/easydiffusion/wiki/xFormers" target="_blank">xFormers</a></li>
|
||||||
|
<li> <a href="https://github.com/easydiffusion/easydiffusion/wiki/The-beta-channel" target="_blank">The beta channel</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<li><span class="help-section">Downloadable Content</span>
|
<h4><span class="help-section"><b>Advanced topics</b></span></h4>
|
||||||
<ul>
|
<ul>
|
||||||
<li> <a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Custom-Models" target="_blank"><i class="fa-solid fa-images fa-fw"></i> Custom Models</a>
|
<li> <a href="https://github.com/easydiffusion/easydiffusion/wiki/Run-on-Multiple-GPUs" target="_blank">Run on Multiple GPUs</a></li>
|
||||||
<li> <a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/UI-Plugins" target="_blank"><i class="fa-solid fa-puzzle-piece fa-fw"></i> UI Plugins</a>
|
<li> <a href="https://github.com/easydiffusion/easydiffusion/wiki/Model-Merging" target="_blank">Model Merging</a></li>
|
||||||
<li> <a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/VAE-Variational-Auto-Encoder" target="_blank"><i class="fa-solid fa-hand-sparkles fa-fw"></i> VAE Variational Auto Encoder</a>
|
<li> <a href="https://github.com/easydiffusion/easydiffusion/wiki/Custom-Modifiers" target="_blank">Custom Modifiers</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</ul>
|
|
||||||
|
<h4><span class="help-section"><b>Misc</b></span></h4>
|
||||||
|
<ul>
|
||||||
|
<li> <a href="https://theally.notion.site/The-Definitive-Stable-Diffusion-Glossary-1d1e6d15059c41e6a6b4306b4ecd9df9" target="_blank">Glossary of Stable Diffusion related terms</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="float-child">
|
<div class="float-child">
|
||||||
@@ -285,7 +562,7 @@
|
|||||||
<ul id="community-links">
|
<ul id="community-links">
|
||||||
<li><a href="https://discord.com/invite/u9yhsFmEkB" target="_blank"><i class="fa-brands fa-discord fa-fw"></i> Discord user community</a></li>
|
<li><a href="https://discord.com/invite/u9yhsFmEkB" target="_blank"><i class="fa-brands fa-discord fa-fw"></i> Discord user community</a></li>
|
||||||
<li><a href="https://www.reddit.com/r/StableDiffusionUI/" target="_blank"><i class="fa-brands fa-reddit fa-fw"></i> Reddit community</a></li>
|
<li><a href="https://www.reddit.com/r/StableDiffusionUI/" target="_blank"><i class="fa-brands fa-reddit fa-fw"></i> Reddit community</a></li>
|
||||||
<li><a href="https://github.com/cmdr2/stable-diffusion-ui" target="_blank"><i class="fa-brands fa-github fa-fw"></i> Source code on GitHub</a></li>
|
<li><a href="https://github.com/easydiffusion/easydiffusion" target="_blank"><i class="fa-brands fa-github fa-fw"></i> Source code on GitHub</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -293,7 +570,85 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="popup" id="splash-screen" data-version="1">
|
||||||
|
<div>
|
||||||
|
<i class="close-button fa-solid fa-xmark"></i>
|
||||||
|
<img class="splash-img" src="/media/images/icon-512x512.png" width="128" height="128">
|
||||||
|
<h1>Diffusers Tech Preview</h1>
|
||||||
|
<p>The Diffusers Tech Preview allows early access to the new features based on <a href="https://huggingface.co/docs/diffusers/index" target="_blank">Diffusers</a>.</p>
|
||||||
|
<p>This is under active development, and is missing a few features. It is experimental! Please report any bugs to the #beta channel in our <a href="https://discord.gg/QUcNZufQNZ" target="_blank">Discord</a> server!</p>
|
||||||
|
<h2>New upcoming features in our new engine</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://huggingface.co/blog/lora" target="_blank">LORA</a> support - Place LORA files in the <tt>models/lora</tt> folder.</li>
|
||||||
|
<li><a href="https://github.com/damian0815/compel/blob/main/Reference.md" target="_blank">Compel Prompt Parser</a> - New, more powerful parser. In short:
|
||||||
|
<ul>
|
||||||
|
<li> no limit to the length of prompts (i.e. long prompts are supported)</li>
|
||||||
|
<li> Use <tt>+</tt> and <tt>-</tt> to increase/decrease the weight. E.g. <tt>apple</tt>, <tt>apple+</tt>, <tt>apple++</tt>, <tt>apple+++</tt>,
|
||||||
|
or <tt>apple-</tt>, <tt>apple--</tt> for different weights.</li>
|
||||||
|
<li> Use exact weights - 0.0 to 1.0 reduces the weight, 1.0 to 2.0 increases the weight.
|
||||||
|
Think of it like a multiplier, like 1.5x or 0.5x: E.g. <tt>(apple)0.8 falling from a tree</tt>,
|
||||||
|
<tt>(apple)1.5 falling from a tree</tt>, <tt>(apple falling)1.4 from a tree</tt></li>
|
||||||
|
<li> You can group tokens together using parentheses/round-brackets. E.g. <tt>(apple falling)++
|
||||||
|
from a tree</tt>. Nested parentheses are supported.</li>
|
||||||
|
</ul>
|
||||||
|
This clarifies a few things:
|
||||||
|
<ul>
|
||||||
|
<li> colon (<tt>:</tt>) is NOT used for blending. Neither is it used for weights. It has no impact and
|
||||||
|
will be considered a part of the prompt.</li>
|
||||||
|
<li> <tt>(())</tt> and <tt>[]</tt> do not affect the prompt's weights.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li> More choices for img2img samplers</li>
|
||||||
|
<li> Support for official inpainting models</li>
|
||||||
|
<li> Generate images that tile seamlessly</li>
|
||||||
|
<li> <a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Clip-Skip" target="_blank">Clip Skip</a> support allows to skip the last CLIP layer (recommended by some LORA models)</li>
|
||||||
|
<li> New samplers: DDPM and DEIS</li>
|
||||||
|
<li> Memory optimizations that allow the use of 2GB GPUs</li>
|
||||||
|
</ul>
|
||||||
|
<h2>Known issues</h2>
|
||||||
|
<ul>
|
||||||
|
<li> Some LoRA consistently fail to load in EasyDiffusion</li>
|
||||||
|
<li> Some LoRA are far more sensitive to alpha (compared to a11)</li>
|
||||||
|
<li> Hangs sometimes on "compel is ready", while making the token.</li>
|
||||||
|
<li> Some custom inpainting models don't work</li>
|
||||||
|
<li> These samplers don't work yet: Unipc SNR, Unipc TQ, Unipc SNR2, DPM++ 2s Ancestral, DPM++ SDE, DPM Fast, DPM Adaptive, DPM2</li>
|
||||||
|
<li> Hypernetwork doesn't work</li>
|
||||||
|
<li> The time remaining in browser differs from the one in the console</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dialog id="download-images-dialog">
|
||||||
|
<div class="dialog-header">
|
||||||
|
<div class="dialog-header-left">
|
||||||
|
<h4>Download all images</h4>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<i id="download-images-close-button" class="fa-solid fa-xmark fa-lg"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="parameters-table">
|
||||||
|
<div>
|
||||||
|
<div><i class="fa fa-file-zipper"></i></div>
|
||||||
|
<div><label for="theme">Download as a ZIP file</label><small>Instead of downloading individual files, generate one zip file with all images</small></div>
|
||||||
|
<div><div class="input-toggle"><input id="zip_toggle" name="zip_toggle" checked="" type="checkbox"><label for="zip_toggle"></label></div></div>
|
||||||
|
</div>
|
||||||
|
<div id="download-add-folders">
|
||||||
|
<div><i class="fa fa-folder-tree"></i></div>
|
||||||
|
<div><label for="theme">Add per-job folders</label><small>Place images into job folders</small></div>
|
||||||
|
<div><div class="input-toggle"><input id="tree_toggle" name="tree_toggle" checked="" type="checkbox"><label for="tree_toggle"></label></div></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div><i class="fa fa-sliders"></i></div>
|
||||||
|
<div><label for="theme">Add metadata files</label><small>For each image, also download a JSON file with all the settings used to generate the image</small></div>
|
||||||
|
<div><div class="input-toggle"><input id="json_toggle" name="json_toggle" checked="" type="checkbox"><label for="json_toggle"></label></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="center">
|
||||||
|
<button id="save-all-images" class="primaryButton"><i class="fa-solid fa-images"></i> Start download</button>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
<div id="save-settings-config" class="popup">
|
<div id="save-settings-config" class="popup">
|
||||||
<div>
|
<div>
|
||||||
<i class="close-button fa-solid fa-xmark"></i>
|
<i class="close-button fa-solid fa-xmark"></i>
|
||||||
@@ -304,53 +659,213 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="modifier-settings-config" class="popup">
|
<div id="editor-modifiers">
|
||||||
|
<div id="editor-modifiers-header" class="dialog-header">
|
||||||
|
<div id="modifiers-header-left" class="dialog-header-left">
|
||||||
|
<h4>Image Modifiers</h4>
|
||||||
|
<span>(drawing style, camera, etc.)</span>
|
||||||
|
</div>
|
||||||
|
<div id="modifiers-header-right">
|
||||||
|
<i id="modifier-settings-btn" class="fa-solid fa-gear section-button">
|
||||||
|
<span class="simple-tooltip left">
|
||||||
|
Add Custom Modifiers
|
||||||
|
</span>
|
||||||
|
</i>
|
||||||
|
<i id="modifiers-container-size-btn" class="fa-solid fa-expand"></i>
|
||||||
|
<i id="modifiers-close-button" class="fa-solid fa-xmark fa-lg"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="editor-modifiers-subheader">
|
||||||
|
<div id="modifiers-action-collapsibles-btn">
|
||||||
|
<i class="modifiers-action-icon fa-solid fa-square-plus"></i>
|
||||||
|
<span class="modifiers-action-text">
|
||||||
|
Expand Categories
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="preview-image">Image Style:</label>
|
||||||
|
<select id="preview-image" name="preview-image" value="portrait">
|
||||||
|
<option value="portrait" selected="">Face</option>
|
||||||
|
<option value="landscape">Landscape</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="modifier-card-size-slider">Thumbnail Size:</label>
|
||||||
|
<input id="modifier-card-size-slider" name="modifier-card-size-slider" value="0" type="range" min="-2" max="3">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="editor-modifiers-entries" class="collapsible-content"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dialog id="modifier-settings-config">
|
||||||
|
<div id="modifier-settings-header" class="dialog-header">
|
||||||
|
<div id="modifier-settings-header-left" class="dialog-header-left">
|
||||||
|
<h4>Custom Modifiers</h4>
|
||||||
|
<span>Set your custom modifiers (one per line)</span>
|
||||||
|
</div>
|
||||||
|
<div id="modifier-settings-header-right">
|
||||||
|
<i id="modifier-settings-close-button" class="fa-solid fa-xmark fa-lg"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea id="custom-modifiers-input" placeholder="Enter your custom modifiers, one-per-line" spellcheck="false"></textarea>
|
||||||
|
<div>
|
||||||
|
<small>
|
||||||
|
<b>Tip:</b> You can include special characters like {} () [] and |. You can also put multiple comma-separated
|
||||||
|
phrases in a single line, to make a single modifier that combines all of those.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<dialog id="embeddings-dialog">
|
||||||
|
<div id="embeddings-dialog-header" class="dialog-header">
|
||||||
|
<div id="embeddings-dialog-header-left" class="dialog-header-left">
|
||||||
|
<h4>Embeddings</h4>
|
||||||
|
<span>
|
||||||
|
<span class="displayNone" id="positive-embedding-text"> Add embeddings to the prompt (click) or negative prompt (shift-click)</span>
|
||||||
|
<span class="displayNone" id="negative-embedding-text"> Add embeddings to the negative prompt</span>
|
||||||
|
<span>
|
||||||
|
</div>
|
||||||
|
<div id="embeddings-dialog-header-right">
|
||||||
|
<button id="add-embeddings-thumb" class="tertiaryButton smallButton" style="background-color: var(--background-color4);"><i class="fa-solid fa-folder-plus"></i> Add thumbnail</button>
|
||||||
|
<input id="add-embeddings-thumb-input" name="add-embeddings-thumb-input" type="file" class="displayNone">
|
||||||
|
<i id="embeddings-dialog-close-button" class="fa-solid fa-xmark fa-lg"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button id="embeddings-action-collapsibles-btn" class="tertiaryButton smallButton">
|
||||||
|
<i class="embeddings-action-icon fa-solid fa-square-plus"></i>
|
||||||
|
<span class="embeddings-action-text">Expand Categories</span>
|
||||||
|
</button>
|
||||||
|
<i class="fa-solid fa-magnifying-glass"></i>
|
||||||
|
<input id="embeddings-search-box" type="text" spellcheck="false" autocomplete="off" placeholder="Search..." inputmode="search">
|
||||||
|
<label for="embedding-card-size-selector"><small>Thumbnail Size:</small></label>
|
||||||
|
<select id="embedding-card-size-selector" name="embedding-card-size-selector">
|
||||||
|
<option value="-2">0</option>
|
||||||
|
<option value="-1" selected>1</option>
|
||||||
|
<option value="0">2</option>
|
||||||
|
<option value="1">3</option>
|
||||||
|
<option value="2">4</option>
|
||||||
|
<option value="3">5</option>
|
||||||
|
</select>
|
||||||
|
<span style="float:right;"><label>Mode:</label> <select id="embeddings-mode"><option value="insert">Insert at cursor position</option><option value="append">Append at the end</option></select>
|
||||||
|
</div>
|
||||||
|
<div id="embeddings-list">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<dialog id="use-as-thumb-dialog">
|
||||||
|
<div id="use-as-thumb-dialog-header" class="dialog-header">
|
||||||
|
<div id="use-as-thumb-dialog-header-left" class="dialog-header-left">
|
||||||
|
<h4>Use as thumbnail</h4>
|
||||||
|
<span>Use a pictures as thumbnail for embeddings, LORAs, etc.</span>
|
||||||
|
</div>
|
||||||
|
<div id="use-as-thumb-dialog-header-right">
|
||||||
|
<i id="use-as-thumb-dialog-close-button" class="fa-solid fa-xmark fa-lg"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="use-as-thumb-grid">
|
||||||
|
<div class="use-as-thumb-preview">
|
||||||
|
<div id="use-as-thumb-img-container"><img id="use-as-thumb-image" src="/media/images/noimg.png" width="512" height="512"></div>
|
||||||
|
</div>
|
||||||
|
<div class="use-as-thumb-select">
|
||||||
|
<label for="use-as-thumb-select">Use the thumbnail for:</label><br>
|
||||||
|
<select id="use-as-thumb-select" size="16" multiple>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="use-as-thumb-buttons">
|
||||||
|
<button class="tertiaryButton" id="use-as-thumb-save">Save thumbnail</button>
|
||||||
|
<button class="tertiaryButton" id="use-as-thumb-cancel">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<div id="image-editor" class="popup image-editor-popup">
|
||||||
<div>
|
<div>
|
||||||
<i class="close-button fa-solid fa-xmark"></i>
|
<i class="close-button fa-solid fa-xmark"></i>
|
||||||
<h1>Modifier Settings</h1>
|
<h1>Image Editor</h1>
|
||||||
<p>Set your custom modifiers (one per line)</p>
|
<div class="flex-container">
|
||||||
<textarea id="custom-modifiers-input" placeholder="Enter your custom modifiers, one-per-line"></textarea>
|
<div class="editor-controls-left"></div>
|
||||||
<p><small><b>Tip:</b> You can include special characters like {} () [] and |. You can also put multiple comma-separated phrases in a single line, to make a single modifier that combines all of those.</small></p>
|
<div class="editor-controls-center">
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
<div class="editor-controls-right">
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="image-inpainter" class="popup image-editor-popup">
|
||||||
|
<div>
|
||||||
|
<i class="close-button fa-solid fa-xmark"></i>
|
||||||
|
<h1>Inpainter</h1>
|
||||||
|
<div class="flex-container">
|
||||||
|
<div class="editor-controls-left"></div>
|
||||||
|
<div class="editor-controls-center">
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
<div class="editor-controls-right">
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="footer-spacer"></div>
|
<div id="footer-spacer"></div>
|
||||||
<div id="footer">
|
<div id="footer">
|
||||||
<div class="line-separator"> </div>
|
<div class="line-separator"> </div>
|
||||||
<p>If you found this project useful and want to help keep it alive, please <a href="https://ko-fi.com/cmdr2_stablediffusion_ui" target="_blank"><img src="/media/images/kofi.png" id="coffeeButton"></a> to help cover the cost of development and maintenance! Thank you for your support!</p>
|
<p>Please feel free to join the <a href="https://discord.com/invite/u9yhsFmEkB" target="_blank">discord community</a> or <a href="https://github.com/easydiffusion/easydiffusion/issues" target="_blank">file an issue</a> if you have any problems or suggestions in using this interface.</p>
|
||||||
<p>Please feel free to join the <a href="https://discord.com/invite/u9yhsFmEkB" target="_blank">discord community</a> or <a href="https://github.com/cmdr2/stable-diffusion-ui/issues" target="_blank">file an issue</a> if you have any problems or suggestions in using this interface.</p>
|
|
||||||
<div id="footer-legal">
|
<div id="footer-legal">
|
||||||
<p><b>Disclaimer:</b> The authors of this project are not responsible for any content generated using this interface.</p>
|
<p><b>Disclaimer:</b> The authors of this project are not responsible for any content generated using this interface.</p>
|
||||||
<p>This license of this software forbids you from sharing any content that violates any laws, produce any harm to a person, disseminate any personal information that would be meant for harm, <br/>spread misinformation and target vulnerable groups. For the full list of restrictions please read <a href="https://github.com/cmdr2/stable-diffusion-ui/blob/main/LICENSE" target="_blank">the license</a>.</p>
|
<p>This license of this software forbids you from sharing any content that violates any laws, produce any harm to a person, disseminate any personal information that would be meant for harm, <br/>spread misinformation and target vulnerable groups. For the full list of restrictions please read <a href="https://github.com/easydiffusion/easydiffusion/blob/main/LICENSE" target="_blank">the license</a>.</p>
|
||||||
<p>By using this software, you consent to the terms and conditions of the license.</p>
|
<p>By using this software, you consent to the terms and conditions of the license.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<input id="test_diffusers" type="checkbox" style="display: none" checked />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
<script src="media/js/utils.js"></script>
|
<script src="media/js/utils.js"></script>
|
||||||
|
<script src="media/js/engine.js"></script>
|
||||||
<script src="media/js/parameters.js"></script>
|
<script src="media/js/parameters.js"></script>
|
||||||
<script src="media/js/plugins.js"></script>
|
|
||||||
<script src="media/js/inpainting-editor.js"></script>
|
|
||||||
<script src="media/js/image-modifiers.js"></script>
|
<script src="media/js/image-modifiers.js"></script>
|
||||||
<script src="media/js/auto-save.js"></script>
|
<script src="media/js/auto-save.js"></script>
|
||||||
|
|
||||||
|
<script src="media/js/searchable-models.js"></script>
|
||||||
|
<script src="media/js/multi-model-selector.js"></script>
|
||||||
|
<script src="media/js/task-manager.js"></script>
|
||||||
<script src="media/js/main.js"></script>
|
<script src="media/js/main.js"></script>
|
||||||
|
<script src="media/js/plugins.js"></script>
|
||||||
<script src="media/js/themes.js"></script>
|
<script src="media/js/themes.js"></script>
|
||||||
<script src="media/js/dnd.js"></script>
|
<script src="media/js/dnd.js"></script>
|
||||||
|
<script src="media/js/image-editor.js"></script>
|
||||||
|
<script src="media/js/image-modal.js"></script>
|
||||||
<script>
|
<script>
|
||||||
async function init() {
|
async function init() {
|
||||||
await initSettings()
|
await initSettings()
|
||||||
await getModels()
|
await getModels(false)
|
||||||
await getDiskPath()
|
|
||||||
await getAppConfig()
|
await getAppConfig()
|
||||||
await loadModifiers()
|
|
||||||
await loadUIPlugins()
|
await loadUIPlugins()
|
||||||
await getDevices()
|
await loadModifiers()
|
||||||
|
await getSystemInfo()
|
||||||
|
// await initPlugins()
|
||||||
|
|
||||||
setInterval(healthCheck, HEALTH_PING_INTERVAL * 1000)
|
SD.init({
|
||||||
healthCheck()
|
events: {
|
||||||
|
statusChange: setServerStatus,
|
||||||
|
idle: onIdle,
|
||||||
|
ping: onPing
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// splashScreen()
|
||||||
|
|
||||||
playSound()
|
// load models again, but scan for malicious this time
|
||||||
|
await getModels(true)
|
||||||
|
|
||||||
|
// playSound()
|
||||||
}
|
}
|
||||||
|
|
||||||
init()
|
init()
|
||||||
|
|||||||
14
ui/main.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from easydiffusion import model_manager, app, server, bucket_manager
|
||||||
|
from easydiffusion.server import server_api # required for uvicorn
|
||||||
|
|
||||||
|
app.init()
|
||||||
|
|
||||||
|
server.init()
|
||||||
|
|
||||||
|
# Init the app
|
||||||
|
model_manager.init()
|
||||||
|
app.init_render_threads()
|
||||||
|
bucket_manager.init()
|
||||||
|
|
||||||
|
# start the browser ui
|
||||||
|
app.open_browser()
|
||||||
68
ui/media/css/animations.css
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
@keyframes ldio-8f673ktaleu-1 {
|
||||||
|
0% { transform: rotate(0deg) }
|
||||||
|
50% { transform: rotate(-45deg) }
|
||||||
|
100% { transform: rotate(0deg) }
|
||||||
|
}
|
||||||
|
@keyframes ldio-8f673ktaleu-2 {
|
||||||
|
0% { transform: rotate(180deg) }
|
||||||
|
50% { transform: rotate(225deg) }
|
||||||
|
100% { transform: rotate(180deg) }
|
||||||
|
}
|
||||||
|
.ldio-8f673ktaleu > div:nth-child(2) {
|
||||||
|
transform: translate(-15px,0);
|
||||||
|
}
|
||||||
|
.ldio-8f673ktaleu > div:nth-child(2) div {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
width: 60px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 60px 60px 0 0;
|
||||||
|
background: #f3b72e;
|
||||||
|
animation: ldio-8f673ktaleu-1 1s linear infinite;
|
||||||
|
transform-origin: 30px 30px
|
||||||
|
}
|
||||||
|
.ldio-8f673ktaleu > div:nth-child(2) div:nth-child(2) {
|
||||||
|
animation: ldio-8f673ktaleu-2 1s linear infinite
|
||||||
|
}
|
||||||
|
.ldio-8f673ktaleu > div:nth-child(2) div:nth-child(3) {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
animation: none;
|
||||||
|
}@keyframes ldio-8f673ktaleu-3 {
|
||||||
|
0% { transform: translate(95px,0); opacity: 0 }
|
||||||
|
20% { opacity: 1 }
|
||||||
|
100% { transform: translate(35px,0); opacity: 1 }
|
||||||
|
}
|
||||||
|
.ldio-8f673ktaleu > div:nth-child(1) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.ldio-8f673ktaleu > div:nth-child(1) div {
|
||||||
|
position: absolute;
|
||||||
|
top: 46px;
|
||||||
|
left: -4px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #3869c5;
|
||||||
|
animation: ldio-8f673ktaleu-3 1s linear infinite
|
||||||
|
}
|
||||||
|
.ldio-8f673ktaleu > div:nth-child(1) div:nth-child(1) { animation-delay: -0.67s }
|
||||||
|
.ldio-8f673ktaleu > div:nth-child(1) div:nth-child(2) { animation-delay: -0.33s }
|
||||||
|
.ldio-8f673ktaleu > div:nth-child(1) div:nth-child(3) { animation-delay: 0s }
|
||||||
|
.loadingio-spinner-bean-eater-x0y3u8qky4n {
|
||||||
|
width: 58px;
|
||||||
|
height: 58px;
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
.ldio-8f673ktaleu {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
transform: translateZ(0) scale(0.58);
|
||||||
|
backface-visibility: hidden;
|
||||||
|
transform-origin: 0 0; /* see note above */
|
||||||
|
}
|
||||||
|
.ldio-8f673ktaleu div { box-sizing: content-box; }
|
||||||
|
/* generated by https://loading.io/ */
|
||||||
@@ -69,13 +69,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.parameters-table > div:first-child {
|
.parameters-table > div:first-child {
|
||||||
border-radius: 12px 12px 0px 0px;
|
border-top-left-radius: 12px;
|
||||||
|
border-top-right-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.parameters-table > div:last-child {
|
.parameters-table > div:last-child {
|
||||||
border-radius: 0px 0px 12px 12px;
|
border-bottom-left-radius: 12px;
|
||||||
|
border-bottom-right-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.parameters-table .fa-fire {
|
.parameters-table .fa-fire,
|
||||||
|
.parameters-table .fa-bolt {
|
||||||
color: #F7630C;
|
color: #F7630C;
|
||||||
}
|
}
|
||||||
58
ui/media/css/croppr.css
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
.croppr-container * {
|
||||||
|
user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.croppr-container img {
|
||||||
|
vertical-align: middle;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.croppr {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.croppr-overlay {
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|
||||||
|
.croppr-region {
|
||||||
|
border: 1px dashed rgba(0, 0, 0, 0.5);
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
cursor: move;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.croppr-imageClipped {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 2;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.croppr-handle {
|
||||||
|
border: 1px solid black;
|
||||||
|
background-color: white;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 4;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
5
ui/media/css/drawingboard.min.css
vendored
@@ -3,7 +3,7 @@
|
|||||||
font-family: 'Work Sans';
|
font-family: 'Work Sans';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local(''),
|
src: local('Work Sans'),
|
||||||
url('/media/fonts/work-sans-v18-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
url('/media/fonts/work-sans-v18-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||||
url('/media/fonts/work-sans-v18-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
url('/media/fonts/work-sans-v18-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
font-family: 'Work Sans';
|
font-family: 'Work Sans';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
src: local(''),
|
src: local('Work Sans'),
|
||||||
url('/media/fonts/work-sans-v18-latin-600.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
url('/media/fonts/work-sans-v18-latin-600.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||||
url('/media/fonts/work-sans-v18-latin-600.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
url('/media/fonts/work-sans-v18-latin-600.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
font-family: 'Work Sans';
|
font-family: 'Work Sans';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
src: local(''),
|
src: local('Work Sans'),
|
||||||
url('/media/fonts/work-sans-v18-latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
url('/media/fonts/work-sans-v18-latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||||
url('/media/fonts/work-sans-v18-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
url('/media/fonts/work-sans-v18-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
font-family: 'Work Sans';
|
font-family: 'Work Sans';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
src: local(''),
|
src: local('Work Sans'),
|
||||||
url('/media/fonts/work-sans-v18-latin-800.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
url('/media/fonts/work-sans-v18-latin-800.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||||
url('/media/fonts/work-sans-v18-latin-800.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
url('/media/fonts/work-sans-v18-latin-800.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||||
}
|
}
|
||||||
|
|||||||
255
ui/media/css/image-editor.css
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
.editor-controls-left {
|
||||||
|
padding-left: 32px;
|
||||||
|
text-align: left;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
max-width: min-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-options-container {
|
||||||
|
display: flex;
|
||||||
|
row-gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-options-container > * {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-options-container > * > * {
|
||||||
|
position: inherit;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: var(--background-color3);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.25s;
|
||||||
|
}
|
||||||
|
.editor-options-container > * > *:hover {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-options-container > * > *.active {
|
||||||
|
border: 1px solid #3584e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image_editor_opacity .editor-options-container > * > *:not(.active) {
|
||||||
|
border: 1px solid var(--background-color3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image_editor_color .editor-options-container {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.image_editor_color .editor-options-container > * {
|
||||||
|
flex: 20%;
|
||||||
|
}
|
||||||
|
.image_editor_color .editor-options-container > * > * {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.image_editor_color .editor-options-container > * > *.active::before {
|
||||||
|
content: "\f00c";
|
||||||
|
display: var(--fa-display,inline-block);
|
||||||
|
font-style: normal;
|
||||||
|
font-variant: normal;
|
||||||
|
line-height: 1;
|
||||||
|
text-rendering: auto;
|
||||||
|
font-family: var(--fa-style-family, "Font Awesome 6 Free");
|
||||||
|
font-weight: var(--fa-style, 900);
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%) scale(125%);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.image_editor_color .editor-options-container > *:first-child {
|
||||||
|
flex: 100%;
|
||||||
|
}
|
||||||
|
.image_editor_color .editor-options-container > *:first-child > * {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.image_editor_color .editor-options-container > *:first-child > * > input {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.image_editor_color .editor-options-container > *:first-child > * > span {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.image_editor_color .editor-options-container > *:first-child > *.active > span {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image_editor_tool .editor-options-container {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image_editor_tool .editor-options-container > * {
|
||||||
|
padding: 2px;
|
||||||
|
flex: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-controls-center {
|
||||||
|
/* background: var(--background-color2); */
|
||||||
|
flex: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-controls-center > div {
|
||||||
|
position: relative;
|
||||||
|
background: black;
|
||||||
|
margin: 20pt;
|
||||||
|
margin-top: 40pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-controls-center canvas {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-controls-right {
|
||||||
|
padding: 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.editor-controls-right > div:last-child {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 200px;
|
||||||
|
gap: 5px;
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-editor-button {
|
||||||
|
width: 100%;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: var(--background-color3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-controls-right .image-editor-button {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#init_image_button_inpaint .input-toggle {
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#init_image_button_inpaint .input-toggle input:not(:checked) ~ label {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-editor-popup {
|
||||||
|
--popup-margin: 16px;
|
||||||
|
--popup-padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 700px) {
|
||||||
|
.image-editor-popup {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-editor-popup > div {
|
||||||
|
margin: var(--popup-margin);
|
||||||
|
padding: var(--popup-padding);
|
||||||
|
min-height: calc(99h - (2 * var(--popup-margin)));
|
||||||
|
max-width: fit-content;
|
||||||
|
min-width: fit-content;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-editor-popup h1 {
|
||||||
|
position: absolute;
|
||||||
|
top: 32px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media screen and (max-width: 700px) {
|
||||||
|
.image-editor-popup > div {
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-editor-popup h1 {
|
||||||
|
position: relative;
|
||||||
|
transform: none;
|
||||||
|
left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.image-editor-popup > div > div {
|
||||||
|
min-height: calc(99vh - (2 * var(--popup-margin)) - (2 * var(--popup-padding)));
|
||||||
|
}
|
||||||
|
|
||||||
|
.inpainter .image_editor_color {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inpainter .editor-canvas-background {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
#init_image_preview_container .button {
|
||||||
|
display: flex;
|
||||||
|
padding: 6px;
|
||||||
|
height: 24px;
|
||||||
|
box-shadow: 2px 2px 1px 1px #00000088;
|
||||||
|
}
|
||||||
|
|
||||||
|
#init_image_preview_container .button:hover {
|
||||||
|
background: var(--background-color4)
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-editor-popup .button {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.image-editor-popup h4 {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-editor-popup .load_mask {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.inpainter .load_mask {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-canvas-overlay {
|
||||||
|
cursor: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-brush-preview {
|
||||||
|
position: fixed;
|
||||||
|
background: black;
|
||||||
|
opacity: 0.3;
|
||||||
|
borderRadius: 50%;
|
||||||
|
cursor: none;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-options-container > * > *:not(.active):not(.button) {
|
||||||
|
border: 1px dotted slategray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image_editor_opacity .editor-options-container > * > *:not(.active):not(.button) {
|
||||||
|
border: 1px dotted slategray;
|
||||||
|
}
|
||||||
|
|
||||||
96
ui/media/css/image-modal.css
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
#viewFullSizeImgModal {
|
||||||
|
--popup-padding: 24px;
|
||||||
|
position: sticky;
|
||||||
|
padding: var(--popup-padding);
|
||||||
|
pointer-events: none;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewFullSizeImgModal:not(.active) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewFullSizeImgModal > * {
|
||||||
|
pointer-events: auto;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewFullSizeImgModal .backdrop {
|
||||||
|
max-width: unset;
|
||||||
|
width: 100%;
|
||||||
|
max-height: unset;
|
||||||
|
height: 100%;
|
||||||
|
inset: 0;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1001;
|
||||||
|
opacity: .5;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewFullSizeImgModal .content {
|
||||||
|
min-height: initial;
|
||||||
|
max-height: calc(100vh - (var(--popup-padding) * 2));
|
||||||
|
height: fit-content;
|
||||||
|
min-width: initial;
|
||||||
|
max-width: calc(100vw - (var(--popup-padding) * 2));
|
||||||
|
width: fit-content;
|
||||||
|
z-index: 1003;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewFullSizeImgModal .image-wrapper {
|
||||||
|
min-height: initial;
|
||||||
|
max-height: calc(100vh - (var(--popup-padding) * 2));
|
||||||
|
height: fit-content;
|
||||||
|
min-width: initial;
|
||||||
|
max-width: calc(100vw - (var(--popup-padding) * 2));
|
||||||
|
width: fit-content;
|
||||||
|
box-sizing: border-box;
|
||||||
|
pointer-events: auto;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewFullSizeImgModal img.natural-zoom {
|
||||||
|
max-width: calc(100vh - (var(--popup-padding) * 2) - 4px);
|
||||||
|
max-height: calc(100vh - (var(--popup-padding) * 2) - 4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewFullSizeImgModal img:not(.natural-zoom) {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewFullSizeImgModal .grabbing img:not(.natural-zoom) {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewFullSizeImgModal .content > div::-webkit-scrollbar-track, #viewFullSizeImgModal .content > div::-webkit-scrollbar-corner {
|
||||||
|
background: rgba(0, 0, 0, .5)
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewFullSizeImgModal .menu-bar {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
padding-right: var(--scrollbar-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
#viewFullSizeImgModal .menu-bar .tertiaryButton {
|
||||||
|
font-size: 1.2em;
|
||||||
|
margin: 12px 12px 0 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
9
ui/media/css/jquery-confirm.min.css
vendored
Normal file
@@ -1,14 +1,16 @@
|
|||||||
.modifier-card {
|
.modifier-card {
|
||||||
|
position: relative;
|
||||||
|
box-sizing: content-box; /* fixes border misalignment */
|
||||||
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
|
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
|
||||||
transition: 0.1s;
|
transition: 0.1s;
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
margin: 3pt 3pt;
|
margin: 3pt 3pt;
|
||||||
float: left;
|
float: left;
|
||||||
width: 8em;
|
width: 6em;
|
||||||
height: 11.5em;
|
height: 9.5em;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: 8em 3.5em;
|
grid-template-rows: 6em 3.5em;
|
||||||
gap: 0px 0px;
|
gap: 0px 0px;
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
@@ -16,82 +18,71 @@
|
|||||||
"modifier-card-container";
|
"modifier-card-container";
|
||||||
border: 2px solid rgba(255, 255, 255, .05);
|
border: 2px solid rgba(255, 255, 255, .05);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
z-index: 2;
|
||||||
.modifier-card-size_5 {
|
|
||||||
width: 18em;
|
|
||||||
grid-template-rows: 18em 3.5em;
|
|
||||||
height: 21.5em;
|
|
||||||
}
|
|
||||||
.modifier-card-size_5 .modifier-card-image-overlay {
|
|
||||||
font-size: 8em;
|
|
||||||
}
|
|
||||||
.modifier-card-size_4 {
|
|
||||||
width: 14em;
|
|
||||||
grid-template-rows: 14em 3.5em;
|
|
||||||
height: 17.5em;
|
|
||||||
}
|
|
||||||
.modifier-card-size_4 .modifier-card-image-overlay {
|
|
||||||
font-size: 7em;
|
|
||||||
}
|
}
|
||||||
.modifier-card-size_3 {
|
.modifier-card-size_3 {
|
||||||
width: 11em;
|
|
||||||
grid-template-rows: 11em 3.5em;
|
|
||||||
height: 14.5em;
|
|
||||||
}
|
|
||||||
.modifier-card-size_3 .modifier-card-image-overlay {
|
|
||||||
font-size: 6em;
|
|
||||||
}
|
|
||||||
.modifier-card-size_2 {
|
|
||||||
width: 10em;
|
width: 10em;
|
||||||
grid-template-rows: 10em 3.5em;
|
grid-template-rows: 10em 3.5em;
|
||||||
height: 13.5em;
|
height: 13.5em;
|
||||||
}
|
}
|
||||||
.modifier-card-size_2 .modifier-card-image-overlay {
|
.modifier-card-size_3 .modifier-card-image-overlay {
|
||||||
font-size: 6em;
|
font-size: 6em;
|
||||||
}
|
}
|
||||||
.modifier-card-size_1 {
|
.modifier-card-size_3 .modifier-card-label {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
.modifier-card-size_2 {
|
||||||
width: 9em;
|
width: 9em;
|
||||||
grid-template-rows: 9em 3.5em;
|
grid-template-rows: 9em 3.5em;
|
||||||
height: 12.5em;
|
height: 12.5em;
|
||||||
}
|
}
|
||||||
.modifier-card-size_1 .modifier-card-image-overlay {
|
.modifier-card-size_2 .modifier-card-image-overlay {
|
||||||
font-size: 5em;
|
font-size: 5em;
|
||||||
}
|
}
|
||||||
.modifier-card-size_-1 {
|
.modifier-card-size_2 .modifier-card-label {
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
.modifier-card-size_1 {
|
||||||
width: 7em;
|
width: 7em;
|
||||||
grid-template-rows: 7em 3.5em;
|
grid-template-rows: 7em 3.5em;
|
||||||
height: 10.5em;
|
height: 10.5em;
|
||||||
}
|
}
|
||||||
.modifier-card-size_-1 .modifier-card-image-overlay {
|
.modifier-card-size_1 .modifier-card-image-overlay {
|
||||||
font-size: 4em;
|
font-size: 4em;
|
||||||
}
|
}
|
||||||
.modifier-card-size_-2 {
|
.modifier-card-size_-1 {
|
||||||
width: 6em;
|
|
||||||
grid-template-rows: 6em 3.5em;
|
|
||||||
height: 9.5em;
|
|
||||||
}
|
|
||||||
.modifier-card-size_-2 .modifier-card-image-overlay {
|
|
||||||
font-size: 3em;
|
|
||||||
}
|
|
||||||
.modifier-card-size_-3 {
|
|
||||||
width: 5em;
|
width: 5em;
|
||||||
grid-template-rows: 5em 3.5em;
|
grid-template-rows: 5em 3.5em;
|
||||||
height: 8.5em;
|
height: 8.5em;
|
||||||
}
|
}
|
||||||
.modifier-card-size_-3 .modifier-card-image-overlay {
|
.modifier-card-size_-1 .modifier-card-image-overlay {
|
||||||
font-size: 3em;
|
font-size: 3em;
|
||||||
}
|
}
|
||||||
.modifier-card-size_-3 .modifier-card-label {
|
.modifier-card-size_-1 .modifier-card-label {
|
||||||
font-size: 0.8em;
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
.modifier-card-size_-2 {
|
||||||
|
width: 4em;
|
||||||
|
grid-template-rows: 3.5em 3em;
|
||||||
|
height: 6.5em;
|
||||||
|
}
|
||||||
|
.modifier-card-size_-2 .modifier-card-image-overlay {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
.modifier-card-size_-2 .modifier-card-label {
|
||||||
|
font-size: 0.7em;
|
||||||
}
|
}
|
||||||
.modifier-card-tiny {
|
.modifier-card-tiny {
|
||||||
width: 6em;
|
width: 5em;
|
||||||
height: 9.5em;
|
grid-template-rows: 5em 3.5em;
|
||||||
grid-template-rows: 6em 3.5em;
|
height: 8.5em;
|
||||||
}
|
}
|
||||||
.modifier-card-tiny .modifier-card-image-overlay {
|
.modifier-card-tiny .modifier-card-image-overlay {
|
||||||
font-size: 4em;
|
font-size: 4em;
|
||||||
}
|
}
|
||||||
|
.modifier-card-tiny .modifier-card-label {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
.modifier-card:hover {
|
.modifier-card:hover {
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
box-shadow: 0 5px 16px 5px rgba(0, 0, 0, 0.25);
|
box-shadow: 0 5px 16px 5px rgba(0, 0, 0, 0.25);
|
||||||
@@ -115,6 +106,7 @@
|
|||||||
}
|
}
|
||||||
.modifier-card-image-container * {
|
.modifier-card-image-container * {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
.modifier-card-container {
|
.modifier-card-container {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -131,6 +123,7 @@
|
|||||||
.modifier-card-label {
|
.modifier-card-label {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
.modifier-card-image-overlay {
|
.modifier-card-image-overlay {
|
||||||
width: inherit;
|
width: inherit;
|
||||||
@@ -140,7 +133,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 5px 5px 0 0;
|
border-radius: 5px 5px 0 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
font-size: 5em;
|
font-size: 4em;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
color: rgb(255 255 255 / 50%);
|
color: rgb(255 255 255 / 50%);
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -153,6 +146,9 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
.modifier-card-active .modifier-card-overlay {
|
||||||
|
background-color: rgb(169 78 241 / 40%);
|
||||||
|
}
|
||||||
.modifier-card:hover > .modifier-card-image-container .modifier-card-image-overlay {
|
.modifier-card:hover > .modifier-card-image-container .modifier-card-image-overlay {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
@@ -163,61 +159,30 @@
|
|||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
box-shadow: 0 5px 16px 5px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 5px 16px 5px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
#preview-image {
|
|
||||||
margin-top: 0.5em;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
.modifier-card-active {
|
.modifier-card-active {
|
||||||
border: 2px solid rgb(179 82 255 / 94%);
|
border: 2px solid rgb(179 82 255 / 94%);
|
||||||
box-shadow: 0 0px 10px 0 rgb(170 0 229 / 58%);
|
box-shadow: 0 0px 10px 0 rgb(170 0 229 / 58%);
|
||||||
}
|
}
|
||||||
.tooltip {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.tooltip .tooltip-text {
|
|
||||||
visibility: hidden;
|
|
||||||
width: 120px;
|
|
||||||
background: rgb(101,97,181);
|
|
||||||
background: linear-gradient(180deg, rgba(101,97,181,1) 0%, rgba(47,45,85,1) 100%);
|
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 5px;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
top: 105%;
|
|
||||||
left: 39%;
|
|
||||||
margin-left: -60px;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
border: 2px solid rgb(90 100 177 / 94%);
|
|
||||||
box-shadow: 0px 10px 20px 5px rgb(11 0 58 / 55%);
|
|
||||||
width: 10em;
|
|
||||||
}
|
|
||||||
.tooltip .tooltip-text::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: -0.9em;
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -5px;
|
|
||||||
border-width: 5px;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: transparent transparent rgb(90 100 177 / 94%) transparent;
|
|
||||||
}
|
|
||||||
.tooltip:hover .tooltip-text {
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
#modifier-card-size-slider {
|
#modifier-card-size-slider {
|
||||||
width: 6em;
|
width: 6em;
|
||||||
margin-bottom: 0.5em;
|
height: 4pt;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
#modifier-settings-btn {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
#modifier-settings-config textarea {
|
#modifier-settings-config textarea {
|
||||||
|
margin-left: 5%;
|
||||||
|
margin-top: 2ex;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
}
|
}
|
||||||
|
.modifier-card .hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.support-long-label .modifier-card-overlay:hover ~ .modifier-card-container .modifier-card-label {
|
||||||
|
font-size: 0.7em;
|
||||||
|
}
|
||||||
|
.support-long-label .modifier-card-overlay:hover ~ .modifier-card-container .long-label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.support-long-label .modifier-card-overlay:hover ~ .modifier-card-container .regular-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|||||||
288
ui/media/css/plugins.css
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
.plugins-table {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugins-table > div {
|
||||||
|
background: var(--background-color2);
|
||||||
|
display: flex;
|
||||||
|
padding: 0px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugins-table > div > div {
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugins-table small {
|
||||||
|
color: rgb(153, 153, 153);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugins-table > div > div:nth-child(1) {
|
||||||
|
font-size: 20px;
|
||||||
|
width: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugins-table > div > div:nth-child(2) {
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: left;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: start;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugins-table > div > div:nth-child(3) {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugins-table > div:first-child {
|
||||||
|
border-radius: 12px 12px 0px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugins-table > div:last-child {
|
||||||
|
border-radius: 0px 0px 12px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-table {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-table > div {
|
||||||
|
background: var(--background-color2);
|
||||||
|
display: flex;
|
||||||
|
padding: 0px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-table > div > div {
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-table small {
|
||||||
|
color: rgb(153, 153, 153);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-table > div > div:nth-child(1) {
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: left;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: start;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-table > div > div:nth-child(2) {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-table > div:first-child {
|
||||||
|
border-radius: 12px 12px 0px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notifications-table > div:last-child {
|
||||||
|
border-radius: 0px 0px 12px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
DIV.no-notification {
|
||||||
|
padding-top: 16px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-manager-intro {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#plugin-filter {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
margin: 4px 0 6px 0;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#refresh-plugins {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#refresh-plugins a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#refresh-plugins a:active {
|
||||||
|
transition-duration: 0.1s;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
left: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-installed-locally {
|
||||||
|
font-style: italic;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-source {
|
||||||
|
font-size: x-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-warning {
|
||||||
|
color: orange;
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-warning.hide {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-warning ul {
|
||||||
|
list-style: square;
|
||||||
|
margin: 0 0 8px 16px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-warning li {
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MODAL DIALOG */
|
||||||
|
#pluginDialog-input-dialog {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pluginDialog-dialog-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(32, 33, 36, 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pluginDialog-dialog-box {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 80%;
|
||||||
|
max-width: 600px;
|
||||||
|
background: var(--background-color2);
|
||||||
|
border: solid 1px var(--background-color3);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0px 0px 30px black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pluginDialog-dialog-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pluginDialog-dialog-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pluginDialog-dialog-close-button {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pluginDialog-dialog-close-button:hover {
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pluginDialog-dialog-content {
|
||||||
|
padding: 0 16px 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pluginDialog-dialog-content textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
border-radius: var(--input-border-radius);
|
||||||
|
padding: 4px;
|
||||||
|
accent-color: var(--accent-color);
|
||||||
|
background: var(--input-background-color);
|
||||||
|
border: var(--input-border-size) solid var(--input-border-color);
|
||||||
|
color: var(--input-text-color);
|
||||||
|
font-size: 9pt;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pluginDialog-dialog-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pluginDialog-dialog-buttons button {
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
/*background: var(--accent-color);*/
|
||||||
|
/*border: var(--primary-button-border);*/
|
||||||
|
/*color: rgb(255, 221, 255);*/
|
||||||
|
background-color: #3071a9;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pluginDialog-dialog-buttons button:hover {
|
||||||
|
/*background: hsl(var(--accent-hue), 100%, 50%);*/
|
||||||
|
background-color: #428bca;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* NOTIFICATION CENTER */
|
||||||
|
#plugin-notification-button {
|
||||||
|
float: right;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#plugin-notification-button:hover {
|
||||||
|
background: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
#plugin-notification-button:active {
|
||||||
|
transition-duration: 0.1s;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
left: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-notification-pill {
|
||||||
|
background-color: red;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: white;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
height: 12px;
|
||||||
|
line-height: 12px;
|
||||||
|
position: relative;
|
||||||
|
right: -8px;
|
||||||
|
text-align: center;
|
||||||
|
top: -20px;
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
99
ui/media/css/searchable-models.css
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
.model-list {
|
||||||
|
position: absolute;
|
||||||
|
margin-block-start: 2px;
|
||||||
|
display: none;
|
||||||
|
padding-inline-start: 0;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow: auto;
|
||||||
|
background: var(--input-background-color);
|
||||||
|
border: var(--input-border-size) solid var(--input-border-color);
|
||||||
|
border-radius: var(--input-border-radius);
|
||||||
|
color: var(--input-text-color);
|
||||||
|
z-index: 1;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-list ul {
|
||||||
|
padding-right: 20px;
|
||||||
|
padding-inline-start: 0;
|
||||||
|
margin-top: 3pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-list li {
|
||||||
|
padding-top: 3px;
|
||||||
|
padding-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-list .icon {
|
||||||
|
padding-right: 3pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-result {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-no-result {
|
||||||
|
color: var(--text-color);
|
||||||
|
list-style: none;
|
||||||
|
padding: 3px 6px 3px 6px;
|
||||||
|
font-size: 9pt;
|
||||||
|
font-style: italic;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-list li.model-folder {
|
||||||
|
color: var(--text-color);
|
||||||
|
list-style: none;
|
||||||
|
padding: 6px 6px 6px 6px;
|
||||||
|
font-size: 9pt;
|
||||||
|
font-weight: bold;
|
||||||
|
border-top: 1px solid var(--background-color1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-list li.model-file {
|
||||||
|
color: var(--input-text-color);
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 12px;
|
||||||
|
padding-right:20px;
|
||||||
|
font-size: 10pt;
|
||||||
|
font-weight: normal;
|
||||||
|
transition: none;
|
||||||
|
transition-property: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-list li.model-file.in-root-folder {
|
||||||
|
padding-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-list li.model-file.selected {
|
||||||
|
background: grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-selector {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-selector-arrow {
|
||||||
|
position: absolute;
|
||||||
|
width: 17px;
|
||||||
|
margin: 5px -17px;
|
||||||
|
padding-top: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 8pt;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-input {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reloadModels {
|
||||||
|
background: var(--background-color2);
|
||||||
|
border: none;
|
||||||
|
padding: 0px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#reload-models.secondaryButton:hover {
|
||||||
|
background: var(--background-color2);
|
||||||
|
}
|
||||||
@@ -13,23 +13,38 @@
|
|||||||
--accent-lightness-hover: 40%;
|
--accent-lightness-hover: 40%;
|
||||||
|
|
||||||
--text-color: #eee;
|
--text-color: #eee;
|
||||||
|
--link-color: rgb(0, 102, 204);
|
||||||
|
--small-label-color: rgb(153, 153, 153);
|
||||||
|
|
||||||
--input-text-color: #eee;
|
--input-text-color: #eee;
|
||||||
--input-background-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (0.7 * var(--value-step))));
|
--input-background-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (0.7 * var(--value-step))));
|
||||||
--input-border-color: var(--background-color4);
|
--input-border-color: var(--background-color4);
|
||||||
|
|
||||||
--button-text-color: var(--input-text-color);
|
--button-text-color: var(--input-text-color);
|
||||||
--button-color: var(--accent-color);
|
--button-color: var(--input-background-color);
|
||||||
--button-border: none;
|
--button-border: none;
|
||||||
|
--button-hover-background: hsl(var(--accent-hue), 100%, calc(var(--accent-lightness) + 6%));
|
||||||
|
--secondary-button-background: rgb(132, 8, 0);
|
||||||
|
--secondary-button-hover-background: rgb(177, 27, 0);
|
||||||
|
|
||||||
/* other */
|
/* other */
|
||||||
--input-border-radius: 4px;
|
--input-border-radius: 4px;
|
||||||
--input-border-size: 1px;
|
--input-border-size: 1px;
|
||||||
--accent-color: hsl(var(--accent-hue), 100%, var(--accent-lightness));
|
--accent-color: hsl(var(--accent-hue), 100%, var(--accent-lightness));
|
||||||
--accent-color-hover: hsl(var(--accent-hue), 100%, var(--accent-lightness-hover));
|
--accent-color-hover: hsl(var(--accent-hue), 100%, var(--accent-lightness-hover));
|
||||||
|
--accent-text-color: rgb(255, 221, 255);
|
||||||
--primary-button-border: none;
|
--primary-button-border: none;
|
||||||
--input-switch-padding: 1px;
|
--input-switch-padding: 1px;
|
||||||
--input-height: 18px;
|
--input-height: 18px;
|
||||||
|
--tertiary-background-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (2 * var(--value-step))));
|
||||||
|
--tertiary-border-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (3 * var(--value-step))));
|
||||||
|
--tertiary-color: var(--input-text-color);
|
||||||
|
|
||||||
|
/* Main theme color, hex color fallback. */
|
||||||
|
--theme-color-fallback: #673AB6;
|
||||||
|
--status-orange: rgb(200, 139, 0);
|
||||||
|
--status-green: green;
|
||||||
|
--status-red: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-light {
|
.theme-light {
|
||||||
@@ -39,11 +54,17 @@
|
|||||||
--background-color4: #cccccc;
|
--background-color4: #cccccc;
|
||||||
|
|
||||||
--text-color: black;
|
--text-color: black;
|
||||||
--button-text-color: white;
|
|
||||||
|
|
||||||
--input-text-color: black;
|
--input-text-color: black;
|
||||||
--input-background-color: #f8f9fa;
|
--input-background-color: #f8f9fa;
|
||||||
--input-border-color: grey;
|
--input-border-color: grey;
|
||||||
|
|
||||||
|
--theme-color-fallback: #aaaaaa;
|
||||||
|
|
||||||
|
--tertiary-background-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (16.8 * var(--value-step))));
|
||||||
|
--tertiary-border-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (12 * var(--value-step))));
|
||||||
|
|
||||||
|
--accent-text-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-discord {
|
.theme-discord {
|
||||||
@@ -58,6 +79,12 @@
|
|||||||
--input-border-size: 2px;
|
--input-border-size: 2px;
|
||||||
--input-background-color: #202225;
|
--input-background-color: #202225;
|
||||||
--input-border-color: var(--input-background-color);
|
--input-border-color: var(--input-background-color);
|
||||||
|
|
||||||
|
--theme-color-fallback: #202225;
|
||||||
|
|
||||||
|
--tertiary-background-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (3.5 * var(--value-step))));
|
||||||
|
--tertiary-border-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (4.5 * var(--value-step))));
|
||||||
|
--accent-text-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-cool-blue {
|
.theme-cool-blue {
|
||||||
@@ -73,6 +100,12 @@
|
|||||||
--input-background-color: var(--background-color3);
|
--input-background-color: var(--background-color3);
|
||||||
|
|
||||||
--accent-hue: 212;
|
--accent-hue: 212;
|
||||||
|
|
||||||
|
--theme-color-fallback: #0056b8;
|
||||||
|
|
||||||
|
--tertiary-background-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (3.5 * var(--value-step))));
|
||||||
|
--tertiary-border-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (4.5 * var(--value-step))));
|
||||||
|
--accent-text-color: #f7fbff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -87,6 +120,11 @@
|
|||||||
--background-color4: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (3 * var(--value-step))));
|
--background-color4: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (3 * var(--value-step))));
|
||||||
|
|
||||||
--input-background-color: var(--background-color3);
|
--input-background-color: var(--background-color3);
|
||||||
|
|
||||||
|
--theme-color-fallback: #5300b8;
|
||||||
|
|
||||||
|
--tertiary-background-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (3.5 * var(--value-step))));
|
||||||
|
--tertiary-border-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (4.5 * var(--value-step))));
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-super-dark {
|
.theme-super-dark {
|
||||||
@@ -101,6 +139,8 @@
|
|||||||
|
|
||||||
--input-background-color: var(--background-color3);
|
--input-background-color: var(--background-color3);
|
||||||
--input-border-size: 0px;
|
--input-border-size: 0px;
|
||||||
|
|
||||||
|
--theme-color-fallback: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-wild {
|
.theme-wild {
|
||||||
@@ -117,10 +157,14 @@
|
|||||||
|
|
||||||
--input-border-size: 1px;
|
--input-border-size: 1px;
|
||||||
--input-background-color: hsl(222, var(--main-saturation), calc(var(--value-base) - (2 * var(--value-step))));
|
--input-background-color: hsl(222, var(--main-saturation), calc(var(--value-base) - (2 * var(--value-step))));
|
||||||
--input-text-color: red;
|
--input-text-color: #FF0000;
|
||||||
--input-border-color: green;
|
--input-border-color: #005E05;
|
||||||
|
|
||||||
|
--tertiary-color: white;
|
||||||
|
--accent-text-color: #f7fbff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.theme-gnomie {
|
.theme-gnomie {
|
||||||
--background-color1: #242424;
|
--background-color1: #242424;
|
||||||
--background-color2: #353535;
|
--background-color2: #353535;
|
||||||
@@ -136,6 +180,8 @@
|
|||||||
--input-background-color: #2a2a2a;
|
--input-background-color: #2a2a2a;
|
||||||
--input-border-size: 0px;
|
--input-border-size: 0px;
|
||||||
--input-border-color: var(--input-background-color);
|
--input-border-color: var(--input-background-color);
|
||||||
|
|
||||||
|
--theme-color-fallback: #2168bf;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-gnomie .panel-box {
|
.theme-gnomie .panel-box {
|
||||||
@@ -143,4 +189,3 @@
|
|||||||
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.25);
|
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.25);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
ui/media/images/fa-eraser.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
4
ui/media/images/fa-eraser.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 576" width="24" height="24">
|
||||||
|
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.-->
|
||||||
|
<path style="filter: drop-shadow(0px 0px 20px white)" d="M290.7 57.4 57.4 290.7c-25 25-25 65.5 0 90.5l80 80c12 12 28.3 18.7 45.3 18.7H512c17.7 0 32-14.3 32-32s-14.3-32-32-32H387.9l130.7-130.6c25-25 25-65.5 0-90.5L381.3 57.4c-25-25-65.5-25-90.5 0zm6.7 358.6H182.6l-80-80 124.7-124.7 137.4 137.4-67.3 67.3z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 571 B |
BIN
ui/media/images/fa-eye-dropper.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
4
ui/media/images/fa-eye-dropper.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="24" height="24">
|
||||||
|
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.-->
|
||||||
|
<path style="filter: drop-shadow(0px 0px 20px white)" d="M341.6 29.2 240.1 130.8l-9.4-9.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-9.4-9.4 101.5-101.6c39-39 39-102.2 0-141.1s-102.2-39-141.1 0zM55.4 323.3c-15 15-23.4 35.4-23.4 56.6v42.4L5.4 462.2c-8.5 12.7-6.8 29.6 4 40.4s27.7 12.5 40.4 4L89.7 480h42.4c21.2 0 41.6-8.4 56.6-23.4l120.7-120.7-45.3-45.3-120.7 120.7c-3 3-7.1 4.7-11.3 4.7H96v-36.1c0-4.2 1.7-8.3 4.7-11.3l120.7-120.7-45.3-45.3L55.4 323.3z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 775 B |
4
ui/media/images/fa-fill.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 576" width="24" height="24">
|
||||||
|
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.-->
|
||||||
|
<path style="filter: drop-shadow(0px 0px 20px white)" d="M118.6 9.4c-12.5-12.5-32.7-12.5-45.2 0s-12.5 32.8 0 45.3l81.3 81.3-92.1 92.1c-37.5 37.5-37.5 98.3 0 135.8l117.5 117.5c37.5 37.5 98.3 37.5 135.8 0l190.4-190.5c28.1-28.1 28.1-73.7 0-101.8L354.9 37.7c-28.1-28.1-73.7-28.1-101.8 0l-53.1 53-81.4-81.3zM200 181.3l49.4 49.4c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L245.3 136l53.1-53.1c3.1-3.1 8.2-3.1 11.3 0l151.4 151.4c3.1 3.1 3.1 8.2 0 11.3L418.7 288H99.5c1.4-5.4 4.2-10.4 8.4-14.6l92.1-92.1z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 763 B |
BIN
ui/media/images/fa-pencil.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
4
ui/media/images/fa-pencil.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="24" height="24">
|
||||||
|
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.-->
|
||||||
|
<path style="filter: drop-shadow(0px 0px 20px white)" d="m410.3 231 11.3-11.3-33.9-33.9-62.1-62.1-33.9-33.9-11.3 11.3-22.6 22.6L58.6 322.9c-10.4 10.4-18 23.3-22.2 37.4L1 480.7c-2.5 8.4-.2 17.5 6.1 23.7s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2l199.2-199.2 22.6-22.7zM160 399.4l-9.1 22.7c-4 3.1-8.5 5.4-13.3 6.9l-78.2 23 23-78.1c1.4-4.9 3.8-9.4 6.9-13.3l22.7-9.1v32c0 8.8 7.2 16 16 16h32zM362.7 18.7l-14.4 14.5-22.6 22.6-11.4 11.3 33.9 33.9 62.1 62.1 33.9 33.9 11.3-11.3 22.6-22.6 14.5-14.5c25-25 25-65.5 0-90.5l-39.3-39.4c-25-25-65.5-25-90.5 0zm-47.4 168-144 144c-6.2 6.2-16.4 6.2-22.6 0s-6.2-16.4 0-22.6l144-144c6.2-6.2 16.4-6.2 22.6 0s6.2 16.4 0 22.6z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 934 B |
|
Before Width: | Height: | Size: 466 B After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 973 B After Width: | Height: | Size: 10 KiB |
BIN
ui/media/images/icon-512x512.png
Normal file
|
After Width: | Height: | Size: 352 KiB |
BIN
ui/media/images/noimg.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
2
ui/media/js/FileSaver.min.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
(function(a,b){if("function"==typeof define&&define.amd)define([],b);else if("undefined"!=typeof exports)b();else{b(),a.FileSaver={exports:{}}.exports}})(this,function(){"use strict";function b(a,b){return"undefined"==typeof b?b={autoBom:!1}:"object"!=typeof b&&(console.warn("Deprecated: Expected third argument to be a object"),b={autoBom:!b}),b.autoBom&&/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type)?new Blob(["\uFEFF",a],{type:a.type}):a}function c(a,b,c){var d=new XMLHttpRequest;d.open("GET",a),d.responseType="blob",d.onload=function(){g(d.response,b,c)},d.onerror=function(){console.error("could not download file")},d.send()}function d(a){var b=new XMLHttpRequest;b.open("HEAD",a,!1);try{b.send()}catch(a){}return 200<=b.status&&299>=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)});
|
||||||
|
|
||||||
@@ -13,18 +13,26 @@ const SETTINGS_IDS_LIST = [
|
|||||||
"num_outputs_total",
|
"num_outputs_total",
|
||||||
"num_outputs_parallel",
|
"num_outputs_parallel",
|
||||||
"stable_diffusion_model",
|
"stable_diffusion_model",
|
||||||
|
"clip_skip",
|
||||||
"vae_model",
|
"vae_model",
|
||||||
"sampler",
|
"sampler_name",
|
||||||
"width",
|
"width",
|
||||||
"height",
|
"height",
|
||||||
"num_inference_steps",
|
"num_inference_steps",
|
||||||
"guidance_scale",
|
"guidance_scale",
|
||||||
"prompt_strength",
|
"prompt_strength",
|
||||||
|
"tiling",
|
||||||
"output_format",
|
"output_format",
|
||||||
|
"output_quality",
|
||||||
|
"output_lossless",
|
||||||
"negative_prompt",
|
"negative_prompt",
|
||||||
"stream_image_progress",
|
"stream_image_progress",
|
||||||
"use_face_correction",
|
"use_face_correction",
|
||||||
|
"gfpgan_model",
|
||||||
"use_upscale",
|
"use_upscale",
|
||||||
|
"upscale_amount",
|
||||||
|
"latent_upscaler_steps",
|
||||||
|
"block_nsfw",
|
||||||
"show_only_filtered_image",
|
"show_only_filtered_image",
|
||||||
"upscale_model",
|
"upscale_model",
|
||||||
"preview-image",
|
"preview-image",
|
||||||
@@ -33,55 +41,79 @@ const SETTINGS_IDS_LIST = [
|
|||||||
"save_to_disk",
|
"save_to_disk",
|
||||||
"diskPath",
|
"diskPath",
|
||||||
"sound_toggle",
|
"sound_toggle",
|
||||||
"turbo",
|
"vram_usage_level",
|
||||||
"use_full_precision",
|
"confirm_dangerous_actions",
|
||||||
"auto_save_settings"
|
"profileName",
|
||||||
|
"metadata_output_format",
|
||||||
|
"auto_save_settings",
|
||||||
|
"apply_color_correction",
|
||||||
|
"process_order_toggle",
|
||||||
|
"thumbnail_size",
|
||||||
|
"auto_scroll",
|
||||||
|
"zip_toggle",
|
||||||
|
"tree_toggle",
|
||||||
|
"json_toggle",
|
||||||
|
"extract_lora_from_prompt",
|
||||||
|
"embedding-card-size-selector",
|
||||||
|
"lora_model",
|
||||||
|
"enable_vae_tiling",
|
||||||
]
|
]
|
||||||
|
|
||||||
const IGNORE_BY_DEFAULT = [
|
const IGNORE_BY_DEFAULT = ["prompt"]
|
||||||
"prompt"
|
|
||||||
]
|
|
||||||
|
|
||||||
const SETTINGS_SECTIONS = [ // gets the "keys" property filled in with an ordered list of settings in this section via initSettings
|
if (!testDiffusers.checked) {
|
||||||
{ id: "editor-inputs", name: "Prompt" },
|
SETTINGS_IDS_LIST.push("hypernetwork_model")
|
||||||
|
SETTINGS_IDS_LIST.push("hypernetwork_strength")
|
||||||
|
}
|
||||||
|
|
||||||
|
const SETTINGS_SECTIONS = [
|
||||||
|
// gets the "keys" property filled in with an ordered list of settings in this section via initSettings
|
||||||
|
{ id: "editor-inputs", name: "Prompt" },
|
||||||
{ id: "editor-settings", name: "Image Settings" },
|
{ id: "editor-settings", name: "Image Settings" },
|
||||||
{ id: "system-settings", name: "System Settings" },
|
{ id: "system-settings", name: "System Settings" },
|
||||||
{ id: "container", name: "Other" }
|
{ id: "container", name: "Other" },
|
||||||
]
|
]
|
||||||
|
|
||||||
async function initSettings() {
|
async function initSettings() {
|
||||||
SETTINGS_IDS_LIST.forEach(id => {
|
SETTINGS_IDS_LIST.forEach((id) => {
|
||||||
var element = document.getElementById(id)
|
var element = document.getElementById(id)
|
||||||
if (!element) {
|
if (!element) {
|
||||||
console.error(`Missing settings element ${id}`)
|
console.error(`Missing settings element ${id}`)
|
||||||
}
|
}
|
||||||
|
if (id in SETTINGS) {
|
||||||
|
// don't create it again
|
||||||
|
return
|
||||||
|
}
|
||||||
SETTINGS[id] = {
|
SETTINGS[id] = {
|
||||||
key: id,
|
key: id,
|
||||||
element: element,
|
element: element,
|
||||||
label: getSettingLabel(element),
|
label: getSettingLabel(element),
|
||||||
default: getSetting(element),
|
default: getSetting(element),
|
||||||
value: getSetting(element),
|
value: getSetting(element),
|
||||||
ignore: IGNORE_BY_DEFAULT.includes(id)
|
ignore: IGNORE_BY_DEFAULT.includes(id),
|
||||||
}
|
}
|
||||||
element.addEventListener("input", settingChangeHandler)
|
element.addEventListener("input", settingChangeHandler)
|
||||||
element.addEventListener("change", settingChangeHandler)
|
element.addEventListener("change", settingChangeHandler)
|
||||||
})
|
})
|
||||||
var unsorted_settings_ids = [...SETTINGS_IDS_LIST]
|
var unsorted_settings_ids = [...SETTINGS_IDS_LIST]
|
||||||
SETTINGS_SECTIONS.forEach(section => {
|
SETTINGS_SECTIONS.forEach((section) => {
|
||||||
var name = section.name
|
var name = section.name
|
||||||
var element = document.getElementById(section.id)
|
var element = document.getElementById(section.id)
|
||||||
var unsorted_ids = unsorted_settings_ids.map(id => `#${id}`).join(",")
|
var unsorted_ids = unsorted_settings_ids.map((id) => `#${id}`).join(",")
|
||||||
var children = unsorted_ids == "" ? [] : Array.from(element.querySelectorAll(unsorted_ids));
|
var children = unsorted_ids == "" ? [] : Array.from(element.querySelectorAll(unsorted_ids))
|
||||||
section.keys = []
|
section.keys = []
|
||||||
children.forEach(e => {
|
children.forEach((e) => {
|
||||||
section.keys.push(e.id)
|
section.keys.push(e.id)
|
||||||
})
|
})
|
||||||
unsorted_settings_ids = unsorted_settings_ids.filter(id => children.find(e => e.id == id) == undefined)
|
unsorted_settings_ids = unsorted_settings_ids.filter((id) => children.find((e) => e.id == id) == undefined)
|
||||||
})
|
})
|
||||||
loadSettings()
|
loadSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSetting(element) {
|
function getSetting(element) {
|
||||||
|
if (element.dataset && "path" in element.dataset) {
|
||||||
|
return element.dataset.path
|
||||||
|
}
|
||||||
if (typeof element === "string" || element instanceof String) {
|
if (typeof element === "string" || element instanceof String) {
|
||||||
element = SETTINGS[element].element
|
element = SETTINGS[element].element
|
||||||
}
|
}
|
||||||
@@ -91,6 +123,10 @@ function getSetting(element) {
|
|||||||
return element.value
|
return element.value
|
||||||
}
|
}
|
||||||
function setSetting(element, value) {
|
function setSetting(element, value) {
|
||||||
|
if (element.dataset && "path" in element.dataset) {
|
||||||
|
element.dataset.path = value
|
||||||
|
return // no need to dispatch any event here because the models are not loaded yet
|
||||||
|
}
|
||||||
if (typeof element === "string" || element instanceof String) {
|
if (typeof element === "string" || element instanceof String) {
|
||||||
element = SETTINGS[element].element
|
element = SETTINGS[element].element
|
||||||
}
|
}
|
||||||
@@ -100,8 +136,7 @@ function setSetting(element, value) {
|
|||||||
}
|
}
|
||||||
if (element.type == "checkbox") {
|
if (element.type == "checkbox") {
|
||||||
element.checked = value
|
element.checked = value
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
element.value = value
|
element.value = value
|
||||||
}
|
}
|
||||||
element.dispatchEvent(new Event("input"))
|
element.dispatchEvent(new Event("input"))
|
||||||
@@ -109,11 +144,11 @@ function setSetting(element, value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveSettings() {
|
function saveSettings() {
|
||||||
var saved_settings = Object.values(SETTINGS).map(setting => {
|
var saved_settings = Object.values(SETTINGS).map((setting) => {
|
||||||
return {
|
return {
|
||||||
key: setting.key,
|
key: setting.key,
|
||||||
value: setting.value,
|
value: setting.value,
|
||||||
ignore: setting.ignore
|
ignore: setting.ignore,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify(saved_settings))
|
localStorage.setItem(SETTINGS_KEY, JSON.stringify(saved_settings))
|
||||||
@@ -124,16 +159,16 @@ function loadSettings() {
|
|||||||
var saved_settings_text = localStorage.getItem(SETTINGS_KEY)
|
var saved_settings_text = localStorage.getItem(SETTINGS_KEY)
|
||||||
if (saved_settings_text) {
|
if (saved_settings_text) {
|
||||||
var saved_settings = JSON.parse(saved_settings_text)
|
var saved_settings = JSON.parse(saved_settings_text)
|
||||||
if (saved_settings.find(s => s.key == "auto_save_settings").value == false) {
|
if (saved_settings.find((s) => s.key == "auto_save_settings")?.value == false) {
|
||||||
setSetting("auto_save_settings", false)
|
setSetting("auto_save_settings", false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
CURRENTLY_LOADING_SETTINGS = true
|
CURRENTLY_LOADING_SETTINGS = true
|
||||||
saved_settings.forEach(saved_setting => {
|
saved_settings.forEach((saved_setting) => {
|
||||||
var setting = SETTINGS[saved_setting.key]
|
var setting = SETTINGS[saved_setting.key]
|
||||||
if (!setting) {
|
if (!setting) {
|
||||||
console.warn(`Attempted to load setting ${saved_setting.key}, but no setting found`);
|
console.warn(`Attempted to load setting ${saved_setting.key}, but no setting found`)
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
setting.ignore = saved_setting.ignore
|
setting.ignore = saved_setting.ignore
|
||||||
if (!setting.ignore) {
|
if (!setting.ignore) {
|
||||||
@@ -142,10 +177,26 @@ function loadSettings() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
CURRENTLY_LOADING_SETTINGS = false
|
CURRENTLY_LOADING_SETTINGS = false
|
||||||
}
|
} else if (localStorage.length < 2) {
|
||||||
else {
|
// localStorage is too short for OldSettings
|
||||||
|
// So this is likely the first time Easy Diffusion is running.
|
||||||
|
// Initialize vram_usage_level based on the available VRAM
|
||||||
|
function initGPUProfile(event) {
|
||||||
|
if (
|
||||||
|
"detail" in event &&
|
||||||
|
"active" in event.detail &&
|
||||||
|
"cuda:0" in event.detail.active &&
|
||||||
|
event.detail.active["cuda:0"].mem_total < 4.5
|
||||||
|
) {
|
||||||
|
vramUsageLevelField.value = "low"
|
||||||
|
vramUsageLevelField.dispatchEvent(new Event("change"))
|
||||||
|
}
|
||||||
|
document.removeEventListener("system_info_update", initGPUProfile)
|
||||||
|
}
|
||||||
|
document.addEventListener("system_info_update", initGPUProfile)
|
||||||
|
} else {
|
||||||
CURRENTLY_LOADING_SETTINGS = true
|
CURRENTLY_LOADING_SETTINGS = true
|
||||||
tryLoadOldSettings();
|
tryLoadOldSettings()
|
||||||
CURRENTLY_LOADING_SETTINGS = false
|
CURRENTLY_LOADING_SETTINGS = false
|
||||||
saveSettings()
|
saveSettings()
|
||||||
}
|
}
|
||||||
@@ -153,9 +204,9 @@ function loadSettings() {
|
|||||||
|
|
||||||
function loadDefaultSettingsSection(section_id) {
|
function loadDefaultSettingsSection(section_id) {
|
||||||
CURRENTLY_LOADING_SETTINGS = true
|
CURRENTLY_LOADING_SETTINGS = true
|
||||||
var section = SETTINGS_SECTIONS.find(s => s.id == section_id);
|
var section = SETTINGS_SECTIONS.find((s) => s.id == section_id)
|
||||||
section.keys.forEach(key => {
|
section.keys.forEach((key) => {
|
||||||
var setting = SETTINGS[key];
|
var setting = SETTINGS[key]
|
||||||
setting.value = setting.default
|
setting.value = setting.default
|
||||||
setSetting(setting.element, setting.value)
|
setSetting(setting.element, setting.value)
|
||||||
})
|
})
|
||||||
@@ -191,10 +242,10 @@ function getSettingLabel(element) {
|
|||||||
|
|
||||||
function fillSaveSettingsConfigTable() {
|
function fillSaveSettingsConfigTable() {
|
||||||
saveSettingsConfigTable.textContent = ""
|
saveSettingsConfigTable.textContent = ""
|
||||||
SETTINGS_SECTIONS.forEach(section => {
|
SETTINGS_SECTIONS.forEach((section) => {
|
||||||
var section_row = `<tr><th>${section.name}</th><td></td></tr>`
|
var section_row = `<tr><th>${section.name}</th><td></td></tr>`
|
||||||
saveSettingsConfigTable.insertAdjacentHTML("beforeend", section_row)
|
saveSettingsConfigTable.insertAdjacentHTML("beforeend", section_row)
|
||||||
section.keys.forEach(key => {
|
section.keys.forEach((key) => {
|
||||||
var setting = SETTINGS[key]
|
var setting = SETTINGS[key]
|
||||||
var element = setting.element
|
var element = setting.element
|
||||||
var checkbox_id = `shouldsave_${element.id}`
|
var checkbox_id = `shouldsave_${element.id}`
|
||||||
@@ -207,7 +258,7 @@ function fillSaveSettingsConfigTable() {
|
|||||||
var newrow = `<tr><td><label for="${checkbox_id}">${setting.label}</label></td><td><input id="${checkbox_id}" name="${checkbox_id}" ${is_checked} type="checkbox" ></td><td><small>(${value})</small></td></tr>`
|
var newrow = `<tr><td><label for="${checkbox_id}">${setting.label}</label></td><td><input id="${checkbox_id}" name="${checkbox_id}" ${is_checked} type="checkbox" ></td><td><small>(${value})</small></td></tr>`
|
||||||
saveSettingsConfigTable.insertAdjacentHTML("beforeend", newrow)
|
saveSettingsConfigTable.insertAdjacentHTML("beforeend", newrow)
|
||||||
var checkbox = document.getElementById(checkbox_id)
|
var checkbox = document.getElementById(checkbox_id)
|
||||||
checkbox.addEventListener("input", event => {
|
checkbox.addEventListener("input", (event) => {
|
||||||
setting.ignore = !checkbox.checked
|
setting.ignore = !checkbox.checked
|
||||||
saveSettings()
|
saveSettings()
|
||||||
})
|
})
|
||||||
@@ -218,9 +269,6 @@ function fillSaveSettingsConfigTable() {
|
|||||||
|
|
||||||
// configureSettingsSaveBtn
|
// configureSettingsSaveBtn
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var autoSaveSettings = document.getElementById("auto_save_settings")
|
var autoSaveSettings = document.getElementById("auto_save_settings")
|
||||||
var configSettingsButton = document.createElement("button")
|
var configSettingsButton = document.createElement("button")
|
||||||
configSettingsButton.textContent = "Configure"
|
configSettingsButton.textContent = "Configure"
|
||||||
@@ -229,72 +277,75 @@ autoSaveSettings.insertAdjacentElement("beforebegin", configSettingsButton)
|
|||||||
autoSaveSettings.addEventListener("change", () => {
|
autoSaveSettings.addEventListener("change", () => {
|
||||||
configSettingsButton.style.display = autoSaveSettings.checked ? "block" : "none"
|
configSettingsButton.style.display = autoSaveSettings.checked ? "block" : "none"
|
||||||
})
|
})
|
||||||
configSettingsButton.addEventListener('click', () => {
|
configSettingsButton.addEventListener("click", () => {
|
||||||
fillSaveSettingsConfigTable()
|
fillSaveSettingsConfigTable()
|
||||||
saveSettingsConfigOverlay.classList.add("active")
|
saveSettingsConfigOverlay.classList.add("active")
|
||||||
})
|
})
|
||||||
resetImageSettingsButton.addEventListener('click', event => {
|
resetImageSettingsButton.addEventListener("click", (event) => {
|
||||||
loadDefaultSettingsSection("editor-settings");
|
loadDefaultSettingsSection("editor-settings")
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
function tryLoadOldSettings() {
|
function tryLoadOldSettings() {
|
||||||
console.log("Loading old user settings")
|
console.log("Loading old user settings")
|
||||||
// load v1 auto-save.js settings
|
// load v1 auto-save.js settings
|
||||||
var old_map = {
|
var old_map = {
|
||||||
"guidance_scale_slider": "guidance_scale",
|
guidance_scale_slider: "guidance_scale",
|
||||||
"prompt_strength_slider": "prompt_strength"
|
prompt_strength_slider: "prompt_strength",
|
||||||
}
|
}
|
||||||
var settings_key_v1 = "user_settings"
|
var settings_key_v1 = "user_settings"
|
||||||
var saved_settings_text = localStorage.getItem(settings_key_v1)
|
var saved_settings_text = localStorage.getItem(settings_key_v1)
|
||||||
if (saved_settings_text) {
|
if (saved_settings_text) {
|
||||||
var saved_settings = JSON.parse(saved_settings_text)
|
var saved_settings = JSON.parse(saved_settings_text)
|
||||||
Object.keys(saved_settings.should_save).forEach(key => {
|
Object.keys(saved_settings.should_save).forEach((key) => {
|
||||||
key = key in old_map ? old_map[key] : key
|
key = key in old_map ? old_map[key] : key
|
||||||
|
if (!(key in SETTINGS)) return
|
||||||
SETTINGS[key].ignore = !saved_settings.should_save[key]
|
SETTINGS[key].ignore = !saved_settings.should_save[key]
|
||||||
});
|
})
|
||||||
Object.keys(saved_settings.values).forEach(key => {
|
Object.keys(saved_settings.values).forEach((key) => {
|
||||||
key = key in old_map ? old_map[key] : key
|
key = key in old_map ? old_map[key] : key
|
||||||
|
if (!(key in SETTINGS)) return
|
||||||
var setting = SETTINGS[key]
|
var setting = SETTINGS[key]
|
||||||
if (!setting.ignore) {
|
if (!setting.ignore) {
|
||||||
setting.value = saved_settings.values[key]
|
setting.value = saved_settings.values[key]
|
||||||
setSetting(setting.element, setting.value)
|
setSetting(setting.element, setting.value)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
localStorage.removeItem(settings_key_v1)
|
localStorage.removeItem(settings_key_v1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// load old individually stored items
|
// load old individually stored items
|
||||||
var individual_settings_map = { // maps old localStorage-key to new SETTINGS-key
|
var individual_settings_map = {
|
||||||
"soundEnabled": "sound_toggle",
|
// maps old localStorage-key to new SETTINGS-key
|
||||||
"saveToDisk": "save_to_disk",
|
soundEnabled: "sound_toggle",
|
||||||
"useCPU": "use_cpu",
|
saveToDisk: "save_to_disk",
|
||||||
"useFullPrecision": "use_full_precision",
|
useCPU: "use_cpu",
|
||||||
"useTurboMode": "turbo",
|
diskPath: "diskPath",
|
||||||
"diskPath": "diskPath",
|
useFaceCorrection: "use_face_correction",
|
||||||
"useFaceCorrection": "use_face_correction",
|
useUpscaling: "use_upscale",
|
||||||
"useUpscaling": "use_upscale",
|
showOnlyFilteredImage: "show_only_filtered_image",
|
||||||
"showOnlyFilteredImage": "show_only_filtered_image",
|
streamImageProgress: "stream_image_progress",
|
||||||
"streamImageProgress": "stream_image_progress",
|
outputFormat: "output_format",
|
||||||
"outputFormat": "output_format",
|
autoSaveSettings: "auto_save_settings",
|
||||||
"autoSaveSettings": "auto_save_settings",
|
}
|
||||||
};
|
Object.keys(individual_settings_map).forEach((localStorageKey) => {
|
||||||
Object.keys(individual_settings_map).forEach(localStorageKey => {
|
var localStorageValue = localStorage.getItem(localStorageKey)
|
||||||
var localStorageValue = localStorage.getItem(localStorageKey);
|
|
||||||
if (localStorageValue !== null) {
|
if (localStorageValue !== null) {
|
||||||
let key = individual_settings_map[localStorageKey]
|
let key = individual_settings_map[localStorageKey]
|
||||||
var setting = SETTINGS[key]
|
var setting = SETTINGS[key]
|
||||||
if (!setting) {
|
if (!setting) {
|
||||||
console.warn(`Attempted to map old setting ${key}, but no setting found`);
|
console.warn(`Attempted to map old setting ${key}, but no setting found`)
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
if (setting.element.type == "checkbox" && (typeof localStorageValue === "string" || localStorageValue instanceof String)) {
|
if (
|
||||||
|
setting.element.type == "checkbox" &&
|
||||||
|
(typeof localStorageValue === "string" || localStorageValue instanceof String)
|
||||||
|
) {
|
||||||
localStorageValue = localStorageValue == "true"
|
localStorageValue = localStorageValue == "true"
|
||||||
}
|
}
|
||||||
setting.value = localStorageValue
|
setting.value = localStorageValue
|
||||||
setSetting(setting.element, setting.value)
|
setSetting(setting.element, setting.value)
|
||||||
localStorage.removeItem(localStorageKey);
|
localStorage.removeItem(localStorageKey)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
1189
ui/media/js/croppr.js
Executable file
@@ -1,167 +1,253 @@
|
|||||||
"use strict" // Opt in to a restricted variant of JavaScript
|
"use strict" // Opt in to a restricted variant of JavaScript
|
||||||
|
|
||||||
const EXT_REGEX = /(?:\.([^.]+))?$/
|
const EXT_REGEX = /(?:\.([^.]+))?$/
|
||||||
const TEXT_EXTENSIONS = ['txt', 'json']
|
const TEXT_EXTENSIONS = ["txt", "json"]
|
||||||
const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'bmp', 'tiff', 'tif', 'tga']
|
const IMAGE_EXTENSIONS = ["jpg", "jpeg", "png", "bmp", "tiff", "tif", "tga", "webp"]
|
||||||
|
|
||||||
function parseBoolean(stringValue) {
|
function parseBoolean(stringValue) {
|
||||||
if (typeof stringValue === 'boolean') {
|
if (typeof stringValue === "boolean") {
|
||||||
return stringValue
|
return stringValue
|
||||||
}
|
}
|
||||||
if (typeof stringValue === 'number') {
|
if (typeof stringValue === "number") {
|
||||||
return stringValue !== 0
|
return stringValue !== 0
|
||||||
}
|
}
|
||||||
if (typeof stringValue !== 'string') {
|
if (typeof stringValue !== "string") {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
switch(stringValue?.toLowerCase()?.trim()) {
|
switch (stringValue?.toLowerCase()?.trim()) {
|
||||||
case "true":
|
case "true":
|
||||||
case "yes":
|
case "yes":
|
||||||
case "on":
|
case "on":
|
||||||
case "1":
|
case "1":
|
||||||
return true;
|
return true
|
||||||
|
|
||||||
case "false":
|
case "false":
|
||||||
case "no":
|
case "no":
|
||||||
case "off":
|
case "off":
|
||||||
case "0":
|
case "0":
|
||||||
|
case "none":
|
||||||
case null:
|
case null:
|
||||||
case undefined:
|
case undefined:
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return Boolean(JSON.parse(stringValue));
|
return Boolean(JSON.parse(stringValue))
|
||||||
} catch {
|
} catch {
|
||||||
return Boolean(stringValue)
|
return Boolean(stringValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// keep in sync with `ui/easydiffusion/utils/save_utils.py`
|
||||||
const TASK_MAPPING = {
|
const TASK_MAPPING = {
|
||||||
prompt: { name: 'Prompt',
|
prompt: {
|
||||||
|
name: "Prompt",
|
||||||
setUI: (prompt) => {
|
setUI: (prompt) => {
|
||||||
promptField.value = prompt
|
promptField.value = prompt
|
||||||
},
|
},
|
||||||
readUI: () => promptField.value,
|
readUI: () => promptField.value,
|
||||||
parse: (val) => val
|
parse: (val) => val,
|
||||||
},
|
},
|
||||||
negative_prompt: { name: 'Negative Prompt',
|
negative_prompt: {
|
||||||
|
name: "Negative Prompt",
|
||||||
setUI: (negative_prompt) => {
|
setUI: (negative_prompt) => {
|
||||||
negativePromptField.value = negative_prompt
|
negativePromptField.value = negative_prompt
|
||||||
},
|
},
|
||||||
readUI: () => negativePromptField.value,
|
readUI: () => negativePromptField.value,
|
||||||
parse: (val) => val
|
parse: (val) => val,
|
||||||
},
|
},
|
||||||
width: { name: 'Width',
|
active_tags: {
|
||||||
|
name: "Image Modifiers",
|
||||||
|
setUI: (active_tags) => {
|
||||||
|
refreshModifiersState(active_tags)
|
||||||
|
},
|
||||||
|
readUI: () => activeTags.map((x) => x.name),
|
||||||
|
parse: (val) => val,
|
||||||
|
},
|
||||||
|
inactive_tags: {
|
||||||
|
name: "Inactive Image Modifiers",
|
||||||
|
setUI: (inactive_tags) => {
|
||||||
|
refreshInactiveTags(inactive_tags)
|
||||||
|
},
|
||||||
|
readUI: () => activeTags.filter((tag) => tag.inactive === true).map((x) => x.name),
|
||||||
|
parse: (val) => val,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
name: "Width",
|
||||||
setUI: (width) => {
|
setUI: (width) => {
|
||||||
const oldVal = widthField.value
|
const oldVal = widthField.value
|
||||||
widthField.value = width
|
widthField.value = width
|
||||||
if (!widthField.value) {
|
if (!widthField.value) {
|
||||||
widthField.value = oldVal
|
widthField.value = oldVal
|
||||||
}
|
}
|
||||||
|
widthField.dispatchEvent(new Event("change"))
|
||||||
},
|
},
|
||||||
readUI: () => parseInt(widthField.value),
|
readUI: () => parseInt(widthField.value),
|
||||||
parse: (val) => parseInt(val)
|
parse: (val) => parseInt(val),
|
||||||
},
|
},
|
||||||
height: { name: 'Height',
|
height: {
|
||||||
|
name: "Height",
|
||||||
setUI: (height) => {
|
setUI: (height) => {
|
||||||
const oldVal = heightField.value
|
const oldVal = heightField.value
|
||||||
heightField.value = height
|
heightField.value = height
|
||||||
if (!heightField.value) {
|
if (!heightField.value) {
|
||||||
heightField.value = oldVal
|
heightField.value = oldVal
|
||||||
}
|
}
|
||||||
|
heightField.dispatchEvent(new Event("change"))
|
||||||
},
|
},
|
||||||
readUI: () => parseInt(heightField.value),
|
readUI: () => parseInt(heightField.value),
|
||||||
parse: (val) => parseInt(val)
|
parse: (val) => parseInt(val),
|
||||||
},
|
},
|
||||||
seed: { name: 'Seed',
|
seed: {
|
||||||
|
name: "Seed",
|
||||||
setUI: (seed) => {
|
setUI: (seed) => {
|
||||||
if (!seed) {
|
if (!seed) {
|
||||||
randomSeedField.checked = true
|
randomSeedField.checked = true
|
||||||
seedField.disabled = true
|
seedField.disabled = true
|
||||||
|
seedField.value = 0
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
randomSeedField.checked = false
|
randomSeedField.checked = false
|
||||||
|
randomSeedField.dispatchEvent(new Event("change")) // let plugins know that the state of the random seed toggle changed
|
||||||
seedField.disabled = false
|
seedField.disabled = false
|
||||||
seedField.value = seed
|
seedField.value = seed
|
||||||
},
|
},
|
||||||
readUI: () => (randomSeedField.checked ? Math.floor(Math.random() * 10000000) : parseInt(seedField.value)),
|
readUI: () => parseInt(seedField.value), // just return the value the user is seeing in the UI
|
||||||
parse: (val) => parseInt(val)
|
parse: (val) => parseInt(val),
|
||||||
},
|
},
|
||||||
num_inference_steps: { name: 'Steps',
|
num_inference_steps: {
|
||||||
|
name: "Steps",
|
||||||
setUI: (num_inference_steps) => {
|
setUI: (num_inference_steps) => {
|
||||||
numInferenceStepsField.value = num_inference_steps
|
numInferenceStepsField.value = num_inference_steps
|
||||||
},
|
},
|
||||||
readUI: () => parseInt(numInferenceStepsField.value),
|
readUI: () => parseInt(numInferenceStepsField.value),
|
||||||
parse: (val) => parseInt(val)
|
parse: (val) => parseInt(val),
|
||||||
},
|
},
|
||||||
guidance_scale: { name: 'Guidance Scale',
|
guidance_scale: {
|
||||||
|
name: "Guidance Scale",
|
||||||
setUI: (guidance_scale) => {
|
setUI: (guidance_scale) => {
|
||||||
guidanceScaleField.value = guidance_scale
|
guidanceScaleField.value = guidance_scale
|
||||||
updateGuidanceScaleSlider()
|
updateGuidanceScaleSlider()
|
||||||
},
|
},
|
||||||
readUI: () => parseFloat(guidanceScaleField.value),
|
readUI: () => parseFloat(guidanceScaleField.value),
|
||||||
parse: (val) => parseFloat(val)
|
parse: (val) => parseFloat(val),
|
||||||
},
|
},
|
||||||
prompt_strength: { name: 'Prompt Strength',
|
prompt_strength: {
|
||||||
|
name: "Prompt Strength",
|
||||||
setUI: (prompt_strength) => {
|
setUI: (prompt_strength) => {
|
||||||
promptStrengthField.value = prompt_strength
|
promptStrengthField.value = prompt_strength
|
||||||
updatePromptStrengthSlider()
|
updatePromptStrengthSlider()
|
||||||
},
|
},
|
||||||
readUI: () => parseFloat(promptStrengthField.value),
|
readUI: () => parseFloat(promptStrengthField.value),
|
||||||
parse: (val) => parseFloat(val)
|
parse: (val) => parseFloat(val),
|
||||||
},
|
},
|
||||||
|
|
||||||
init_image: { name: 'Initial Image',
|
init_image: {
|
||||||
|
name: "Initial Image",
|
||||||
setUI: (init_image) => {
|
setUI: (init_image) => {
|
||||||
initImagePreview.src = init_image
|
initImagePreview.src = init_image
|
||||||
},
|
},
|
||||||
readUI: () => initImagePreview.src,
|
readUI: () => initImagePreview.src,
|
||||||
parse: (val) => val
|
parse: (val) => val,
|
||||||
},
|
},
|
||||||
mask: { name: 'Mask',
|
mask: {
|
||||||
|
name: "Mask",
|
||||||
setUI: (mask) => {
|
setUI: (mask) => {
|
||||||
inpaintingEditor.setImg(mask)
|
setTimeout(() => {
|
||||||
|
// add a delay to insure this happens AFTER the main image loads (which reloads the inpainter)
|
||||||
|
imageInpainter.setImg(mask)
|
||||||
|
}, 250)
|
||||||
maskSetting.checked = Boolean(mask)
|
maskSetting.checked = Boolean(mask)
|
||||||
},
|
},
|
||||||
readUI: () => (maskSetting.checked ? inpaintingEditor.getImg() : undefined),
|
readUI: () => (maskSetting.checked ? imageInpainter.getImg() : undefined),
|
||||||
parse: (val) => val
|
parse: (val) => val,
|
||||||
|
},
|
||||||
|
preserve_init_image_color_profile: {
|
||||||
|
name: "Preserve Color Profile",
|
||||||
|
setUI: (preserve_init_image_color_profile) => {
|
||||||
|
applyColorCorrectionField.checked = parseBoolean(preserve_init_image_color_profile)
|
||||||
|
},
|
||||||
|
readUI: () => applyColorCorrectionField.checked,
|
||||||
|
parse: (val) => parseBoolean(val),
|
||||||
},
|
},
|
||||||
|
|
||||||
use_face_correction: { name: 'Use Face Correction',
|
use_face_correction: {
|
||||||
|
name: "Use Face Correction",
|
||||||
setUI: (use_face_correction) => {
|
setUI: (use_face_correction) => {
|
||||||
useFaceCorrectionField.checked = parseBoolean(use_face_correction)
|
const oldVal = gfpganModelField.value
|
||||||
|
console.log("use face correction", use_face_correction)
|
||||||
|
if (use_face_correction == null || use_face_correction == "None") {
|
||||||
|
gfpganModelField.disabled = true
|
||||||
|
useFaceCorrectionField.checked = false
|
||||||
|
} else {
|
||||||
|
gfpganModelField.value = getModelPath(use_face_correction, [".pth"])
|
||||||
|
if (gfpganModelField.value) {
|
||||||
|
// Is a valid value for the field.
|
||||||
|
useFaceCorrectionField.checked = true
|
||||||
|
gfpganModelField.disabled = false
|
||||||
|
} else {
|
||||||
|
// Not a valid value, restore the old value and disable the filter.
|
||||||
|
gfpganModelField.disabled = true
|
||||||
|
gfpganModelField.value = oldVal
|
||||||
|
useFaceCorrectionField.checked = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//useFaceCorrectionField.checked = parseBoolean(use_face_correction)
|
||||||
},
|
},
|
||||||
readUI: () => useFaceCorrectionField.checked,
|
readUI: () => (useFaceCorrectionField.checked ? gfpganModelField.value : undefined),
|
||||||
parse: (val) => parseBoolean(val)
|
parse: (val) => val,
|
||||||
},
|
},
|
||||||
use_upscale: { name: 'Use Upscaling',
|
use_upscale: {
|
||||||
|
name: "Use Upscaling",
|
||||||
setUI: (use_upscale) => {
|
setUI: (use_upscale) => {
|
||||||
const oldVal = upscaleModelField.value
|
const oldVal = upscaleModelField.value
|
||||||
upscaleModelField.value = use_upscale
|
upscaleModelField.value = getModelPath(use_upscale, [".pth"])
|
||||||
if (upscaleModelField.value) { // Is a valid value for the field.
|
if (upscaleModelField.value) {
|
||||||
|
// Is a valid value for the field.
|
||||||
useUpscalingField.checked = true
|
useUpscalingField.checked = true
|
||||||
upscaleModelField.disabled = false
|
upscaleModelField.disabled = false
|
||||||
} else { // Not a valid value, restore the old value and disable the filter.
|
upscaleAmountField.disabled = false
|
||||||
|
} else {
|
||||||
|
// Not a valid value, restore the old value and disable the filter.
|
||||||
upscaleModelField.disabled = true
|
upscaleModelField.disabled = true
|
||||||
|
upscaleAmountField.disabled = true
|
||||||
upscaleModelField.value = oldVal
|
upscaleModelField.value = oldVal
|
||||||
useUpscalingField.checked = false
|
useUpscalingField.checked = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
readUI: () => (useUpscalingField.checked ? upscaleModelField.value : undefined),
|
readUI: () => (useUpscalingField.checked ? upscaleModelField.value : undefined),
|
||||||
parse: (val) => val
|
parse: (val) => val,
|
||||||
},
|
},
|
||||||
sampler: { name: 'Sampler',
|
upscale_amount: {
|
||||||
setUI: (sampler) => {
|
name: "Upscale By",
|
||||||
samplerField.value = sampler
|
setUI: (upscale_amount) => {
|
||||||
|
upscaleAmountField.value = upscale_amount
|
||||||
|
},
|
||||||
|
readUI: () => upscaleAmountField.value,
|
||||||
|
parse: (val) => val,
|
||||||
|
},
|
||||||
|
latent_upscaler_steps: {
|
||||||
|
name: "Latent Upscaler Steps",
|
||||||
|
setUI: (latent_upscaler_steps) => {
|
||||||
|
latentUpscalerStepsField.value = latent_upscaler_steps
|
||||||
|
},
|
||||||
|
readUI: () => latentUpscalerStepsField.value,
|
||||||
|
parse: (val) => val,
|
||||||
|
},
|
||||||
|
sampler_name: {
|
||||||
|
name: "Sampler",
|
||||||
|
setUI: (sampler_name) => {
|
||||||
|
samplerField.value = sampler_name
|
||||||
},
|
},
|
||||||
readUI: () => samplerField.value,
|
readUI: () => samplerField.value,
|
||||||
parse: (val) => val
|
parse: (val) => val,
|
||||||
},
|
},
|
||||||
use_stable_diffusion_model: { name: 'Stable Diffusion model',
|
use_stable_diffusion_model: {
|
||||||
|
name: "Stable Diffusion model",
|
||||||
setUI: (use_stable_diffusion_model) => {
|
setUI: (use_stable_diffusion_model) => {
|
||||||
const oldVal = stableDiffusionModelField.value
|
const oldVal = stableDiffusionModelField.value
|
||||||
|
|
||||||
use_stable_diffusion_model = getModelPath(use_stable_diffusion_model, ['.ckpt'])
|
use_stable_diffusion_model = getModelPath(use_stable_diffusion_model, [".ckpt", ".safetensors"])
|
||||||
stableDiffusionModelField.value = use_stable_diffusion_model
|
stableDiffusionModelField.value = use_stable_diffusion_model
|
||||||
|
|
||||||
if (!stableDiffusionModelField.value) {
|
if (!stableDiffusionModelField.value) {
|
||||||
@@ -169,93 +255,212 @@ const TASK_MAPPING = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
readUI: () => stableDiffusionModelField.value,
|
readUI: () => stableDiffusionModelField.value,
|
||||||
parse: (val) => val
|
parse: (val) => val,
|
||||||
},
|
},
|
||||||
use_vae_model: { name: 'VAE model',
|
clip_skip: {
|
||||||
|
name: "Clip Skip",
|
||||||
|
setUI: (value) => {
|
||||||
|
clip_skip.checked = value
|
||||||
|
},
|
||||||
|
readUI: () => clip_skip.checked,
|
||||||
|
parse: (val) => Boolean(val),
|
||||||
|
},
|
||||||
|
tiling: {
|
||||||
|
name: "Tiling",
|
||||||
|
setUI: (val) => {
|
||||||
|
if (val === null || val === "None") {
|
||||||
|
tilingField.value = "none"
|
||||||
|
} else {
|
||||||
|
tilingField.value = val
|
||||||
|
}
|
||||||
|
},
|
||||||
|
readUI: () => tilingField.value,
|
||||||
|
parse: (val) => val,
|
||||||
|
},
|
||||||
|
use_vae_model: {
|
||||||
|
name: "VAE model",
|
||||||
setUI: (use_vae_model) => {
|
setUI: (use_vae_model) => {
|
||||||
const oldVal = vaeModelField.value
|
const oldVal = vaeModelField.value
|
||||||
|
use_vae_model =
|
||||||
|
use_vae_model === undefined || use_vae_model === null || use_vae_model === "None" ? "" : use_vae_model
|
||||||
|
|
||||||
if (use_vae_model !== '') {
|
if (use_vae_model !== "") {
|
||||||
use_vae_model = getModelPath(use_vae_model, ['.vae.pt', '.ckpt'])
|
use_vae_model = getModelPath(use_vae_model, [".vae.pt", ".ckpt"])
|
||||||
use_vae_model = use_vae_model !== '' ? use_vae_model : oldVal
|
use_vae_model = use_vae_model !== "" ? use_vae_model : oldVal
|
||||||
}
|
}
|
||||||
vaeModelField.value = use_vae_model
|
vaeModelField.value = use_vae_model
|
||||||
},
|
},
|
||||||
readUI: () => vaeModelField.value,
|
readUI: () => vaeModelField.value,
|
||||||
parse: (val) => val
|
parse: (val) => val,
|
||||||
|
},
|
||||||
|
use_controlnet_model: {
|
||||||
|
name: "ControlNet model",
|
||||||
|
setUI: (use_controlnet_model) => {
|
||||||
|
controlnetModelField.value = getModelPath(use_controlnet_model, [".pth", ".safetensors"])
|
||||||
|
},
|
||||||
|
readUI: () => controlnetModelField.value,
|
||||||
|
parse: (val) => val,
|
||||||
|
},
|
||||||
|
control_filter_to_apply: {
|
||||||
|
name: "ControlNet Filter",
|
||||||
|
setUI: (control_filter_to_apply) => {
|
||||||
|
controlImageFilterField.value = control_filter_to_apply
|
||||||
|
},
|
||||||
|
readUI: () => controlImageFilterField.value,
|
||||||
|
parse: (val) => val,
|
||||||
|
},
|
||||||
|
use_lora_model: {
|
||||||
|
name: "LoRA model",
|
||||||
|
setUI: (use_lora_model) => {
|
||||||
|
let modelPaths = []
|
||||||
|
use_lora_model = Array.isArray(use_lora_model) ? use_lora_model : [use_lora_model]
|
||||||
|
use_lora_model.forEach((m) => {
|
||||||
|
if (m.includes("models\\lora\\")) {
|
||||||
|
m = m.split("models\\lora\\")[1]
|
||||||
|
} else if (m.includes("models\\\\lora\\\\")) {
|
||||||
|
m = m.split("models\\\\lora\\\\")[1]
|
||||||
|
} else if (m.includes("models/lora/")) {
|
||||||
|
m = m.split("models/lora/")[1]
|
||||||
|
}
|
||||||
|
m = m.replaceAll("\\\\", "/")
|
||||||
|
m = getModelPath(m, [".ckpt", ".safetensors"])
|
||||||
|
modelPaths.push(m)
|
||||||
|
})
|
||||||
|
loraModelField.modelNames = modelPaths
|
||||||
|
},
|
||||||
|
readUI: () => {
|
||||||
|
return loraModelField.modelNames
|
||||||
|
},
|
||||||
|
parse: (val) => {
|
||||||
|
val = !val || val === "None" ? "" : val
|
||||||
|
if (typeof val === "string" && val.includes(",")) {
|
||||||
|
val = val.split(",")
|
||||||
|
val = val.map((v) => v.trim())
|
||||||
|
val = val.map((v) => v.replaceAll("\\", "\\\\"))
|
||||||
|
val = val.map((v) => v.replaceAll('"', ""))
|
||||||
|
val = val.map((v) => v.replaceAll("'", ""))
|
||||||
|
val = val.map((v) => '"' + v + '"')
|
||||||
|
val = "[" + val + "]"
|
||||||
|
val = JSON.parse(val)
|
||||||
|
}
|
||||||
|
val = Array.isArray(val) ? val : [val]
|
||||||
|
return val
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lora_alpha: {
|
||||||
|
name: "LoRA Strength",
|
||||||
|
setUI: (lora_alpha) => {
|
||||||
|
lora_alpha = Array.isArray(lora_alpha) ? lora_alpha : [lora_alpha]
|
||||||
|
loraModelField.modelWeights = lora_alpha
|
||||||
|
},
|
||||||
|
readUI: () => {
|
||||||
|
return loraModelField.modelWeights
|
||||||
|
},
|
||||||
|
parse: (val) => {
|
||||||
|
if (typeof val === "string" && val.includes(",")) {
|
||||||
|
val = "[" + val.replaceAll("'", '"') + "]"
|
||||||
|
val = JSON.parse(val)
|
||||||
|
}
|
||||||
|
val = Array.isArray(val) ? val : [val]
|
||||||
|
val = val.map((e) => parseFloat(e))
|
||||||
|
return val
|
||||||
|
},
|
||||||
|
},
|
||||||
|
use_hypernetwork_model: {
|
||||||
|
name: "Hypernetwork model",
|
||||||
|
setUI: (use_hypernetwork_model) => {
|
||||||
|
const oldVal = hypernetworkModelField.value
|
||||||
|
use_hypernetwork_model =
|
||||||
|
use_hypernetwork_model === undefined ||
|
||||||
|
use_hypernetwork_model === null ||
|
||||||
|
use_hypernetwork_model === "None"
|
||||||
|
? ""
|
||||||
|
: use_hypernetwork_model
|
||||||
|
|
||||||
|
if (use_hypernetwork_model !== "") {
|
||||||
|
use_hypernetwork_model = getModelPath(use_hypernetwork_model, [".pt"])
|
||||||
|
use_hypernetwork_model = use_hypernetwork_model !== "" ? use_hypernetwork_model : oldVal
|
||||||
|
}
|
||||||
|
hypernetworkModelField.value = use_hypernetwork_model
|
||||||
|
hypernetworkModelField.dispatchEvent(new Event("change"))
|
||||||
|
},
|
||||||
|
readUI: () => hypernetworkModelField.value,
|
||||||
|
parse: (val) => val,
|
||||||
|
},
|
||||||
|
hypernetwork_strength: {
|
||||||
|
name: "Hypernetwork Strength",
|
||||||
|
setUI: (hypernetwork_strength) => {
|
||||||
|
hypernetworkStrengthField.value = hypernetwork_strength
|
||||||
|
updateHypernetworkStrengthSlider()
|
||||||
|
},
|
||||||
|
readUI: () => parseFloat(hypernetworkStrengthField.value),
|
||||||
|
parse: (val) => parseFloat(val),
|
||||||
},
|
},
|
||||||
|
|
||||||
numOutputsParallel: { name: 'Parallel Images',
|
num_outputs: {
|
||||||
setUI: (numOutputsParallel) => {
|
name: "Parallel Images",
|
||||||
numOutputsParallelField.value = numOutputsParallel
|
setUI: (num_outputs) => {
|
||||||
|
numOutputsParallelField.value = num_outputs
|
||||||
},
|
},
|
||||||
readUI: () => parseInt(numOutputsParallelField.value),
|
readUI: () => parseInt(numOutputsParallelField.value),
|
||||||
parse: (val) => val
|
parse: (val) => val,
|
||||||
},
|
},
|
||||||
|
|
||||||
use_cpu: { name: 'Use CPU',
|
use_cpu: {
|
||||||
|
name: "Use CPU",
|
||||||
setUI: (use_cpu) => {
|
setUI: (use_cpu) => {
|
||||||
useCPUField.checked = use_cpu
|
useCPUField.checked = use_cpu
|
||||||
},
|
},
|
||||||
readUI: () => useCPUField.checked,
|
readUI: () => useCPUField.checked,
|
||||||
parse: (val) => val
|
parse: (val) => val,
|
||||||
},
|
|
||||||
turbo: { name: 'Turbo',
|
|
||||||
setUI: (turbo) => {
|
|
||||||
turboField.checked = turbo
|
|
||||||
},
|
|
||||||
readUI: () => turboField.checked,
|
|
||||||
parse: (val) => Boolean(val)
|
|
||||||
},
|
|
||||||
use_full_precision: { name: 'Use Full Precision',
|
|
||||||
setUI: (use_full_precision) => {
|
|
||||||
useFullPrecisionField.checked = use_full_precision
|
|
||||||
},
|
|
||||||
readUI: () => useFullPrecisionField.checked,
|
|
||||||
parse: (val) => Boolean(val)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
stream_image_progress: { name: 'Stream Image Progress',
|
stream_image_progress: {
|
||||||
|
name: "Stream Image Progress",
|
||||||
setUI: (stream_image_progress) => {
|
setUI: (stream_image_progress) => {
|
||||||
streamImageProgressField.checked = (parseInt(numOutputsTotalField.value) > 50 ? false : stream_image_progress)
|
streamImageProgressField.checked = parseInt(numOutputsTotalField.value) > 50 ? false : stream_image_progress
|
||||||
},
|
},
|
||||||
readUI: () => streamImageProgressField.checked,
|
readUI: () => streamImageProgressField.checked,
|
||||||
parse: (val) => Boolean(val)
|
parse: (val) => Boolean(val),
|
||||||
},
|
},
|
||||||
show_only_filtered_image: { name: 'Show only the corrected/upscaled image',
|
show_only_filtered_image: {
|
||||||
|
name: "Show only the corrected/upscaled image",
|
||||||
setUI: (show_only_filtered_image) => {
|
setUI: (show_only_filtered_image) => {
|
||||||
showOnlyFilteredImageField.checked = show_only_filtered_image
|
showOnlyFilteredImageField.checked = show_only_filtered_image
|
||||||
},
|
},
|
||||||
readUI: () => showOnlyFilteredImageField.checked,
|
readUI: () => showOnlyFilteredImageField.checked,
|
||||||
parse: (val) => Boolean(val)
|
parse: (val) => Boolean(val),
|
||||||
},
|
},
|
||||||
output_format: { name: 'Output Format',
|
output_format: {
|
||||||
|
name: "Output Format",
|
||||||
setUI: (output_format) => {
|
setUI: (output_format) => {
|
||||||
outputFormatField.value = output_format
|
outputFormatField.value = output_format
|
||||||
},
|
},
|
||||||
readUI: () => outputFormatField.value,
|
readUI: () => outputFormatField.value,
|
||||||
parse: (val) => val
|
parse: (val) => val,
|
||||||
},
|
},
|
||||||
save_to_disk_path: { name: 'Save to disk path',
|
save_to_disk_path: {
|
||||||
|
name: "Save to disk path",
|
||||||
setUI: (save_to_disk_path) => {
|
setUI: (save_to_disk_path) => {
|
||||||
saveToDiskField.checked = Boolean(save_to_disk_path)
|
saveToDiskField.checked = Boolean(save_to_disk_path)
|
||||||
diskPathField.value = save_to_disk_path
|
diskPathField.value = save_to_disk_path
|
||||||
},
|
},
|
||||||
readUI: () => diskPathField.value,
|
readUI: () => diskPathField.value,
|
||||||
parse: (val) => val
|
parse: (val) => val,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreTaskToUI(task, fieldsToSkip) {
|
function restoreTaskToUI(task, fieldsToSkip) {
|
||||||
fieldsToSkip = fieldsToSkip || []
|
fieldsToSkip = fieldsToSkip || []
|
||||||
|
|
||||||
if ('numOutputsTotal' in task) {
|
if ("numOutputsTotal" in task) {
|
||||||
numOutputsTotalField.value = task.numOutputsTotal
|
numOutputsTotalField.value = task.numOutputsTotal
|
||||||
}
|
}
|
||||||
if ('seed' in task) {
|
if ("seed" in task) {
|
||||||
randomSeedField.checked = false
|
randomSeedField.checked = false
|
||||||
seedField.value = task.seed
|
seedField.value = task.seed
|
||||||
}
|
}
|
||||||
if (!('reqBody' in task)) {
|
if (!("reqBody" in task)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for (const key in TASK_MAPPING) {
|
for (const key in TASK_MAPPING) {
|
||||||
@@ -264,109 +469,164 @@ function restoreTaskToUI(task, fieldsToSkip) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// restore the original tag
|
// properly reset fields not present in the task
|
||||||
promptField.value = task.reqBody.original_prompt || task.reqBody.prompt
|
if (!("use_hypernetwork_model" in task.reqBody)) {
|
||||||
|
hypernetworkModelField.value = ""
|
||||||
// Restore modifiers
|
hypernetworkModelField.dispatchEvent(new Event("change"))
|
||||||
if (task.reqBody.active_tags) {
|
|
||||||
refreshModifiersState(task.reqBody.active_tags)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!("use_lora_model" in task.reqBody)) {
|
||||||
|
loraModelField.modelNames = []
|
||||||
|
loraModelField.modelWeights = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// restore the original prompt if provided (e.g. use settings), fallback to prompt as needed (e.g. copy/paste or d&d)
|
||||||
|
promptField.value = task.reqBody.original_prompt
|
||||||
|
if (!("original_prompt" in task.reqBody)) {
|
||||||
|
promptField.value = task.reqBody.prompt
|
||||||
|
}
|
||||||
|
promptField.dispatchEvent(new Event("input"))
|
||||||
|
|
||||||
// properly reset checkboxes
|
// properly reset checkboxes
|
||||||
if (!('use_face_correction' in task.reqBody)) {
|
if (!("use_face_correction" in task.reqBody)) {
|
||||||
useFaceCorrectionField.checked = false
|
useFaceCorrectionField.checked = false
|
||||||
|
gfpganModelField.disabled = true
|
||||||
}
|
}
|
||||||
if (!('use_upscale' in task.reqBody)) {
|
if (!("use_upscale" in task.reqBody)) {
|
||||||
useUpscalingField.checked = false
|
useUpscalingField.checked = false
|
||||||
}
|
}
|
||||||
if (!('mask' in task.reqBody)) {
|
if (!("mask" in task.reqBody) && maskSetting.checked) {
|
||||||
maskSetting.checked = false
|
maskSetting.checked = false
|
||||||
|
maskSetting.dispatchEvent(new Event("click"))
|
||||||
}
|
}
|
||||||
upscaleModelField.disabled = !useUpscalingField.checked
|
upscaleModelField.disabled = !useUpscalingField.checked
|
||||||
|
upscaleAmountField.disabled = !useUpscalingField.checked
|
||||||
|
|
||||||
// Show the source picture if present
|
// hide/show source picture as needed
|
||||||
initImagePreview.src = (task.reqBody.init_image == undefined ? '' : task.reqBody.init_image)
|
if (IMAGE_REGEX.test(initImagePreview.src) && task.reqBody.init_image == undefined) {
|
||||||
if (IMAGE_REGEX.test(initImagePreview.src)) {
|
// hide source image
|
||||||
Boolean(task.reqBody.mask) ? inpaintingEditor.setImg(task.reqBody.mask) : inpaintingEditor.resetBackground()
|
initImageClearBtn.dispatchEvent(new Event("click"))
|
||||||
initImagePreviewContainer.style.display = 'block'
|
} else if (task.reqBody.init_image !== undefined) {
|
||||||
inpaintingEditorContainer.style.display = 'none'
|
// listen for inpainter loading event, which happens AFTER the main image loads (which reloads the inpainter)
|
||||||
promptStrengthContainer.style.display = 'table-row'
|
initImagePreview.addEventListener(
|
||||||
//samplerSelectionContainer.style.display = 'none'
|
"load",
|
||||||
// maskSetting.checked = false
|
function() {
|
||||||
inpaintingEditorContainer.style.display = maskSetting.checked ? 'block' : 'none'
|
if (Boolean(task.reqBody.mask)) {
|
||||||
} else {
|
imageInpainter.setImg(task.reqBody.mask)
|
||||||
initImagePreviewContainer.style.display = 'none'
|
maskSetting.checked = true
|
||||||
// inpaintingEditorContainer.style.display = 'none'
|
}
|
||||||
promptStrengthContainer.style.display = 'none'
|
},
|
||||||
// maskSetting.style.display = 'none'
|
{ once: true }
|
||||||
|
)
|
||||||
|
initImagePreview.src = task.reqBody.init_image
|
||||||
|
}
|
||||||
|
|
||||||
|
// hide/show controlnet picture as needed
|
||||||
|
if (IMAGE_REGEX.test(controlImagePreview.src) && task.reqBody.control_image == undefined) {
|
||||||
|
// hide source image
|
||||||
|
controlImageClearBtn.dispatchEvent(new Event("click"))
|
||||||
|
} else if (task.reqBody.control_image !== undefined) {
|
||||||
|
// listen for inpainter loading event, which happens AFTER the main image loads (which reloads the inpai
|
||||||
|
controlImagePreview.src = task.reqBody.control_image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function readUI() {
|
function readUI() {
|
||||||
const reqBody = {}
|
const reqBody = {}
|
||||||
for (const key in TASK_MAPPING) {
|
for (const key in TASK_MAPPING) {
|
||||||
|
if (testDiffusers.checked && (key === "use_hypernetwork_model" || key === "hypernetwork_strength")) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
reqBody[key] = TASK_MAPPING[key].readUI()
|
reqBody[key] = TASK_MAPPING[key].readUI()
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
'numOutputsTotal': parseInt(numOutputsTotalField.value),
|
numOutputsTotal: parseInt(numOutputsTotalField.value),
|
||||||
'seed': TASK_MAPPING['seed'].readUI(),
|
seed: TASK_MAPPING["seed"].readUI(),
|
||||||
'reqBody': reqBody
|
reqBody: reqBody,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function getModelPath(filename, extensions)
|
function getModelPath(filename, extensions) {
|
||||||
{
|
if (typeof filename !== "string") {
|
||||||
let pathIdx = filename.lastIndexOf('/') // Linux, Mac paths
|
return
|
||||||
if (pathIdx < 0) {
|
}
|
||||||
pathIdx = filename.lastIndexOf('\\') // Windows paths.
|
|
||||||
|
let pathIdx
|
||||||
|
if (filename.includes("/models/stable-diffusion/")) {
|
||||||
|
pathIdx = filename.indexOf("/models/stable-diffusion/") + 25 // Linux, Mac paths
|
||||||
|
} else if (filename.includes("\\models\\stable-diffusion\\")) {
|
||||||
|
pathIdx = filename.indexOf("\\models\\stable-diffusion\\") + 25 // Linux, Mac paths
|
||||||
}
|
}
|
||||||
if (pathIdx >= 0) {
|
if (pathIdx >= 0) {
|
||||||
filename = filename.slice(pathIdx + 1)
|
filename = filename.slice(pathIdx)
|
||||||
}
|
}
|
||||||
extensions.forEach(ext => {
|
extensions.forEach((ext) => {
|
||||||
if (filename.endsWith(ext)) {
|
if (filename.endsWith(ext)) {
|
||||||
filename = filename.slice(0, filename.length - ext.length)
|
filename = filename.slice(0, filename.length - ext.length)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
return filename
|
||||||
}
|
}
|
||||||
|
|
||||||
const TASK_TEXT_MAPPING = {
|
const TASK_TEXT_MAPPING = {
|
||||||
width: 'Width',
|
prompt: "Prompt",
|
||||||
height: 'Height',
|
width: "Width",
|
||||||
seed: 'Seed',
|
height: "Height",
|
||||||
num_inference_steps: 'Steps',
|
seed: "Seed",
|
||||||
guidance_scale: 'Guidance Scale',
|
num_inference_steps: "Steps",
|
||||||
prompt_strength: 'Prompt Strength',
|
guidance_scale: "Guidance Scale",
|
||||||
use_face_correction: 'Use Face Correction',
|
prompt_strength: "Prompt Strength",
|
||||||
use_upscale: 'Use Upscaling',
|
use_face_correction: "Use Face Correction",
|
||||||
sampler: 'Sampler',
|
use_upscale: "Use Upscaling",
|
||||||
negative_prompt: 'Negative Prompt',
|
upscale_amount: "Upscale By",
|
||||||
use_stable_diffusion_model: 'Stable Diffusion model'
|
sampler_name: "Sampler",
|
||||||
|
negative_prompt: "Negative Prompt",
|
||||||
|
use_stable_diffusion_model: "Stable Diffusion model",
|
||||||
|
use_hypernetwork_model: "Hypernetwork model",
|
||||||
|
hypernetwork_strength: "Hypernetwork Strength",
|
||||||
|
use_lora_model: "LoRA model",
|
||||||
|
lora_alpha: "LoRA Strength",
|
||||||
|
use_controlnet_model: "ControlNet model",
|
||||||
|
control_filter_to_apply: "ControlNet Filter",
|
||||||
|
tiling: "Seamless Tiling",
|
||||||
}
|
}
|
||||||
const afterPromptRe = /^\s*Width\s*:\s*\d+\s*(?:\r\n|\r|\n)+\s*Height\s*:\s*\d+\s*(\r\n|\r|\n)+Seed\s*:\s*\d+\s*$/igm
|
|
||||||
function parseTaskFromText(str) {
|
function parseTaskFromText(str) {
|
||||||
const taskReqBody = {}
|
const taskReqBody = {}
|
||||||
|
|
||||||
// Prompt
|
const lines = str.split("\n")
|
||||||
afterPromptRe.lastIndex = 0
|
if (lines.length === 0) {
|
||||||
const match = afterPromptRe.exec(str)
|
return
|
||||||
if (match) {
|
|
||||||
let prompt = str.slice(0, match.index)
|
|
||||||
str = str.slice(prompt.length)
|
|
||||||
taskReqBody.prompt = prompt.trim()
|
|
||||||
console.log('Prompt:', taskReqBody.prompt)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prompt
|
||||||
|
let knownKeyOnFirstLine = false
|
||||||
|
for (let key in TASK_TEXT_MAPPING) {
|
||||||
|
if (lines[0].startsWith(TASK_TEXT_MAPPING[key] + ":")) {
|
||||||
|
knownKeyOnFirstLine = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!knownKeyOnFirstLine) {
|
||||||
|
taskReqBody.prompt = lines[0]
|
||||||
|
console.log("Prompt:", taskReqBody.prompt)
|
||||||
|
}
|
||||||
|
|
||||||
for (const key in TASK_TEXT_MAPPING) {
|
for (const key in TASK_TEXT_MAPPING) {
|
||||||
const name = TASK_TEXT_MAPPING[key];
|
if (key in taskReqBody) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = TASK_TEXT_MAPPING[key]
|
||||||
let val = undefined
|
let val = undefined
|
||||||
|
|
||||||
const reName = new RegExp(`${name}\\ *:\\ *(.*)(?:\\r\\n|\\r|\\n)*`, 'igm')
|
const reName = new RegExp(`${name}\\ *:\\ *(.*)(?:\\r\\n|\\r|\\n)*`, "igm")
|
||||||
const match = reName.exec(str);
|
const match = reName.exec(str)
|
||||||
if (match) {
|
if (match) {
|
||||||
str = str.slice(0, match.index) + str.slice(match.index + match[0].length)
|
str = str.slice(0, match.index) + str.slice(match.index + match[0].length)
|
||||||
val = match[1]
|
val = match[1]
|
||||||
}
|
}
|
||||||
if (val !== undefined) {
|
if (val !== undefined) {
|
||||||
taskReqBody[key] = TASK_MAPPING[key].parse(val.trim())
|
taskReqBody[key] = TASK_MAPPING[key].parse(val.trim())
|
||||||
console.log(TASK_MAPPING[key].name + ':', taskReqBody[key])
|
console.log(TASK_MAPPING[key].name + ":", taskReqBody[key])
|
||||||
if (!str) {
|
if (!str) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -376,17 +636,21 @@ function parseTaskFromText(str) {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
const task = { reqBody: taskReqBody }
|
const task = { reqBody: taskReqBody }
|
||||||
if ('seed' in taskReqBody) {
|
if ("seed" in taskReqBody) {
|
||||||
task.seed = taskReqBody.seed
|
task.seed = taskReqBody.seed
|
||||||
}
|
}
|
||||||
return task
|
return task
|
||||||
}
|
}
|
||||||
|
|
||||||
async function parseContent(text) {
|
async function parseContent(text) {
|
||||||
text = text.trim();
|
text = text.trim()
|
||||||
if (text.startsWith('{') && text.endsWith('}')) {
|
if (text.startsWith("{") && text.endsWith("}")) {
|
||||||
try {
|
try {
|
||||||
const task = JSON.parse(text)
|
const task = JSON.parse(text)
|
||||||
|
if (!("reqBody" in task)) {
|
||||||
|
// support the format saved to the disk, by the UI
|
||||||
|
task.reqBody = Object.assign({}, task)
|
||||||
|
}
|
||||||
restoreTaskToUI(task)
|
restoreTaskToUI(task)
|
||||||
return true
|
return true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -396,37 +660,43 @@ async function parseContent(text) {
|
|||||||
}
|
}
|
||||||
// Normal txt file.
|
// Normal txt file.
|
||||||
const task = parseTaskFromText(text)
|
const task = parseTaskFromText(text)
|
||||||
if (task) {
|
if (text.toLowerCase().includes("seed:") && task) {
|
||||||
|
// only parse valid task content
|
||||||
restoreTaskToUI(task)
|
restoreTaskToUI(task)
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Raw text content couldn't be parsed.`)
|
console.warn(`Raw text content couldn't be parsed.`)
|
||||||
|
promptField.value = text
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readFile(file, i) {
|
async function readFile(file, i) {
|
||||||
console.log(`Event %o reading file[${i}]:${file.name}...`, e)
|
console.log(`Event %o reading file[${i}]:${file.name}...`)
|
||||||
const fileContent = (await file.text()).trim()
|
const fileContent = (await file.text()).trim()
|
||||||
return await parseContent(fileContent)
|
return await parseContent(fileContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
function dropHandler(ev) {
|
function dropHandler(ev) {
|
||||||
console.log('Content dropped...')
|
console.log("Content dropped...")
|
||||||
let items = []
|
let items = []
|
||||||
|
|
||||||
if (ev?.dataTransfer?.items) { // Use DataTransferItemList interface
|
if (ev?.dataTransfer?.items) {
|
||||||
|
// Use DataTransferItemList interface
|
||||||
items = Array.from(ev.dataTransfer.items)
|
items = Array.from(ev.dataTransfer.items)
|
||||||
items = items.filter(item => item.kind === 'file')
|
items = items.filter((item) => item.kind === "file")
|
||||||
items = items.map(item => item.getAsFile())
|
items = items.map((item) => item.getAsFile())
|
||||||
} else if (ev?.dataTransfer?.files) { // Use DataTransfer interface
|
} else if (ev?.dataTransfer?.files) {
|
||||||
|
// Use DataTransfer interface
|
||||||
items = Array.from(ev.dataTransfer.files)
|
items = Array.from(ev.dataTransfer.files)
|
||||||
}
|
}
|
||||||
|
|
||||||
items.forEach(item => {item.file_ext = EXT_REGEX.exec(item.name.toLowerCase())[1]})
|
items.forEach((item) => {
|
||||||
|
item.file_ext = EXT_REGEX.exec(item.name.toLowerCase())[1]
|
||||||
|
})
|
||||||
|
|
||||||
let text_items = items.filter(item => TEXT_EXTENSIONS.includes(item.file_ext))
|
let text_items = items.filter((item) => TEXT_EXTENSIONS.includes(item.file_ext))
|
||||||
let image_items = items.filter(item => IMAGE_EXTENSIONS.includes(item.file_ext))
|
let image_items = items.filter((item) => IMAGE_EXTENSIONS.includes(item.file_ext))
|
||||||
|
|
||||||
if (image_items.length > 0 && ev.target == initImageSelector) {
|
if (image_items.length > 0 && ev.target == initImageSelector) {
|
||||||
return // let the event bubble up, so that the Init Image filepicker can receive this
|
return // let the event bubble up, so that the Init Image filepicker can receive this
|
||||||
@@ -436,7 +706,7 @@ function dropHandler(ev) {
|
|||||||
text_items.forEach(readFile)
|
text_items.forEach(readFile)
|
||||||
}
|
}
|
||||||
function dragOverHandler(ev) {
|
function dragOverHandler(ev) {
|
||||||
console.log('Content in drop zone')
|
console.log("Content in drop zone")
|
||||||
|
|
||||||
// Prevent default behavior (Prevent file/content from being opened)
|
// Prevent default behavior (Prevent file/content from being opened)
|
||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
@@ -444,75 +714,72 @@ function dragOverHandler(ev) {
|
|||||||
ev.dataTransfer.dropEffect = "copy"
|
ev.dataTransfer.dropEffect = "copy"
|
||||||
|
|
||||||
let img = new Image()
|
let img = new Image()
|
||||||
img.src = location.host + '/media/images/favicon-32x32.png'
|
img.src = "//" + location.host + "/media/images/favicon-32x32.png"
|
||||||
ev.dataTransfer.setDragImage(img, 16, 16)
|
ev.dataTransfer.setDragImage(img, 16, 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("drop", dropHandler)
|
document.addEventListener("drop", dropHandler)
|
||||||
document.addEventListener("dragover", dragOverHandler)
|
document.addEventListener("dragover", dragOverHandler)
|
||||||
|
|
||||||
const TASK_REQ_NO_EXPORT = [
|
const TASK_REQ_NO_EXPORT = ["use_cpu", "save_to_disk_path"]
|
||||||
"use_cpu",
|
const resetSettings = document.getElementById("reset-image-settings")
|
||||||
"turbo",
|
|
||||||
"use_full_precision",
|
|
||||||
"save_to_disk_path"
|
|
||||||
]
|
|
||||||
const resetSettings = document.getElementById('reset-image-settings')
|
|
||||||
|
|
||||||
function checkReadTextClipboardPermission (result) {
|
function checkReadTextClipboardPermission(result) {
|
||||||
if (result.state != "granted" && result.state != "prompt") {
|
if (result.state != "granted" && result.state != "prompt") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// PASTE ICON
|
// PASTE ICON
|
||||||
const pasteIcon = document.createElement('i')
|
const pasteIcon = document.createElement("i")
|
||||||
pasteIcon.className = 'fa-solid fa-paste section-button'
|
pasteIcon.className = "fa-solid fa-paste section-button"
|
||||||
pasteIcon.innerHTML = `<span class="simple-tooltip right">Paste Image Settings</span>`
|
pasteIcon.innerHTML = `<span class="simple-tooltip top-left">Paste Image Settings</span>`
|
||||||
pasteIcon.addEventListener('click', async (event) => {
|
pasteIcon.addEventListener("click", async (event) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
// Add css class 'active'
|
// Add css class 'active'
|
||||||
pasteIcon.classList.add('active')
|
pasteIcon.classList.add("active")
|
||||||
// In 350 ms remove the 'active' class
|
// In 350 ms remove the 'active' class
|
||||||
asyncDelay(350).then(() => pasteIcon.classList.remove('active'))
|
asyncDelay(350).then(() => pasteIcon.classList.remove("active"))
|
||||||
|
|
||||||
// Retrieve clipboard content and try to parse it
|
// Retrieve clipboard content and try to parse it
|
||||||
const text = await navigator.clipboard.readText();
|
const text = await navigator.clipboard.readText()
|
||||||
await parseContent(text)
|
await parseContent(text)
|
||||||
})
|
})
|
||||||
resetSettings.parentNode.insertBefore(pasteIcon, resetSettings)
|
resetSettings.parentNode.insertBefore(pasteIcon, resetSettings)
|
||||||
}
|
}
|
||||||
navigator.permissions.query({ name: "clipboard-read" }).then(checkReadTextClipboardPermission, (reason) => console.log('clipboard-read is not available. %o', reason))
|
navigator.permissions
|
||||||
|
.query({ name: "clipboard-read" })
|
||||||
|
.then(checkReadTextClipboardPermission, (reason) => console.log("clipboard-read is not available. %o", reason))
|
||||||
|
|
||||||
document.addEventListener('paste', async (event) => {
|
document.addEventListener("paste", async (event) => {
|
||||||
if (event.target) {
|
if (event.target) {
|
||||||
const targetTag = event.target.tagName.toLowerCase()
|
const targetTag = event.target.tagName.toLowerCase()
|
||||||
// Disable when targeting input elements.
|
// Disable when targeting input elements.
|
||||||
if (targetTag === 'input' || targetTag === 'textarea') {
|
if (targetTag === "input" || targetTag === "textarea") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const paste = (event.clipboardData || window.clipboardData).getData('text')
|
const paste = (event.clipboardData || window.clipboardData).getData("text")
|
||||||
const selection = window.getSelection()
|
const selection = window.getSelection()
|
||||||
if (selection.toString().trim().length <= 0 && await parseContent(paste)) {
|
if (paste != "" && selection.toString().trim().length <= 0 && (await parseContent(paste))) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Adds a copy and a paste icon if the browser grants permission to write to clipboard.
|
// Adds a copy and a paste icon if the browser grants permission to write to clipboard.
|
||||||
function checkWriteToClipboardPermission (result) {
|
function checkWriteToClipboardPermission(result) {
|
||||||
if (result.state != "granted" && result.state != "prompt") {
|
if (result.state != "granted" && result.state != "prompt") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// COPY ICON
|
// COPY ICON
|
||||||
const copyIcon = document.createElement('i')
|
const copyIcon = document.createElement("i")
|
||||||
copyIcon.className = 'fa-solid fa-clipboard section-button'
|
copyIcon.className = "fa-solid fa-clipboard section-button"
|
||||||
copyIcon.innerHTML = `<span class="simple-tooltip right">Copy Image Settings</span>`
|
copyIcon.innerHTML = `<span class="simple-tooltip top-left">Copy Image Settings</span>`
|
||||||
copyIcon.addEventListener('click', (event) => {
|
copyIcon.addEventListener("click", (event) => {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
// Add css class 'active'
|
// Add css class 'active'
|
||||||
copyIcon.classList.add('active')
|
copyIcon.classList.add("active")
|
||||||
// In 350 ms remove the 'active' class
|
// In 350 ms remove the 'active' class
|
||||||
asyncDelay(350).then(() => copyIcon.classList.remove('active'))
|
asyncDelay(350).then(() => copyIcon.classList.remove("active"))
|
||||||
const uiState = readUI()
|
const uiState = readUI()
|
||||||
TASK_REQ_NO_EXPORT.forEach((key) => delete uiState.reqBody[key])
|
TASK_REQ_NO_EXPORT.forEach((key) => delete uiState.reqBody[key])
|
||||||
if (uiState.reqBody.init_image && !IMAGE_REGEX.test(uiState.reqBody.init_image)) {
|
if (uiState.reqBody.init_image && !IMAGE_REGEX.test(uiState.reqBody.init_image)) {
|
||||||
@@ -525,8 +792,8 @@ function checkWriteToClipboardPermission (result) {
|
|||||||
}
|
}
|
||||||
// Determine which access we have to the clipboard. Clipboard access is only available on localhost or via TLS.
|
// Determine which access we have to the clipboard. Clipboard access is only available on localhost or via TLS.
|
||||||
navigator.permissions.query({ name: "clipboard-write" }).then(checkWriteToClipboardPermission, (e) => {
|
navigator.permissions.query({ name: "clipboard-write" }).then(checkWriteToClipboardPermission, (e) => {
|
||||||
if (e instanceof TypeError && typeof navigator?.clipboard?.writeText === 'function') {
|
if (e instanceof TypeError && typeof navigator?.clipboard?.writeText === "function") {
|
||||||
// Fix for firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1560373
|
// Fix for firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1560373
|
||||||
checkWriteToClipboardPermission({state:"granted"})
|
checkWriteToClipboardPermission({ state: "granted" })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
4
ui/media/js/drawingboard.min.js
vendored
1457
ui/media/js/engine.js
Normal file
2
ui/media/js/exif-reader.js
Normal file
943
ui/media/js/image-editor.js
Normal file
@@ -0,0 +1,943 @@
|
|||||||
|
var editorControlsLeft = document.getElementById("image-editor-controls-left")
|
||||||
|
|
||||||
|
const IMAGE_EDITOR_MAX_SIZE = 800
|
||||||
|
|
||||||
|
const IMAGE_EDITOR_BUTTONS = [
|
||||||
|
{
|
||||||
|
name: "Cancel",
|
||||||
|
icon: "fa-regular fa-circle-xmark",
|
||||||
|
handler: (editor) => {
|
||||||
|
editor.hide()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Save",
|
||||||
|
icon: "fa-solid fa-floppy-disk",
|
||||||
|
handler: (editor) => {
|
||||||
|
editor.saveImage()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const defaultToolBegin = (editor, ctx, x, y, is_overlay = false) => {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(x, y)
|
||||||
|
}
|
||||||
|
const defaultToolMove = (editor, ctx, x, y, is_overlay = false) => {
|
||||||
|
ctx.lineTo(x, y)
|
||||||
|
if (is_overlay) {
|
||||||
|
ctx.clearRect(0, 0, editor.width, editor.height)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const defaultToolEnd = (editor, ctx, x, y, is_overlay = false) => {
|
||||||
|
ctx.stroke()
|
||||||
|
if (is_overlay) {
|
||||||
|
ctx.clearRect(0, 0, editor.width, editor.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const toolDoNothing = (editor, ctx, x, y, is_overlay = false) => {}
|
||||||
|
|
||||||
|
const IMAGE_EDITOR_TOOLS = [
|
||||||
|
{
|
||||||
|
id: "draw",
|
||||||
|
name: "Draw",
|
||||||
|
icon: "fa-solid fa-pencil",
|
||||||
|
cursor: "url(/media/images/fa-pencil.svg) 0 24, pointer",
|
||||||
|
begin: defaultToolBegin,
|
||||||
|
move: defaultToolMove,
|
||||||
|
end: defaultToolEnd,
|
||||||
|
hotkey: "d",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "erase",
|
||||||
|
name: "Erase",
|
||||||
|
icon: "fa-solid fa-eraser",
|
||||||
|
cursor: "url(/media/images/fa-eraser.svg) 0 14, pointer",
|
||||||
|
begin: defaultToolBegin,
|
||||||
|
move: (editor, ctx, x, y, is_overlay = false) => {
|
||||||
|
ctx.lineTo(x, y)
|
||||||
|
if (is_overlay) {
|
||||||
|
ctx.clearRect(0, 0, editor.width, editor.height)
|
||||||
|
ctx.globalCompositeOperation = "source-over"
|
||||||
|
ctx.globalAlpha = 1
|
||||||
|
ctx.filter = "none"
|
||||||
|
ctx.drawImage(editor.canvas_current, 0, 0)
|
||||||
|
editor.setBrush(editor.layers.overlay)
|
||||||
|
ctx.stroke()
|
||||||
|
editor.canvas_current.style.opacity = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
end: (editor, ctx, x, y, is_overlay = false) => {
|
||||||
|
ctx.stroke()
|
||||||
|
if (is_overlay) {
|
||||||
|
ctx.clearRect(0, 0, editor.width, editor.height)
|
||||||
|
editor.canvas_current.style.opacity = ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setBrush: (editor, layer) => {
|
||||||
|
layer.ctx.globalCompositeOperation = "destination-out"
|
||||||
|
},
|
||||||
|
hotkey: "e",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "fill",
|
||||||
|
name: "Fill",
|
||||||
|
icon: "fa-solid fa-fill",
|
||||||
|
cursor: "url(/media/images/fa-fill.svg) 20 6, pointer",
|
||||||
|
begin: (editor, ctx, x, y, is_overlay = false) => {
|
||||||
|
if (!is_overlay) {
|
||||||
|
var color = hexToRgb(ctx.fillStyle)
|
||||||
|
color.a = parseInt(ctx.globalAlpha * 255) // layer.ctx.globalAlpha
|
||||||
|
flood_fill(editor, ctx, parseInt(x), parseInt(y), color)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
move: toolDoNothing,
|
||||||
|
end: toolDoNothing,
|
||||||
|
hotkey: "f",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "colorpicker",
|
||||||
|
name: "Picker",
|
||||||
|
icon: "fa-solid fa-eye-dropper",
|
||||||
|
cursor: "url(/media/images/fa-eye-dropper.svg) 0 24, pointer",
|
||||||
|
begin: (editor, ctx, x, y, is_overlay = false) => {
|
||||||
|
if (!is_overlay) {
|
||||||
|
var img_rgb = editor.layers.background.ctx.getImageData(x, y, 1, 1).data
|
||||||
|
var drawn_rgb = editor.ctx_current.getImageData(x, y, 1, 1).data
|
||||||
|
var drawn_opacity = drawn_rgb[3] / 255
|
||||||
|
editor.custom_color_input.value = rgbToHex({
|
||||||
|
r: drawn_rgb[0] * drawn_opacity + img_rgb[0] * (1 - drawn_opacity),
|
||||||
|
g: drawn_rgb[1] * drawn_opacity + img_rgb[1] * (1 - drawn_opacity),
|
||||||
|
b: drawn_rgb[2] * drawn_opacity + img_rgb[2] * (1 - drawn_opacity),
|
||||||
|
})
|
||||||
|
editor.custom_color_input.dispatchEvent(new Event("change"))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
move: toolDoNothing,
|
||||||
|
end: toolDoNothing,
|
||||||
|
hotkey: "p",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const IMAGE_EDITOR_ACTIONS = [
|
||||||
|
{
|
||||||
|
id: "load_mask",
|
||||||
|
name: "Load mask from file",
|
||||||
|
className: "load_mask",
|
||||||
|
icon: "fa-regular fa-folder-open",
|
||||||
|
handler: (editor) => {
|
||||||
|
let el = document.createElement("input")
|
||||||
|
el.setAttribute("type", "file")
|
||||||
|
el.addEventListener("change", function() {
|
||||||
|
if (this.files.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let reader = new FileReader()
|
||||||
|
let file = this.files[0]
|
||||||
|
|
||||||
|
reader.addEventListener("load", function(event) {
|
||||||
|
let maskData = reader.result
|
||||||
|
|
||||||
|
editor.layers.drawing.ctx.clearRect(0, 0, editor.width, editor.height)
|
||||||
|
var image = new Image()
|
||||||
|
image.onload = () => {
|
||||||
|
editor.layers.drawing.ctx.drawImage(image, 0, 0, editor.width, editor.height)
|
||||||
|
}
|
||||||
|
image.src = maskData
|
||||||
|
})
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
el.click()
|
||||||
|
},
|
||||||
|
trackHistory: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "fill_all",
|
||||||
|
name: "Fill all",
|
||||||
|
icon: "fa-solid fa-paint-roller",
|
||||||
|
handler: (editor) => {
|
||||||
|
editor.ctx_current.globalCompositeOperation = "source-over"
|
||||||
|
editor.ctx_current.rect(0, 0, editor.width, editor.height)
|
||||||
|
editor.ctx_current.fill()
|
||||||
|
editor.setBrush()
|
||||||
|
},
|
||||||
|
trackHistory: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "clear",
|
||||||
|
name: "Clear",
|
||||||
|
icon: "fa-solid fa-xmark",
|
||||||
|
handler: (editor) => {
|
||||||
|
editor.ctx_current.clearRect(0, 0, editor.width, editor.height)
|
||||||
|
imageEditor.setImage(null, editor.width, editor.height) // properly reset the drawing canvas
|
||||||
|
},
|
||||||
|
trackHistory: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "undo",
|
||||||
|
name: "Undo",
|
||||||
|
icon: "fa-solid fa-rotate-left",
|
||||||
|
handler: (editor) => {
|
||||||
|
editor.history.undo()
|
||||||
|
},
|
||||||
|
trackHistory: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "redo",
|
||||||
|
name: "Redo",
|
||||||
|
icon: "fa-solid fa-rotate-right",
|
||||||
|
handler: (editor) => {
|
||||||
|
editor.history.redo()
|
||||||
|
},
|
||||||
|
trackHistory: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
var IMAGE_EDITOR_SECTIONS = [
|
||||||
|
{
|
||||||
|
name: "tool",
|
||||||
|
title: "Tool",
|
||||||
|
default: "draw",
|
||||||
|
options: Array.from(IMAGE_EDITOR_TOOLS.map((t) => t.id)),
|
||||||
|
initElement: (element, option) => {
|
||||||
|
var tool_info = IMAGE_EDITOR_TOOLS.find((t) => t.id == option)
|
||||||
|
element.className = "image-editor-button button"
|
||||||
|
var sub_element = document.createElement("div")
|
||||||
|
var icon = document.createElement("i")
|
||||||
|
tool_info.icon.split(" ").forEach((c) => icon.classList.add(c))
|
||||||
|
sub_element.appendChild(icon)
|
||||||
|
var label_element = document.createElement("div")
|
||||||
|
label_element.classList.add("image-editor-button-label")
|
||||||
|
label_element.textContent=tool_info.name
|
||||||
|
sub_element.appendChild(label_element)
|
||||||
|
element.appendChild(sub_element)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "color",
|
||||||
|
title: "Color",
|
||||||
|
default: "#f1c232",
|
||||||
|
options: [
|
||||||
|
"custom",
|
||||||
|
"#ea9999",
|
||||||
|
"#e06666",
|
||||||
|
"#cc0000",
|
||||||
|
"#990000",
|
||||||
|
"#660000",
|
||||||
|
"#f9cb9c",
|
||||||
|
"#f6b26b",
|
||||||
|
"#e69138",
|
||||||
|
"#b45f06",
|
||||||
|
"#783f04",
|
||||||
|
"#ffe599",
|
||||||
|
"#ffd966",
|
||||||
|
"#f1c232",
|
||||||
|
"#bf9000",
|
||||||
|
"#7f6000",
|
||||||
|
"#b6d7a8",
|
||||||
|
"#93c47d",
|
||||||
|
"#6aa84f",
|
||||||
|
"#38761d",
|
||||||
|
"#274e13",
|
||||||
|
"#a4c2f4",
|
||||||
|
"#6d9eeb",
|
||||||
|
"#3c78d8",
|
||||||
|
"#1155cc",
|
||||||
|
"#1c4587",
|
||||||
|
"#b4a7d6",
|
||||||
|
"#8e7cc3",
|
||||||
|
"#674ea7",
|
||||||
|
"#351c75",
|
||||||
|
"#20124d",
|
||||||
|
"#d5a6bd",
|
||||||
|
"#c27ba0",
|
||||||
|
"#a64d79",
|
||||||
|
"#741b47",
|
||||||
|
"#4c1130",
|
||||||
|
"#ffffff",
|
||||||
|
"#c0c0c0",
|
||||||
|
"#838383",
|
||||||
|
"#525252",
|
||||||
|
"#000000",
|
||||||
|
],
|
||||||
|
initElement: (element, option) => {
|
||||||
|
if (option == "custom") {
|
||||||
|
var input = document.createElement("input")
|
||||||
|
input.type = "color"
|
||||||
|
element.appendChild(input)
|
||||||
|
var span = document.createElement("span")
|
||||||
|
span.textContent = "Custom"
|
||||||
|
span.onclick = function(e) {
|
||||||
|
input.click()
|
||||||
|
}
|
||||||
|
element.appendChild(span)
|
||||||
|
} else {
|
||||||
|
element.style.background = option
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getCustom: (editor) => {
|
||||||
|
var input = editor.popup.querySelector(".image_editor_color input")
|
||||||
|
return input.value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "brush_size",
|
||||||
|
title: "Brush Size",
|
||||||
|
default: 48,
|
||||||
|
options: [6, 12, 16, 24, 30, 40, 48, 64],
|
||||||
|
initElement: (element, option) => {
|
||||||
|
element.parentElement.style.flex = option
|
||||||
|
element.style.width = option + "px"
|
||||||
|
element.style.height = option + "px"
|
||||||
|
element.style["margin-right"] = "2px"
|
||||||
|
element.style["border-radius"] = (option / 2).toFixed() + "px"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "opacity",
|
||||||
|
title: "Opacity",
|
||||||
|
default: 0,
|
||||||
|
options: [0, 0.2, 0.4, 0.6, 0.8],
|
||||||
|
initElement: (element, option) => {
|
||||||
|
element.style.background = `repeating-conic-gradient(rgba(0, 0, 0, ${option}) 0% 25%, rgba(255, 255, 255, ${option}) 0% 50%) 50% / 10px 10px`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sharpness",
|
||||||
|
title: "Sharpness",
|
||||||
|
default: 0,
|
||||||
|
options: [0, 0.05, 0.1, 0.2, 0.3],
|
||||||
|
initElement: (element, option) => {
|
||||||
|
var size = 32
|
||||||
|
var blur_amount = parseInt(option * size)
|
||||||
|
var sub_element = document.createElement("div")
|
||||||
|
sub_element.style.background = `var(--background-color3)`
|
||||||
|
sub_element.style.filter = `blur(${blur_amount}px)`
|
||||||
|
sub_element.style.width = `${size - 2}px`
|
||||||
|
sub_element.style.height = `${size - 2}px`
|
||||||
|
sub_element.style["border-radius"] = `${size}px`
|
||||||
|
element.style.background = "none"
|
||||||
|
element.appendChild(sub_element)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
class EditorHistory {
|
||||||
|
constructor(editor) {
|
||||||
|
this.editor = editor
|
||||||
|
this.events = [] // stack of all events (actions/edits)
|
||||||
|
this.current_edit = null
|
||||||
|
this.rewind_index = 0 // how many events back into the history we've rewound to. (current state is just after event at index 'length - this.rewind_index - 1')
|
||||||
|
}
|
||||||
|
push(event) {
|
||||||
|
// probably add something here eventually to save state every x events
|
||||||
|
if (this.rewind_index != 0) {
|
||||||
|
this.events = this.events.slice(0, 0 - this.rewind_index)
|
||||||
|
this.rewind_index = 0
|
||||||
|
}
|
||||||
|
var snapshot_frequency = 20 // (every x edits, take a snapshot of the current drawing state, for faster rewinding)
|
||||||
|
if (this.events.length > 0 && this.events.length % snapshot_frequency == 0) {
|
||||||
|
event.snapshot = this.editor.layers.drawing.ctx.getImageData(0, 0, this.editor.width, this.editor.height)
|
||||||
|
}
|
||||||
|
this.events.push(event)
|
||||||
|
}
|
||||||
|
pushAction(action) {
|
||||||
|
this.push({
|
||||||
|
type: "action",
|
||||||
|
id: action,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
editBegin(x, y) {
|
||||||
|
this.current_edit = {
|
||||||
|
type: "edit",
|
||||||
|
id: this.editor.getOptionValue("tool"),
|
||||||
|
options: Object.assign({}, this.editor.options),
|
||||||
|
points: [{ x: x, y: y }],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editMove(x, y) {
|
||||||
|
if (this.current_edit) {
|
||||||
|
this.current_edit.points.push({ x: x, y: y })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
editEnd(x, y) {
|
||||||
|
if (this.current_edit) {
|
||||||
|
this.push(this.current_edit)
|
||||||
|
this.current_edit = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clear() {
|
||||||
|
this.events = []
|
||||||
|
}
|
||||||
|
undo() {
|
||||||
|
this.rewindTo(this.rewind_index + 1)
|
||||||
|
}
|
||||||
|
redo() {
|
||||||
|
this.rewindTo(this.rewind_index - 1)
|
||||||
|
}
|
||||||
|
rewindTo(new_rewind_index) {
|
||||||
|
if (new_rewind_index < 0 || new_rewind_index > this.events.length) {
|
||||||
|
return // do nothing if target index is out of bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
var ctx = this.editor.layers.drawing.ctx
|
||||||
|
ctx.clearRect(0, 0, this.editor.width, this.editor.height)
|
||||||
|
|
||||||
|
var target_index = this.events.length - 1 - new_rewind_index
|
||||||
|
var snapshot_index = target_index
|
||||||
|
while (snapshot_index > -1) {
|
||||||
|
if (this.events[snapshot_index].snapshot) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
snapshot_index--
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot_index != -1) {
|
||||||
|
ctx.putImageData(this.events[snapshot_index].snapshot, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = snapshot_index + 1; i <= target_index; i++) {
|
||||||
|
var event = this.events[i]
|
||||||
|
if (event.type == "action") {
|
||||||
|
var action = IMAGE_EDITOR_ACTIONS.find((a) => a.id == event.id)
|
||||||
|
action.handler(this.editor)
|
||||||
|
} else if (event.type == "edit") {
|
||||||
|
var tool = IMAGE_EDITOR_TOOLS.find((t) => t.id == event.id)
|
||||||
|
this.editor.setBrush(this.editor.layers.drawing, event.options)
|
||||||
|
|
||||||
|
var first_point = event.points[0]
|
||||||
|
tool.begin(this.editor, ctx, first_point.x, first_point.y)
|
||||||
|
for (var point_i = 1; point_i < event.points.length; point_i++) {
|
||||||
|
tool.move(this.editor, ctx, event.points[point_i].x, event.points[point_i].y)
|
||||||
|
}
|
||||||
|
var last_point = event.points[event.points.length - 1]
|
||||||
|
tool.end(this.editor, ctx, last_point.x, last_point.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-set brush to current settings
|
||||||
|
this.editor.setBrush(this.editor.layers.drawing)
|
||||||
|
|
||||||
|
this.rewind_index = new_rewind_index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImageEditor {
|
||||||
|
constructor(popup, inpainter = false) {
|
||||||
|
this.inpainter = inpainter
|
||||||
|
this.popup = popup
|
||||||
|
this.history = new EditorHistory(this)
|
||||||
|
if (inpainter) {
|
||||||
|
this.popup.classList.add("inpainter")
|
||||||
|
}
|
||||||
|
this.drawing = false
|
||||||
|
this.temp_previous_tool = null // used for the ctrl-colorpicker functionality
|
||||||
|
this.container = popup.querySelector(".editor-controls-center > div")
|
||||||
|
this.layers = {}
|
||||||
|
var layer_names = ["background", "drawing", "overlay"]
|
||||||
|
layer_names.forEach((name) => {
|
||||||
|
let canvas = document.createElement("canvas")
|
||||||
|
canvas.className = `editor-canvas-${name}`
|
||||||
|
this.container.appendChild(canvas)
|
||||||
|
this.layers[name] = {
|
||||||
|
name: name,
|
||||||
|
canvas: canvas,
|
||||||
|
ctx: canvas.getContext("2d"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// add mouse handlers
|
||||||
|
this.container.addEventListener("mousedown", this.mouseHandler.bind(this))
|
||||||
|
this.container.addEventListener("mouseup", this.mouseHandler.bind(this))
|
||||||
|
this.container.addEventListener("mousemove", this.mouseHandler.bind(this))
|
||||||
|
this.container.addEventListener("mouseout", this.mouseHandler.bind(this))
|
||||||
|
this.container.addEventListener("mouseenter", this.mouseHandler.bind(this))
|
||||||
|
|
||||||
|
this.container.addEventListener("touchstart", this.mouseHandler.bind(this))
|
||||||
|
this.container.addEventListener("touchmove", this.mouseHandler.bind(this))
|
||||||
|
this.container.addEventListener("touchcancel", this.mouseHandler.bind(this))
|
||||||
|
this.container.addEventListener("touchend", this.mouseHandler.bind(this))
|
||||||
|
|
||||||
|
// initialize editor controls
|
||||||
|
this.options = {}
|
||||||
|
this.optionElements = {}
|
||||||
|
IMAGE_EDITOR_SECTIONS.forEach((section) => {
|
||||||
|
section.id = `image_editor_${section.name}`
|
||||||
|
var sectionElement = document.createElement("div")
|
||||||
|
sectionElement.className = section.id
|
||||||
|
|
||||||
|
var title = document.createElement("h4")
|
||||||
|
title.innerText = section.title
|
||||||
|
sectionElement.appendChild(title)
|
||||||
|
|
||||||
|
var optionsContainer = document.createElement("div")
|
||||||
|
optionsContainer.classList.add("editor-options-container")
|
||||||
|
|
||||||
|
this.optionElements[section.name] = []
|
||||||
|
section.options.forEach((option, index) => {
|
||||||
|
var optionHolder = document.createElement("div")
|
||||||
|
var optionElement = document.createElement("div")
|
||||||
|
optionHolder.appendChild(optionElement)
|
||||||
|
section.initElement(optionElement, option)
|
||||||
|
optionElement.addEventListener("click", (target) => this.selectOption(section.name, index))
|
||||||
|
optionsContainer.appendChild(optionHolder)
|
||||||
|
this.optionElements[section.name].push(optionElement)
|
||||||
|
})
|
||||||
|
this.selectOption(section.name, section.options.indexOf(section.default))
|
||||||
|
|
||||||
|
sectionElement.appendChild(optionsContainer)
|
||||||
|
|
||||||
|
this.popup.querySelector(".editor-controls-left").appendChild(sectionElement)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.custom_color_input = this.popup.querySelector(`input[type="color"]`)
|
||||||
|
this.custom_color_input.addEventListener("change", () => {
|
||||||
|
this.custom_color_input.parentElement.style.background = this.custom_color_input.value
|
||||||
|
this.selectOption("color", 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.inpainter) {
|
||||||
|
this.selectOption("color", IMAGE_EDITOR_SECTIONS.find((s) => s.name == "color").options.indexOf("#ffffff"))
|
||||||
|
this.selectOption("opacity", IMAGE_EDITOR_SECTIONS.find((s) => s.name == "opacity").options.indexOf(0.4))
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize the right-side controls
|
||||||
|
var buttonContainer = document.createElement("div")
|
||||||
|
IMAGE_EDITOR_BUTTONS.forEach((button) => {
|
||||||
|
var element = document.createElement("div")
|
||||||
|
var icon = document.createElement("i")
|
||||||
|
element.className = "image-editor-button button"
|
||||||
|
icon.className = button.icon
|
||||||
|
element.appendChild(icon)
|
||||||
|
element.append(button.name)
|
||||||
|
buttonContainer.appendChild(element)
|
||||||
|
element.addEventListener("click", (event) => button.handler(this))
|
||||||
|
})
|
||||||
|
var actionsContainer = document.createElement("div")
|
||||||
|
var actionsTitle = document.createElement("h4")
|
||||||
|
actionsTitle.textContent = "Actions"
|
||||||
|
actionsContainer.appendChild(actionsTitle)
|
||||||
|
IMAGE_EDITOR_ACTIONS.forEach((action) => {
|
||||||
|
var element = document.createElement("div")
|
||||||
|
var icon = document.createElement("i")
|
||||||
|
element.className = "image-editor-button button"
|
||||||
|
if (action.className) {
|
||||||
|
element.className += " " + action.className
|
||||||
|
}
|
||||||
|
icon.className = action.icon
|
||||||
|
element.appendChild(icon)
|
||||||
|
element.append(action.name)
|
||||||
|
actionsContainer.appendChild(element)
|
||||||
|
element.addEventListener("click", (event) => this.runAction(action.id))
|
||||||
|
})
|
||||||
|
this.popup.querySelector(".editor-controls-right").appendChild(actionsContainer)
|
||||||
|
this.popup.querySelector(".editor-controls-right").appendChild(buttonContainer)
|
||||||
|
|
||||||
|
this.keyHandlerBound = this.keyHandler.bind(this)
|
||||||
|
|
||||||
|
this.setSize(512, 512)
|
||||||
|
}
|
||||||
|
show() {
|
||||||
|
this.popup.classList.add("active")
|
||||||
|
document.addEventListener("keydown", this.keyHandlerBound, true)
|
||||||
|
document.addEventListener("keyup", this.keyHandlerBound, true)
|
||||||
|
}
|
||||||
|
hide() {
|
||||||
|
this.popup.classList.remove("active")
|
||||||
|
document.removeEventListener("keydown", this.keyHandlerBound, true)
|
||||||
|
document.removeEventListener("keyup", this.keyHandlerBound, true)
|
||||||
|
}
|
||||||
|
setSize(width, height) {
|
||||||
|
if (width == this.width && height == this.height) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width > height) {
|
||||||
|
var max_size = Math.min(parseInt(window.innerWidth * 0.9), width, 768)
|
||||||
|
var multiplier = max_size / width
|
||||||
|
width = (multiplier * width).toFixed()
|
||||||
|
height = (multiplier * height).toFixed()
|
||||||
|
} else {
|
||||||
|
var max_size = Math.min(parseInt(window.innerHeight * 0.9), height, 768)
|
||||||
|
var multiplier = max_size / height
|
||||||
|
width = (multiplier * width).toFixed()
|
||||||
|
height = (multiplier * height).toFixed()
|
||||||
|
}
|
||||||
|
this.width = parseInt(width)
|
||||||
|
this.height = parseInt(height)
|
||||||
|
|
||||||
|
this.container.style.width = width + "px"
|
||||||
|
this.container.style.height = height + "px"
|
||||||
|
|
||||||
|
Object.values(this.layers).forEach((layer) => {
|
||||||
|
layer.canvas.width = width
|
||||||
|
layer.canvas.height = height
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.inpainter) {
|
||||||
|
this.saveImage() // We've reset the size of the image so inpainting is different
|
||||||
|
}
|
||||||
|
this.setBrush()
|
||||||
|
this.history.clear()
|
||||||
|
}
|
||||||
|
get tool() {
|
||||||
|
var tool_id = this.getOptionValue("tool")
|
||||||
|
return IMAGE_EDITOR_TOOLS.find((t) => t.id == tool_id)
|
||||||
|
}
|
||||||
|
loadTool() {
|
||||||
|
this.drawing = false
|
||||||
|
this.container.style.cursor = this.tool.cursor
|
||||||
|
}
|
||||||
|
setImage(url, width, height) {
|
||||||
|
this.setSize(width, height)
|
||||||
|
this.layers.background.ctx.clearRect(0, 0, this.width, this.height)
|
||||||
|
if (!(url && this.inpainter)) {
|
||||||
|
this.layers.drawing.ctx.clearRect(0, 0, this.width, this.height)
|
||||||
|
}
|
||||||
|
if (url) {
|
||||||
|
var image = new Image()
|
||||||
|
image.onload = () => {
|
||||||
|
this.layers.background.ctx.drawImage(image, 0, 0, this.width, this.height)
|
||||||
|
}
|
||||||
|
image.src = url
|
||||||
|
} else {
|
||||||
|
this.layers.background.ctx.fillStyle = "#ffffff"
|
||||||
|
this.layers.background.ctx.beginPath()
|
||||||
|
this.layers.background.ctx.rect(0, 0, this.width, this.height)
|
||||||
|
this.layers.background.ctx.fill()
|
||||||
|
}
|
||||||
|
this.history.clear()
|
||||||
|
}
|
||||||
|
saveImage() {
|
||||||
|
if (!this.inpainter) {
|
||||||
|
// This is not an inpainter, so save the image as the new img2img input
|
||||||
|
this.layers.background.ctx.drawImage(this.layers.drawing.canvas, 0, 0, this.width, this.height)
|
||||||
|
var base64 = this.layers.background.canvas.toDataURL()
|
||||||
|
initImagePreview.src = base64 // this will trigger the rest of the app to use it
|
||||||
|
} else {
|
||||||
|
// This is an inpainter, so make sure the toggle is set accordingly
|
||||||
|
var is_blank = !this.layers.drawing.ctx
|
||||||
|
.getImageData(0, 0, this.width, this.height)
|
||||||
|
.data.some((channel) => channel !== 0)
|
||||||
|
maskSetting.checked = !is_blank
|
||||||
|
maskSetting.dispatchEvent(new Event("change"))
|
||||||
|
}
|
||||||
|
this.hide()
|
||||||
|
}
|
||||||
|
getImg() {
|
||||||
|
// a drop-in replacement of the drawingboard version
|
||||||
|
return this.layers.drawing.canvas.toDataURL()
|
||||||
|
}
|
||||||
|
setImg(dataUrl) {
|
||||||
|
// a drop-in replacement of the drawingboard version
|
||||||
|
var image = new Image()
|
||||||
|
image.onload = () => {
|
||||||
|
var ctx = this.layers.drawing.ctx
|
||||||
|
ctx.clearRect(0, 0, this.width, this.height)
|
||||||
|
ctx.globalCompositeOperation = "source-over"
|
||||||
|
ctx.globalAlpha = 1
|
||||||
|
ctx.filter = "none"
|
||||||
|
ctx.drawImage(image, 0, 0, this.width, this.height)
|
||||||
|
this.setBrush(this.layers.drawing)
|
||||||
|
}
|
||||||
|
image.src = dataUrl
|
||||||
|
}
|
||||||
|
runAction(action_id) {
|
||||||
|
var action = IMAGE_EDITOR_ACTIONS.find((a) => a.id == action_id)
|
||||||
|
if (action.trackHistory) {
|
||||||
|
this.history.pushAction(action_id)
|
||||||
|
}
|
||||||
|
action.handler(this)
|
||||||
|
}
|
||||||
|
setBrush(layer = null, options = null) {
|
||||||
|
if (options == null) {
|
||||||
|
options = this.options
|
||||||
|
}
|
||||||
|
if (layer) {
|
||||||
|
layer.ctx.lineCap = "round"
|
||||||
|
layer.ctx.lineJoin = "round"
|
||||||
|
layer.ctx.lineWidth = options.brush_size
|
||||||
|
layer.ctx.fillStyle = options.color
|
||||||
|
layer.ctx.strokeStyle = options.color
|
||||||
|
var sharpness = parseInt(options.sharpness * options.brush_size)
|
||||||
|
layer.ctx.filter = sharpness == 0 ? `none` : `blur(${sharpness}px)`
|
||||||
|
layer.ctx.globalAlpha = 1 - options.opacity
|
||||||
|
layer.ctx.globalCompositeOperation = "source-over"
|
||||||
|
var tool = IMAGE_EDITOR_TOOLS.find((t) => t.id == options.tool)
|
||||||
|
if (tool && tool.setBrush) {
|
||||||
|
tool.setBrush(editor, layer)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Object.values(["drawing", "overlay"])
|
||||||
|
.map((name) => this.layers[name])
|
||||||
|
.forEach((l) => {
|
||||||
|
this.setBrush(l)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get ctx_overlay() {
|
||||||
|
return this.layers.overlay.ctx
|
||||||
|
}
|
||||||
|
get ctx_current() {
|
||||||
|
// the idea is this will help support having custom layers and editing each one
|
||||||
|
return this.layers.drawing.ctx
|
||||||
|
}
|
||||||
|
get canvas_current() {
|
||||||
|
return this.layers.drawing.canvas
|
||||||
|
}
|
||||||
|
keyHandler(event) {
|
||||||
|
// handles keybinds like ctrl+z, ctrl+y
|
||||||
|
if (!this.popup.classList.contains("active")) {
|
||||||
|
document.removeEventListener("keydown", this.keyHandlerBound)
|
||||||
|
document.removeEventListener("keyup", this.keyHandlerBound)
|
||||||
|
return // this catches if something else closes the window but doesnt properly unbind the key handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// keybindings
|
||||||
|
if (event.type == "keydown") {
|
||||||
|
if ((event.key == "z" || event.key == "Z") && event.ctrlKey) {
|
||||||
|
if (!event.shiftKey) {
|
||||||
|
this.history.undo()
|
||||||
|
} else {
|
||||||
|
this.history.redo()
|
||||||
|
}
|
||||||
|
event.stopPropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
else if (event.key == "y" && event.ctrlKey) {
|
||||||
|
this.history.redo()
|
||||||
|
event.stopPropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
else if (event.key === "Escape") {
|
||||||
|
this.hide()
|
||||||
|
event.stopPropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
} else {
|
||||||
|
let toolIndex = IMAGE_EDITOR_TOOLS.findIndex( t => t.hotkey ==event.key )
|
||||||
|
if (toolIndex != -1) {
|
||||||
|
this.selectOption("tool", toolIndex)
|
||||||
|
event.stopPropagation()
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dropper ctrl holding handler stuff
|
||||||
|
var dropper_active = this.temp_previous_tool != null
|
||||||
|
if (dropper_active && !event.ctrlKey) {
|
||||||
|
this.selectOption(
|
||||||
|
"tool",
|
||||||
|
IMAGE_EDITOR_TOOLS.findIndex((t) => t.id == this.temp_previous_tool)
|
||||||
|
)
|
||||||
|
this.temp_previous_tool = null
|
||||||
|
} else if (!dropper_active && event.ctrlKey) {
|
||||||
|
this.temp_previous_tool = this.getOptionValue("tool")
|
||||||
|
this.selectOption(
|
||||||
|
"tool",
|
||||||
|
IMAGE_EDITOR_TOOLS.findIndex((t) => t.id == "colorpicker")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mouseHandler(event) {
|
||||||
|
var bbox = this.layers.overlay.canvas.getBoundingClientRect()
|
||||||
|
var x = (event.clientX || 0) - bbox.left
|
||||||
|
var y = (event.clientY || 0) - bbox.top
|
||||||
|
var type = event.type
|
||||||
|
var touchmap = {
|
||||||
|
touchstart: "mousedown",
|
||||||
|
touchmove: "mousemove",
|
||||||
|
touchend: "mouseup",
|
||||||
|
touchcancel: "mouseup",
|
||||||
|
}
|
||||||
|
if (type in touchmap) {
|
||||||
|
type = touchmap[type]
|
||||||
|
if (event.touches && event.touches[0]) {
|
||||||
|
var touch = event.touches[0]
|
||||||
|
var x = (touch.clientX || 0) - bbox.left
|
||||||
|
var y = (touch.clientY || 0) - bbox.top
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
// do drawing-related stuff
|
||||||
|
if (type == "mousedown" || (type == "mouseenter" && event.buttons == 1)) {
|
||||||
|
this.drawing = true
|
||||||
|
this.tool.begin(this, this.ctx_current, x, y)
|
||||||
|
this.tool.begin(this, this.ctx_overlay, x, y, true)
|
||||||
|
this.history.editBegin(x, y)
|
||||||
|
}
|
||||||
|
if (type == "mouseup" || type == "mousemove") {
|
||||||
|
if (this.drawing) {
|
||||||
|
if (x > 0 && y > 0) {
|
||||||
|
this.tool.move(this, this.ctx_current, x, y)
|
||||||
|
this.tool.move(this, this.ctx_overlay, x, y, true)
|
||||||
|
this.history.editMove(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type == "mouseup" || type == "mouseout") {
|
||||||
|
if (this.drawing) {
|
||||||
|
this.drawing = false
|
||||||
|
this.tool.end(this, this.ctx_current, x, y)
|
||||||
|
this.tool.end(this, this.ctx_overlay, x, y, true)
|
||||||
|
this.history.editEnd(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getOptionValue(section_name) {
|
||||||
|
var section = IMAGE_EDITOR_SECTIONS.find((s) => s.name == section_name)
|
||||||
|
return this.options && section_name in this.options ? this.options[section_name] : section.default
|
||||||
|
}
|
||||||
|
selectOption(section_name, option_index) {
|
||||||
|
var section = IMAGE_EDITOR_SECTIONS.find((s) => s.name == section_name)
|
||||||
|
var value = section.options[option_index]
|
||||||
|
this.options[section_name] = value == "custom" ? section.getCustom(this) : value
|
||||||
|
|
||||||
|
this.optionElements[section_name].forEach((element) => element.classList.remove("active"))
|
||||||
|
this.optionElements[section_name][option_index].classList.add("active")
|
||||||
|
|
||||||
|
// change the editor
|
||||||
|
this.setBrush()
|
||||||
|
if (section.name == "tool") {
|
||||||
|
this.loadTool()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageEditor = new ImageEditor(document.getElementById("image-editor"))
|
||||||
|
const imageInpainter = new ImageEditor(document.getElementById("image-inpainter"), true)
|
||||||
|
|
||||||
|
imageEditor.setImage(null, 512, 512)
|
||||||
|
imageInpainter.setImage(null, 512, 512)
|
||||||
|
|
||||||
|
document.getElementById("init_image_button_draw").addEventListener("click", () => {
|
||||||
|
imageEditor.show()
|
||||||
|
})
|
||||||
|
document.getElementById("init_image_button_inpaint").addEventListener("click", () => {
|
||||||
|
imageInpainter.show()
|
||||||
|
})
|
||||||
|
|
||||||
|
img2imgUnload() // no init image when the app starts
|
||||||
|
|
||||||
|
function rgbToHex(rgb) {
|
||||||
|
function componentToHex(c) {
|
||||||
|
var hex = parseInt(c).toString(16)
|
||||||
|
return hex.length == 1 ? "0" + hex : hex
|
||||||
|
}
|
||||||
|
return "#" + componentToHex(rgb.r) + componentToHex(rgb.g) + componentToHex(rgb.b)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hexToRgb(hex) {
|
||||||
|
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||||
|
return result
|
||||||
|
? {
|
||||||
|
r: parseInt(result[1], 16),
|
||||||
|
g: parseInt(result[2], 16),
|
||||||
|
b: parseInt(result[3], 16),
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
|
||||||
|
function pixelCompare(int1, int2) {
|
||||||
|
return Math.abs(int1 - int2) < 4
|
||||||
|
}
|
||||||
|
|
||||||
|
// adapted from https://ben.akrin.com/canvas_fill/fill_04.html
|
||||||
|
// May 2023 - look at using a library instead of custom code: https://github.com/shaneosullivan/example-canvas-fill
|
||||||
|
function flood_fill(editor, the_canvas_context, x, y, color) {
|
||||||
|
pixel_stack = [{ x: x, y: y }]
|
||||||
|
pixels = the_canvas_context.getImageData(0, 0, editor.width, editor.height)
|
||||||
|
var linear_cords = (y * editor.width + x) * 4
|
||||||
|
var original_color = {
|
||||||
|
r: pixels.data[linear_cords],
|
||||||
|
g: pixels.data[linear_cords + 1],
|
||||||
|
b: pixels.data[linear_cords + 2],
|
||||||
|
a: pixels.data[linear_cords + 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
var opacity = color.a / 255
|
||||||
|
var new_color = {
|
||||||
|
r: parseInt(color.r * opacity + original_color.r * (1 - opacity)),
|
||||||
|
g: parseInt(color.g * opacity + original_color.g * (1 - opacity)),
|
||||||
|
b: parseInt(color.b * opacity + original_color.b * (1 - opacity)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
pixelCompare(new_color.r, original_color.r) &&
|
||||||
|
pixelCompare(new_color.g, original_color.g) &&
|
||||||
|
pixelCompare(new_color.b, original_color.b)
|
||||||
|
) {
|
||||||
|
return // This color is already the color we want, so do nothing
|
||||||
|
}
|
||||||
|
var max_stack_size = editor.width * editor.height
|
||||||
|
while (pixel_stack.length > 0 && pixel_stack.length < max_stack_size) {
|
||||||
|
new_pixel = pixel_stack.shift()
|
||||||
|
x = new_pixel.x
|
||||||
|
y = new_pixel.y
|
||||||
|
|
||||||
|
linear_cords = (y * editor.width + x) * 4
|
||||||
|
while (
|
||||||
|
y-- >= 0 &&
|
||||||
|
pixelCompare(pixels.data[linear_cords], original_color.r) &&
|
||||||
|
pixelCompare(pixels.data[linear_cords + 1], original_color.g) &&
|
||||||
|
pixelCompare(pixels.data[linear_cords + 2], original_color.b)
|
||||||
|
) {
|
||||||
|
linear_cords -= editor.width * 4
|
||||||
|
}
|
||||||
|
linear_cords += editor.width * 4
|
||||||
|
y++
|
||||||
|
|
||||||
|
var reached_left = false
|
||||||
|
var reached_right = false
|
||||||
|
while (
|
||||||
|
y++ < editor.height &&
|
||||||
|
pixelCompare(pixels.data[linear_cords], original_color.r) &&
|
||||||
|
pixelCompare(pixels.data[linear_cords + 1], original_color.g) &&
|
||||||
|
pixelCompare(pixels.data[linear_cords + 2], original_color.b)
|
||||||
|
) {
|
||||||
|
pixels.data[linear_cords] = new_color.r
|
||||||
|
pixels.data[linear_cords + 1] = new_color.g
|
||||||
|
pixels.data[linear_cords + 2] = new_color.b
|
||||||
|
pixels.data[linear_cords + 3] = 255
|
||||||
|
|
||||||
|
if (x > 0) {
|
||||||
|
if (
|
||||||
|
pixelCompare(pixels.data[linear_cords - 4], original_color.r) &&
|
||||||
|
pixelCompare(pixels.data[linear_cords - 4 + 1], original_color.g) &&
|
||||||
|
pixelCompare(pixels.data[linear_cords - 4 + 2], original_color.b)
|
||||||
|
) {
|
||||||
|
if (!reached_left) {
|
||||||
|
pixel_stack.push({ x: x - 1, y: y })
|
||||||
|
reached_left = true
|
||||||
|
}
|
||||||
|
} else if (reached_left) {
|
||||||
|
reached_left = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x < editor.width - 1) {
|
||||||
|
if (
|
||||||
|
pixelCompare(pixels.data[linear_cords + 4], original_color.r) &&
|
||||||
|
pixelCompare(pixels.data[linear_cords + 4 + 1], original_color.g) &&
|
||||||
|
pixelCompare(pixels.data[linear_cords + 4 + 2], original_color.b)
|
||||||
|
) {
|
||||||
|
if (!reached_right) {
|
||||||
|
pixel_stack.push({ x: x + 1, y: y })
|
||||||
|
reached_right = true
|
||||||
|
}
|
||||||
|
} else if (reached_right) {
|
||||||
|
reached_right = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
linear_cords += editor.width * 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
the_canvas_context.putImageData(pixels, 0, 0)
|
||||||
|
}
|
||||||
228
ui/media/js/image-modal.js
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
"use strict"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} ImageModalRequest
|
||||||
|
* @property {string} src
|
||||||
|
* @property {ImageModalRequest | () => ImageModalRequest | undefined} previous
|
||||||
|
* @property {ImageModalRequest | () => ImageModalRequest | undefined} next
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {(() => (string | ImageModalRequest) | string | ImageModalRequest) => {}}
|
||||||
|
*/
|
||||||
|
const imageModal = (function() {
|
||||||
|
const backElem = createElement("i", undefined, ["fa-solid", "fa-arrow-left", "tertiaryButton"])
|
||||||
|
|
||||||
|
const forwardElem = createElement("i", undefined, ["fa-solid", "fa-arrow-right", "tertiaryButton"])
|
||||||
|
|
||||||
|
const zoomElem = createElement("i", undefined, ["fa-solid", "tertiaryButton"])
|
||||||
|
|
||||||
|
const closeElem = createElement("i", undefined, ["fa-solid", "fa-xmark", "tertiaryButton"])
|
||||||
|
|
||||||
|
const menuBarElem = createElement("div", undefined, "menu-bar", [backElem, forwardElem, zoomElem, closeElem])
|
||||||
|
|
||||||
|
const imageContainer = createElement("div", undefined, "image-wrapper")
|
||||||
|
|
||||||
|
const backdrop = createElement("div", undefined, "backdrop")
|
||||||
|
|
||||||
|
const modalContainer = createElement("div", undefined, "content", [menuBarElem, imageContainer])
|
||||||
|
|
||||||
|
const modalElem = createElement("div", { id: "viewFullSizeImgModal" }, ["popup"], [backdrop, modalContainer])
|
||||||
|
document.body.appendChild(modalElem)
|
||||||
|
|
||||||
|
const setZoomLevel = (value) => {
|
||||||
|
const img = imageContainer.querySelector("img")
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
zoomElem.classList.remove("fa-magnifying-glass-plus")
|
||||||
|
zoomElem.classList.add("fa-magnifying-glass-minus")
|
||||||
|
if (img) {
|
||||||
|
img.classList.remove("natural-zoom")
|
||||||
|
|
||||||
|
let zoomLevel = typeof value === "number" ? value : img.dataset.zoomLevel
|
||||||
|
if (!zoomLevel) {
|
||||||
|
zoomLevel = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
img.dataset.zoomLevel = zoomLevel
|
||||||
|
img.width = img.naturalWidth * (+zoomLevel / 100)
|
||||||
|
img.height = img.naturalHeight * (+zoomLevel / 100)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
zoomElem.classList.remove("fa-magnifying-glass-minus")
|
||||||
|
zoomElem.classList.add("fa-magnifying-glass-plus")
|
||||||
|
if (img) {
|
||||||
|
img.classList.add("natural-zoom")
|
||||||
|
img.removeAttribute("width")
|
||||||
|
img.removeAttribute("height")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zoomElem.addEventListener("click", () =>
|
||||||
|
setZoomLevel(imageContainer.querySelector("img")?.classList?.contains("natural-zoom"))
|
||||||
|
)
|
||||||
|
|
||||||
|
const initialState = () => ({
|
||||||
|
previous: undefined,
|
||||||
|
next: undefined,
|
||||||
|
|
||||||
|
start: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
scroll: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const state = initialState()
|
||||||
|
|
||||||
|
// Allow grabbing the image to scroll
|
||||||
|
const stopGrabbing = (e) => {
|
||||||
|
if(imageContainer.classList.contains("grabbing")) {
|
||||||
|
imageContainer.classList.remove("grabbing")
|
||||||
|
e?.preventDefault()
|
||||||
|
console.log(`stopGrabbing()`, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addImageGrabbing = (image) => {
|
||||||
|
image?.addEventListener('mousedown', (e) => {
|
||||||
|
if (!image.classList.contains("natural-zoom")) {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.stopImmediatePropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
imageContainer.classList.add("grabbing")
|
||||||
|
state.start.x = e.pageX - imageContainer.offsetLeft
|
||||||
|
state.scroll.x = imageContainer.scrollLeft
|
||||||
|
state.start.y = e.pageY - imageContainer.offsetTop
|
||||||
|
state.scroll.y = imageContainer.scrollTop
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
image?.addEventListener('mouseup', stopGrabbing)
|
||||||
|
image?.addEventListener('mouseleave', stopGrabbing)
|
||||||
|
image?.addEventListener('mousemove', (e) => {
|
||||||
|
if(imageContainer.classList.contains("grabbing")) {
|
||||||
|
e.stopPropagation()
|
||||||
|
e.stopImmediatePropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
// Might need to increase this multiplier based on the image size to window size ratio
|
||||||
|
// The default 1:1 is pretty slow
|
||||||
|
const multiplier = 1.0
|
||||||
|
|
||||||
|
const deltaX = e.pageX - imageContainer.offsetLeft - state.start.x
|
||||||
|
imageContainer.scrollLeft = state.scroll.x - (deltaX * multiplier)
|
||||||
|
const deltaY = e.pageY - imageContainer.offsetTop - state.start.y
|
||||||
|
imageContainer.scrollTop = state.scroll.y - (deltaY * multiplier)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
imageContainer.innerHTML = ""
|
||||||
|
|
||||||
|
Object.entries(initialState()).forEach(([key, value]) => state[key] = value)
|
||||||
|
|
||||||
|
stopGrabbing()
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
clear()
|
||||||
|
modalElem.classList.remove("active")
|
||||||
|
document.body.style.overflow = "initial"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {() => (string | ImageModalRequest) | string | ImageModalRequest} optionsFactory
|
||||||
|
*/
|
||||||
|
function init(optionsFactory) {
|
||||||
|
if (!optionsFactory) {
|
||||||
|
close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clear()
|
||||||
|
|
||||||
|
const options = typeof optionsFactory === "function" ? optionsFactory() : optionsFactory
|
||||||
|
const src = typeof options === "string" ? options : options.src
|
||||||
|
|
||||||
|
const imgElem = createElement("img", { src }, "natural-zoom")
|
||||||
|
addImageGrabbing(imgElem)
|
||||||
|
imageContainer.appendChild(imgElem)
|
||||||
|
modalElem.classList.add("active")
|
||||||
|
document.body.style.overflow = "hidden"
|
||||||
|
setZoomLevel(false)
|
||||||
|
|
||||||
|
if (typeof options === "object" && options.previous) {
|
||||||
|
state.previous = options.previous
|
||||||
|
backElem.style.display = "unset"
|
||||||
|
} else {
|
||||||
|
backElem.style.display = "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof options === "object" && options.next) {
|
||||||
|
state.next = options.next
|
||||||
|
forwardElem.style.display = "unset"
|
||||||
|
} else {
|
||||||
|
forwardElem.style.display = "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const back = () => {
|
||||||
|
if (state.previous) {
|
||||||
|
init(state.previous)
|
||||||
|
} else {
|
||||||
|
backElem.style.display = "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const forward = () => {
|
||||||
|
if (state.next) {
|
||||||
|
init(state.next)
|
||||||
|
} else {
|
||||||
|
forwardElem.style.display = "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("keydown", (e) => {
|
||||||
|
if (modalElem.classList.contains("active")) {
|
||||||
|
switch (e.key) {
|
||||||
|
case "Escape":
|
||||||
|
close()
|
||||||
|
break
|
||||||
|
case "ArrowLeft":
|
||||||
|
back()
|
||||||
|
break
|
||||||
|
case "ArrowRight":
|
||||||
|
forward()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
window.addEventListener("click", (e) => {
|
||||||
|
if (modalElem.classList.contains("active")) {
|
||||||
|
if (e.target === backdrop || e.target === closeElem) {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
e.stopPropagation()
|
||||||
|
e.stopImmediatePropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
backElem.addEventListener("click", back)
|
||||||
|
|
||||||
|
forwardElem.addEventListener("click", forward)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {() => (string | ImageModalRequest) | string | ImageModalRequest} optionsFactory
|
||||||
|
*/
|
||||||
|
return (optionsFactory) => init(optionsFactory)
|
||||||
|
})()
|
||||||
@@ -1,308 +1,394 @@
|
|||||||
let activeTags = []
|
let activeTags = []
|
||||||
let modifiers = []
|
let modifiers = []
|
||||||
let customModifiersGroupElement = undefined
|
let customModifiersGroupElement = undefined
|
||||||
|
let customModifiersInitialContent = ""
|
||||||
|
let modifierPanelFreezed = false
|
||||||
|
|
||||||
let editorModifierEntries = document.querySelector('#editor-modifiers-entries')
|
let modifiersMainContainer = document.querySelector("#editor-modifiers")
|
||||||
let editorModifierTagsList = document.querySelector('#editor-inputs-tags-list')
|
let modifierDropdown = document.querySelector("#image-modifier-dropdown")
|
||||||
let editorTagsContainer = document.querySelector('#editor-inputs-tags-container')
|
let editorModifiersContainer = document.querySelector("#editor-modifiers")
|
||||||
let modifierCardSizeSlider = document.querySelector('#modifier-card-size-slider')
|
let editorModifierEntries = document.querySelector("#editor-modifiers-entries")
|
||||||
let previewImageField = document.querySelector('#preview-image')
|
let editorModifierTagsList = document.querySelector("#editor-inputs-tags-list")
|
||||||
let modifierSettingsBtn = document.querySelector('#modifier-settings-btn')
|
let editorTagsContainer = document.querySelector("#editor-inputs-tags-container")
|
||||||
let modifierSettingsOverlay = document.querySelector('#modifier-settings-config')
|
let modifierCardSizeSlider = document.querySelector("#modifier-card-size-slider")
|
||||||
let customModifiersTextBox = document.querySelector('#custom-modifiers-input')
|
let previewImageField = document.querySelector("#preview-image")
|
||||||
let customModifierEntriesToolbar = document.querySelector('#editor-modifiers-entries-toolbar')
|
let modifierSettingsBtn = document.querySelector("#modifier-settings-btn")
|
||||||
|
let modifiersContainerSizeBtn = document.querySelector("#modifiers-container-size-btn")
|
||||||
|
let modifiersCloseBtn = document.querySelector("#modifiers-close-button")
|
||||||
|
let modifiersCollapsiblesBtn = document.querySelector("#modifiers-action-collapsibles-btn")
|
||||||
|
let modifierSettingsDialog = document.querySelector("#modifier-settings-config")
|
||||||
|
let customModifiersTextBox = document.querySelector("#custom-modifiers-input")
|
||||||
|
let customModifierEntriesToolbar = document.querySelector("#editor-modifiers-subheader")
|
||||||
|
let modifierSettingsCloseBtn = document.querySelector("#modifier-settings-close-button")
|
||||||
|
|
||||||
const modifierThumbnailPath = 'media/modifier-thumbnails'
|
const modifierThumbnailPath = "media/modifier-thumbnails"
|
||||||
const activeCardClass = 'modifier-card-active'
|
const activeCardClass = "modifier-card-active"
|
||||||
const CUSTOM_MODIFIERS_KEY = "customModifiers"
|
const CUSTOM_MODIFIERS_KEY = "customModifiers"
|
||||||
|
|
||||||
function createModifierCard(name, previews) {
|
function createModifierCard(name, previews, removeBy) {
|
||||||
const modifierCard = document.createElement('div')
|
let cardPreviewImageType = previewImageField.value
|
||||||
modifierCard.className = 'modifier-card'
|
|
||||||
|
const modifierCard = document.createElement("div")
|
||||||
|
modifierCard.className = "modifier-card"
|
||||||
modifierCard.innerHTML = `
|
modifierCard.innerHTML = `
|
||||||
<div class="modifier-card-overlay"></div>
|
<div class="modifier-card-overlay"></div>
|
||||||
<div class="modifier-card-image-container">
|
<div class="modifier-card-image-container">
|
||||||
<div class="modifier-card-image-overlay">+</div>
|
<div class="modifier-card-image-overlay">+</div>
|
||||||
<p class="modifier-card-error-label"></p>
|
<p class="modifier-card-error-label">No Image</p>
|
||||||
<img onerror="this.remove()" alt="Modifier Image" class="modifier-card-image">
|
<img onerror="this.remove()" alt="Modifier Image" class="modifier-card-image">
|
||||||
</div>
|
</div>
|
||||||
<div class="modifier-card-container">
|
<div class="modifier-card-container">
|
||||||
<div class="modifier-card-label"><p></p></div>
|
<div class="modifier-card-label">
|
||||||
|
<span class="long-label hidden"></span>
|
||||||
|
<p class="regular-label"></p>
|
||||||
|
</div>
|
||||||
</div>`
|
</div>`
|
||||||
|
|
||||||
const image = modifierCard.querySelector('.modifier-card-image')
|
const image = modifierCard.querySelector(".modifier-card-image")
|
||||||
const errorText = modifierCard.querySelector('.modifier-card-error-label')
|
const longLabel = modifierCard.querySelector(".modifier-card-label span.long-label")
|
||||||
const label = modifierCard.querySelector('.modifier-card-label')
|
const regularLabel = modifierCard.querySelector(".modifier-card-label p.regular-label")
|
||||||
|
|
||||||
errorText.innerText = 'No Image'
|
if (typeof previews == "object") {
|
||||||
|
image.src = previews[cardPreviewImageType == "portrait" ? 0 : 1] // 0 index is portrait, 1 landscape
|
||||||
if (typeof previews == 'object') {
|
image.setAttribute("preview-type", cardPreviewImageType)
|
||||||
image.src = previews[0]; // portrait
|
|
||||||
image.setAttribute('preview-type', 'portrait')
|
|
||||||
} else {
|
} else {
|
||||||
image.remove()
|
image.remove()
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxLabelLength = 30
|
const maxLabelLength = 30
|
||||||
const nameWithoutBy = name.replace('by ', '')
|
const cardLabel = removeBy ? name.replace("by ", "") : name
|
||||||
|
|
||||||
if(nameWithoutBy.length <= maxLabelLength) {
|
function getFormattedLabel(length) {
|
||||||
label.querySelector('p').innerText = nameWithoutBy
|
if (cardLabel?.length <= length) {
|
||||||
} else {
|
return cardLabel
|
||||||
const tooltipText = document.createElement('span')
|
} else {
|
||||||
tooltipText.className = 'tooltip-text'
|
return cardLabel.substring(0, length) + "..."
|
||||||
tooltipText.innerText = name
|
}
|
||||||
|
}
|
||||||
|
|
||||||
label.classList.add('tooltip')
|
modifierCard.dataset.fullName = name // preserve the full name
|
||||||
label.appendChild(tooltipText)
|
regularLabel.dataset.fullName = name // preserve the full name, legacy support for older plugins
|
||||||
|
|
||||||
label.querySelector('p').innerText = nameWithoutBy.substring(0, maxLabelLength) + '...'
|
longLabel.innerText = getFormattedLabel(maxLabelLength * 2)
|
||||||
|
regularLabel.innerText = getFormattedLabel(maxLabelLength)
|
||||||
|
|
||||||
|
if (cardLabel.length > maxLabelLength) {
|
||||||
|
modifierCard.classList.add("support-long-label")
|
||||||
|
|
||||||
|
if (cardLabel.length > maxLabelLength * 2) {
|
||||||
|
modifierCard.title = `"${name}"`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return modifierCard
|
return modifierCard
|
||||||
}
|
}
|
||||||
|
|
||||||
function createModifierGroup(modifierGroup, initiallyExpanded) {
|
function createModifierGroup(modifierGroup, isInitiallyOpen, removeBy) {
|
||||||
const title = modifierGroup.category
|
const title = modifierGroup.category
|
||||||
const modifiers = modifierGroup.modifiers
|
const modifiers = modifierGroup.modifiers
|
||||||
|
|
||||||
const titleEl = document.createElement('h5')
|
const titleEl = document.createElement("h5")
|
||||||
titleEl.className = 'collapsible'
|
titleEl.className = "collapsible"
|
||||||
titleEl.innerText = title
|
titleEl.innerText = title
|
||||||
|
|
||||||
const modifiersEl = document.createElement('div')
|
const modifiersEl = document.createElement("div")
|
||||||
modifiersEl.classList.add('collapsible-content', 'editor-modifiers-leaf')
|
modifiersEl.classList.add("collapsible-content", "editor-modifiers-leaf")
|
||||||
|
|
||||||
if (initiallyExpanded === true) {
|
if (isInitiallyOpen === true) {
|
||||||
titleEl.className += ' active'
|
titleEl.classList.add("active")
|
||||||
}
|
}
|
||||||
|
|
||||||
modifiers.forEach(modObj => {
|
modifiers.forEach((modObj) => {
|
||||||
const modifierName = modObj.modifier
|
const modifierName = modObj.modifier
|
||||||
const modifierPreviews = modObj?.previews?.map(preview => `${modifierThumbnailPath}/${preview.path}`)
|
const modifierPreviews = modObj?.previews?.map(
|
||||||
|
(preview) =>
|
||||||
|
`${IMAGE_REGEX.test(preview.image) ? preview.image : modifierThumbnailPath + "/" + preview.path}`
|
||||||
|
)
|
||||||
|
|
||||||
const modifierCard = createModifierCard(modifierName, modifierPreviews)
|
const modifierCard = createModifierCard(modifierName, modifierPreviews, removeBy)
|
||||||
|
|
||||||
if(typeof modifierCard == 'object') {
|
if (typeof modifierCard == "object") {
|
||||||
modifiersEl.appendChild(modifierCard)
|
modifiersEl.appendChild(modifierCard)
|
||||||
|
const trimmedName = trimModifiers(modifierName)
|
||||||
|
|
||||||
modifierCard.addEventListener('click', () => {
|
modifierCard.addEventListener("click", () => {
|
||||||
if (activeTags.map(x => x.name).includes(modifierName)) {
|
if (activeTags.map((x) => trimModifiers(x.name)).includes(trimmedName)) {
|
||||||
// remove modifier from active array
|
// remove modifier from active array
|
||||||
activeTags = activeTags.filter(x => x.name != modifierName)
|
activeTags = activeTags.filter((x) => trimModifiers(x.name) != trimmedName)
|
||||||
modifierCard.classList.remove(activeCardClass)
|
toggleCardState(trimmedName, false)
|
||||||
|
|
||||||
modifierCard.querySelector('.modifier-card-image-overlay').innerText = '+'
|
|
||||||
} else {
|
} else {
|
||||||
// add modifier to active array
|
// add modifier to active array
|
||||||
activeTags.push({
|
activeTags.push({
|
||||||
'name': modifierName,
|
name: modifierName,
|
||||||
'element': modifierCard.cloneNode(true),
|
element: modifierCard.cloneNode(true),
|
||||||
'originElement': modifierCard,
|
originElement: modifierCard,
|
||||||
'previews': modifierPreviews
|
previews: modifierPreviews,
|
||||||
})
|
})
|
||||||
|
toggleCardState(trimmedName, true)
|
||||||
modifierCard.classList.add(activeCardClass)
|
|
||||||
|
|
||||||
modifierCard.querySelector('.modifier-card-image-overlay').innerText = '-'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshTagsList()
|
refreshTagsList()
|
||||||
|
document.dispatchEvent(new Event("refreshImageModifiers"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let brk = document.createElement('br')
|
let brk = document.createElement("br")
|
||||||
brk.style.clear = 'both'
|
brk.style.clear = "both"
|
||||||
modifiersEl.appendChild(brk)
|
modifiersEl.appendChild(brk)
|
||||||
|
|
||||||
let e = document.createElement('div')
|
let e = document.createElement("div")
|
||||||
|
e.className = "modifier-category"
|
||||||
e.appendChild(titleEl)
|
e.appendChild(titleEl)
|
||||||
e.appendChild(modifiersEl)
|
e.appendChild(modifiersEl)
|
||||||
|
|
||||||
editorModifierEntries.insertBefore(e, customModifierEntriesToolbar.nextSibling)
|
editorModifierEntries.prepend(e)
|
||||||
|
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trimModifiers(tag) {
|
||||||
|
// Remove trailing '-' and/or '+'
|
||||||
|
tag = tag.replace(/[-+]+$/, "")
|
||||||
|
// Remove parentheses at beginning and end
|
||||||
|
return tag.replace(/^[(]+|[\s)]+$/g, "")
|
||||||
|
}
|
||||||
|
|
||||||
async function loadModifiers() {
|
async function loadModifiers() {
|
||||||
try {
|
try {
|
||||||
let res = await fetch('/get/modifiers')
|
let res = await fetch("/get/modifiers")
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
res = await res.json()
|
res = await res.json()
|
||||||
|
|
||||||
modifiers = res; // update global variable
|
modifiers = res // update global variable
|
||||||
|
|
||||||
res.reverse()
|
res.reverse()
|
||||||
|
|
||||||
res.forEach((modifierGroup, idx) => {
|
res.forEach((modifierGroup, idx) => {
|
||||||
createModifierGroup(modifierGroup, idx === res.length - 1)
|
const isInitiallyOpen = false // idx === res.length - 1
|
||||||
|
const removeBy = modifierGroup === "Artist" ? true : false // only remove "By " for artists
|
||||||
|
|
||||||
|
createModifierGroup(modifierGroup, isInitiallyOpen, removeBy)
|
||||||
})
|
})
|
||||||
|
|
||||||
createCollapsibles(editorModifierEntries)
|
createCollapsibles(editorModifierEntries)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('error fetching modifiers', e)
|
console.error("error fetching modifiers", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
loadCustomModifiers()
|
loadCustomModifiers()
|
||||||
|
resizeModifierCards(modifierCardSizeSlider.value)
|
||||||
|
document.dispatchEvent(new Event("loadImageModifiers"))
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshModifiersState(newTags) {
|
function refreshModifiersState(newTags, inactiveTags) {
|
||||||
// clear existing modifiers
|
// clear existing modifiers
|
||||||
document.querySelector('#editor-modifiers').querySelectorAll('.modifier-card').forEach(modifierCard => {
|
document
|
||||||
const modifierName = modifierCard.querySelector('.modifier-card-label').innerText
|
.querySelector("#editor-modifiers")
|
||||||
if (activeTags.map(x => x.name).includes(modifierName)) {
|
.querySelectorAll(".modifier-card")
|
||||||
modifierCard.classList.remove(activeCardClass)
|
.forEach((modifierCard) => {
|
||||||
modifierCard.querySelector('.modifier-card-image-overlay').innerText = '+'
|
const modifierName = modifierCard.dataset.fullName // pick the full modifier name
|
||||||
}
|
if (activeTags.map((x) => x.name).includes(modifierName)) {
|
||||||
})
|
modifierCard.classList.remove(activeCardClass)
|
||||||
|
modifierCard.querySelector(".modifier-card-image-overlay").innerText = "+"
|
||||||
|
}
|
||||||
|
})
|
||||||
activeTags = []
|
activeTags = []
|
||||||
|
|
||||||
// set new modifiers
|
// set new modifiers
|
||||||
newTags.forEach(tag => {
|
newTags.forEach((tag) => {
|
||||||
let found = false
|
let found = false
|
||||||
document.querySelector('#editor-modifiers').querySelectorAll('.modifier-card').forEach(modifierCard => {
|
document
|
||||||
const modifierName = modifierCard.querySelector('.modifier-card-label').innerText
|
.querySelector("#editor-modifiers")
|
||||||
if (tag == modifierName) {
|
.querySelectorAll(".modifier-card")
|
||||||
// add modifier to active array
|
.forEach((modifierCard) => {
|
||||||
if (!activeTags.map(x => x.name).includes(tag)) { // only add each tag once even if several custom modifier cards share the same tag
|
const modifierName = modifierCard.dataset.fullName
|
||||||
activeTags.push({
|
const shortModifierName = modifierCard.querySelector(".modifier-card-label p").innerText
|
||||||
'name': modifierName,
|
|
||||||
'element': modifierCard.cloneNode(true),
|
|
||||||
'originElement': modifierCard
|
|
||||||
})
|
|
||||||
}
|
|
||||||
modifierCard.classList.add(activeCardClass)
|
|
||||||
modifierCard.querySelector('.modifier-card-image-overlay').innerText = '-'
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (found == false) { // custom tag went missing, create one here
|
|
||||||
let modifierCard = createModifierCard(tag, undefined) // create a modifier card for the missing tag, no image
|
|
||||||
|
|
||||||
modifierCard.addEventListener('click', () => {
|
if (trimModifiers(tag) == trimModifiers(modifierName)) {
|
||||||
if (activeTags.map(x => x.name).includes(tag)) {
|
// add modifier to active array
|
||||||
|
if (!activeTags.map((x) => x.name).includes(tag)) {
|
||||||
|
// only add each tag once even if several custom modifier cards share the same tag
|
||||||
|
const imageModifierCard = modifierCard.cloneNode(true)
|
||||||
|
imageModifierCard.querySelector(".modifier-card-label p").innerText = tag.replace(
|
||||||
|
modifierName,
|
||||||
|
shortModifierName
|
||||||
|
)
|
||||||
|
activeTags.push({
|
||||||
|
name: tag,
|
||||||
|
element: imageModifierCard,
|
||||||
|
originElement: modifierCard,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
modifierCard.classList.add(activeCardClass)
|
||||||
|
modifierCard.querySelector(".modifier-card-image-overlay").innerText = "-"
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (found == false) {
|
||||||
|
// custom tag went missing, create one here
|
||||||
|
let modifierCard = createModifierCard(tag, undefined, false) // create a modifier card for the missing tag, no image
|
||||||
|
|
||||||
|
modifierCard.addEventListener("click", () => {
|
||||||
|
if (activeTags.map((x) => x.name).includes(tag)) {
|
||||||
// remove modifier from active array
|
// remove modifier from active array
|
||||||
activeTags = activeTags.filter(x => x.name != tag)
|
activeTags = activeTags.filter((x) => x.name != tag)
|
||||||
modifierCard.classList.remove(activeCardClass)
|
modifierCard.classList.remove(activeCardClass)
|
||||||
|
|
||||||
modifierCard.querySelector('.modifier-card-image-overlay').innerText = '+'
|
modifierCard.querySelector(".modifier-card-image-overlay").innerText = "+"
|
||||||
}
|
}
|
||||||
refreshTagsList()
|
refreshTagsList()
|
||||||
})
|
})
|
||||||
|
|
||||||
activeTags.push({
|
activeTags.push({
|
||||||
'name': tag,
|
name: tag,
|
||||||
'element': modifierCard,
|
element: modifierCard,
|
||||||
'originElement': undefined // no origin element for missing tags
|
originElement: undefined, // no origin element for missing tags
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
refreshTagsList()
|
refreshTagsList(inactiveTags)
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshTagsList() {
|
function refreshInactiveTags(inactiveTags) {
|
||||||
editorModifierTagsList.innerHTML = ''
|
// update inactive tags
|
||||||
|
if (inactiveTags !== undefined && inactiveTags.length > 0) {
|
||||||
|
activeTags.forEach((tag) => {
|
||||||
|
if (inactiveTags.find((element) => element === tag.name) !== undefined) {
|
||||||
|
tag.inactive = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// update cards
|
||||||
|
let overlays = editorModifierTagsList.querySelectorAll(".modifier-card-overlay")
|
||||||
|
overlays.forEach((i) => {
|
||||||
|
let modifierName = i.parentElement.dataset.fullName
|
||||||
|
|
||||||
|
if (inactiveTags?.find((element) => trimModifiers(element) === modifierName) !== undefined) {
|
||||||
|
i.parentElement.classList.add("modifier-toggle-inactive")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshTagsList(inactiveTags) {
|
||||||
|
editorModifierTagsList.innerHTML = ""
|
||||||
|
|
||||||
if (activeTags.length == 0) {
|
if (activeTags.length == 0) {
|
||||||
editorTagsContainer.style.display = 'none'
|
editorTagsContainer.style.display = "none"
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
editorTagsContainer.style.display = 'block'
|
editorTagsContainer.style.display = "block"
|
||||||
|
}
|
||||||
|
|
||||||
|
if(activeTags.length > 15) {
|
||||||
|
editorModifierTagsList.style["overflow-y"] = "auto"
|
||||||
|
} else {
|
||||||
|
editorModifierTagsList.style["overflow-y"] = "unset"
|
||||||
}
|
}
|
||||||
|
|
||||||
activeTags.forEach((tag, index) => {
|
activeTags.forEach((tag, index) => {
|
||||||
tag.element.querySelector('.modifier-card-image-overlay').innerText = '-'
|
tag.element.querySelector(".modifier-card-image-overlay").innerText = "-"
|
||||||
tag.element.classList.add('modifier-card-tiny')
|
tag.element.classList.add("modifier-card-tiny")
|
||||||
|
|
||||||
editorModifierTagsList.appendChild(tag.element)
|
editorModifierTagsList.appendChild(tag.element)
|
||||||
|
|
||||||
tag.element.addEventListener('click', () => {
|
tag.element.addEventListener("click", () => {
|
||||||
let idx = activeTags.indexOf(tag)
|
let idx = activeTags.findIndex((o) => {
|
||||||
|
return o.name === tag.name
|
||||||
|
})
|
||||||
|
|
||||||
if (idx !== -1 && activeTags[idx].originElement !== undefined) {
|
if (idx !== -1) {
|
||||||
activeTags[idx].originElement.classList.remove(activeCardClass)
|
toggleCardState(activeTags[idx].name, false)
|
||||||
activeTags[idx].originElement.querySelector('.modifier-card-image-overlay').innerText = '+'
|
|
||||||
|
|
||||||
activeTags.splice(idx, 1)
|
activeTags.splice(idx, 1)
|
||||||
refreshTagsList()
|
refreshTagsList()
|
||||||
}
|
}
|
||||||
|
document.dispatchEvent(new Event("refreshImageModifiers"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
let brk = document.createElement('br')
|
let brk = document.createElement("br")
|
||||||
brk.style.clear = 'both'
|
brk.style.clear = "both"
|
||||||
|
|
||||||
editorModifierTagsList.appendChild(brk)
|
editorModifierTagsList.appendChild(brk)
|
||||||
|
|
||||||
|
refreshInactiveTags(inactiveTags)
|
||||||
|
|
||||||
|
document.dispatchEvent(new Event("refreshImageModifiers")) // notify plugins that the image tags have been refreshed
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCardState(modifierName, makeActive) {
|
||||||
|
const cards = [...document.querySelectorAll("#editor-modifiers .modifier-card")]
|
||||||
|
.filter(cardElem => trimModifiers(cardElem.dataset.fullName) == trimModifiers(modifierName))
|
||||||
|
|
||||||
|
const cardExists = typeof cards == "object" && cards?.length > 0
|
||||||
|
|
||||||
|
if (cardExists) {
|
||||||
|
const card = cards[0]
|
||||||
|
|
||||||
|
if (makeActive) {
|
||||||
|
card.classList.add(activeCardClass)
|
||||||
|
card.querySelector(".modifier-card-image-overlay").innerText = "-"
|
||||||
|
} else {
|
||||||
|
card.classList.remove(activeCardClass)
|
||||||
|
card.querySelector(".modifier-card-image-overlay").innerText = "+"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePreviewImages(val) {
|
function changePreviewImages(val) {
|
||||||
const previewImages = document.querySelectorAll('.modifier-card-image-container img')
|
const previewImages = document.querySelectorAll(".modifier-card-image-container img")
|
||||||
|
|
||||||
let previewArr = []
|
const previewArr = modifiers.flatMap((x) => x.modifiers.map((m) => m.previews))
|
||||||
|
.map((x) => x.reduce((obj, preview) => {
|
||||||
modifiers.map(x => x.modifiers).forEach(x => previewArr.push(...x.map(m => m.previews)))
|
|
||||||
|
|
||||||
previewArr = previewArr.map(x => {
|
|
||||||
let obj = {}
|
|
||||||
|
|
||||||
x.forEach(preview => {
|
|
||||||
obj[preview.name] = preview.path
|
obj[preview.name] = preview.path
|
||||||
})
|
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
})
|
}, {}))
|
||||||
|
|
||||||
previewImages.forEach(previewImage => {
|
previewImages.forEach((previewImage) => {
|
||||||
const currentPreviewType = previewImage.getAttribute('preview-type')
|
const currentPreviewType = previewImage.getAttribute("preview-type")
|
||||||
const relativePreviewPath = previewImage.src.split(modifierThumbnailPath + '/').pop()
|
const relativePreviewPath = previewImage.src.split(modifierThumbnailPath + "/").pop()
|
||||||
|
|
||||||
const previews = previewArr.find(preview => relativePreviewPath == preview[currentPreviewType])
|
const previews = previewArr.find((preview) => relativePreviewPath == preview[currentPreviewType])
|
||||||
|
|
||||||
if(typeof previews == 'object') {
|
if (typeof previews == "object") {
|
||||||
let preview = null
|
let preview = null
|
||||||
|
|
||||||
if (val == 'portrait') {
|
if (val == "portrait") {
|
||||||
preview = previews.portrait
|
preview = previews.portrait
|
||||||
}
|
} else if (val == "landscape") {
|
||||||
else if (val == 'landscape') {
|
|
||||||
preview = previews.landscape
|
preview = previews.landscape
|
||||||
}
|
}
|
||||||
|
|
||||||
if(preview != null) {
|
if (preview) {
|
||||||
previewImage.src = `${modifierThumbnailPath}/${preview}`
|
previewImage.src = `${modifierThumbnailPath}/${preview}`
|
||||||
previewImage.setAttribute('preview-type', val)
|
previewImage.setAttribute("preview-type", val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function resizeModifierCards(val) {
|
function resizeModifierCards(val) {
|
||||||
const cardSizePrefix = 'modifier-card-size_'
|
const cardSizePrefix = "modifier-card-size_"
|
||||||
const modifierCardClass = 'modifier-card'
|
const modifierCardClass = "modifier-card"
|
||||||
|
|
||||||
const modifierCards = document.querySelectorAll(`.${modifierCardClass}`)
|
const modifierCards = document.querySelectorAll(`.${modifierCardClass}`)
|
||||||
const cardSize = n => `${cardSizePrefix}${n}`
|
const cardSize = (n) => `${cardSizePrefix}${n}`
|
||||||
|
|
||||||
modifierCards.forEach(card => {
|
modifierCards.forEach((card) => {
|
||||||
// remove existing size classes
|
// remove existing size classes
|
||||||
const classes = card.className.split(' ').filter(c => !c.startsWith(cardSizePrefix))
|
const classes = card.className.split(" ").filter((c) => !c.startsWith(cardSizePrefix))
|
||||||
card.className = classes.join(' ').trim()
|
card.className = classes.join(" ").trim()
|
||||||
|
|
||||||
if(val != 0) {
|
if (val != 0) {
|
||||||
card.classList.add(cardSize(val))
|
card.classList.add(cardSize(val))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
modifierCardSizeSlider.onchange = () => resizeModifierCards(modifierCardSizeSlider.value)
|
|
||||||
previewImageField.onchange = () => changePreviewImages(previewImageField.value)
|
|
||||||
|
|
||||||
modifierSettingsBtn.addEventListener('click', function(e) {
|
|
||||||
modifierSettingsOverlay.classList.add("active")
|
|
||||||
e.stopPropagation()
|
|
||||||
})
|
|
||||||
|
|
||||||
function saveCustomModifiers() {
|
function saveCustomModifiers() {
|
||||||
localStorage.setItem(CUSTOM_MODIFIERS_KEY, customModifiersTextBox.value.trim())
|
localStorage.setItem(CUSTOM_MODIFIERS_KEY, customModifiersTextBox.value.trim())
|
||||||
|
|
||||||
@@ -310,31 +396,159 @@ function saveCustomModifiers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadCustomModifiers() {
|
function loadCustomModifiers() {
|
||||||
let customModifiers = localStorage.getItem(CUSTOM_MODIFIERS_KEY, '')
|
PLUGINS["MODIFIERS_LOAD"].forEach((fn) => fn.loader.call())
|
||||||
customModifiersTextBox.value = customModifiers
|
}
|
||||||
|
|
||||||
if (customModifiersGroupElement !== undefined) {
|
function showModifierContainer() {
|
||||||
customModifiersGroupElement.remove()
|
document.addEventListener("mousedown", checkIfClickedOutsideDropdownElem)
|
||||||
}
|
|
||||||
|
|
||||||
if (customModifiers && customModifiers.trim() !== '') {
|
modifierDropdown.dataset.active = true
|
||||||
customModifiers = customModifiers.split('\n')
|
editorModifiersContainer.classList.add("active")
|
||||||
customModifiers = customModifiers.filter(m => m.trim() !== '')
|
}
|
||||||
customModifiers = customModifiers.map(function(m) {
|
|
||||||
return {
|
|
||||||
"modifier": m
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
let customGroup = {
|
function hideModifierContainer() {
|
||||||
'category': 'Custom Modifiers',
|
document.removeEventListener("click", checkIfClickedOutsideDropdownElem)
|
||||||
'modifiers': customModifiers
|
|
||||||
}
|
|
||||||
|
|
||||||
customModifiersGroupElement = createModifierGroup(customGroup, true)
|
modifierDropdown.dataset.active = false
|
||||||
|
editorModifiersContainer.classList.remove("active")
|
||||||
|
}
|
||||||
|
|
||||||
createCollapsibles(customModifiersGroupElement)
|
function checkIfClickedOutsideDropdownElem(e) {
|
||||||
|
const clickedElement = e.target
|
||||||
|
|
||||||
|
const clickedInsideSpecificElems = [modifierDropdown, editorModifiersContainer, modifierSettingsDialog].some((div) =>
|
||||||
|
div && (div.contains(clickedElement) || div === clickedElement))
|
||||||
|
|
||||||
|
if (!clickedInsideSpecificElems && !modifierPanelFreezed) {
|
||||||
|
hideModifierContainer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customModifiersTextBox.addEventListener('change', saveCustomModifiers)
|
function collapseAllModifierCategory() {
|
||||||
|
collapseAll(".modifier-category .collapsible")
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandAllModifierCategory() {
|
||||||
|
expandAll(".modifier-category .collapsible")
|
||||||
|
}
|
||||||
|
|
||||||
|
customModifiersTextBox.addEventListener("change", saveCustomModifiers)
|
||||||
|
|
||||||
|
modifierCardSizeSlider.onchange = () => resizeModifierCards(modifierCardSizeSlider.value)
|
||||||
|
previewImageField.onchange = () => changePreviewImages(previewImageField.value)
|
||||||
|
|
||||||
|
modifierSettingsDialog.addEventListener("keydown", function(e) {
|
||||||
|
switch (e.key) {
|
||||||
|
case "Escape": // Escape to cancel
|
||||||
|
customModifiersTextBox.value = customModifiersInitialContent // undo the changes
|
||||||
|
modifierSettingsDialog.close()
|
||||||
|
e.stopPropagation()
|
||||||
|
break
|
||||||
|
case "Enter":
|
||||||
|
if (e.ctrlKey) {
|
||||||
|
// Ctrl+Enter to confirm
|
||||||
|
modifierSettingsDialog.close()
|
||||||
|
e.stopPropagation()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
modifierDropdown.addEventListener("click", e => {
|
||||||
|
const targetElem = e.target
|
||||||
|
const isDropdownActive = targetElem.dataset.active == "true" ? true : false
|
||||||
|
|
||||||
|
if (!isDropdownActive)
|
||||||
|
showModifierContainer()
|
||||||
|
else
|
||||||
|
hideModifierContainer()
|
||||||
|
})
|
||||||
|
|
||||||
|
let collapsiblesBtnState = false
|
||||||
|
|
||||||
|
modifiersCollapsiblesBtn.addEventListener("click", (e) => {
|
||||||
|
const btnElem = modifiersCollapsiblesBtn
|
||||||
|
|
||||||
|
const collapseText = "Collapse Categories"
|
||||||
|
const expandText = "Expand Categories"
|
||||||
|
|
||||||
|
const collapseIconClasses = ["fa-solid", "fa-square-minus"]
|
||||||
|
const expandIconClasses = ["fa-solid", "fa-square-plus"]
|
||||||
|
|
||||||
|
const iconElem = btnElem.querySelector(".modifiers-action-icon")
|
||||||
|
const textElem = btnElem.querySelector(".modifiers-action-text")
|
||||||
|
|
||||||
|
if (collapsiblesBtnState) {
|
||||||
|
collapseAllModifierCategory()
|
||||||
|
|
||||||
|
collapsiblesBtnState = false
|
||||||
|
|
||||||
|
collapseIconClasses.forEach((c) => iconElem.classList.remove(c))
|
||||||
|
expandIconClasses.forEach((c) => iconElem.classList.add(c))
|
||||||
|
|
||||||
|
textElem.innerText = expandText
|
||||||
|
} else {
|
||||||
|
expandAllModifierCategory()
|
||||||
|
|
||||||
|
collapsiblesBtnState = true
|
||||||
|
|
||||||
|
expandIconClasses.forEach((c) => iconElem.classList.remove(c))
|
||||||
|
collapseIconClasses.forEach((c) => iconElem.classList.add(c))
|
||||||
|
|
||||||
|
textElem.innerText = collapseText
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let containerSizeBtnState = false
|
||||||
|
|
||||||
|
modifiersContainerSizeBtn.addEventListener("click", (e) => {
|
||||||
|
const btnElem = modifiersContainerSizeBtn
|
||||||
|
|
||||||
|
const maximizeIconClasses = ["fa-solid", "fa-expand"]
|
||||||
|
const revertIconClasses = ["fa-solid", "fa-compress"]
|
||||||
|
|
||||||
|
modifiersMainContainer.classList.toggle("modifiers-maximized")
|
||||||
|
|
||||||
|
if(containerSizeBtnState) {
|
||||||
|
revertIconClasses.forEach((c) => btnElem.classList.remove(c))
|
||||||
|
maximizeIconClasses.forEach((c) => btnElem.classList.add(c))
|
||||||
|
|
||||||
|
containerSizeBtnState = false
|
||||||
|
} else {
|
||||||
|
maximizeIconClasses.forEach((c) => btnElem.classList.remove(c))
|
||||||
|
revertIconClasses.forEach((c) => btnElem.classList.add(c))
|
||||||
|
|
||||||
|
containerSizeBtnState = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
modifierSettingsBtn.addEventListener("click", (e) => {
|
||||||
|
modifierSettingsDialog.showModal()
|
||||||
|
customModifiersTextBox.setSelectionRange(0, 0)
|
||||||
|
customModifiersTextBox.focus()
|
||||||
|
customModifiersInitialContent = customModifiersTextBox.value // preserve the initial content
|
||||||
|
e.stopPropagation()
|
||||||
|
})
|
||||||
|
|
||||||
|
modifiersCloseBtn.addEventListener("click", (e) => {
|
||||||
|
hideModifierContainer()
|
||||||
|
})
|
||||||
|
|
||||||
|
// prevents the modifier panel closing at the same time as the settings overlay
|
||||||
|
new MutationObserver(() => {
|
||||||
|
const isActive = modifierSettingsDialog.open
|
||||||
|
|
||||||
|
if (!isActive) {
|
||||||
|
modifierPanelFreezed = true
|
||||||
|
|
||||||
|
setTimeout(() => modifierPanelFreezed = false, 25)
|
||||||
|
}
|
||||||
|
}).observe(modifierSettingsDialog, { attributes: true })
|
||||||
|
|
||||||
|
modifierSettingsCloseBtn.addEventListener("click", (e) => {
|
||||||
|
modifierSettingsDialog.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
modalDialogCloseOnBackdropClick(modifierSettingsDialog)
|
||||||
|
makeDialogDraggable(modifierSettingsDialog)
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
const INPAINTING_EDITOR_SIZE = 450
|
|
||||||
|
|
||||||
let inpaintingEditorContainer = document.querySelector('#inpaintingEditor')
|
|
||||||
let inpaintingEditor = new DrawingBoard.Board('inpaintingEditor', {
|
|
||||||
color: "#ffffff",
|
|
||||||
background: false,
|
|
||||||
size: 30,
|
|
||||||
webStorage: false,
|
|
||||||
controls: [{'DrawingMode': {'filler': false}}, 'Size', 'Navigation']
|
|
||||||
})
|
|
||||||
let inpaintingEditorCanvasBackground = document.querySelector('.drawing-board-canvas-wrapper')
|
|
||||||
|
|
||||||
function resizeInpaintingEditor(widthValue, heightValue) {
|
|
||||||
if (widthValue === heightValue) {
|
|
||||||
widthValue = INPAINTING_EDITOR_SIZE
|
|
||||||
heightValue = INPAINTING_EDITOR_SIZE
|
|
||||||
} else if (widthValue > heightValue) {
|
|
||||||
heightValue = (heightValue / widthValue) * INPAINTING_EDITOR_SIZE
|
|
||||||
widthValue = INPAINTING_EDITOR_SIZE
|
|
||||||
} else {
|
|
||||||
widthValue = (widthValue / heightValue) * INPAINTING_EDITOR_SIZE
|
|
||||||
heightValue = INPAINTING_EDITOR_SIZE
|
|
||||||
}
|
|
||||||
if (inpaintingEditor.opts.aspectRatio === (widthValue / heightValue).toFixed(3)) {
|
|
||||||
// Same ratio, don't reset the canvas.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
inpaintingEditor.opts.aspectRatio = (widthValue / heightValue).toFixed(3)
|
|
||||||
|
|
||||||
inpaintingEditorContainer.style.width = widthValue + 'px'
|
|
||||||
inpaintingEditorContainer.style.height = heightValue + 'px'
|
|
||||||
inpaintingEditor.opts.enlargeYourContainer = true
|
|
||||||
|
|
||||||
inpaintingEditor.opts.size = inpaintingEditor.ctx.lineWidth
|
|
||||||
inpaintingEditor.resize()
|
|
||||||
|
|
||||||
inpaintingEditor.ctx.lineCap = "round"
|
|
||||||
inpaintingEditor.ctx.lineJoin = "round"
|
|
||||||
inpaintingEditor.ctx.lineWidth = inpaintingEditor.opts.size
|
|
||||||
inpaintingEditor.setColor(inpaintingEditor.opts.color)
|
|
||||||
}
|
|
||||||