mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-01 15:25:55 -05:00
Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2a7a40f5f | ||
|
|
9085c288bd | ||
|
|
dd71d022ae | ||
|
|
692c409e1b | ||
|
|
3ed775cf76 | ||
|
|
08ea29d44c | ||
|
|
43df6c9181 | ||
|
|
a8e029497f | ||
|
|
4936662ddd | ||
|
|
883e32220c | ||
|
|
727eead5a7 | ||
|
|
b4cc5b84cf | ||
|
|
d7e77775ca | ||
|
|
b75472c434 | ||
|
|
a4f876daa9 | ||
|
|
4b309a5dd8 | ||
|
|
08384955e4 | ||
|
|
31037b83a3 | ||
|
|
53d93cb1ac | ||
|
|
a0de8fc603 | ||
|
|
ef3527dd85 | ||
|
|
1a887d7414 | ||
|
|
48abc0d731 | ||
|
|
0233105393 | ||
|
|
8f94f81db4 | ||
|
|
3f25aefc3f | ||
|
|
c2929cfdb6 | ||
|
|
a859bd427b | ||
|
|
706becfd7e | ||
|
|
ada1f46e62 | ||
|
|
3556dcbfd3 | ||
|
|
4ab6ca1caa | ||
|
|
74f5b14d18 |
Generated
+1
@@ -2,6 +2,7 @@
|
||||
<dictionary name="bt">
|
||||
<words>
|
||||
<w>arpabet</w>
|
||||
<w>artcraft</w>
|
||||
<w>backfill</w>
|
||||
<w>backfilling</w>
|
||||
<w>backfills</w>
|
||||
|
||||
Generated
+159
-71
@@ -664,7 +664,11 @@ dependencies = [
|
||||
"candle-nn",
|
||||
"candle-transformers",
|
||||
"cudarc",
|
||||
"directories 6.0.0",
|
||||
"dirs 6.0.0",
|
||||
"filesys",
|
||||
"futures 0.3.29",
|
||||
"half",
|
||||
"hf-hub 0.3.2",
|
||||
"image",
|
||||
"libloading 0.8.6",
|
||||
@@ -672,13 +676,19 @@ dependencies = [
|
||||
"memory_caching",
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
"reqwest 0.12.12",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"serial_test",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-log",
|
||||
"tempdir",
|
||||
"tempfile",
|
||||
"tokenizers 0.15.2",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1079,7 +1089,7 @@ dependencies = [
|
||||
"rustversion",
|
||||
"serde",
|
||||
"sync_wrapper 0.1.2",
|
||||
"tower",
|
||||
"tower 0.4.13",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
@@ -1815,6 +1825,7 @@ dependencies = [
|
||||
"candle-flash-attn",
|
||||
"candle-nn",
|
||||
"fancy-regex",
|
||||
"half",
|
||||
"intel-mkl-src",
|
||||
"num-traits",
|
||||
"rand 0.8.5",
|
||||
@@ -2832,19 +2843,6 @@ dependencies = [
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "5.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"hashbrown 0.14.5",
|
||||
"lock_api 0.4.12",
|
||||
"once_cell",
|
||||
"parking_lot_core 0.9.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dasp_sample"
|
||||
version = "0.11.0"
|
||||
@@ -3187,6 +3185,15 @@ dependencies = [
|
||||
"dirs-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "directories"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d"
|
||||
dependencies = [
|
||||
"dirs-sys 0.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "5.0.1"
|
||||
@@ -3316,7 +3323,7 @@ dependencies = [
|
||||
"google_drive_common",
|
||||
"hashing",
|
||||
"hostname 0.4.0",
|
||||
"hyper 1.3.1",
|
||||
"hyper 1.6.0",
|
||||
"jobs_common",
|
||||
"log",
|
||||
"mimetypes",
|
||||
@@ -3762,12 +3769,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.9"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
|
||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3886,9 +3893,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.1.0"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
@@ -4616,6 +4623,18 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"wasi 0.13.3+wasi-0.2.2",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getset"
|
||||
version = "0.1.4"
|
||||
@@ -4762,7 +4781,7 @@ dependencies = [
|
||||
"errors",
|
||||
"jwt-simple",
|
||||
"my-workspace-hack",
|
||||
"reqwest 0.12.7",
|
||||
"reqwest 0.12.12",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
@@ -4988,7 +5007,7 @@ dependencies = [
|
||||
"native-tls",
|
||||
"num_cpus",
|
||||
"rand 0.8.5",
|
||||
"reqwest 0.12.7",
|
||||
"reqwest 0.12.12",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.11",
|
||||
@@ -5192,9 +5211,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.8.0"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
@@ -5234,9 +5253,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.3.1"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d"
|
||||
checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
|
||||
dependencies = [
|
||||
"bytes 1.10.0",
|
||||
"futures-channel",
|
||||
@@ -5260,7 +5279,7 @@ checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http 1.1.0",
|
||||
"hyper 1.3.1",
|
||||
"hyper 1.6.0",
|
||||
"hyper-util",
|
||||
"rustls 0.23.13",
|
||||
"rustls-pki-types",
|
||||
@@ -5303,7 +5322,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||
dependencies = [
|
||||
"bytes 1.10.0",
|
||||
"http-body-util",
|
||||
"hyper 1.3.1",
|
||||
"hyper 1.6.0",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
@@ -5313,20 +5332,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.5"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56"
|
||||
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
|
||||
dependencies = [
|
||||
"bytes 1.10.0",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.0",
|
||||
"hyper 1.3.1",
|
||||
"hyper 1.6.0",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
@@ -5563,7 +5581,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"redis_common",
|
||||
"redis_schema",
|
||||
"reqwest 0.12.7",
|
||||
"reqwest 0.12.12",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
@@ -5971,7 +5989,7 @@ dependencies = [
|
||||
"chumsky",
|
||||
"email-encoding",
|
||||
"email_address",
|
||||
"fastrand 2.1.0",
|
||||
"fastrand 2.3.0",
|
||||
"futures-util",
|
||||
"hostname 0.3.1",
|
||||
"httpdate",
|
||||
@@ -6189,6 +6207,12 @@ version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9"
|
||||
|
||||
[[package]]
|
||||
name = "local-channel"
|
||||
version = "0.1.5"
|
||||
@@ -6852,7 +6876,7 @@ dependencies = [
|
||||
"heck 0.4.1",
|
||||
"hmac 0.12.1",
|
||||
"hyper 0.14.28",
|
||||
"hyper 1.3.1",
|
||||
"hyper 1.6.0",
|
||||
"hyper-util",
|
||||
"indexmap 2.2.6",
|
||||
"itertools 0.12.1",
|
||||
@@ -6878,7 +6902,7 @@ dependencies = [
|
||||
"regex",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
"reqwest 0.12.7",
|
||||
"reqwest 0.12.12",
|
||||
"rsa",
|
||||
"rustls-pki-types",
|
||||
"semver 1.0.23",
|
||||
@@ -6904,7 +6928,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower 0.4.13",
|
||||
"tracing",
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
@@ -7557,7 +7581,7 @@ dependencies = [
|
||||
"base16ct",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"directories",
|
||||
"directories 5.0.1",
|
||||
"flate2",
|
||||
"lazy_static",
|
||||
"log",
|
||||
@@ -9269,9 +9293,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.12.7"
|
||||
version = "0.12.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63"
|
||||
checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes 1.10.0",
|
||||
@@ -9283,7 +9307,7 @@ dependencies = [
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.0",
|
||||
"http-body-util",
|
||||
"hyper 1.3.1",
|
||||
"hyper 1.6.0",
|
||||
"hyper-rustls",
|
||||
"hyper-tls 0.6.0",
|
||||
"hyper-util",
|
||||
@@ -9308,6 +9332,7 @@ dependencies = [
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower 0.5.2",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
@@ -9502,7 +9527,7 @@ dependencies = [
|
||||
"minidom",
|
||||
"percent-encoding",
|
||||
"quick-xml 0.36.2",
|
||||
"reqwest 0.12.7",
|
||||
"reqwest 0.12.12",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
@@ -9590,10 +9615,23 @@ dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"linux-raw-sys 0.4.14",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.9.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.21.12"
|
||||
@@ -9725,6 +9763,15 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scc"
|
||||
version = "2.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea091f6cac2595aa38993f04f4ee692ed43757035c36e67c180b6828356385b1"
|
||||
dependencies = [
|
||||
"sdd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.23"
|
||||
@@ -9814,6 +9861,12 @@ dependencies = [
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sdd"
|
||||
version = "3.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "584e070911c7017da6cb2eb0788d09f43d789029b5877d3e5ecc8acf86ceee21"
|
||||
|
||||
[[package]]
|
||||
name = "seahash"
|
||||
version = "4.1.0"
|
||||
@@ -10134,23 +10187,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serial_test"
|
||||
version = "3.0.0"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "953ad9342b3aaca7cb43c45c097dd008d4907070394bd0751a0aa8817e5a018d"
|
||||
checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9"
|
||||
dependencies = [
|
||||
"dashmap",
|
||||
"futures 0.3.29",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"once_cell",
|
||||
"parking_lot 0.12.2",
|
||||
"scc",
|
||||
"serial_test_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serial_test_derive"
|
||||
version = "3.0.0"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b93fb4adc70021ac1b47f7d45e8cc4169baaa7ea58483bc5b721d19a26202212"
|
||||
checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -10833,7 +10886,7 @@ dependencies = [
|
||||
"redis_common",
|
||||
"redis_schema",
|
||||
"regex",
|
||||
"reqwest 0.12.7",
|
||||
"reqwest 0.12.12",
|
||||
"reusable_types",
|
||||
"rust-s3",
|
||||
"serde",
|
||||
@@ -11597,7 +11650,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"plist",
|
||||
"raw-window-handle",
|
||||
"reqwest 0.12.7",
|
||||
"reqwest 0.12.12",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
@@ -11825,14 +11878,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.10.1"
|
||||
version = "3.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
|
||||
checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"fastrand 2.1.0",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
"fastrand 2.3.0",
|
||||
"getrandom 0.3.1",
|
||||
"once_cell",
|
||||
"rustix 1.0.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -11945,7 +12000,7 @@ dependencies = [
|
||||
"easyenv",
|
||||
"log",
|
||||
"my-workspace-hack",
|
||||
"reqwest 0.12.7",
|
||||
"reqwest 0.12.12",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
@@ -12399,7 +12454,7 @@ dependencies = [
|
||||
"prost",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tower",
|
||||
"tower 0.4.13",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -12435,16 +12490,31 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.2"
|
||||
name = "tower"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
|
||||
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper 1.0.1",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
|
||||
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
@@ -12581,7 +12651,7 @@ dependencies = [
|
||||
"google_drive_common",
|
||||
"hashing",
|
||||
"hostname 0.4.0",
|
||||
"hyper 1.3.1",
|
||||
"hyper 1.6.0",
|
||||
"jobs_common",
|
||||
"log",
|
||||
"my-workspace-hack",
|
||||
@@ -12619,7 +12689,7 @@ dependencies = [
|
||||
"my-workspace-hack",
|
||||
"mysql_queries",
|
||||
"r2d2_redis",
|
||||
"reqwest 0.12.7",
|
||||
"reqwest 0.12.12",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
@@ -12979,7 +13049,7 @@ dependencies = [
|
||||
"actix-web",
|
||||
"mime_guess",
|
||||
"regex",
|
||||
"reqwest 0.12.7",
|
||||
"reqwest 0.12.12",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -13108,7 +13178,7 @@ dependencies = [
|
||||
"google_drive_common",
|
||||
"hashing",
|
||||
"hostname 0.4.0",
|
||||
"hyper 1.3.1",
|
||||
"hyper 1.6.0",
|
||||
"jobs_common",
|
||||
"log",
|
||||
"my-workspace-hack",
|
||||
@@ -13138,7 +13208,7 @@ dependencies = [
|
||||
"filesys",
|
||||
"google_drive_common",
|
||||
"hostname 0.4.0",
|
||||
"hyper 1.3.1",
|
||||
"hyper 1.6.0",
|
||||
"jobs_common",
|
||||
"log",
|
||||
"my-workspace-hack",
|
||||
@@ -13190,6 +13260,15 @@ version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.13.3+wasi-0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
|
||||
dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasite"
|
||||
version = "0.1.0"
|
||||
@@ -13315,7 +13394,7 @@ dependencies = [
|
||||
"log",
|
||||
"my-workspace-hack",
|
||||
"once_cell",
|
||||
"reqwest 0.12.7",
|
||||
"reqwest 0.12.12",
|
||||
"rss",
|
||||
"scraper",
|
||||
"serde",
|
||||
@@ -13959,6 +14038,15 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wry"
|
||||
version = "0.48.1"
|
||||
@@ -14049,8 +14137,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"rustix",
|
||||
"linux-raw-sys 0.4.14",
|
||||
"rustix 0.38.34",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+5
-4
@@ -1,7 +1,8 @@
|
||||
image_width: 512
|
||||
image_height: 512
|
||||
image_width: 1024
|
||||
image_height: 1024
|
||||
#scheduler_steps: 15
|
||||
# NB(bt): It looks like we can get down to 2 sampling steps, perhaps ?
|
||||
scheduler_steps: 2
|
||||
scheduler_steps: 8
|
||||
scheduler_samples: 1
|
||||
cfg_scale: 1.1
|
||||
cfg_scale: 15.0
|
||||
seed: 42
|
||||
@@ -0,0 +1,4 @@
|
||||
# Don't restart the dev server on Rust changes
|
||||
crates/
|
||||
Cargo.toml
|
||||
Cargo.lock
|
||||
@@ -27,30 +27,49 @@ memory_caching = { path = "../../lib/caching/memory_caching" }
|
||||
cudarc = "0.13.7"
|
||||
|
||||
# External
|
||||
#openssl = { version = "0.10", features = ["vendored"] }
|
||||
anyhow = "1.0"
|
||||
bytes = "1.10"
|
||||
base64 = "0.22.1"
|
||||
bytes = "1.10"
|
||||
candle-core = { path = "../../vendor/candle/candle-core", features = ["cuda"] }
|
||||
candle-nn = { path = "../../vendor/candle/candle-nn", features = ["cuda"] }
|
||||
candle-transformers = { path = "../../vendor/candle/candle-transformers", features = ["cuda"] }
|
||||
dirs = "6.0.0"
|
||||
directories = "6.0.0"
|
||||
futures = "0.3"
|
||||
hf-hub = "0.3.2"
|
||||
image = "0.25.5"
|
||||
serde_yaml = "0.9"
|
||||
log = "0.4"
|
||||
once_cell = "1.20.3"
|
||||
rand = "0.8"
|
||||
rand = "0.8.5"
|
||||
# NB(bt,2025-03-10): default-features = false on Windows while I debug openssl / rustls
|
||||
reqwest = { version = "0.12.12", features = ["stream"], default-features = false }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.9"
|
||||
tauri = { version = "2.2.4", features = [] }
|
||||
tauri-plugin-log = "2"
|
||||
tempdir = "0.3.7"
|
||||
tempfile = "3.18.0"
|
||||
tokenizers = "0.15.2"
|
||||
tokio = { version = "1.42", features = ["rt", "rt-multi-thread", "fs"] }
|
||||
tokio-util = { version = "0.7", features = ["codec"] }
|
||||
half = { version = "2.3.1", features = ["num-traits", "use-intrinsics", "rand_distr"] }
|
||||
|
||||
[features]
|
||||
default = ["cuda"]
|
||||
cuda = ["candle-core/cuda", "candle-nn/cuda", "candle-transformers/cuda"]
|
||||
cuda = [
|
||||
"candle-core/cuda",
|
||||
"candle-nn/cuda",
|
||||
"candle-transformers/cuda",
|
||||
# NB(bt,2025-03-10): Building without Flash attention on Windows for now to bypass linker issues
|
||||
"candle-transformers/flash-attn",
|
||||
"candle-core/cudnn"
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
libloading = "0.8.6"
|
||||
serial_test = "3.2.0"
|
||||
|
||||
[profile.dev]
|
||||
incremental = true # Compile your binary in smaller steps.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
use crate::ml::downloads::download_all_models::download_all_models;
|
||||
use crate::state::app_dir::AppDataRoot;
|
||||
use tauri::State;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn download_models(
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
) -> Result<String, String> {
|
||||
|
||||
download_all_models(&app_data_root).await
|
||||
.map_err(|err| {
|
||||
"error downloading models".to_string()
|
||||
})?;
|
||||
|
||||
Ok(("downloaded".to_string()))
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::ml::downloads::download_all_models::download_all_models;
|
||||
use crate::ml::model_cache::ModelCache;
|
||||
use crate::ml::prompt_cache::PromptCache;
|
||||
use crate::ml::stable_diffusion::stable_diffusion_pipeline::{stable_diffusion_pipeline, Args};
|
||||
use crate::ml::stable_diffusion::lcm_pipeline::{lcm_pipeline, Args};
|
||||
use crate::state::app_config::AppConfig;
|
||||
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine};
|
||||
use bytes::BytesMut;
|
||||
@@ -10,12 +11,15 @@ use log::{error, info};
|
||||
use std::io::Cursor;
|
||||
use std::path::PathBuf;
|
||||
use tauri::{AppHandle, State};
|
||||
use crate::state::app_dir::AppDataRoot;
|
||||
|
||||
const PROMPT_FILENAME : &str = "prompt.txt";
|
||||
const PROMPT: &str = "A beautiful landscape with mountains and a lake";
|
||||
const PROMPT: &str = "green goblin, blood dripping from lips, detailed, photorealistic, 8k";
|
||||
|
||||
const NEGATIVE_PROMPT: &str = "bad quality, bad faces, poor quality, blurry faces, watermark";
|
||||
|
||||
const RANDOM_SEED: u32 = 42;
|
||||
|
||||
// TODO: Mark async https://github.com/tauri-apps/tauri/discussions/7737
|
||||
|
||||
|
||||
@@ -29,6 +33,7 @@ pub async fn infer_image(
|
||||
model_config: State<'_, AppConfig>,
|
||||
model_cache: State<'_, ModelCache>,
|
||||
prompt_cache: State<'_, PromptCache>,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
app: AppHandle,
|
||||
) -> Result<String, String> {
|
||||
|
||||
@@ -40,7 +45,23 @@ pub async fn infer_image(
|
||||
let image = hydrate_base64_image(image)
|
||||
.map_err(|err| format!("Couldn't hydrate image from base64: {}", err))?;
|
||||
|
||||
let result = do_infer_image(&prompt, image, strength, &model_config, &model_cache, prompt_cache, app);
|
||||
// TODO(bt): Move this to another endpoint
|
||||
download_all_models(&app_data_root).await
|
||||
.map_err(|err| {
|
||||
error!("couldn't download models: {:?}", err);
|
||||
"Couldn't download models".to_string()
|
||||
})?;
|
||||
|
||||
let result = do_infer_image(
|
||||
&prompt,
|
||||
image,
|
||||
strength,
|
||||
&model_config,
|
||||
&model_cache,
|
||||
prompt_cache,
|
||||
app,
|
||||
&app_data_root
|
||||
).await;
|
||||
|
||||
if let Err(err) = result.as_deref() {
|
||||
error!("There was an error: {:?}", err);
|
||||
@@ -49,31 +70,32 @@ pub async fn infer_image(
|
||||
result
|
||||
}
|
||||
|
||||
fn do_infer_image(
|
||||
async fn do_infer_image(
|
||||
prompt: &str,
|
||||
image: DynamicImage,
|
||||
strength: Option<u8>,
|
||||
config: &AppConfig,
|
||||
model_cache: &ModelCache,
|
||||
prompt_cache: State<PromptCache>,
|
||||
prompt_cache: State<'_, PromptCache>,
|
||||
app: AppHandle,
|
||||
app_data_root: &AppDataRoot,
|
||||
) -> Result<String, String> {
|
||||
println!("infer_image called; generating image with SDXL Turbo...");
|
||||
|
||||
|
||||
let args = Args {
|
||||
image: &image,
|
||||
prompt: prompt.to_string(),
|
||||
uncond_prompt: NEGATIVE_PROMPT.to_string(),
|
||||
//guidance_scale: Some(0.0),
|
||||
model_cache,
|
||||
configs: config,
|
||||
prompt_cache: &prompt_cache,
|
||||
i2i_strength: strength,
|
||||
cfg_scale: config.cfg_scale,
|
||||
app: &app,
|
||||
app_data_root,
|
||||
use_flash_attn: true,
|
||||
};
|
||||
|
||||
match stable_diffusion_pipeline(args) {
|
||||
match lcm_pipeline(args) {
|
||||
Ok(image) => {
|
||||
let mut bytes = Vec::with_capacity(1024*1024);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod download_models;
|
||||
pub mod flip_image;
|
||||
pub mod image_endpoint;
|
||||
pub mod test_counter;
|
||||
|
||||
@@ -3,9 +3,11 @@ use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Copy, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ModelType {
|
||||
pub enum NotificationModelType {
|
||||
Unet,
|
||||
Vae,
|
||||
Json,
|
||||
ClipEncoder,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
@@ -13,10 +15,16 @@ pub enum ModelType {
|
||||
pub enum NotificationEvent<'a> {
|
||||
ModelDownloadStarted {
|
||||
model_name: &'a str,
|
||||
model_type: ModelType,
|
||||
model_type: NotificationModelType,
|
||||
},
|
||||
ModelDownloadProgress {
|
||||
model_name: &'a str,
|
||||
model_type: NotificationModelType,
|
||||
currently_downloaded_bytes: usize,
|
||||
total_file_bytes: usize,
|
||||
},
|
||||
ModelDownloadComplete {
|
||||
model_name: &'a str,
|
||||
model_type: ModelType,
|
||||
model_type: NotificationModelType,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,16 @@ pub mod endpoints;
|
||||
pub mod events;
|
||||
pub mod ml;
|
||||
pub mod state;
|
||||
pub mod threads;
|
||||
pub mod transfer;
|
||||
|
||||
use crate::endpoints::download_models::download_models;
|
||||
use crate::endpoints::image_endpoint::infer_image;
|
||||
use crate::endpoints::test_counter::test_counter;
|
||||
use crate::ml::model_cache::ModelCache;
|
||||
use crate::ml::prompt_cache::PromptCache;
|
||||
use crate::state::app_config::AppConfig;
|
||||
use endpoints::image_endpoint::infer_image;
|
||||
use endpoints::test_counter::test_counter;
|
||||
use crate::threads::downloader_thread::downloader_thread;
|
||||
use tauri_plugin_log::Target;
|
||||
use tauri_plugin_log::TargetKind;
|
||||
|
||||
@@ -26,6 +30,9 @@ pub fn run() {
|
||||
println!("Creating model cache...");
|
||||
|
||||
let model_cache = ModelCache::new();
|
||||
|
||||
let app_data_root = config.app_data_root.clone();
|
||||
let app_data_root2 = config.app_data_root.clone();
|
||||
|
||||
println!("Initializing backend runtime...");
|
||||
|
||||
@@ -42,14 +49,20 @@ pub fn run() {
|
||||
// .build(),
|
||||
// )?;
|
||||
//}
|
||||
let app = app.handle().clone();
|
||||
|
||||
tauri::async_runtime::spawn(downloader_thread(app_data_root2, app));
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.manage(config)
|
||||
.manage(prompt_cache)
|
||||
.manage(model_cache)
|
||||
.manage(app_data_root)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
test_counter,
|
||||
infer_image,
|
||||
download_models,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
use crate::ml::downloads::download_model_registry_file::download_model_registry_file;
|
||||
use crate::ml::model_type::ModelType;
|
||||
use crate::state::app_dir::AppDataRoot;
|
||||
|
||||
pub async fn download_all_models(app_data_root: &AppDataRoot) -> anyhow::Result<()> {
|
||||
let weights_dir = app_data_root.weights_dir();
|
||||
|
||||
//download_model_registry_file(ModelType::ClipJson, weights_dir).await?;
|
||||
//download_model_registry_file(ModelType::SdxlTurboUnet, weights_dir).await?;
|
||||
//download_model_registry_file(ModelType::SdxlTurboVae, weights_dir).await?;
|
||||
//download_model_registry_file(ModelType::SdxlTurboClipEncoder, weights_dir).await?;
|
||||
//download_model_registry_file(ModelType::SdxlTurboClipEncoder2, weights_dir).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
use crate::ml::model_type::ModelType;
|
||||
use crate::state::app_dir::AppWeightsDir;
|
||||
use log::info;
|
||||
use std::fs::File;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub async fn download_model_registry_file(
|
||||
model_type: ModelType,
|
||||
weights_dir: &AppWeightsDir,
|
||||
) -> anyhow::Result<PathBuf> {
|
||||
|
||||
let path = weights_dir.model_path(&model_type);
|
||||
|
||||
if path.exists() {
|
||||
return Ok(path);
|
||||
}
|
||||
|
||||
info!("downloading model: {:?} to {:?}", model_type, path);
|
||||
|
||||
let body = reqwest::get(model_type.get_download_url())
|
||||
.await?
|
||||
.bytes()
|
||||
.await?;
|
||||
|
||||
let _file = File::create(&path)?;
|
||||
|
||||
std::fs::write(&path, body.as_ref())?;
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod download_model_registry_file;
|
||||
pub mod download_all_models;
|
||||
@@ -3,4 +3,6 @@ pub mod model_cache;
|
||||
pub mod model_file;
|
||||
pub mod models;
|
||||
pub mod prompt_cache;
|
||||
pub mod stable_diffusion;
|
||||
pub mod stable_diffusion;
|
||||
pub mod model_type;
|
||||
pub mod downloads;
|
||||
@@ -4,80 +4,177 @@ extern crate accelerate_src;
|
||||
extern crate intel_mkl_src;
|
||||
|
||||
use candle_transformers::models::stable_diffusion;
|
||||
use anyhow::{Error as E, Result};
|
||||
use hf_hub::api::sync::Api;
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StableDiffusionVersion {
|
||||
V1_5,
|
||||
V1_5Inpaint,
|
||||
V2_1,
|
||||
V2Inpaint,
|
||||
Xl,
|
||||
XlInpaint,
|
||||
Turbo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ModelFile {
|
||||
Tokenizer,
|
||||
Tokenizer2,
|
||||
Clip,
|
||||
Clip2,
|
||||
Unet,
|
||||
Vae,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StableDiffusionVersion {
|
||||
V1_5,
|
||||
V1_5Inpaint,
|
||||
V2_1,
|
||||
V2Inpaint,
|
||||
Xl,
|
||||
XlInpaint,
|
||||
Turbo,
|
||||
Tokenizer,
|
||||
Tokenizer2,
|
||||
Clip,
|
||||
Clip2,
|
||||
Unet,
|
||||
UnetLcm,
|
||||
Vae,
|
||||
}
|
||||
|
||||
impl StableDiffusionVersion {
|
||||
pub fn repo(&self) -> &'static str {
|
||||
match self {
|
||||
Self::XlInpaint => "diffusers/stable-diffusion-xl-1.0-inpainting-0.1",
|
||||
Self::Xl => "stabilityai/stable-diffusion-xl-base-1.0",
|
||||
Self::V2Inpaint => "stabilityai/stable-diffusion-2-inpainting",
|
||||
Self::V2_1 => "stabilityai/stable-diffusion-2-1",
|
||||
Self::V1_5 => "runwayml/stable-diffusion-v1-5",
|
||||
Self::V1_5Inpaint => "stable-diffusion-v1-5/stable-diffusion-inpainting",
|
||||
Self::Turbo => "stabilityai/sdxl-turbo",
|
||||
pub fn repo(&self) -> &'static str {
|
||||
match self {
|
||||
Self::XlInpaint => "diffusers/stable-diffusion-xl-1.0-inpainting-0.1",
|
||||
Self::Xl => "stabilityai/stable-diffusion-xl-base-1.0",
|
||||
Self::V2Inpaint => "stabilityai/stable-diffusion-2-inpainting",
|
||||
Self::V2_1 => "stabilityai/stable-diffusion-2-1",
|
||||
Self::V1_5 => "Lykon/dreamshaper-7",
|
||||
Self::V1_5Inpaint => "stable-diffusion-v1-5/stable-diffusion-inpainting",
|
||||
Self::Turbo => "Lykon/dreamshaper-7",
|
||||
}
|
||||
}
|
||||
|
||||
fn unet_file(&self, use_f16: bool) -> &'static str {
|
||||
match self {
|
||||
Self::V1_5
|
||||
| Self::V1_5Inpaint
|
||||
| Self::V2_1
|
||||
| Self::V2Inpaint
|
||||
| Self::Xl
|
||||
| Self::XlInpaint
|
||||
| Self::Turbo => {
|
||||
if use_f16 {
|
||||
"unet/diffusion_pytorch_model.fp16.safetensors"
|
||||
} else {
|
||||
"unet/diffusion_pytorch_model.safetensors"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn vae_file(&self, use_f16: bool) -> &'static str {
|
||||
match self {
|
||||
Self::V1_5
|
||||
| Self::V1_5Inpaint
|
||||
| Self::V2_1
|
||||
| Self::V2Inpaint
|
||||
| Self::Xl
|
||||
| Self::XlInpaint
|
||||
| Self::Turbo => {
|
||||
if use_f16 {
|
||||
"vae/diffusion_pytorch_model.fp16.safetensors"
|
||||
} else {
|
||||
"vae/diffusion_pytorch_model.safetensors"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clip_file(&self, use_f16: bool) -> &'static str {
|
||||
match self {
|
||||
Self::V1_5
|
||||
| Self::V1_5Inpaint
|
||||
| Self::V2_1
|
||||
| Self::V2Inpaint
|
||||
| Self::Xl
|
||||
| Self::XlInpaint
|
||||
| Self::Turbo => {
|
||||
if use_f16 {
|
||||
"text_encoder/model.fp16.safetensors"
|
||||
} else {
|
||||
"text_encoder/model.safetensors"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clip2_file(&self, use_f16: bool) -> &'static str {
|
||||
match self {
|
||||
Self::V1_5
|
||||
| Self::V1_5Inpaint
|
||||
| Self::V2_1
|
||||
| Self::V2Inpaint
|
||||
| Self::Xl
|
||||
| Self::XlInpaint
|
||||
| Self::Turbo => {
|
||||
if use_f16 {
|
||||
"text_encoder_2/model.fp16.safetensors"
|
||||
} else {
|
||||
"text_encoder_2/model.safetensors"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ModelFile {
|
||||
pub fn get(
|
||||
&self,
|
||||
filename: Option<String>,
|
||||
version: StableDiffusionVersion,
|
||||
use_f16: bool,
|
||||
) -> anyhow::Result<std::path::PathBuf> {
|
||||
use hf_hub::api::sync::Api;
|
||||
match filename {
|
||||
Some(filename) => Ok(std::path::PathBuf::from(filename)),
|
||||
None => {
|
||||
let (repo, path) = match self {
|
||||
Self::Tokenizer => {
|
||||
let tokenizer_repo = match version {
|
||||
StableDiffusionVersion::V1_5
|
||||
| StableDiffusionVersion::V2_1
|
||||
| StableDiffusionVersion::V1_5Inpaint
|
||||
| StableDiffusionVersion::V2Inpaint => "openai/clip-vit-base-patch32",
|
||||
StableDiffusionVersion::Xl
|
||||
| StableDiffusionVersion::XlInpaint
|
||||
| StableDiffusionVersion::Turbo => {
|
||||
"openai/clip-vit-large-patch14"
|
||||
}
|
||||
};
|
||||
(tokenizer_repo, "tokenizer.json")
|
||||
}
|
||||
Self::Tokenizer2 => {
|
||||
("laion/CLIP-ViT-bigG-14-laion2B-39B-b160k", "tokenizer.json")
|
||||
}
|
||||
Self::Clip => (version.repo(), "text_encoder/model.safetensors"),
|
||||
Self::Clip2 => (version.repo(), "text_encoder_2/model.safetensors"),
|
||||
Self::Unet => (version.repo(), "unet/diffusion_pytorch_model.safetensors"),
|
||||
Self::Vae => (version.repo(), "vae/diffusion_pytorch_model.safetensors"),
|
||||
};
|
||||
let filename = Api::new()?.model(repo.to_string()).get(path)?;
|
||||
Ok(filename)
|
||||
}
|
||||
pub fn get(
|
||||
&self,
|
||||
filename: Option<String>,
|
||||
version: StableDiffusionVersion,
|
||||
use_f16: bool,
|
||||
) -> Result<std::path::PathBuf> {
|
||||
match filename {
|
||||
Some(filename) => Ok(std::path::PathBuf::from(filename)),
|
||||
None => {
|
||||
let (repo, path) = match self {
|
||||
Self::Tokenizer => {
|
||||
let tokenizer_repo = match version {
|
||||
StableDiffusionVersion::V1_5
|
||||
| StableDiffusionVersion::V2_1
|
||||
| StableDiffusionVersion::V1_5Inpaint
|
||||
| StableDiffusionVersion::V2Inpaint => "openai/clip-vit-base-patch32",
|
||||
StableDiffusionVersion::Xl
|
||||
| StableDiffusionVersion::XlInpaint
|
||||
| StableDiffusionVersion::Turbo => {
|
||||
// This seems similar to the patch32 version except some very small
|
||||
// difference in the split regex.
|
||||
"openai/clip-vit-large-patch14"
|
||||
}
|
||||
};
|
||||
(tokenizer_repo, "tokenizer.json")
|
||||
}
|
||||
Self::Tokenizer2 => {
|
||||
("laion/CLIP-ViT-bigG-14-laion2B-39B-b160k", "tokenizer.json")
|
||||
}
|
||||
Self::Clip => (version.repo(), version.clip_file(use_f16)),
|
||||
Self::Clip2 => (version.repo(), version.clip2_file(use_f16)),
|
||||
Self::UnetLcm => (
|
||||
"SimianLuo/LCM_Dreamshaper_v7",
|
||||
if use_f16 {
|
||||
"unet/diffusion_pytorch_model.fp16.safetensors"
|
||||
} else {
|
||||
"unet/diffusion_pytorch_model.safetensors"
|
||||
},
|
||||
),
|
||||
Self::Unet => (version.repo(), version.unet_file(use_f16)),
|
||||
Self::Vae => {
|
||||
// Override for SDXL when using f16 weights.
|
||||
// See https://github.com/huggingface/candle/issues/1060
|
||||
if matches!(
|
||||
version,
|
||||
StableDiffusionVersion::Xl | StableDiffusionVersion::Turbo,
|
||||
) && use_f16
|
||||
{
|
||||
(
|
||||
"madebyollin/sdxl-vae-fp16-fix",
|
||||
"diffusion_pytorch_model.safetensors",
|
||||
)
|
||||
} else {
|
||||
(version.repo(), version.vae_file(use_f16))
|
||||
}
|
||||
}
|
||||
};
|
||||
let filename = Api::new()?.model(repo.to_string()).get(path)?;
|
||||
Ok(filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
use crate::events::notification_event::NotificationModelType;
|
||||
use crate::state::app_dir::AppWeightsDir;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum ModelType {
|
||||
ClipJson,
|
||||
SdxlTurboUnet,
|
||||
SdxlTurboVae,
|
||||
SdxlTurboClipEncoder,
|
||||
SdxlTurboClipEncoder2,
|
||||
SimianLuoLcmDreamshaperV7Unet,
|
||||
LykonDreamshaper7Vae,
|
||||
LykonDreamshaper7TextEncoderFp16,
|
||||
}
|
||||
|
||||
impl ModelType {
|
||||
pub fn get_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::ClipJson => "CLIP (JSON)",
|
||||
Self::SdxlTurboUnet => "SDXL Turbo UNET",
|
||||
Self::SdxlTurboVae => "SDXL Turbo VAE",
|
||||
Self::SdxlTurboClipEncoder => "Clip Encoder #1",
|
||||
Self::SdxlTurboClipEncoder2 => "Clip Encoder #2",
|
||||
Self::SimianLuoLcmDreamshaperV7Unet => "SimianLuo LCM Dreamshaper v7 UNET",
|
||||
Self::LykonDreamshaper7Vae => "Lykon Dreamshaper 7 VAE",
|
||||
Self::LykonDreamshaper7TextEncoderFp16 => "Lykon Dreamshaper Text Encoder (fp16)",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_notification_type(&self) -> NotificationModelType {
|
||||
match self {
|
||||
Self::ClipJson => NotificationModelType::Json,
|
||||
Self::SdxlTurboUnet => NotificationModelType::Unet,
|
||||
Self::SdxlTurboVae => NotificationModelType::Vae,
|
||||
Self::SdxlTurboClipEncoder => NotificationModelType::ClipEncoder,
|
||||
Self::SdxlTurboClipEncoder2 => NotificationModelType::ClipEncoder,
|
||||
Self::SimianLuoLcmDreamshaperV7Unet => NotificationModelType::Unet,
|
||||
Self::LykonDreamshaper7Vae => NotificationModelType::Vae,
|
||||
Self::LykonDreamshaper7TextEncoderFp16 => NotificationModelType::ClipEncoder,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_hf_id(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
Self::ClipJson => Some("laion/CLIP-ViT-bigG-14-laion2B-39B-b160k"),
|
||||
Self::SdxlTurboUnet
|
||||
| Self::SdxlTurboVae
|
||||
| Self::SdxlTurboClipEncoder
|
||||
| Self::SdxlTurboClipEncoder2 => Some("stabilityai/sdxl-turbo"),
|
||||
Self::SimianLuoLcmDreamshaperV7Unet => Some("SimianLuo/LCM_Dreamshaper_v7"),
|
||||
Self::LykonDreamshaper7Vae
|
||||
| Self::LykonDreamshaper7TextEncoderFp16 => Some("Lykon/dreamshaper-7"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_download_url(&self) -> &'static str {
|
||||
match self {
|
||||
Self::ClipJson => "https://pub-bc5e2bc0cdee4bb5ae8fca9d641ca0d6.r2.dev/tokenizer.json",
|
||||
Self::SdxlTurboUnet => "https://pub-bc5e2bc0cdee4bb5ae8fca9d641ca0d6.r2.dev/diffusion_pytorch_model.unet.safetensors",
|
||||
Self::SdxlTurboVae => "https://pub-bc5e2bc0cdee4bb5ae8fca9d641ca0d6.r2.dev/diffusion_pytorch_model.vae.safetensors",
|
||||
Self::SdxlTurboClipEncoder => "https://pub-bc5e2bc0cdee4bb5ae8fca9d641ca0d6.r2.dev/clip_text_encoder.safetensors",
|
||||
Self::SdxlTurboClipEncoder2 => "https://pub-bc5e2bc0cdee4bb5ae8fca9d641ca0d6.r2.dev/clip_text_encoder_2.safetensors",
|
||||
Self::SimianLuoLcmDreamshaperV7Unet => "https://pub-bc5e2bc0cdee4bb5ae8fca9d641ca0d6.r2.dev/simianluo_lcm_dreamshaper_v7_unet.safetensors",
|
||||
Self::LykonDreamshaper7Vae => "https://pub-bc5e2bc0cdee4bb5ae8fca9d641ca0d6.r2.dev/lykon_dreamshaper_7_vae.safetensors",
|
||||
Self::LykonDreamshaper7TextEncoderFp16 => "https://pub-bc5e2bc0cdee4bb5ae8fca9d641ca0d6.r2.dev/lykon_dreamshaper_7_text_encoder.fp16.safetensors",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_filename(&self) -> &'static str {
|
||||
match self {
|
||||
Self::ClipJson => "clip_vit_bigg_14_lion2b_39b_b160k.tokenizer.json",
|
||||
Self::SdxlTurboUnet => "diffusion_pytorch_model.unet.safetensors",
|
||||
Self::SdxlTurboVae => "diffusion_pytorch_model.vae.safetensors",
|
||||
Self::SdxlTurboClipEncoder => "clip_text_encoder.safetensors",
|
||||
Self::SdxlTurboClipEncoder2 => "clip_text_encoder_2.safetensors",
|
||||
Self::SimianLuoLcmDreamshaperV7Unet => "simianluo_lcm_dreamshaper_v7_unet.safetensors",
|
||||
Self::LykonDreamshaper7Vae => "lykon_dreamshaper_7_vae.safetensors",
|
||||
Self::LykonDreamshaper7TextEncoderFp16 => "lykon_dreamshaper_7_text_encoder.fp16.safetensors",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_path(&self, weights_dir: &AppWeightsDir) -> PathBuf {
|
||||
weights_dir.model_path(self)
|
||||
}
|
||||
}
|
||||
@@ -8,28 +8,23 @@ pub struct UNetModel {
|
||||
}
|
||||
|
||||
impl UNetModel {
|
||||
pub fn new<P: AsRef<Path>>(
|
||||
sd_config: &StableDiffusionConfig,
|
||||
unet_weights_file: P,
|
||||
device: &Device,
|
||||
dtype: DType,
|
||||
) -> anyhow::Result<Self> {
|
||||
pub fn new<P: AsRef<Path>>(sd_config: &StableDiffusionConfig, unet_weights_file: P, device: &Device, dtype: DType) -> anyhow::Result<Self> {
|
||||
println!("building unet model... (1)");
|
||||
|
||||
let model = sd_config.build_unet(
|
||||
unet_weights_file,
|
||||
&device,
|
||||
4,
|
||||
false,
|
||||
dtype
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
model,
|
||||
})
|
||||
|
||||
let model = sd_config.build_unet(unet_weights_file, &device, 4, true, dtype)?;
|
||||
|
||||
Ok(Self { model })
|
||||
}
|
||||
|
||||
|
||||
pub fn new_with_inner(model: UNet2DConditionModel) -> Self {
|
||||
Self { model }
|
||||
}
|
||||
|
||||
pub fn inference(&self, latent_model_input: &Tensor, timestep: f64, text_embeddings: &Tensor) -> anyhow::Result<Tensor> {
|
||||
Ok(self.model.forward(&latent_model_input, timestep, &text_embeddings)?)
|
||||
}
|
||||
|
||||
pub fn forward_with_guidance(&self, latent_model_input: &Tensor, timestep: f64, text_embeddings: &Tensor, guidance_scale_embedding: &Tensor) -> anyhow::Result<Tensor> {
|
||||
Ok(self.model.forward_with_guidance(&latent_model_input, timestep, &text_embeddings, Some(guidance_scale_embedding))?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ extern crate accelerate_src;
|
||||
#[cfg(feature = "mkl")]
|
||||
extern crate intel_mkl_src;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use candle_transformers::models::stable_diffusion;
|
||||
|
||||
use crate::ml::model_file::{ModelFile, StableDiffusionVersion};
|
||||
@@ -10,27 +11,15 @@ use anyhow::Error as E;
|
||||
use candle_core::{DType, Device, Module, Tensor, D};
|
||||
use log::info;
|
||||
use tokenizers::Tokenizer;
|
||||
use crate::ml::model_type::ModelType;
|
||||
use crate::state::app_dir::AppWeightsDir;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn infer_clip_text_embeddings(
|
||||
prompt: &str,
|
||||
uncond_prompt: &str,
|
||||
tokenizer: Option<String>,
|
||||
clip_weights: Option<String>,
|
||||
clip2_weights: Option<String>,
|
||||
sd_version: StableDiffusionVersion,
|
||||
sd_config: &stable_diffusion::StableDiffusionConfig,
|
||||
use_f16: bool,
|
||||
device: &Device,
|
||||
dtype: DType,
|
||||
use_guide_scale: bool,
|
||||
) -> anyhow::Result<Tensor> {
|
||||
info!("Preparing text embeddings...");
|
||||
pub fn infer_clip_text_embeddings(prompt: &str, uncond_prompt: &str, tokenizer: Option<String>, clip_weights: Option<String>, clip2_weights: Option<String>, sd_version: StableDiffusionVersion, sd_config: &stable_diffusion::StableDiffusionConfig, use_f16: bool, device: &Device, dtype: DType, use_guide_scale: bool, weights_dir: &AppWeightsDir) -> anyhow::Result<Tensor> {
|
||||
info!("Preparing text embeddings... for {:?}", sd_version);
|
||||
|
||||
let which = match sd_version {
|
||||
StableDiffusionVersion::Xl
|
||||
| StableDiffusionVersion::XlInpaint
|
||||
| StableDiffusionVersion::Turbo => vec![true, false],
|
||||
StableDiffusionVersion::Xl | StableDiffusionVersion::XlInpaint | StableDiffusionVersion::Turbo => vec![true, false],
|
||||
_ => vec![true],
|
||||
};
|
||||
|
||||
@@ -41,7 +30,7 @@ pub fn infer_clip_text_embeddings(
|
||||
&prompt,
|
||||
&uncond_prompt,
|
||||
None, // tokenizer
|
||||
None, // clip_weights
|
||||
clip_weights.clone(),
|
||||
None, // clip2_weights
|
||||
sd_version,
|
||||
&sd_config,
|
||||
@@ -50,55 +39,45 @@ pub fn infer_clip_text_embeddings(
|
||||
dtype,
|
||||
use_guide_scale,
|
||||
*first,
|
||||
weights_dir,
|
||||
)
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
|
||||
let text_embeddings = Tensor::cat(&text_embeddings, D::Minus1)?;
|
||||
|
||||
|
||||
Ok(text_embeddings)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn do_infer_clip_text_embeddings(
|
||||
prompt: &str,
|
||||
uncond_prompt: &str,
|
||||
tokenizer: Option<String>,
|
||||
clip_weights: Option<String>,
|
||||
clip2_weights: Option<String>,
|
||||
sd_version: StableDiffusionVersion,
|
||||
sd_config: &stable_diffusion::StableDiffusionConfig,
|
||||
use_f16: bool,
|
||||
device: &Device,
|
||||
dtype: DType,
|
||||
use_guide_scale: bool,
|
||||
first: bool,
|
||||
) -> anyhow::Result<Tensor> {
|
||||
|
||||
let tokenizer_file = if first {
|
||||
ModelFile::Tokenizer
|
||||
} else {
|
||||
ModelFile::Tokenizer2
|
||||
};
|
||||
let tokenizer = tokenizer_file.get(tokenizer, sd_version, use_f16)?;
|
||||
fn do_infer_clip_text_embeddings(prompt: &str, uncond_prompt: &str, tokenizer: Option<String>, clip_weights: Option<String>, clip2_weights: Option<String>, sd_version: StableDiffusionVersion, sd_config: &stable_diffusion::StableDiffusionConfig, use_f16: bool, device: &Device, dtype: DType, use_guide_scale: bool, first: bool, weights_dir: &AppWeightsDir) -> anyhow::Result<Tensor> {
|
||||
info!("do_infer_clip_text_embeddings called with args {:?}", (prompt, uncond_prompt, tokenizer.clone().unwrap_or_else(|| "None".to_string()), clip_weights.clone().unwrap_or_else(|| "None".to_string()), clip2_weights.clone().unwrap_or_else(|| "None".to_string()), sd_version, sd_config, use_f16, device, dtype, use_guide_scale, first));
|
||||
|
||||
//let tokenizer_file = if first {
|
||||
// info!("ModelFile::Tokenizer");
|
||||
// ModelFile::Tokenizer
|
||||
//} else {
|
||||
// info!("ModelFile::Tokenizer2");
|
||||
// ModelFile::Tokenizer2
|
||||
//};
|
||||
|
||||
//let tokenizer = tokenizer_file.get(tokenizer, sd_version, use_f16)?;
|
||||
|
||||
let tokenizer = weights_dir.model_path(&ModelType::ClipJson);
|
||||
|
||||
info!("Loading Clip Tokenizer path: {:?}", tokenizer);
|
||||
|
||||
let tokenizer = Tokenizer::from_file(tokenizer).map_err(E::msg)?;
|
||||
|
||||
let pad_id = match &sd_config.clip.pad_with {
|
||||
Some(padding) => *tokenizer.get_vocab(true).get(padding.as_str()).unwrap(),
|
||||
None => *tokenizer.get_vocab(true).get("<|endoftext|>").unwrap(),
|
||||
};
|
||||
|
||||
println!("Running with prompt \"{prompt}\".");
|
||||
let mut tokens = tokenizer
|
||||
.encode(prompt, true)
|
||||
.map_err(E::msg)?
|
||||
.get_ids()
|
||||
.to_vec();
|
||||
let mut tokens = tokenizer.encode(prompt, true).map_err(E::msg)?.get_ids().to_vec();
|
||||
if tokens.len() > sd_config.clip.max_position_embeddings {
|
||||
anyhow::bail!(
|
||||
"the prompt is too long, {} > max-tokens ({})",
|
||||
tokens.len(),
|
||||
sd_config.clip.max_position_embeddings
|
||||
)
|
||||
anyhow::bail!("the prompt is too long, {} > max-tokens ({})", tokens.len(), sd_config.clip.max_position_embeddings)
|
||||
}
|
||||
while tokens.len() < sd_config.clip.max_position_embeddings {
|
||||
tokens.push(pad_id)
|
||||
@@ -106,37 +85,23 @@ fn do_infer_clip_text_embeddings(
|
||||
let tokens = Tensor::new(tokens.as_slice(), device)?.unsqueeze(0)?;
|
||||
|
||||
println!("Building the Clip transformer.");
|
||||
let clip_weights_file = if first {
|
||||
ModelFile::Clip
|
||||
} else {
|
||||
ModelFile::Clip2
|
||||
};
|
||||
let clip_weights_file = if first { ModelFile::Clip } else { ModelFile::Clip2 };
|
||||
let clip_weights = if first {
|
||||
clip_weights_file.get(clip_weights, sd_version, use_f16)?
|
||||
//clip_weights_file.get(clip_weights, sd_version, use_f16)?
|
||||
weights_dir.model_path(&ModelType::SdxlTurboClipEncoder)
|
||||
} else {
|
||||
clip_weights_file.get(clip2_weights, sd_version, use_f16)?
|
||||
};
|
||||
let clip_config = if first {
|
||||
&sd_config.clip
|
||||
} else {
|
||||
sd_config.clip2.as_ref().unwrap()
|
||||
//clip_weights_file.get(clip2_weights, sd_version, use_f16)?
|
||||
weights_dir.model_path(&ModelType::SdxlTurboClipEncoder2)
|
||||
};
|
||||
let clip_config = if first { &sd_config.clip } else { sd_config.clip2.as_ref().unwrap() };
|
||||
let text_model = stable_diffusion::build_clip_transformer(clip_config, clip_weights, device, DType::F32)?;
|
||||
|
||||
let text_embeddings = text_model.forward(&tokens)?;
|
||||
|
||||
let text_embeddings = if use_guide_scale {
|
||||
let mut uncond_tokens = tokenizer
|
||||
.encode(uncond_prompt, true)
|
||||
.map_err(E::msg)?
|
||||
.get_ids()
|
||||
.to_vec();
|
||||
let mut uncond_tokens = tokenizer.encode(uncond_prompt, true).map_err(E::msg)?.get_ids().to_vec();
|
||||
if uncond_tokens.len() > sd_config.clip.max_position_embeddings {
|
||||
anyhow::bail!(
|
||||
"the negative prompt is too long, {} > max-tokens ({})",
|
||||
uncond_tokens.len(),
|
||||
sd_config.clip.max_position_embeddings
|
||||
)
|
||||
anyhow::bail!("the negative prompt is too long, {} > max-tokens ({})", uncond_tokens.len(), sd_config.clip.max_position_embeddings)
|
||||
}
|
||||
while uncond_tokens.len() < sd_config.clip.max_position_embeddings {
|
||||
uncond_tokens.push(pad_id)
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
|
||||
use crate::events::notification_event::{NotificationEvent, NotificationModelType};
|
||||
use crate::ml::image::dynamic_image_to_tensor::dynamic_image_to_tensor;
|
||||
use crate::ml::image::tensor_to_image_buffer::{tensor_to_image_buffer, RgbImage};
|
||||
use crate::ml::model_cache::ModelCache;
|
||||
use crate::ml::model_file::{ModelFile, StableDiffusionVersion};
|
||||
use crate::ml::model_type::ModelType;
|
||||
use crate::ml::models::unet_model::UNetModel;
|
||||
use crate::ml::prompt_cache::PromptCache;
|
||||
use crate::ml::stable_diffusion::get_vae_scale::get_vae_scale;
|
||||
use crate::ml::stable_diffusion::infer_clip_text_embeddings::infer_clip_text_embeddings;
|
||||
use crate::state::app_config::AppConfig;
|
||||
use crate::state::app_dir::AppDataRoot;
|
||||
use anyhow::{anyhow, Error as E, Result};
|
||||
use candle_core::{DType, IndexOp, Tensor, D};
|
||||
use candle_nn::VarBuilder;
|
||||
use candle_transformers::models::stable_diffusion::lcm::LCMScheduler;
|
||||
use candle_transformers::models::stable_diffusion::unet_2d::BlockConfig;
|
||||
use candle_transformers::models::stable_diffusion::unet_2d::UNet2DConditionModel;
|
||||
use candle_transformers::models::stable_diffusion::unet_2d::UNet2DConditionModelConfig;
|
||||
use candle_transformers::models::stable_diffusion::vae::{AutoEncoderKL, DiagonalGaussianDistribution};
|
||||
use image::DynamicImage;
|
||||
use log::info;
|
||||
use rand::Rng;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
pub struct Args<'a> {
|
||||
pub image: &'a DynamicImage,
|
||||
pub prompt: String,
|
||||
pub uncond_prompt: String,
|
||||
pub cfg_scale: Option<f64>,
|
||||
pub i2i_strength: Option<u8>,
|
||||
pub configs: &'a AppConfig,
|
||||
pub model_cache: &'a ModelCache,
|
||||
pub prompt_cache: &'a PromptCache,
|
||||
pub app_data_root: &'a AppDataRoot,
|
||||
pub app: &'a AppHandle,
|
||||
pub use_flash_attn: bool,
|
||||
}
|
||||
|
||||
pub fn lcm_pipeline(args: Args<'_>) -> Result<RgbImage> {
|
||||
let Args { prompt, uncond_prompt, cfg_scale, i2i_strength, configs, model_cache, prompt_cache, app, image, app_data_root, use_flash_attn } = args;
|
||||
|
||||
let weights_dir = app_data_root.weights_dir();
|
||||
|
||||
let img2img_strength = args.i2i_strength.unwrap_or(75) as f64 / 100.0;
|
||||
let use_f16 = true;
|
||||
|
||||
//println!("Starting image generation with the following configuration:");
|
||||
//println!(" Model: {:?}", configs.sd_version);
|
||||
//println!(" Prompt: {}", prompt);
|
||||
//println!(" Steps: {}", configs.scheduler_steps);
|
||||
//println!(" Device: {:?}", configs.device);
|
||||
|
||||
//println!("Model dimensions: {}x{}", configs.sd_config.width, configs.sd_config.height);
|
||||
|
||||
// Use LCM Scheduler instead of Euler Ancestral for better speed and quality
|
||||
let mut scheduler = LCMScheduler::new(configs.scheduler_steps, img2img_strength, candle_transformers::models::stable_diffusion::lcm::LCMSchedulerConfig::default())?;
|
||||
|
||||
let seed = configs.seed.unwrap_or_else(|| rand::thread_rng().gen());
|
||||
configs.device.set_seed(seed)?;
|
||||
|
||||
//info!("Using seed: {}", seed);
|
||||
|
||||
let guidance_scale = match cfg_scale {
|
||||
Some(guidance_scale) => guidance_scale,
|
||||
None => match configs.sd_version {
|
||||
StableDiffusionVersion::V1_5 | StableDiffusionVersion::V2_1 | StableDiffusionVersion::Xl => 7.5,
|
||||
StableDiffusionVersion::Turbo => 0.,
|
||||
_ => 0., // NB: Not sure what the other model families should use, so sticking with "0"
|
||||
},
|
||||
};
|
||||
|
||||
// let use_guide_scale = guidance_scale > 1.0;
|
||||
let use_guide_scale = false;
|
||||
|
||||
//info!("Using guide scale: {}", use_guide_scale);
|
||||
|
||||
//info!("Checking if prompt is cached");
|
||||
let maybe_cached = prompt_cache.get_copy(&prompt)?;
|
||||
|
||||
let mut text_embeddings = if let Some(tensor) = maybe_cached {
|
||||
tensor
|
||||
} else {
|
||||
info!("Loading clip weights...");
|
||||
//println!("Loading clip weights for {:?} with model {:?}", configs.sd_version, configs.sd_version.repo());
|
||||
//let api = configs.hf_api.clone();
|
||||
|
||||
//let clip_path = api.model(configs.sd_version.repo().to_string()).get(if use_f16 { "text_encoder/model.fp16.safetensors" } else { "text_encoder/model.safetensors" })?;
|
||||
|
||||
//println!(">>>> CLIP REPO = {:?}", configs.sd_version.repo());
|
||||
//println!(">>>> CLIP PATH = {:?}", clip_path);
|
||||
//println!(">>>> USE_FP16 = {:?}", use_f16);
|
||||
//let clip_weights = Some(clip_path.to_string_lossy().to_string());
|
||||
|
||||
let clip_weights = weights_dir.model_path(&ModelType::LykonDreamshaper7TextEncoderFp16);
|
||||
let clip_weights = Some(clip_weights.to_string_lossy().to_string());
|
||||
|
||||
info!("Prompt is NOT cached! Calculating embedding...");
|
||||
let tensor = infer_clip_text_embeddings(
|
||||
&prompt,
|
||||
&uncond_prompt,
|
||||
None, // tokenizer
|
||||
clip_weights,
|
||||
None, // clip2_weights
|
||||
configs.sd_version,
|
||||
&configs.sd_config,
|
||||
use_f16, // use_f16
|
||||
&configs.device,
|
||||
configs.dtype,
|
||||
use_guide_scale,
|
||||
weights_dir,
|
||||
)?;
|
||||
prompt_cache.store_copy(&prompt, &tensor)?;
|
||||
tensor
|
||||
};
|
||||
|
||||
println!("Text embeddings shape: {:?}", text_embeddings.shape());
|
||||
|
||||
println!("Loading input image into tensor...");
|
||||
|
||||
let input_image = dynamic_image_to_tensor(image, &configs.device, configs.dtype)?;
|
||||
|
||||
println!("Reference image shape: {:?}", input_image.shape());
|
||||
|
||||
let vae_scale = get_vae_scale(configs.sd_version);
|
||||
|
||||
let maybe_vae = model_cache.get_vae()?;
|
||||
|
||||
let vae = match maybe_vae {
|
||||
Some(vae) => vae,
|
||||
None => {
|
||||
//let repo = configs.sd_version.repo();
|
||||
//println!(">>>>> VAE REPO = {:?}", repo);
|
||||
//println!("Building VAE model from : {:?} ... (3)", repo);
|
||||
//let vae_file = configs.hf_api.model(repo.to_string()).get("vae/diffusion_pytorch_model.safetensors")?;
|
||||
|
||||
let vae_file = weights_dir.model_path(&ModelType::LykonDreamshaper7Vae);
|
||||
|
||||
println!("Building VAE model from file {:?}...", &vae_file);
|
||||
|
||||
let vae = configs.sd_config.build_vae(vae_file, &configs.device, configs.dtype)?;
|
||||
|
||||
let vae = Arc::new(vae);
|
||||
|
||||
model_cache.set_vae(vae.clone())?;
|
||||
|
||||
vae
|
||||
}
|
||||
};
|
||||
|
||||
let maybe_unet = model_cache.get_unet()?;
|
||||
|
||||
let unet = match maybe_unet {
|
||||
Some(unet) => unet,
|
||||
None => {
|
||||
info!("No unet found in cache; loading...");
|
||||
|
||||
let unet_weights = weights_dir.model_path(&ModelType::SimianLuoLcmDreamshaperV7Unet);
|
||||
|
||||
let in_channels = match configs.sd_version {
|
||||
StableDiffusionVersion::XlInpaint | StableDiffusionVersion::V2Inpaint | StableDiffusionVersion::V1_5Inpaint => 9,
|
||||
_ => 4,
|
||||
};
|
||||
|
||||
// For LCM models, ensure that:
|
||||
// 1. We're using the correct prediction type (epsilon)
|
||||
// 2. The cross-attention dimension is correct (768 for Dreamshaper)
|
||||
// 3. The in_channels matches the expected input (4 for standard, 9 for inpainting)
|
||||
let unet_config = UNet2DConditionModelConfig {
|
||||
// LCM specific config values
|
||||
cross_attention_dim: 768, // Dreamshaper v7 uses 768
|
||||
time_cond_proj_dim: Some(256), // LCM models use 256-dimensional guidance embeddings
|
||||
// Rest of configuration...
|
||||
center_input_sample: false,
|
||||
flip_sin_to_cos: true,
|
||||
freq_shift: 0.0,
|
||||
blocks: vec![
|
||||
BlockConfig { out_channels: 320, use_cross_attn: Some(1), attention_head_dim: 8 },
|
||||
BlockConfig { out_channels: 640, use_cross_attn: Some(1), attention_head_dim: 8 },
|
||||
BlockConfig { out_channels: 1280, use_cross_attn: Some(1), attention_head_dim: 8 },
|
||||
BlockConfig { out_channels: 1280, use_cross_attn: None, attention_head_dim: 8 }
|
||||
],
|
||||
layers_per_block: 2,
|
||||
downsample_padding: 1,
|
||||
mid_block_scale_factor: 1.0,
|
||||
norm_num_groups: 32,
|
||||
norm_eps: 1e-5,
|
||||
sliced_attention_size: None,
|
||||
use_linear_projection: false,
|
||||
time_embed_dim: Some(1280),
|
||||
// time_embed_dim: Some(256),
|
||||
};
|
||||
|
||||
// Load the model directly from the safetensors file
|
||||
let vs_unet = unsafe { VarBuilder::from_mmaped_safetensors(&[unet_weights], configs.dtype, &configs.device)? };
|
||||
|
||||
let unet = UNet2DConditionModel::new(vs_unet, in_channels, 4, use_flash_attn, unet_config)?;
|
||||
|
||||
let unet = UNetModel::new_with_inner(unet);
|
||||
|
||||
let unet = Arc::new(unet);
|
||||
|
||||
model_cache.set_unet(unet.clone())?;
|
||||
|
||||
unet
|
||||
},
|
||||
};
|
||||
|
||||
let init_latent_dist: DiagonalGaussianDistribution = vae.encode(&input_image)?;
|
||||
|
||||
// Parse and calculate the img2img strength
|
||||
let img2img_strength = match i2i_strength {
|
||||
None => 0.75f64,
|
||||
Some(strength) => (strength as f64) / 100.0f64,
|
||||
};
|
||||
|
||||
// Create LCM guidance scale embeddings
|
||||
let embedding_dim = 1280;
|
||||
let guidance_scale_embedding = scheduler.get_guidance_scale_embedding(guidance_scale, embedding_dim, &configs.device, configs.dtype)?;
|
||||
|
||||
// Generate latents from input image using the LCM approach
|
||||
println!("Generating latents from input image...");
|
||||
let init_latents = (init_latent_dist.sample()? * vae_scale)?;
|
||||
println!("Initial latents shape: {:?}", init_latents.shape());
|
||||
|
||||
// Calculate timesteps for LCM with proper img2img handling
|
||||
let timesteps = LCMScheduler::get_timesteps_for_steps(configs.scheduler_steps, img2img_strength);
|
||||
let t_start = timesteps[0]; // First denoising step
|
||||
|
||||
// Add noise at the appropriate timestep for img2img
|
||||
println!("Adding noise to latents for t_start={}", t_start);
|
||||
let noise = init_latents.randn_like(0f64, 1f64)?;
|
||||
let latents = scheduler.add_noise(&init_latents, noise, t_start)?;
|
||||
|
||||
println!("Latents initialized successfully");
|
||||
println!("Initial noise shape: {:?}", latents.shape());
|
||||
|
||||
println!("Starting diffusion process...");
|
||||
|
||||
let mut latents = latents;
|
||||
let timesteps: Vec<_> = scheduler.timesteps().iter().copied().collect();
|
||||
|
||||
for (timestep_index, ×tep) in timesteps.iter().enumerate() {
|
||||
println!("Processing step {}/{} @ timestamp = {}", timestep_index + 1, timesteps.len(), timestep);
|
||||
|
||||
let latent_model_input = if use_guide_scale { Tensor::cat(&[&latents, &latents], 0)? } else { latents.clone() };
|
||||
|
||||
let latent_model_input = scheduler.scale_model_input(latent_model_input, timestep);
|
||||
|
||||
// Use the guidance scale embedding in the UNet inference with the proper method name
|
||||
let mut noise_pred = match unet.forward_with_guidance(&latent_model_input, timestep as f64, &text_embeddings, &guidance_scale_embedding) {
|
||||
Ok(pred) => pred,
|
||||
Err(e) => {
|
||||
println!("UNet inference failed with error: {}", e);
|
||||
return Err(anyhow::anyhow!("UNet inference failed: {}", e));
|
||||
},
|
||||
};
|
||||
|
||||
if use_guide_scale {
|
||||
let chunks = noise_pred.chunk(2, 0)?;
|
||||
let (noise_pred_uncond, noise_pred_text) = (&chunks[0], &chunks[1]);
|
||||
noise_pred = (noise_pred_uncond + (guidance_scale * (noise_pred_text - noise_pred_uncond)?)?)?;
|
||||
}
|
||||
|
||||
// Apply the LCM scheduler step
|
||||
latents = scheduler.step(&noise_pred, timestep, &latents, timestep_index, configs.scheduler_steps)?;
|
||||
}
|
||||
|
||||
println!("Diffusion process completed, decoding image...");
|
||||
let image = vae.decode(&(latents / vae_scale)?)?;
|
||||
|
||||
println!("VAE decode completed");
|
||||
|
||||
println!("Scaling image back");
|
||||
let image = ((image / 2.)? + 0.5)?;
|
||||
|
||||
println!("Normalized image values");
|
||||
let image = (image.clamp(0f32, 1.)? * 255.)?;
|
||||
|
||||
println!("Convert to int8");
|
||||
let image = image.to_dtype(DType::U8)?;
|
||||
|
||||
println!("Converted to 8-bit format");
|
||||
|
||||
let image = tensor_to_image_buffer(&image.i(0)?)?;
|
||||
|
||||
Ok(image)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod get_vae_scale;
|
||||
pub mod infer_clip_text_embeddings;
|
||||
pub mod stable_diffusion_pipeline;
|
||||
pub mod stable_diffusion_pipeline;
|
||||
pub mod lcm_pipeline;
|
||||
@@ -1,9 +1,12 @@
|
||||
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
|
||||
use crate::events::notification_event::{NotificationModelType, NotificationEvent};
|
||||
use crate::ml::image::dynamic_image_to_tensor::dynamic_image_to_tensor;
|
||||
use crate::ml::image::tensor_to_image_buffer::{tensor_to_image_buffer, RgbImage};
|
||||
use crate::ml::model_cache::ModelCache;
|
||||
use crate::ml::model_file::StableDiffusionVersion;
|
||||
use crate::ml::model_type::ModelType;
|
||||
use crate::ml::models::unet_model::UNetModel;
|
||||
use crate::ml::prompt_cache::PromptCache;
|
||||
use crate::ml::stable_diffusion::get_vae_scale::get_vae_scale;
|
||||
use crate::ml::stable_diffusion::infer_clip_text_embeddings::infer_clip_text_embeddings;
|
||||
@@ -15,8 +18,7 @@ use image::DynamicImage;
|
||||
use log::info;
|
||||
use rand::Rng;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use crate::events::notification_event::{ModelType, NotificationEvent};
|
||||
use crate::ml::models::unet_model::UNetModel;
|
||||
use crate::state::app_dir::AppDataRoot;
|
||||
|
||||
pub struct Args<'a> {
|
||||
pub image: &'a DynamicImage,
|
||||
@@ -27,6 +29,7 @@ pub struct Args<'a> {
|
||||
pub configs: &'a AppConfig,
|
||||
pub model_cache: &'a ModelCache,
|
||||
pub prompt_cache: &'a PromptCache,
|
||||
pub app_data_root: &'a AppDataRoot,
|
||||
pub app: &'a AppHandle,
|
||||
}
|
||||
|
||||
@@ -40,9 +43,12 @@ pub fn stable_diffusion_pipeline(args: Args<'_>) -> Result<RgbImage> {
|
||||
model_cache,
|
||||
prompt_cache,
|
||||
app,
|
||||
image,
|
||||
image,
|
||||
app_data_root,
|
||||
} = args;
|
||||
|
||||
let weights_dir = app_data_root.weights_dir();
|
||||
|
||||
println!("Starting image generation with the following configuration:");
|
||||
println!(" Model: {:?}", configs.sd_version);
|
||||
println!(" Prompt: {}", prompt);
|
||||
@@ -97,6 +103,7 @@ pub fn stable_diffusion_pipeline(args: Args<'_>) -> Result<RgbImage> {
|
||||
&configs.device,
|
||||
configs.dtype,
|
||||
use_guide_scale,
|
||||
weights_dir,
|
||||
)?;
|
||||
prompt_cache.store_copy(&prompt, &tensor)?;
|
||||
tensor
|
||||
@@ -122,25 +129,18 @@ pub fn stable_diffusion_pipeline(args: Args<'_>) -> Result<RgbImage> {
|
||||
None => {
|
||||
info!("No vae found in cache; loading...");
|
||||
|
||||
let repo = configs.sd_version.repo();
|
||||
|
||||
println!("Building VAE model from : {:?} ... (3)", repo);
|
||||
|
||||
let vae_file = configs.hf_api.model(repo.to_string())
|
||||
.get("vae/diffusion_pytorch_model.safetensors")?;
|
||||
|
||||
println!("Building VAE model from file {:?}...", &vae_file);
|
||||
|
||||
let mut notify_download_complete = false;
|
||||
//if !vae_file.exists() {
|
||||
if true {
|
||||
notify_download_complete = true;
|
||||
app.emit("notification", NotificationEvent::ModelDownloadStarted {
|
||||
model_name: repo,
|
||||
model_type: ModelType::Vae,
|
||||
model_name: "sdxl-turbo-vae",
|
||||
model_type: NotificationModelType::Vae,
|
||||
})?;
|
||||
}
|
||||
|
||||
let vae_file = weights_dir.model_path(&ModelType::SdxlTurboVae);
|
||||
|
||||
let vae = configs
|
||||
.sd_config
|
||||
.build_vae(vae_file, &configs.device, configs.dtype)?;
|
||||
@@ -151,8 +151,8 @@ pub fn stable_diffusion_pipeline(args: Args<'_>) -> Result<RgbImage> {
|
||||
|
||||
if notify_download_complete {
|
||||
app.emit("notification", NotificationEvent::ModelDownloadComplete {
|
||||
model_name: repo,
|
||||
model_type: ModelType::Vae,
|
||||
model_name: "sdxl-turbo-vae",
|
||||
model_type: NotificationModelType::Vae,
|
||||
})?;
|
||||
}
|
||||
|
||||
@@ -167,23 +167,17 @@ pub fn stable_diffusion_pipeline(args: Args<'_>) -> Result<RgbImage> {
|
||||
None => {
|
||||
info!("No unet found in cache; loading...");
|
||||
|
||||
let repo = configs.sd_version.repo();
|
||||
|
||||
info!("Downloading UNET model files from: {} ...", repo);
|
||||
|
||||
let unet_file = configs.hf_api.model(repo.to_string())
|
||||
.get("unet/diffusion_pytorch_model.safetensors")
|
||||
.map_err(|err| anyhow!("error fetching model: {:?}", err))?;
|
||||
|
||||
let mut notify_download_complete = false;
|
||||
//if !unet_file.exists() {
|
||||
if true {
|
||||
notify_download_complete = true;
|
||||
app.emit("notification", NotificationEvent::ModelDownloadStarted {
|
||||
model_name: repo,
|
||||
model_type: ModelType::Unet,
|
||||
model_name: "sdxl-turbo-unet",
|
||||
model_type: NotificationModelType::Unet,
|
||||
})?;
|
||||
}
|
||||
|
||||
let unet_file = weights_dir.model_path(&ModelType::SdxlTurboUnet);
|
||||
|
||||
let unet = UNetModel::new(&configs.sd_config, unet_file, &configs.device, configs.dtype)
|
||||
.map_err(|err| anyhow!("error initializing unet model: {:?}", err))?;
|
||||
@@ -194,8 +188,8 @@ pub fn stable_diffusion_pipeline(args: Args<'_>) -> Result<RgbImage> {
|
||||
|
||||
if notify_download_complete {
|
||||
app.emit("notification", NotificationEvent::ModelDownloadComplete {
|
||||
model_name: repo,
|
||||
model_type: ModelType::Unet,
|
||||
model_name: "sdxl-turbo-unet",
|
||||
model_type: NotificationModelType::Unet,
|
||||
})?;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
use crate::ml::model_file::StableDiffusionVersion;
|
||||
use crate::state::app_dir::AppDataRoot;
|
||||
use crate::state::os_platform::OsPlatform;
|
||||
use crate::state::yaml_config::YamlConfig;
|
||||
use candle_core::{DType, Device};
|
||||
use candle_transformers::models::stable_diffusion::StableDiffusionConfig;
|
||||
use filesys::recursively_find_file_by_name::recursively_find_file_by_name;
|
||||
use hf_hub::api::sync::Api;
|
||||
use log::info;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const CONFIG_FILENAME : &str = "app_config.yaml";
|
||||
|
||||
const DEFAULT_SD_IMAGE_WIDTH: usize = 512;
|
||||
const DEFAULT_SD_IMAGE_HEIGHT: usize = 512;
|
||||
const DEFAULT_SD_IMAGE_WIDTH: usize = 1024;
|
||||
const DEFAULT_SD_IMAGE_HEIGHT: usize = 1024;
|
||||
|
||||
const DEFAULT_SD_NUMERIC_DATATYPE: DType = DType::F32;
|
||||
const SD_VERSION: StableDiffusionVersion = StableDiffusionVersion::Turbo;
|
||||
const SD_VERSION: StableDiffusionVersion = StableDiffusionVersion::V1_5;
|
||||
|
||||
/// A central place to configure app-wide details.
|
||||
pub struct AppConfig {
|
||||
@@ -37,13 +38,15 @@ pub struct AppConfig {
|
||||
|
||||
/// Probably shouldn't be here.
|
||||
pub hf_api: Api,
|
||||
|
||||
/// Root location for application data.
|
||||
pub app_data_root: AppDataRoot,
|
||||
}
|
||||
|
||||
impl AppConfig {
|
||||
|
||||
pub fn init() -> anyhow::Result<Self> {
|
||||
println!("Configuration filename: {}", CONFIG_FILENAME);
|
||||
let yaml_configs = load_yaml_configs();
|
||||
let yaml_configs = YamlConfig::load_from_config_file_recursive();
|
||||
|
||||
println!("Configs: {:?}", yaml_configs);
|
||||
|
||||
@@ -52,18 +55,42 @@ impl AppConfig {
|
||||
info!("CUDA not available ({}), falling back to CPU", e);
|
||||
Device::Cpu
|
||||
});
|
||||
|
||||
|
||||
let sd_config = match SD_VERSION {
|
||||
StableDiffusionVersion::Turbo => {
|
||||
StableDiffusionConfig::sdxl_turbo(None, Some(DEFAULT_SD_IMAGE_HEIGHT), Some(DEFAULT_SD_IMAGE_WIDTH))
|
||||
}
|
||||
_ => {
|
||||
StableDiffusionVersion::V1_5 | StableDiffusionVersion::V1_5Inpaint => {
|
||||
StableDiffusionConfig::v1_5(None, Some(DEFAULT_SD_IMAGE_HEIGHT), Some(DEFAULT_SD_IMAGE_WIDTH))
|
||||
}
|
||||
StableDiffusionVersion::V2_1 | StableDiffusionVersion::V2Inpaint => {
|
||||
StableDiffusionConfig::v2_1(None, Some(DEFAULT_SD_IMAGE_HEIGHT), Some(DEFAULT_SD_IMAGE_WIDTH))
|
||||
}
|
||||
StableDiffusionVersion::Xl | StableDiffusionVersion::XlInpaint => {
|
||||
StableDiffusionConfig::sdxl(None, Some(DEFAULT_SD_IMAGE_HEIGHT), Some(DEFAULT_SD_IMAGE_WIDTH))
|
||||
}
|
||||
};
|
||||
|
||||
let hf_api = Api::new()?;
|
||||
|
||||
let os_platform = OsPlatform::get();
|
||||
|
||||
let default_data_root = match os_platform {
|
||||
OsPlatform::Linux => yaml_configs.linux_default_data_path.as_deref(),
|
||||
OsPlatform::MacOs => yaml_configs.mac_default_data_path.as_deref(),
|
||||
OsPlatform::Windows => yaml_configs.windows_default_data_path.as_deref(),
|
||||
};
|
||||
|
||||
println!("Possible user-defined data root: {:?}", default_data_root);
|
||||
|
||||
let app_data_root = match default_data_root {
|
||||
None => AppDataRoot::create_default()?,
|
||||
Some(path) => AppDataRoot::create_existing(path)?,
|
||||
};
|
||||
|
||||
println!("App data root: {:?}", app_data_root.path());
|
||||
|
||||
Ok(Self {
|
||||
device,
|
||||
dtype: DEFAULT_SD_NUMERIC_DATATYPE,
|
||||
@@ -71,26 +98,12 @@ impl AppConfig {
|
||||
sd_config,
|
||||
image_height: yaml_configs.image_height.unwrap_or(DEFAULT_SD_IMAGE_HEIGHT),
|
||||
image_width: yaml_configs.image_width.unwrap_or(DEFAULT_SD_IMAGE_WIDTH),
|
||||
scheduler_steps: yaml_configs.scheduler_steps.unwrap_or(1),
|
||||
scheduler_steps: yaml_configs.scheduler_steps.unwrap_or(4),
|
||||
scheduler_samples: yaml_configs.scheduler_samples.unwrap_or(15),
|
||||
seed: yaml_configs.seed,
|
||||
cfg_scale: yaml_configs.cfg_scale,
|
||||
hf_api,
|
||||
app_data_root,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn load_yaml_configs() -> YamlConfig {
|
||||
let maybe_path = recursively_find_file_by_name(CONFIG_FILENAME, "../../../", 5)
|
||||
.ok()
|
||||
.flatten();
|
||||
if let Some(path) = maybe_path {
|
||||
let path = path.canonicalize().unwrap_or(path);
|
||||
println!("Attempting to load configs from: {:?}", &path);
|
||||
if let Ok(config) = YamlConfig::read_from_file(path) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
println!("Loading default configs.");
|
||||
YamlConfig::default()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
use crate::ml::model_type::ModelType;
|
||||
use crate::state::expanduser::expanduser;
|
||||
use crate::state::os_platform::OsPlatform;
|
||||
use anyhow::anyhow;
|
||||
use directories::UserDirs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tempdir::TempDir;
|
||||
use tempfile::{Builder, NamedTempFile};
|
||||
const DEFAULT_DATA_DIR : &str = "artcraft";
|
||||
const ASSETS_SUBDIRECTORY : &str = "assets";
|
||||
const WEIGHTS_SUBDIRECTORY : &str = "weights";
|
||||
|
||||
const TEMPORARY_SUBDIRECTORY : &str = "temp";
|
||||
|
||||
/// The path to the application data directory, which includes "asset" and "weights" data.
|
||||
#[derive(Clone)]
|
||||
pub struct AppDataRoot {
|
||||
path: PathBuf,
|
||||
assets_dir: AppAssetsDir,
|
||||
weights_dir: AppWeightsDir,
|
||||
temp_dir: TemporaryDir,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppAssetsDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppWeightsDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TemporaryDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl AppDataRoot {
|
||||
pub fn create_default() -> anyhow::Result<Self> {
|
||||
let directory = get_default_data_dir()?;
|
||||
Self::create_existing(directory)
|
||||
}
|
||||
|
||||
pub fn create_existing<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
|
||||
let mut dir = dir.as_ref().to_path_buf();
|
||||
|
||||
match OsPlatform::get() {
|
||||
OsPlatform::Linux | OsPlatform::MacOs => {
|
||||
if let Some(d) = dir.as_os_str().to_str() {
|
||||
dir = expanduser(d)?;
|
||||
}
|
||||
},
|
||||
OsPlatform::Windows => {}
|
||||
}
|
||||
|
||||
if !dir.is_dir() {
|
||||
println!("Creating directory {:?}", dir);
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
}
|
||||
|
||||
match dir.canonicalize() {
|
||||
Ok(d) => dir = d,
|
||||
Err(err) => {
|
||||
println!("Error canonicalizing {:?}: {}", dir, err);
|
||||
}
|
||||
}
|
||||
|
||||
let assets_dir = AppAssetsDir::create_existing(dir.join(ASSETS_SUBDIRECTORY))?;
|
||||
let weights_dir = AppWeightsDir::create_existing(dir.join(WEIGHTS_SUBDIRECTORY))?;
|
||||
let temp_dir = TemporaryDir::create_existing(dir.join(TEMPORARY_SUBDIRECTORY))?;
|
||||
|
||||
Ok(Self {
|
||||
path: dir,
|
||||
assets_dir,
|
||||
weights_dir,
|
||||
temp_dir,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn assets_dir(&self) -> &AppAssetsDir {
|
||||
&self.assets_dir
|
||||
}
|
||||
|
||||
pub fn weights_dir(&self) -> &AppWeightsDir {
|
||||
&self.weights_dir
|
||||
}
|
||||
|
||||
pub fn temp_dir(&self) -> &TemporaryDir {
|
||||
&self.temp_dir
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl AppAssetsDir {
|
||||
pub fn create_existing<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
|
||||
let mut dir = dir.as_ref().to_path_buf();
|
||||
match dir.canonicalize() {
|
||||
Ok(d) => dir = d,
|
||||
Err(err) => {
|
||||
println!("Error canonicalizing {:?}: {}", dir, err);
|
||||
}
|
||||
}
|
||||
if !dir.exists() {
|
||||
println!("Creating directory {:?}", dir);
|
||||
std::fs::create_dir(&dir)?;
|
||||
}
|
||||
Ok(Self {
|
||||
path: dir,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl AppWeightsDir {
|
||||
pub fn create_existing<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
|
||||
let mut dir = dir.as_ref().to_path_buf();
|
||||
match dir.canonicalize() {
|
||||
Ok(d) => dir = d,
|
||||
Err(err) => {
|
||||
println!("Error canonicalizing {:?}: {}", dir, err);
|
||||
}
|
||||
}
|
||||
if !dir.exists() {
|
||||
println!("Creating directory {:?}", dir);
|
||||
std::fs::create_dir(&dir)?;
|
||||
}
|
||||
Ok(Self {
|
||||
path: dir,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn model_path(&self, model_registry: &ModelType) -> PathBuf {
|
||||
self.path.join(model_registry.get_filename())
|
||||
}
|
||||
}
|
||||
|
||||
impl TemporaryDir {
|
||||
pub fn create_existing<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
|
||||
let mut dir = dir.as_ref().to_path_buf();
|
||||
match dir.canonicalize() {
|
||||
Ok(d) => dir = d,
|
||||
Err(err) => {
|
||||
println!("Error canonicalizing {:?}: {}", dir, err);
|
||||
}
|
||||
}
|
||||
if !dir.exists() {
|
||||
println!("Creating directory {:?}", dir);
|
||||
std::fs::create_dir(&dir)?;
|
||||
}
|
||||
Ok(Self {
|
||||
path: dir,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_prefix(&self, prefix: &str) -> anyhow::Result<TempDir> {
|
||||
let tempdir = TempDir::new_in(&self.path, prefix)?;
|
||||
Ok(tempdir)
|
||||
}
|
||||
|
||||
pub fn new_named_temp_file(&self) -> anyhow::Result<NamedTempFile> {
|
||||
let tempfile = Builder::new()
|
||||
.prefix("temp_")
|
||||
.suffix(".bin")
|
||||
.tempfile_in(&self.path)?;
|
||||
Ok(tempfile)
|
||||
}
|
||||
}
|
||||
|
||||
// eg. /home/bob/artcraft, /Users/bob/artcraft, or C:\Users\Alice\artcraft
|
||||
fn get_default_data_dir() -> anyhow::Result<PathBuf> {
|
||||
Ok(UserDirs::new()
|
||||
.ok_or_else(|| anyhow!("could not determine user home directory"))?
|
||||
.home_dir()
|
||||
.join(DEFAULT_DATA_DIR))
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use std::io;
|
||||
use std::path::{PathBuf, MAIN_SEPARATOR};
|
||||
|
||||
static PREFIX: Lazy<String> = Lazy::new(|| format!("~{}", MAIN_SEPARATOR));
|
||||
|
||||
/// The "expanduser" crate doesn't compile on Windows, so we replace its functionality slightly
|
||||
pub fn expanduser<P: AsRef<str>>(path: P) -> io::Result<PathBuf> {
|
||||
|
||||
Ok(match path.as_ref() {
|
||||
// matches an exact "~"
|
||||
s if s == "~" => {
|
||||
home_dir()?
|
||||
},
|
||||
// matches paths that start with `~/`
|
||||
s if s.starts_with(&*PREFIX) => {
|
||||
let home = home_dir()?;
|
||||
home.join(&s[2..])
|
||||
},
|
||||
// // matches paths that start with `~` but not `~/`, might be a `~username/` path
|
||||
// s if s.starts_with("~") => {
|
||||
// let mut parts = s[1..].splitn(2, MAIN_SEPARATOR);
|
||||
// let user = parts.next()
|
||||
// .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "malformed path"))?;
|
||||
// let user = Passwd::from_name(&user)
|
||||
// .map_err(|_| io::Error::new(io::ErrorKind::Other, "error searching for user"))?
|
||||
// .ok_or_else(|| io::Error::new(io::ErrorKind::Other, format!("user '{}', does not exist", &user)))?;
|
||||
// if let Some(ref path) = parts.next() {
|
||||
// PathBuf::from(user.dir).join(&path)
|
||||
// } else {
|
||||
// PathBuf::from(user.dir)
|
||||
// }
|
||||
// },
|
||||
// nothing to expand, just make a PathBuf
|
||||
s => PathBuf::from(s)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn home_dir() -> io::Result<PathBuf> {
|
||||
dirs::home_dir()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "no home directory is set"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::state::expanduser::expanduser;
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn test_success() {
|
||||
let old_home = env::var("HOME").expect("no home dir set");
|
||||
let new_home = "/home/foo";
|
||||
env::set_var("HOME", new_home);
|
||||
let path = expanduser("~/path/to/directory");
|
||||
env::set_var("HOME", old_home);
|
||||
assert_eq!(path.expect("io error"), PathBuf::from("/home/foo/path/to/directory"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn test_only_tilde() {
|
||||
let old_home = env::var("HOME").expect("no home dir set");
|
||||
let new_home = "/home/foo";
|
||||
env::set_var("HOME", new_home);
|
||||
let pathstr = "~";
|
||||
let path = expanduser(pathstr);
|
||||
env::set_var("HOME", old_home);
|
||||
assert_eq!(path.expect("io error"), PathBuf::from("/home/foo"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,2 +1,5 @@
|
||||
pub mod app_config;
|
||||
pub mod app_dir;
|
||||
pub mod os_platform;
|
||||
pub mod yaml_config;
|
||||
pub mod app_config;
|
||||
pub mod expanduser;
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
#[derive(Copy,Clone,Eq,PartialEq,Debug)]
|
||||
pub enum OsPlatform {
|
||||
Linux,
|
||||
MacOs,
|
||||
Windows,
|
||||
}
|
||||
|
||||
impl OsPlatform {
|
||||
pub fn get() -> OsPlatform {
|
||||
// Totally scientific.
|
||||
// https://doc.rust-lang.org/std/env/consts/constant.OS.html
|
||||
match std::env::consts::OS {
|
||||
"linux" => OsPlatform::Linux,
|
||||
"windows" => OsPlatform::Windows,
|
||||
"macos" | "apple" => OsPlatform::MacOs,
|
||||
_ => OsPlatform::Linux,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
use filesys::recursively_find_file_by_name::recursively_find_file_by_name;
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::path::Path;
|
||||
|
||||
const CONFIG_FILENAME : &str = "app_config.yaml";
|
||||
|
||||
/// Configuration read or derived from a YAML config file at startup
|
||||
/// This is not meant to contain any runtime config values.
|
||||
/// This may not be exhaustive.
|
||||
@@ -13,6 +16,15 @@ pub struct YamlConfig {
|
||||
pub scheduler_samples: Option<usize>,
|
||||
pub seed: Option<u64>,
|
||||
pub cfg_scale: Option<f64>,
|
||||
|
||||
/// Override the default location to store data on Linux
|
||||
pub linux_default_data_path: Option<String>,
|
||||
|
||||
/// Override the default location to store data on Windows
|
||||
pub windows_default_data_path: Option<String>,
|
||||
|
||||
/// Override the default location to store data on Mac
|
||||
pub mac_default_data_path: Option<String>,
|
||||
}
|
||||
|
||||
impl YamlConfig {
|
||||
@@ -22,4 +34,19 @@ impl YamlConfig {
|
||||
let config = serde_yaml::from_reader(reader)?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn load_from_config_file_recursive() -> YamlConfig {
|
||||
let maybe_path = recursively_find_file_by_name(CONFIG_FILENAME, "../../../", 5)
|
||||
.ok()
|
||||
.flatten();
|
||||
if let Some(path) = maybe_path {
|
||||
let path = path.canonicalize().unwrap_or(path);
|
||||
println!("Attempting to load configs from: {:?}", &path);
|
||||
if let Ok(config) = YamlConfig::read_from_file(path) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
println!("Couldn't load config file. Using default configs.");
|
||||
YamlConfig::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
use crate::events::notification_event::{NotificationEvent, NotificationModelType};
|
||||
use crate::ml::model_type::ModelType;
|
||||
use crate::state::app_dir::AppDataRoot;
|
||||
use crate::transfer::download::{download_async, ProgressUpdate};
|
||||
use futures::stream::FuturesUnordered;
|
||||
use futures::StreamExt;
|
||||
use log::{debug, error, info};
|
||||
use std::collections::VecDeque;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::Receiver;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tempfile::NamedTempFile;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
const NOTIFICATION_CHANNEL_NAME : &str = "notification";
|
||||
const MAX_FILES : usize = 8;
|
||||
const CHUNK_SIZE : usize = 1024 * 1024;
|
||||
const PARALLEL_FAILURES : usize = 8;
|
||||
const MAX_RETRIES : usize = 30;
|
||||
|
||||
struct ModelDownloadStatusChannel {
|
||||
pub model_name: String,
|
||||
pub model_type: NotificationModelType,
|
||||
pub final_file_path: PathBuf,
|
||||
pub receiver: Receiver<ProgressUpdate>,
|
||||
}
|
||||
|
||||
pub async fn downloader_thread(app_data_root: AppDataRoot, app: AppHandle) -> ! {
|
||||
|
||||
let mut status_check_queue : Arc<Mutex<Vec<ModelDownloadStatusChannel>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
{
|
||||
let status_check_queue2 = status_check_queue.clone();
|
||||
let app2 = app.clone();
|
||||
|
||||
tokio::spawn(download_notify_thread(status_check_queue2, app2));
|
||||
}
|
||||
|
||||
let mut download_queue = VecDeque::new();
|
||||
|
||||
// TODO: Automatic enqueue of known important models + enqueue new models on-demand
|
||||
download_queue.push_back(ModelType::ClipJson);
|
||||
//download_queue.push_back(ModelType::SdxlTurboUnet);
|
||||
//download_queue.push_back(ModelType::SdxlTurboVae);
|
||||
download_queue.push_back(ModelType::SdxlTurboClipEncoder); // TODO(bt): Why is this still needed?
|
||||
//download_queue.push_back(ModelType::SdxlTurboClipEncoder2);
|
||||
download_queue.push_back(ModelType::SimianLuoLcmDreamshaperV7Unet);
|
||||
download_queue.push_back(ModelType::LykonDreamshaper7Vae);
|
||||
download_queue.push_back(ModelType::LykonDreamshaper7TextEncoderFp16);
|
||||
|
||||
let mut handles = FuturesUnordered::new();
|
||||
|
||||
while let Some(model) = download_queue.pop_front() {
|
||||
let url = model.get_download_url().to_string();
|
||||
|
||||
let filename = app_data_root.weights_dir().model_path(&model);
|
||||
|
||||
if filename.exists() {
|
||||
info!("File already exists: {:?}", &filename);
|
||||
continue;
|
||||
}
|
||||
|
||||
let headers = None;
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
|
||||
info!("Download: {:?}", filename);
|
||||
|
||||
let temp_file = match app_data_root.temp_dir().new_named_temp_file() {
|
||||
Ok(file) => file,
|
||||
Err(err) => {
|
||||
error!("Couldn't create a temporary file: {:?}", err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let result = app.emit(NOTIFICATION_CHANNEL_NAME, NotificationEvent::ModelDownloadStarted {
|
||||
model_name: model.get_name(),
|
||||
model_type: model.get_notification_type(),
|
||||
});
|
||||
|
||||
if let Err(err) = result {
|
||||
error!("Could not emit event: {}", err);
|
||||
}
|
||||
|
||||
let task = tokio::spawn(download_async(
|
||||
url,
|
||||
temp_file,
|
||||
filename.clone(),
|
||||
MAX_FILES,
|
||||
CHUNK_SIZE,
|
||||
PARALLEL_FAILURES,
|
||||
MAX_RETRIES,
|
||||
headers,
|
||||
Some(tx),
|
||||
));
|
||||
|
||||
match status_check_queue.lock() {
|
||||
Err(err) => error!("Could not unlock: {:?}", err),
|
||||
Ok(mut lock) => {
|
||||
lock.push(ModelDownloadStatusChannel {
|
||||
receiver: rx,
|
||||
final_file_path: filename.clone(),
|
||||
model_name: model.get_name().to_string(),
|
||||
model_type: model.get_notification_type(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handles.push(task);
|
||||
}
|
||||
|
||||
while let Some(result) = handles.next().await {
|
||||
match result {
|
||||
Err(err) => error!("Error downloading: {:?}", err),
|
||||
Ok(_) => {},
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
// TODO(bt,2025-03-10): Queue management for new files entering the queue.
|
||||
tokio::time::sleep(std::time::Duration::from_millis(60_000)).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn download_notify_thread(queue: Arc<Mutex<Vec<ModelDownloadStatusChannel>>>, app: AppHandle) -> ! {
|
||||
loop {
|
||||
match queue.lock() {
|
||||
Err(err) => error!("Could not unlock: {:?}", err),
|
||||
Ok(mut lock) => {
|
||||
debug!(">>>>> Notifier has {} items", lock.len());
|
||||
|
||||
let mut reenqueue = Vec::with_capacity(lock.len());
|
||||
|
||||
for item in (*lock).drain(..) {
|
||||
if let Ok(update) = item.receiver.try_recv() {
|
||||
let result = app.emit(NOTIFICATION_CHANNEL_NAME, NotificationEvent::ModelDownloadProgress {
|
||||
model_name: &item.model_name,
|
||||
model_type: item.model_type,
|
||||
currently_downloaded_bytes: update.complete,
|
||||
total_file_bytes: update.total_length,
|
||||
});
|
||||
|
||||
if let Err(err) = result {
|
||||
error!("Could not emit event: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
if item.final_file_path.exists() {
|
||||
debug!("No longer sending notifications for {:?}", &item.final_file_path);
|
||||
|
||||
let result = app.emit(NOTIFICATION_CHANNEL_NAME, NotificationEvent::ModelDownloadComplete {
|
||||
model_name: &item.model_name,
|
||||
model_type: item.model_type,
|
||||
});
|
||||
|
||||
if let Err(err) = result {
|
||||
error!("Could not emit event: {}", err);
|
||||
}
|
||||
} else {
|
||||
reenqueue.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
lock.extend(reenqueue);
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod downloader_thread;
|
||||
@@ -0,0 +1,241 @@
|
||||
//! Implemented from https://github.com/huggingface/hf_transfer/blob/main/src/lib.rs
|
||||
|
||||
use crate::transfer::error::Error;
|
||||
use crate::transfer::util::{exponential_backoff, BASE_WAIT_TIME, MAX_WAIT_TIME};
|
||||
use futures::stream::FuturesUnordered;
|
||||
use futures::StreamExt;
|
||||
use reqwest::header::{
|
||||
HeaderMap, HeaderName, HeaderValue, AUTHORIZATION, CONTENT_RANGE,
|
||||
RANGE,
|
||||
};
|
||||
use reqwest::Url;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use std::fs::remove_file;
|
||||
use std::io::SeekFrom;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::time::Duration;
|
||||
use log::info;
|
||||
use tempfile::NamedTempFile;
|
||||
use tokio::fs::OpenOptions;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::io::AsyncSeekExt;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::time::sleep;
|
||||
|
||||
pub struct ProgressUpdate {
|
||||
pub complete: usize,
|
||||
pub total_length: usize,
|
||||
}
|
||||
|
||||
/// max_files: Number of open file handles, which determines the maximum number of parallel downloads
|
||||
/// parallel_failures: Number of maximum failures of different chunks in parallel (cannot exceed max_files)
|
||||
/// max_retries: Number of maximum attempts per chunk. (Retries are exponentially backed off + jitter)
|
||||
///
|
||||
/// The number of threads can be tuned by the environment variable `TOKIO_WORKER_THREADS` as documented in
|
||||
/// https://docs.rs/tokio/latest/tokio/runtime/struct.Builder.html#method.worker_threads
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn download_async<P: AsRef<Path>>(
|
||||
url: String,
|
||||
filename: NamedTempFile,
|
||||
final_filename: P,
|
||||
max_files: usize,
|
||||
chunk_size: usize,
|
||||
parallel_failures: usize,
|
||||
max_retries: usize,
|
||||
input_headers: Option<HashMap<String, String>>,
|
||||
maybe_progress_sender: Option<Sender<ProgressUpdate>>,
|
||||
) -> Result<(), Error> {
|
||||
info!(">>> Downloading: {}", filename.as_ref().display());
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
// https://github.com/hyperium/hyper/issues/2136#issuecomment-589488526
|
||||
.http2_keep_alive_timeout(Duration::from_secs(15))
|
||||
.build()?;
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
let mut auth_token = None;
|
||||
if let Some(input_headers) = input_headers {
|
||||
headers.reserve(input_headers.len());
|
||||
for (k, v) in input_headers {
|
||||
let name: HeaderName = k
|
||||
.try_into()
|
||||
.map_err(|err| Error::new_err(format!("Invalid header: {err}")))?;
|
||||
let value: HeaderValue = AsRef::<str>::as_ref(&v)
|
||||
.try_into()
|
||||
.map_err(|err| Error::new_err(format!("Invalid header value: {err}")))?;
|
||||
if name == AUTHORIZATION {
|
||||
auth_token = Some(value);
|
||||
} else {
|
||||
headers.insert(name, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let response = if let Some(token) = auth_token.as_ref() {
|
||||
client.get(&url).header(AUTHORIZATION, token)
|
||||
} else {
|
||||
client.get(&url)
|
||||
}
|
||||
.headers(headers.clone())
|
||||
.header(RANGE, "bytes=0-0")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| Error::new_err(format!("Error while downloading: {err}")))?
|
||||
.error_for_status()
|
||||
.map_err(|err| Error::new_err(err.to_string()))?;
|
||||
|
||||
// Only call the final redirect URL to avoid overloading the Hub with requests and also
|
||||
// altering the download count
|
||||
let redirected_url = response.url();
|
||||
if Url::parse(&url)
|
||||
.map_err(|err| Error::new_err(format!("failed to parse url: {err}")))?
|
||||
.host()
|
||||
== redirected_url.host()
|
||||
{
|
||||
if let Some(token) = auth_token {
|
||||
headers.insert(AUTHORIZATION, token);
|
||||
}
|
||||
}
|
||||
|
||||
let content_range = response
|
||||
.headers()
|
||||
.get(CONTENT_RANGE)
|
||||
.ok_or(Error::new_err("No content length".to_string()))?
|
||||
.to_str()
|
||||
.map_err(|err| Error::new_err(format!("Error while downloading: {err}")))?;
|
||||
|
||||
let size: Vec<&str> = content_range.split('/').collect();
|
||||
// Content-Range: bytes 0-0/702517648
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range
|
||||
let length: usize = size
|
||||
.last()
|
||||
.ok_or(Error::new_err(
|
||||
"Error while downloading: No size was detected".to_string(),
|
||||
))?
|
||||
.parse()
|
||||
.map_err(|err| Error::new_err(format!("Error while downloading: {err}")))?;
|
||||
|
||||
|
||||
info!("File {:?} is {:?} length", filename.as_ref(), length);
|
||||
|
||||
let mut handles = FuturesUnordered::new();
|
||||
let semaphore = Arc::new(Semaphore::new(max_files));
|
||||
let parallel_failures_semaphore = Arc::new(Semaphore::new(parallel_failures));
|
||||
let max_chunk_complete = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
for start in (0..length).step_by(chunk_size) {
|
||||
let url = redirected_url.to_string();
|
||||
let filename = filename.as_ref().to_owned();
|
||||
let client = client.clone();
|
||||
let headers = headers.clone();
|
||||
|
||||
let stop = std::cmp::min(start + chunk_size - 1, length);
|
||||
let semaphore = semaphore.clone();
|
||||
let parallel_failures_semaphore = parallel_failures_semaphore.clone();
|
||||
let maybe_progress = maybe_progress_sender.clone();
|
||||
let max_chunk_complete = max_chunk_complete.clone();
|
||||
handles.push(tokio::spawn(async move {
|
||||
let permit = semaphore
|
||||
.acquire_owned()
|
||||
.await
|
||||
.map_err(|err| Error::new_err(format!("Error while downloading: {err}")))?;
|
||||
let mut chunk = download_chunk(&client, &url, &filename, start, stop, headers.clone()).await;
|
||||
let mut i = 0;
|
||||
if parallel_failures > 0 {
|
||||
while let Err(dlerr) = chunk {
|
||||
if i >= max_retries {
|
||||
return Err(Error::new_err(format!(
|
||||
"Failed after too many retries ({max_retries}): {dlerr}"
|
||||
)));
|
||||
}
|
||||
let parallel_failure_permit = parallel_failures_semaphore.clone().try_acquire_owned().map_err(|err| {
|
||||
Error::new_err(format!(
|
||||
"Failed too many failures in parallel ({parallel_failures}): {dlerr} ({err})"
|
||||
))
|
||||
})?;
|
||||
|
||||
let wait_time = exponential_backoff(BASE_WAIT_TIME, i, MAX_WAIT_TIME);
|
||||
sleep(Duration::from_millis(wait_time as u64)).await;
|
||||
|
||||
chunk = download_chunk(&client, &url, &filename, start, stop, headers.clone()).await;
|
||||
i += 1;
|
||||
drop(parallel_failure_permit);
|
||||
|
||||
if let Some(sender) = maybe_progress.as_ref() {
|
||||
// NB: Calling fetch_max twice. First call will return the lower value.
|
||||
let _old = max_chunk_complete.fetch_max(stop, Ordering::SeqCst);
|
||||
let max_chunk = max_chunk_complete.fetch_max(stop, Ordering::SeqCst);
|
||||
let progress_update = ProgressUpdate {
|
||||
complete: max_chunk,
|
||||
total_length: length,
|
||||
};
|
||||
if let Err(_err) = sender.send(progress_update) {
|
||||
// Fail silently, not essential to issue progress updates
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(permit);
|
||||
chunk.map_err(|e| Error::new_err(format!("Downloading error {e}"))).and(Ok(stop - start))
|
||||
}));
|
||||
}
|
||||
|
||||
// Output the chained result
|
||||
while let Some(result) = handles.next().await {
|
||||
match result {
|
||||
Ok(Ok(size)) => {
|
||||
//if let Some(ref callback) = callback {
|
||||
// callback.call((size,), None)?;
|
||||
//}
|
||||
}
|
||||
Ok(Err(py_err)) => {
|
||||
return Err(py_err);
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(Error::new_err(format!(
|
||||
"Error while downloading: {err}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let file = filename.persist(final_filename)
|
||||
.map_err(|e| Error::new_err(format!("Could not rename to final filename: {:?}", e)))?;
|
||||
|
||||
file.sync_all()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download_chunk<P: AsRef<Path>>(
|
||||
client: &reqwest::Client,
|
||||
url: &str,
|
||||
filename: P,
|
||||
start: usize,
|
||||
stop: usize,
|
||||
headers: HeaderMap,
|
||||
) -> Result<(), Error> {
|
||||
// Process each socket concurrently.
|
||||
let range = format!("bytes={start}-{stop}");
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(false)
|
||||
.create(true)
|
||||
.open(filename)
|
||||
.await?;
|
||||
file.seek(SeekFrom::Start(start as u64)).await?;
|
||||
let response = client
|
||||
.get(url)
|
||||
.headers(headers)
|
||||
.header(RANGE, range)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
let content = response.bytes().await?;
|
||||
file.write_all(&content).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//! Implemented from https://github.com/huggingface/hf_transfer/blob/main/src/lib.rs
|
||||
|
||||
use std::fmt::Display;
|
||||
use reqwest::header::ToStrError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Io(std::io::Error),
|
||||
Request(reqwest::Error),
|
||||
ToStrError(ToStrError),
|
||||
Misc(String),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
Self::Io(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for Error {
|
||||
fn from(value: reqwest::Error) -> Self {
|
||||
Self::Request(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ToStrError> for Error {
|
||||
fn from(value: ToStrError) -> Self {
|
||||
Self::ToStrError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Io(io) => write!(f, "Io: {io}"),
|
||||
Self::Request(req) => write!(f, "Request: {req}"),
|
||||
Self::ToStrError(req) => write!(f, "Response non ascii: {req}"),
|
||||
Self::Misc(err) => write!(f, "Misc error: {err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl Error {
|
||||
pub fn new_err(reason: String) -> Self {
|
||||
Self::Misc(reason)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod download;
|
||||
pub mod upload;
|
||||
pub mod util;
|
||||
pub mod error;
|
||||
@@ -0,0 +1,189 @@
|
||||
//! Implemented from https://github.com/huggingface/hf_transfer/blob/main/src/lib.rs
|
||||
|
||||
use crate::transfer::error::Error;
|
||||
use crate::transfer::util::{exponential_backoff, BASE_WAIT_TIME, MAX_WAIT_TIME};
|
||||
use futures::stream::FuturesUnordered;
|
||||
use futures::StreamExt;
|
||||
use reqwest::header::CONTENT_LENGTH;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use std::io::SeekFrom;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::fs::OpenOptions;
|
||||
use tokio::io::{AsyncReadExt, AsyncSeekExt};
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::time::sleep;
|
||||
use tokio_util::codec::{BytesCodec, FramedRead};
|
||||
|
||||
/// parts_urls: Dictionary consisting of part numbers as keys and the associated url as values
|
||||
/// completion_url: The url that should be called when the upload is finished
|
||||
/// max_files: Number of open file handles, which determines the maximum number of parallel uploads
|
||||
/// parallel_failures: Number of maximum failures of different chunks in parallel (cannot exceed max_files)
|
||||
/// max_retries: Number of maximum attempts per chunk. (Retries are exponentially backed off + jitter)
|
||||
///
|
||||
/// The number of threads can be tuned by the environment variable `TOKIO_WORKER_THREADS` as documented in
|
||||
/// https://docs.rs/tokio/latest/tokio/runtime/struct.Builder.html#method.worker_threads
|
||||
///
|
||||
/// See https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html for more information
|
||||
/// on the multipart upload
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn multipart_upload(
|
||||
file_path: String,
|
||||
parts_urls: Vec<String>,
|
||||
chunk_size: u64,
|
||||
max_files: usize,
|
||||
parallel_failures: usize,
|
||||
max_retries: usize,
|
||||
) -> Result<Vec<HashMap<String, String>>, Error> {
|
||||
if parallel_failures > max_files {
|
||||
return Err(Error::new_err(
|
||||
"Error parallel_failures cannot be > max_files".to_string(),
|
||||
));
|
||||
}
|
||||
if (parallel_failures == 0) != (max_retries == 0) {
|
||||
return Err(Error::new_err(
|
||||
"For retry mechanism you need to set both `parallel_failures` and `max_retries`"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()?
|
||||
.block_on(async {
|
||||
upload_async(
|
||||
file_path,
|
||||
parts_urls,
|
||||
chunk_size,
|
||||
max_files,
|
||||
parallel_failures,
|
||||
max_retries,
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn upload_async(
|
||||
file_path: String,
|
||||
parts_urls: Vec<String>,
|
||||
chunk_size: u64,
|
||||
max_files: usize,
|
||||
parallel_failures: usize,
|
||||
max_retries: usize,
|
||||
) -> Result<Vec<HashMap<String, String>>, Error> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let mut handles = FuturesUnordered::new();
|
||||
let semaphore = Arc::new(Semaphore::new(max_files));
|
||||
let parallel_failures_semaphore = Arc::new(Semaphore::new(parallel_failures));
|
||||
|
||||
for (part_number, part_url) in parts_urls.iter().enumerate() {
|
||||
let url = part_url.to_string();
|
||||
let path = file_path.to_owned();
|
||||
let client = client.clone();
|
||||
|
||||
let start = (part_number as u64) * chunk_size;
|
||||
let semaphore = semaphore.clone();
|
||||
let parallel_failures_semaphore = parallel_failures_semaphore.clone();
|
||||
handles.push(tokio::spawn(async move {
|
||||
let permit = semaphore
|
||||
.clone()
|
||||
.acquire_owned()
|
||||
.await
|
||||
.map_err(|err| Error::new_err(format!("Error acquiring semaphore: {err}")))?;
|
||||
let mut chunk = upload_chunk(&client, &url, &path, start, chunk_size).await;
|
||||
let mut i = 0;
|
||||
if parallel_failures > 0 {
|
||||
while let Err(ul_err) = chunk {
|
||||
if i >= max_retries {
|
||||
return Err(Error::new_err(format!(
|
||||
"Failed after too many retries ({max_retries}): {ul_err}"
|
||||
)));
|
||||
}
|
||||
|
||||
let parallel_failure_permit = parallel_failures_semaphore.clone().try_acquire_owned().map_err(|err| {
|
||||
Error::new_err(format!(
|
||||
"Failed too many failures in parallel ({parallel_failures}): {ul_err} ({err})"
|
||||
))
|
||||
})?;
|
||||
|
||||
let wait_time = exponential_backoff(BASE_WAIT_TIME, i, MAX_WAIT_TIME);
|
||||
sleep(Duration::from_millis(wait_time as u64)).await;
|
||||
|
||||
chunk = upload_chunk(&client, &url, &path, start, chunk_size).await;
|
||||
i += 1;
|
||||
drop(parallel_failure_permit);
|
||||
}
|
||||
}
|
||||
drop(permit);
|
||||
chunk.map_err(|e|{
|
||||
//match e {
|
||||
// Error::Io(io) => Error::new_err(format!("Io error {io}")),
|
||||
// Error::Request(req) => Error::new_err(format!("Error while sending chunk {req}")),
|
||||
// Error::ToStrError(req) => Error::new_err(format!("Response header contains non ASCII chars: {req}")),
|
||||
//}
|
||||
e
|
||||
}).map(|chunk| (part_number, chunk, chunk_size))
|
||||
}));
|
||||
}
|
||||
|
||||
let mut results: Vec<HashMap<String, String>> = vec![HashMap::default(); parts_urls.len()];
|
||||
|
||||
while let Some(result) = handles.next().await {
|
||||
match result {
|
||||
Ok(Ok((part_number, headers, size))) => {
|
||||
//if let Some(ref callback) = callback {
|
||||
// callback.call((size,), None)?;
|
||||
//}
|
||||
results[part_number] = headers;
|
||||
}
|
||||
Ok(Err(py_err)) => {
|
||||
return Err(py_err);
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(Error::new_err(format!(
|
||||
"Error occurred while uploading: {err}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
async fn upload_chunk(
|
||||
client: &reqwest::Client,
|
||||
url: &str,
|
||||
path: &str,
|
||||
start: u64,
|
||||
chunk_size: u64,
|
||||
) -> Result<HashMap<String, String>, Error> {
|
||||
let mut options = OpenOptions::new();
|
||||
let mut file = options.read(true).open(path).await?;
|
||||
let file_size = file.metadata().await?.len();
|
||||
let bytes_transferred = std::cmp::min(file_size - start, chunk_size);
|
||||
|
||||
file.seek(SeekFrom::Start(start)).await?;
|
||||
let chunk = file.take(chunk_size);
|
||||
|
||||
let response = client
|
||||
.put(url)
|
||||
.header(CONTENT_LENGTH, bytes_transferred)
|
||||
.body(reqwest::Body::wrap_stream(FramedRead::new(
|
||||
chunk,
|
||||
BytesCodec::new(),
|
||||
)))
|
||||
.send()
|
||||
.await?;
|
||||
let response = response.error_for_status()?;
|
||||
let mut headers = HashMap::new();
|
||||
for (name, value) in response.headers().into_iter() {
|
||||
headers.insert(name.to_string(), value.to_str()?.to_owned());
|
||||
}
|
||||
Ok(headers)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
//! Implemented from https://github.com/huggingface/hf_transfer/blob/main/src/lib.rs
|
||||
|
||||
use rand::{thread_rng, Rng};
|
||||
use std::fmt::Display;
|
||||
|
||||
pub const BASE_WAIT_TIME: usize = 300;
|
||||
pub const MAX_WAIT_TIME: usize = 10_000;
|
||||
|
||||
fn jitter() -> usize {
|
||||
thread_rng().gen_range(0..=500)
|
||||
}
|
||||
|
||||
pub fn exponential_backoff(base_wait_time: usize, n: usize, max: usize) -> usize {
|
||||
(base_wait_time + n.pow(2) + jitter()).min(max)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
|
||||
"productName": "Realtime Desktop App",
|
||||
"productName": "ArtCraft",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.unique.value.thing.why",
|
||||
"build": {
|
||||
@@ -12,9 +12,9 @@
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Realtime Desktop App Title Goes Here",
|
||||
"width": 1200,
|
||||
"height": 800,
|
||||
"title": "ArtCraft",
|
||||
"width": 2400,
|
||||
"height": 1300,
|
||||
"resizable": true,
|
||||
"fullscreen": false
|
||||
}
|
||||
|
||||
@@ -118,24 +118,40 @@ pub static DEVELOPMENT_PREMIUM_PLANS: Lazy<HashSet<Plan>> = Lazy::new(|| {
|
||||
|
||||
/// All paid-for production premium plans in the system.
|
||||
pub static PRODUCTION_PREMIUM_PLANS: Lazy<HashSet<Plan>> = Lazy::new(|| {
|
||||
let mut plans = HashSet::new();
|
||||
let mut plans = HashSet::new();
|
||||
|
||||
// ========== All-locale plans ==========
|
||||
// ========== All-locale plans ==========
|
||||
|
||||
plans.insert(PlanBuilder::new("fakeyou_plus")
|
||||
.plan_category(PlanCategory::Paid)
|
||||
.stripe_product_id("prod_MoLv23HrxPiY7D")
|
||||
.stripe_price_id("price_1M4jDCEU5se17MekOaJ92HYX")
|
||||
.cost_per_month_dollars(7)
|
||||
.tts_base_priority_level(20)
|
||||
.tts_max_character_length(2048)
|
||||
.tts_max_duration_seconds(30)
|
||||
.web_vc_base_priority_level(20)
|
||||
.web_vc_requires_frontend_keepalive(false)
|
||||
.lipsync_requires_frontend_keepalive(false)
|
||||
.build());
|
||||
// Old "plus" price
|
||||
plans.insert(PlanBuilder::new("fakeyou_plus")
|
||||
.plan_category(PlanCategory::Paid)
|
||||
.stripe_product_id("prod_MoLv23HrxPiY7D")
|
||||
.stripe_price_id("price_1M4jDCEU5se17MekOaJ92HYX")
|
||||
.cost_per_month_dollars(7)
|
||||
.tts_base_priority_level(20)
|
||||
.tts_max_character_length(2048)
|
||||
.tts_max_duration_seconds(30)
|
||||
.web_vc_base_priority_level(20)
|
||||
.web_vc_requires_frontend_keepalive(false)
|
||||
.lipsync_requires_frontend_keepalive(false)
|
||||
.build());
|
||||
|
||||
plans.insert(PlanBuilder::new("fakeyou_pro")
|
||||
// New "plus" price
|
||||
plans.insert(PlanBuilder::new("fakeyou_plus_plan")
|
||||
.plan_category(PlanCategory::Paid)
|
||||
.stripe_product_id("prod_RvC5qUgLTSLGSD")
|
||||
.stripe_price_id("price_1R1Lh8EU5se17MekaUNtWc59")
|
||||
.cost_per_month_dollars(12)
|
||||
.tts_base_priority_level(20)
|
||||
.tts_max_character_length(2048)
|
||||
.tts_max_duration_seconds(30)
|
||||
.web_vc_base_priority_level(20)
|
||||
.web_vc_requires_frontend_keepalive(false)
|
||||
.lipsync_requires_frontend_keepalive(false)
|
||||
.build());
|
||||
|
||||
// Old "pro" price
|
||||
plans.insert(PlanBuilder::new("fakeyou_pro")
|
||||
.plan_category(PlanCategory::Paid)
|
||||
.stripe_product_id("prod_MoLw8nA6eFxHzc")
|
||||
.stripe_price_id("price_1M4jEQEU5se17MeksNfA0EKm")
|
||||
@@ -149,7 +165,23 @@ pub static PRODUCTION_PREMIUM_PLANS: Lazy<HashSet<Plan>> = Lazy::new(|| {
|
||||
.can_remove_visual_watermarks(true)
|
||||
.build());
|
||||
|
||||
plans.insert(PlanBuilder::new("fakeyou_elite")
|
||||
// New "pro" price
|
||||
plans.insert(PlanBuilder::new("fakeyou_pro_plan")
|
||||
.plan_category(PlanCategory::Paid)
|
||||
.stripe_product_id("prod_RvC6nB9rw6yAsI")
|
||||
.stripe_price_id("price_1R1LhzEU5se17Mek3JLwN3Gg")
|
||||
.cost_per_month_dollars(25)
|
||||
.tts_base_priority_level(30)
|
||||
.tts_max_character_length(3072)
|
||||
.tts_max_duration_seconds(60 * 2)
|
||||
.web_vc_base_priority_level(30)
|
||||
.web_vc_requires_frontend_keepalive(false)
|
||||
.lipsync_requires_frontend_keepalive(false)
|
||||
.can_remove_visual_watermarks(true)
|
||||
.build());
|
||||
|
||||
// Old "elite" price
|
||||
plans.insert(PlanBuilder::new("fakeyou_elite")
|
||||
.plan_category(PlanCategory::Paid)
|
||||
.stripe_product_id("prod_MoLxQmLA4R24fv")
|
||||
.stripe_price_id("price_1M4jFREU5se17Mekc8pQaSKB")
|
||||
@@ -163,6 +195,21 @@ pub static PRODUCTION_PREMIUM_PLANS: Lazy<HashSet<Plan>> = Lazy::new(|| {
|
||||
.can_remove_visual_watermarks(true)
|
||||
.build());
|
||||
|
||||
// New "elite" price
|
||||
plans.insert(PlanBuilder::new("fakeyou_elite_plan")
|
||||
.plan_category(PlanCategory::Paid)
|
||||
.stripe_product_id("prod_RvC6i84YiAYStd")
|
||||
.stripe_price_id("price_1R1LiQEU5se17MekiJdBcsHQ")
|
||||
.cost_per_month_dollars(40)
|
||||
.tts_base_priority_level(40)
|
||||
.tts_max_character_length(4096)
|
||||
.tts_max_duration_seconds(60 * 5)
|
||||
.web_vc_base_priority_level(40)
|
||||
.web_vc_requires_frontend_keepalive(false)
|
||||
.lipsync_requires_frontend_keepalive(false)
|
||||
.can_remove_visual_watermarks(true)
|
||||
.build());
|
||||
|
||||
// ========== Spanish plans ==========
|
||||
|
||||
plans.insert(PlanBuilder::new("fakeyou_basic")
|
||||
@@ -293,7 +340,7 @@ mod test {
|
||||
// making ourselves check twice when we make additions, removals, or other changes.
|
||||
#[test]
|
||||
fn test_number_of_plans_is_expected() {
|
||||
assert_eq!(11, ALL_PLANS_BY_SLUG.len());
|
||||
assert_eq!(14, ALL_PLANS_BY_SLUG.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -303,7 +350,7 @@ mod test {
|
||||
.filter(|plan| !plan.is_synthetic_plan()) // NB: Synthetic plans are production!
|
||||
.collect::<Vec<&Plan>>();
|
||||
|
||||
let expected = 4;
|
||||
let expected = 7;
|
||||
|
||||
assert_eq!(expected, production_plans.len());
|
||||
assert_eq!(expected, PRODUCTION_PREMIUM_PLANS.len());
|
||||
@@ -346,6 +393,9 @@ mod test {
|
||||
"prod_MoLw8nA6eFxHzc".to_string(), // NB: Production
|
||||
"prod_MoLxQmLA4R24fv".to_string(), // NB: Production
|
||||
"prod_MoLyt8qMDmscjr".to_string(), // NB: Production
|
||||
"prod_RvC5qUgLTSLGSD".to_string(), // NB: Production - new "plus" product
|
||||
"prod_RvC6nB9rw6yAsI".to_string(), // NB: Production - new "pro" product
|
||||
"prod_RvC6i84YiAYStd".to_string(), // NB: Production - new "elite" product
|
||||
];
|
||||
|
||||
assert_that(&product_ids).contains_all_of(&expected_product_ids.iter());
|
||||
@@ -366,6 +416,9 @@ mod test {
|
||||
"price_1M4jEQEU5se17MeksNfA0EKm".to_string(), // NB: Production
|
||||
"price_1M4jFREU5se17Mekc8pQaSKB".to_string(), // NB: Production
|
||||
"price_1M4jGPEU5se17Mek2FiztNE5".to_string(), // NB: Production
|
||||
"price_1R1Lh8EU5se17MekaUNtWc59".to_string(), // NB: Production - new "plus" price
|
||||
"price_1R1LhzEU5se17Mek3JLwN3Gg".to_string(), // NB: Production - new "pro" price
|
||||
"price_1R1LiQEU5se17MekiJdBcsHQ".to_string(), // NB: Production - new "elite" price
|
||||
];
|
||||
|
||||
assert_that(&price_ids).contains_all_of(&expected_price_ids.iter());
|
||||
|
||||
+934
-738
File diff suppressed because it is too large
Load Diff
+8
-1
@@ -86,6 +86,8 @@ fn main() -> Result<()> {
|
||||
.arg("--expt-relaxed-constexpr")
|
||||
.arg("--expt-extended-lambda")
|
||||
.arg("--use_fast_math")
|
||||
.arg("--compiler-options")
|
||||
.arg("-fPIC")
|
||||
.arg("--verbose");
|
||||
|
||||
if let Ok(target) = std::env::var("TARGET") {
|
||||
@@ -100,7 +102,12 @@ fn main() -> Result<()> {
|
||||
println!("cargo:rustc-link-search={}", build_dir.display());
|
||||
println!("cargo:rustc-link-lib=flashattention");
|
||||
println!("cargo:rustc-link-lib=dylib=cudart");
|
||||
println!("cargo:rustc-link-lib=dylib=stdc++");
|
||||
|
||||
|
||||
// TODO(bt,2025-03-12): Fix builds on windows
|
||||
//println!("cargo:rustc-link-lib=dylib=stdc++");
|
||||
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+1
@@ -32,3 +32,4 @@ metal = ["candle/metal", "dep:candle-metal-kernels", "dep:metal"]
|
||||
[[bench]]
|
||||
name = "bench_main"
|
||||
harness = false
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ serde = { version = "1.0.171", features = ["derive"] }
|
||||
serde_json = "1.0.99"
|
||||
serde_plain = "1.0.2"
|
||||
tracing = "0.1.37"
|
||||
half = { version = "2.3.1", features = ["num-traits", "use-intrinsics", "rand_distr"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
+454
-696
File diff suppressed because it is too large
Load Diff
@@ -84,6 +84,8 @@ impl StableDiffusionConfig {
|
||||
};
|
||||
// https://huggingface.co/runwayml/stable-diffusion-v1-5/blob/main/unet/config.json
|
||||
let unet = unet_2d::UNet2DConditionModelConfig {
|
||||
time_embed_dim: None,
|
||||
time_cond_proj_dim: None,
|
||||
blocks: vec![
|
||||
bc(320, Some(1), 8),
|
||||
bc(640, Some(1), 8),
|
||||
@@ -153,6 +155,8 @@ impl StableDiffusionConfig {
|
||||
};
|
||||
// https://huggingface.co/stabilityai/stable-diffusion-2-1/blob/main/unet/config.json
|
||||
let unet = unet_2d::UNet2DConditionModelConfig {
|
||||
time_cond_proj_dim: None,
|
||||
time_embed_dim: None,
|
||||
blocks: vec![
|
||||
bc(320, Some(1), 5),
|
||||
bc(640, Some(1), 10),
|
||||
@@ -237,6 +241,8 @@ impl StableDiffusionConfig {
|
||||
};
|
||||
// https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/blob/main/unet/config.json
|
||||
let unet = unet_2d::UNet2DConditionModelConfig {
|
||||
time_cond_proj_dim: None,
|
||||
time_embed_dim: None,
|
||||
blocks: vec![
|
||||
bc(320, None, 5),
|
||||
bc(640, Some(2), 10),
|
||||
@@ -306,6 +312,8 @@ impl StableDiffusionConfig {
|
||||
};
|
||||
// https://huggingface.co/stabilityai/sdxl-turbo/blob/main/unet/config.json
|
||||
let unet = unet_2d::UNet2DConditionModelConfig {
|
||||
time_cond_proj_dim: None,
|
||||
time_embed_dim: None,
|
||||
blocks: vec![
|
||||
bc(320, None, 5),
|
||||
bc(640, Some(2), 10),
|
||||
@@ -405,6 +413,8 @@ impl StableDiffusionConfig {
|
||||
};
|
||||
// https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/blob/main/unet/config.json
|
||||
let unet = unet_2d::UNet2DConditionModelConfig {
|
||||
time_cond_proj_dim: None,
|
||||
time_embed_dim: None,
|
||||
blocks: vec![
|
||||
bc(320, None, 5),
|
||||
bc(640, Some(2), 10),
|
||||
|
||||
+144
-10
@@ -10,6 +10,10 @@ use candle_nn as nn;
|
||||
use candle_nn::Module;
|
||||
use std::collections::HashMap;
|
||||
|
||||
// fn save_tensor_as_numpy(tensor: &Tensor, filename: &str) -> Result<()> {
|
||||
// // Use Candle's built-in NPY serialization
|
||||
// tensor.write_npy(filename).map_err(|e| anyhow::anyhow!("{}", e))
|
||||
// }
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BlockConfig {
|
||||
pub out_channels: usize,
|
||||
@@ -33,11 +37,13 @@ pub struct UNet2DConditionModelConfig {
|
||||
pub cross_attention_dim: usize,
|
||||
pub sliced_attention_size: Option<usize>,
|
||||
pub use_linear_projection: bool,
|
||||
pub time_cond_proj_dim: Option<usize>,
|
||||
pub time_embed_dim: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for UNet2DConditionModelConfig {
|
||||
fn default() -> Self {
|
||||
Self { center_input_sample: false, flip_sin_to_cos: true, freq_shift: 0., blocks: vec![BlockConfig { out_channels: 320, use_cross_attn: Some(1), attention_head_dim: 8 }, BlockConfig { out_channels: 640, use_cross_attn: Some(1), attention_head_dim: 8 }, BlockConfig { out_channels: 1280, use_cross_attn: Some(1), attention_head_dim: 8 }, BlockConfig { out_channels: 1280, use_cross_attn: None, attention_head_dim: 8 }], layers_per_block: 2, downsample_padding: 1, mid_block_scale_factor: 1., norm_num_groups: 32, norm_eps: 1e-5, cross_attention_dim: 1280, sliced_attention_size: None, use_linear_projection: false }
|
||||
Self { center_input_sample: false, flip_sin_to_cos: true, freq_shift: 0., blocks: vec![BlockConfig { out_channels: 320, use_cross_attn: Some(1), attention_head_dim: 8 }, BlockConfig { out_channels: 640, use_cross_attn: Some(1), attention_head_dim: 8 }, BlockConfig { out_channels: 1280, use_cross_attn: Some(1), attention_head_dim: 8 }, BlockConfig { out_channels: 1280, use_cross_attn: None, attention_head_dim: 8 }], layers_per_block: 2, downsample_padding: 1, mid_block_scale_factor: 1., norm_num_groups: 32, norm_eps: 1e-5, cross_attention_dim: 1280, sliced_attention_size: None, use_linear_projection: false, time_cond_proj_dim: Some(256), time_embed_dim: None }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +79,16 @@ impl UNet2DConditionModel {
|
||||
let b_channels = config.blocks[0].out_channels;
|
||||
let bl_channels = config.blocks.last().unwrap().out_channels;
|
||||
let bl_attention_head_dim = config.blocks.last().unwrap().attention_head_dim;
|
||||
let time_embed_dim = b_channels * 4;
|
||||
let time_embed_dim = config.time_embed_dim.unwrap_or(b_channels * 4);
|
||||
let conv_cfg = nn::Conv2dConfig { padding: 1, ..Default::default() };
|
||||
|
||||
// Log important configuration details
|
||||
println!("Creating UNet2DConditionModel with {} blocks", n_blocks);
|
||||
if let Some(proj_dim) = config.time_cond_proj_dim {
|
||||
println!("UNet configured with time_cond_proj_dim: {}", proj_dim);
|
||||
println!("This means the model expects guidance embeddings of dimension {}", proj_dim);
|
||||
}
|
||||
|
||||
let conv_in = conv2d(in_channels, b_channels, 3, conv_cfg, vs.pp("conv_in"))?;
|
||||
|
||||
let time_proj = Timesteps::new(b_channels, config.flip_sin_to_cos, config.freq_shift);
|
||||
@@ -241,18 +255,138 @@ impl UNet2DConditionModel {
|
||||
|
||||
// Add guidance embedding if provided
|
||||
if let Some(guidance_emb) = guidance_embedding {
|
||||
// Print shapes for debugging
|
||||
println!("Guidance embedding shape: {:?}", guidance_emb.shape());
|
||||
println!("Time embedding shape: {:?}", time_emb.shape());
|
||||
// Get dimensions of both embeddings
|
||||
let (batch_size, time_emb_dim) = time_emb.dims2()?;
|
||||
let (guidance_batch, guidance_emb_dim) = guidance_emb.dims2()?;
|
||||
|
||||
// Get the batch size and embedding dimension
|
||||
let (batch_size, emb_dim) = time_emb.dims2()?;
|
||||
println!("==== Guidance Embedding Integration ====");
|
||||
println!("Time embedding dims: [{}, {}]", batch_size, time_emb_dim);
|
||||
println!("Guidance embedding dims: [{}, {}]", guidance_batch, guidance_emb_dim);
|
||||
|
||||
// Reshape guidance embedding to match time embedding dimensions
|
||||
let guidance_emb = guidance_emb.reshape(&[1, emb_dim])?.expand(&[batch_size, emb_dim])?;
|
||||
// Use time_cond_proj_dim from config to determine expected guidance embedding dimension
|
||||
let expected_guidance_dim = self.config.time_cond_proj_dim.unwrap_or(time_emb_dim);
|
||||
println!("Expected guidance dimension from config: {}", expected_guidance_dim);
|
||||
|
||||
// Check if dimensions match the expected projection dimension
|
||||
let processed_guidance_emb = if guidance_emb_dim != time_emb_dim {
|
||||
// Convert guidance_emb to f32 for calculations
|
||||
let guidance_emb_f32 = guidance_emb.to_dtype(DType::F32)?;
|
||||
println!("Converting guidance embedding to F32 for processing");
|
||||
|
||||
// If the guidance embedding dimension matches the expected projection dimension,
|
||||
// we need to project it to match the time embedding dimension
|
||||
if guidance_emb_dim == expected_guidance_dim {
|
||||
println!("Projecting guidance embedding from {} to {}", guidance_emb_dim, time_emb_dim);
|
||||
|
||||
// Use linear interpolation
|
||||
let src_positions: Vec<f32> = (0..guidance_emb_dim).map(|i| i as f32 / (guidance_emb_dim - 1) as f32).collect();
|
||||
let dst_positions: Vec<f32> = (0..time_emb_dim).map(|i| i as f32 / (time_emb_dim - 1) as f32).collect();
|
||||
|
||||
// Get guidance embedding data (now f32)
|
||||
let guidance_data = guidance_emb_f32.reshape((guidance_emb_dim,))?.to_vec1::<f32>()?;
|
||||
println!("Reshaping guidance data to vector of length {}", guidance_data.len());
|
||||
|
||||
// Perform linear interpolation
|
||||
let mut interpolated_data = Vec::with_capacity(time_emb_dim);
|
||||
for &t in &dst_positions {
|
||||
// Find the two source positions that surround the target position
|
||||
let mut i = 0;
|
||||
while i < src_positions.len() - 1 && src_positions[i + 1] < t {
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if i == src_positions.len() - 1 {
|
||||
// At or beyond the last position
|
||||
interpolated_data.push(guidance_data[i]);
|
||||
} else if t <= src_positions[0] {
|
||||
// At or before the first position
|
||||
interpolated_data.push(guidance_data[0]);
|
||||
} else {
|
||||
// Linear interpolation between two points
|
||||
let t_norm = (t - src_positions[i]) / (src_positions[i + 1] - src_positions[i]);
|
||||
let value = guidance_data[i] * (1.0 - t_norm) + guidance_data[i + 1] * t_norm;
|
||||
interpolated_data.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
println!("Created interpolated data of length {}", interpolated_data.len());
|
||||
|
||||
// Create tensor and convert to time_emb's dtype
|
||||
let tensor = Tensor::from_vec(interpolated_data, (1, time_emb_dim), device)?;
|
||||
let result = tensor.to_dtype(time_emb.dtype())?;
|
||||
println!("Final projected guidance embedding shape: {:?}", result.shape());
|
||||
result
|
||||
} else {
|
||||
// Unusual case - dimensions don't match expected pattern
|
||||
println!("Warning: Guidance embedding dimension ({}) doesn't match expected projection dimension ({})", guidance_emb_dim, expected_guidance_dim);
|
||||
|
||||
// Fallback to generic interpolation
|
||||
let guidance_data = guidance_emb_f32.reshape((guidance_emb_dim,))?.to_vec1::<f32>()?;
|
||||
|
||||
let src_positions: Vec<f32> = (0..guidance_emb_dim).map(|i| i as f32 / (guidance_emb_dim - 1) as f32).collect();
|
||||
let dst_positions: Vec<f32> = (0..time_emb_dim).map(|i| i as f32 / (time_emb_dim - 1) as f32).collect();
|
||||
|
||||
let mut interpolated_data = Vec::with_capacity(time_emb_dim);
|
||||
for &t in &dst_positions {
|
||||
let mut i = 0;
|
||||
while i < src_positions.len() - 1 && src_positions[i + 1] < t {
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if i == src_positions.len() - 1 {
|
||||
interpolated_data.push(guidance_data[i]);
|
||||
} else if t <= src_positions[0] {
|
||||
interpolated_data.push(guidance_data[0]);
|
||||
} else {
|
||||
let t_norm = (t - src_positions[i]) / (src_positions[i + 1] - src_positions[i]);
|
||||
let value = guidance_data[i] * (1.0 - t_norm) + guidance_data[i + 1] * t_norm;
|
||||
interpolated_data.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
println!("Created interpolated data of length {}", interpolated_data.len());
|
||||
let tensor = Tensor::from_vec(interpolated_data, (1, time_emb_dim), device)?;
|
||||
let result = tensor.to_dtype(time_emb.dtype())?;
|
||||
println!("Final fallback guidance embedding shape: {:?}", result.shape());
|
||||
result
|
||||
}
|
||||
} else {
|
||||
// Dimensions match, no processing needed
|
||||
println!("Guidance dimension already matches time embedding dimension, no interpolation needed");
|
||||
let result = guidance_emb.reshape((1, time_emb_dim))?.to_dtype(time_emb.dtype())?;
|
||||
println!("Reshaped guidance embedding shape: {:?}", result.shape());
|
||||
result
|
||||
};
|
||||
|
||||
// Expand to match batch size if needed
|
||||
let expanded_guidance = processed_guidance_emb.broadcast_as(time_emb.shape())?;
|
||||
println!("Expanded guidance embedding shape: {:?}", expanded_guidance.shape());
|
||||
|
||||
// Save the processed and expanded guidance embeddings to disk if requested
|
||||
if let Ok(save_dir) = std::env::var("CANDLE_SAVE_TENSORS_DIR") {
|
||||
// Save pre-expansion (original projection)
|
||||
let proc_path = format!("{}/unet_processed_guidance_embedding.npy", save_dir);
|
||||
println!("Saving processed guidance embedding to {}", proc_path);
|
||||
if let Err(e) = processed_guidance_emb.write_npy(&proc_path) {
|
||||
println!("Error saving processed guidance embedding: {}", e);
|
||||
} else {
|
||||
println!("Successfully saved processed guidance embedding to {}", proc_path);
|
||||
}
|
||||
|
||||
// Save post-expansion (final tensor used for addition to time_emb)
|
||||
let exp_path = format!("{}/unet_expanded_guidance_embedding.npy", save_dir);
|
||||
println!("Saving expanded guidance embedding to {}", exp_path);
|
||||
if let Err(e) = expanded_guidance.write_npy(&exp_path) {
|
||||
println!("Error saving expanded guidance embedding: {}", e);
|
||||
} else {
|
||||
println!("Successfully saved expanded guidance embedding to {}", exp_path);
|
||||
}
|
||||
}
|
||||
|
||||
// Add to time embeddings
|
||||
time_emb = (time_emb + guidance_emb)?;
|
||||
time_emb = (time_emb + expanded_guidance)?;
|
||||
println!("Final time embedding with guidance shape: {:?}", time_emb.shape());
|
||||
println!("==== End Guidance Integration ====");
|
||||
}
|
||||
|
||||
// 2. pre-process
|
||||
|
||||
@@ -546,7 +546,7 @@ export class Engine {
|
||||
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
|
||||
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
|
||||
shapeType: shapeNode.shapeType,
|
||||
size: shapeNode.size,
|
||||
size: shapeNode.kNode.size(),
|
||||
color: nodeColor.color,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
@@ -581,7 +581,7 @@ export class Engine {
|
||||
canvasPosition: this.realTimeDrawEngine.captureCanvas.position(),
|
||||
canvasSize: this.realTimeDrawEngine.captureCanvas.size(),
|
||||
shapeType: type,
|
||||
size: size,
|
||||
size: { width: size, height: size },
|
||||
color: color,
|
||||
mediaLayerRef: this.mediaLayer,
|
||||
selectionManagerRef: this.selectionManager,
|
||||
|
||||
@@ -7,9 +7,11 @@ import { RealTimeDrawEngine } from "../RenderingPrimitives/RealTimeDrawEngine";
|
||||
import { MouseEventHandler } from "react";
|
||||
import { KonvaEventObject } from "konva/lib/Node";
|
||||
|
||||
export type SupportedNode = Konva.Image | Konva.Group | Konva.Shape;
|
||||
|
||||
export abstract class BaseNode {
|
||||
protected selectionManagerRef: SelectionManager;
|
||||
public kNode: Konva.Image | Konva.Group;
|
||||
public kNode: SupportedNode;
|
||||
protected mediaLayerRef: Konva.Layer;
|
||||
|
||||
// Internal State members
|
||||
@@ -31,7 +33,7 @@ export abstract class BaseNode {
|
||||
}: {
|
||||
selectionManagerRef: SelectionManager;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
kNode: Konva.Image | Konva.Group | Konva.Shape;
|
||||
kNode: SupportedNode;
|
||||
}) {
|
||||
// console.log("Node constructed");
|
||||
// this.uuid = uuidv4();
|
||||
@@ -62,7 +64,7 @@ export abstract class BaseNode {
|
||||
}
|
||||
public highlight() {
|
||||
// console.log("Highlight", this.kNode._id);
|
||||
if (this.kNode instanceof Konva.Image) {
|
||||
if (this.kNode instanceof Konva.Image || this.kNode instanceof Konva.Shape) {
|
||||
this.kNode.stroke(primaryOrange);
|
||||
this.kNode.strokeWidth(highlightStrokeWidth);
|
||||
return;
|
||||
@@ -85,7 +87,7 @@ export abstract class BaseNode {
|
||||
|
||||
public unhighlight() {
|
||||
// console.log("unHighlight", this.kNode._id);
|
||||
if (this.kNode instanceof Konva.Image) {
|
||||
if (this.kNode instanceof Konva.Image || this.kNode instanceof Konva.Shape) {
|
||||
this.kNode.strokeWidth(0);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Konva from "konva";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { SelectionManager } from "../NodesManagers";
|
||||
import { BaseNode } from "./BaseNode";
|
||||
import { BaseNode, SupportedNode } from "./BaseNode";
|
||||
import { Size } from "../types";
|
||||
import { LoadingVideosProvider } from "../EngineUtitlities/LoadingVideosProvider";
|
||||
import { NodeProgressEventDetail } from "../types/events";
|
||||
@@ -20,7 +20,7 @@ export enum UploadStatus {
|
||||
}
|
||||
|
||||
export abstract class NetworkedNode extends BaseNode {
|
||||
public kNode: Konva.Image;
|
||||
public kNode: SupportedNode;
|
||||
|
||||
// members to deal with loading progress
|
||||
public didFinishLoading: boolean = false;
|
||||
@@ -49,7 +49,7 @@ export abstract class NetworkedNode extends BaseNode {
|
||||
localFile,
|
||||
loadingVideosProviderRef,
|
||||
}: {
|
||||
kNode: Konva.Image;
|
||||
kNode: SupportedNode;
|
||||
selectionManagerRef: SelectionManager;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
loadingVideosProviderRef?: LoadingVideosProvider;
|
||||
|
||||
@@ -15,7 +15,7 @@ interface ShapeNodeConstructor {
|
||||
canvasPosition: Position;
|
||||
canvasSize: Size;
|
||||
shapeType: ShapeType;
|
||||
size: number;
|
||||
size: Size;
|
||||
color?: string; // Hex color string
|
||||
transform?: TransformationData;
|
||||
mediaLayerRef: Konva.Layer;
|
||||
@@ -29,7 +29,7 @@ export class ShapeNode extends NetworkedNode {
|
||||
}
|
||||
|
||||
public shapeType: ShapeType;
|
||||
public size: number;
|
||||
public size: Size;
|
||||
public color: string;
|
||||
|
||||
constructor({
|
||||
@@ -46,6 +46,7 @@ export class ShapeNode extends NetworkedNode {
|
||||
// Create the actual shape inside the group
|
||||
const transform = NodeUtilities.getInitialTransform({
|
||||
existingTransform,
|
||||
mediaFileSize: size,
|
||||
canvasPosition,
|
||||
canvasSize,
|
||||
});
|
||||
@@ -54,110 +55,48 @@ export class ShapeNode extends NetworkedNode {
|
||||
|
||||
switch (shapeType) {
|
||||
case ShapeType.CIRCLE:
|
||||
const circleShape = new Konva.Circle({
|
||||
radius: 50,
|
||||
shape = new Konva.Ellipse({
|
||||
x: size.width,
|
||||
y: size.height,
|
||||
radiusX: size.width,
|
||||
radiusY: size.height,
|
||||
...transform,
|
||||
fill: color,
|
||||
strokeScaleEnabled: false,
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
// Convert circle to image
|
||||
const dataURL = circleShape.toDataURL();
|
||||
const imageObj = new Image();
|
||||
imageObj.src = dataURL;
|
||||
|
||||
shape = new Konva.Image({
|
||||
image: imageObj,
|
||||
x: circleShape.x(),
|
||||
y: circleShape.y(),
|
||||
width: circleShape.width(),
|
||||
height: circleShape.height(),
|
||||
...transform,
|
||||
fill: "transparent",
|
||||
strokeScaleEnabled: false,
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
imageObj.onload = () => {
|
||||
this.mediaLayerRef.draw();
|
||||
loaded();
|
||||
};
|
||||
|
||||
// Clean up the temporary circle
|
||||
circleShape.destroy();
|
||||
break;
|
||||
|
||||
case ShapeType.SQUARE:
|
||||
const squareShape = new Konva.Rect({
|
||||
width: 100,
|
||||
height: 100,
|
||||
shape = new Konva.Rect({
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
...transform,
|
||||
fill: color,
|
||||
strokeScaleEnabled: false,
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
// Convert square to image
|
||||
const squareDataURL = squareShape.toDataURL();
|
||||
const squareImageObj = new Image();
|
||||
squareImageObj.src = squareDataURL;
|
||||
|
||||
shape = new Konva.Image({
|
||||
image: squareImageObj,
|
||||
x: squareShape.x(),
|
||||
y: squareShape.y(),
|
||||
width: squareShape.width(),
|
||||
height: squareShape.height(),
|
||||
...transform,
|
||||
fill: "transparent",
|
||||
strokeScaleEnabled: false,
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
squareImageObj.onload = () => {
|
||||
this.mediaLayerRef.draw();
|
||||
loaded();
|
||||
};
|
||||
|
||||
// Clean up the temporary square
|
||||
squareShape.destroy();
|
||||
break;
|
||||
|
||||
case ShapeType.TRIANGLE:
|
||||
const triangleShape = new Konva.RegularPolygon({
|
||||
sides: 3,
|
||||
radius: 50,
|
||||
shape = new Konva.Shape({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
...transform,
|
||||
fill: color,
|
||||
strokeScaleEnabled: false,
|
||||
draggable: true,
|
||||
sceneFunc: (context, shape) => {
|
||||
context.beginPath();
|
||||
context.moveTo(shape.width() / 2, 0);
|
||||
context.lineTo(shape.width(), shape.height());
|
||||
context.lineTo(0, shape.height());
|
||||
context.closePath();
|
||||
context.fillStrokeShape(shape);
|
||||
}
|
||||
});
|
||||
|
||||
// Convert triangle to image
|
||||
const triangleDataURL = triangleShape.toDataURL();
|
||||
const triangleImageObj = new Image();
|
||||
triangleImageObj.src = triangleDataURL;
|
||||
|
||||
shape = new Konva.Image({
|
||||
image: triangleImageObj,
|
||||
x: triangleShape.x(),
|
||||
y: triangleShape.y(),
|
||||
width: triangleShape.width(),
|
||||
height: triangleShape.height(),
|
||||
...transform,
|
||||
fill: "transparent",
|
||||
strokeScaleEnabled: false,
|
||||
draggable: true,
|
||||
});
|
||||
|
||||
triangleImageObj.onload = () => {
|
||||
this.mediaLayerRef.draw();
|
||||
loaded();
|
||||
};
|
||||
|
||||
// Clean up the temporary triangle
|
||||
triangleShape.destroy();
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
@@ -252,8 +252,6 @@ export class SelectionManager {
|
||||
},
|
||||
),
|
||||
);
|
||||
this.updateContextComponents();
|
||||
this.showContextComponents();
|
||||
}
|
||||
|
||||
public updateNodeTransformer() {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Shape } from "konva/lib/Shape";
|
||||
import { Group } from "konva/lib/Group";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
import { FileUtilities } from "../FileUtilities/FileUtilities";
|
||||
import { ImageNode, VideoNode, TextNode, ShapeNode, ShapeType } from "../Nodes";
|
||||
@@ -12,7 +12,7 @@ import { MediaNode } from "../types";
|
||||
|
||||
import { RenderTask } from "./RenderTask";
|
||||
import { OffScreenSceneCanvas } from "./OffScreenSceneCanvas";
|
||||
import { Image } from "@tauri-apps/api/image";
|
||||
|
||||
|
||||
import { PaintNode } from "../Nodes/PaintNode";
|
||||
|
||||
@@ -31,7 +31,6 @@ interface ServerSettings {
|
||||
lora_path?: string;
|
||||
}
|
||||
|
||||
|
||||
interface GenerateImageParams {
|
||||
image: string;
|
||||
prompt: string;
|
||||
@@ -62,9 +61,13 @@ export class RealTimeDrawEngine {
|
||||
private positionPreviewY: number;
|
||||
|
||||
private port: MessagePort | undefined;
|
||||
|
||||
public captureCanvas: Konva.Rect;
|
||||
public backgroundRasterRect: Konva.Image;
|
||||
|
||||
|
||||
public previewCanvas: Konva.Image;
|
||||
|
||||
|
||||
|
||||
public videoLoadingCanvas: VideoNode | undefined;
|
||||
|
||||
@@ -88,13 +91,15 @@ export class RealTimeDrawEngine {
|
||||
y: number;
|
||||
},
|
||||
) => void;
|
||||
private offscreenRenderDiv: HTMLDivElement;
|
||||
|
||||
private onPreviewCopyCallback?: (previewCopy: Konva.Image) => void; // New Callback
|
||||
|
||||
private client: WebSocketClient | null = null;
|
||||
private isConnected: boolean = false;
|
||||
|
||||
private backgroundNode: ShapeNode | null = null;
|
||||
|
||||
|
||||
public backgroundColor: string = "#d2d2d2";
|
||||
constructor({
|
||||
width,
|
||||
@@ -184,25 +189,33 @@ export class RealTimeDrawEngine {
|
||||
fill: "white",
|
||||
});
|
||||
|
||||
this.offscreenRenderDiv = document.createElement("div");
|
||||
|
||||
this.backgroundRasterRect = new Konva.Image({
|
||||
name: "backgroundRasterRect",
|
||||
x: this.positionX,
|
||||
y: this.positionY,
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
fill: this.backgroundColor,
|
||||
image: undefined,
|
||||
stroke: "black",
|
||||
strokeWidth: 1,
|
||||
draggable: false,
|
||||
});
|
||||
|
||||
this.mediaLayerRef.add(this.captureCanvas);
|
||||
this.mediaLayerRef.add(this.backgroundRasterRect);
|
||||
this.mediaLayerRef.add(this.previewCanvas);
|
||||
// send back
|
||||
this.captureCanvas.setZIndex(0);
|
||||
this.backgroundRasterRect.setZIndex(0);
|
||||
this.previewCanvas.setZIndex(1);
|
||||
// Add mouse events for preview canvas copying
|
||||
this.previewCopyListener();
|
||||
|
||||
//this.startServer();
|
||||
// isLoadingVisible.value = true;
|
||||
// const fakeTimer = setInterval(() => {
|
||||
// loadingProgress.value += 1;
|
||||
// if (loadingProgress.value >= 100) {
|
||||
// clearInterval(fakeTimer);
|
||||
// isLoadingVisible.value = false;
|
||||
// }
|
||||
// }, 1000);
|
||||
|
||||
|
||||
this.listenToServerEvents();
|
||||
|
||||
}
|
||||
|
||||
public async listenToServerEvents() {
|
||||
@@ -210,16 +223,18 @@ export class RealTimeDrawEngine {
|
||||
console.log(event);
|
||||
// Handle model download events
|
||||
const payload = event.payload as any;
|
||||
|
||||
|
||||
// Model download started
|
||||
if (payload.model_download_started) {
|
||||
const modelInfo = payload.model_download_started;
|
||||
console.log(`Model download started: ${modelInfo.model_name} (${modelInfo.model_type})`);
|
||||
|
||||
console.log(
|
||||
`Model download started: ${modelInfo.model_name} (${modelInfo.model_type})`,
|
||||
);
|
||||
|
||||
// Set up loading indicator
|
||||
isLoadingVisible.value = true;
|
||||
loadingProgress.value = 0;
|
||||
|
||||
|
||||
// Create fake progress updates
|
||||
const downloadTimer = setInterval(() => {
|
||||
loadingProgress.value += 2;
|
||||
@@ -230,23 +245,23 @@ export class RealTimeDrawEngine {
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
|
||||
// Model download completed
|
||||
if (payload.model_download_complete) {
|
||||
const modelInfo = payload.model_download_complete;
|
||||
console.log(`Model download completed: ${modelInfo.model_name} (${modelInfo.model_type})`);
|
||||
|
||||
console.log(
|
||||
`Model download completed: ${modelInfo.model_name} (${modelInfo.model_type})`,
|
||||
);
|
||||
|
||||
// Complete the loading progress
|
||||
loadingProgress.value = 100;
|
||||
|
||||
|
||||
// Hide loading indicator after a short delay
|
||||
setTimeout(() => {
|
||||
isLoadingVisible.value = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private isEnabled: boolean = false;
|
||||
@@ -320,9 +335,16 @@ export class RealTimeDrawEngine {
|
||||
const ctx = cursorCanvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
// Draw the circle
|
||||
// Draw the outer circle with a light stroke
|
||||
ctx.beginPath();
|
||||
ctx.arc(size, size, size / 2, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = "white";
|
||||
ctx.lineWidth = 3;
|
||||
ctx.stroke();
|
||||
|
||||
// Draw the inner circle with a dark stroke
|
||||
ctx.beginPath();
|
||||
ctx.arc(size, size, size / 2 - 1, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = "black";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
@@ -785,6 +807,27 @@ export class RealTimeDrawEngine {
|
||||
}
|
||||
}
|
||||
|
||||
private cloneStageForRender(stage: Konva.Stage, layerOfInterest: Konva.Layer): Konva.Stage {
|
||||
const stageClone = new Konva.Stage({
|
||||
width: stage.width(),
|
||||
height: stage.height(),
|
||||
container: this.offscreenRenderDiv,
|
||||
});
|
||||
|
||||
const renderLayer = new Konva.Layer();
|
||||
|
||||
// Clone all the nodes then reset them to render right
|
||||
layerOfInterest.getChildren().forEach((node) => {
|
||||
const dupNode = node.clone();
|
||||
dupNode.strokeWidth(0);
|
||||
renderLayer.add(dupNode);
|
||||
});
|
||||
|
||||
stageClone.add(renderLayer);
|
||||
|
||||
return stageClone;
|
||||
}
|
||||
|
||||
private async renderFrame(config: {
|
||||
layerOfInterest: Konva.Layer; // layer where the element that you want to clip lives.
|
||||
// XY and height of a captureCanvas ( region of interest )
|
||||
@@ -805,54 +848,25 @@ export class RealTimeDrawEngine {
|
||||
const y = config.y !== undefined ? config.y : Math.floor(box.y);
|
||||
const pixelRatio = config.pixelRatio || 1;
|
||||
|
||||
const container = config.layerOfInterest as Container<Group | Shape>;
|
||||
|
||||
const offScreenSceneCanvas = new OffScreenSceneCanvas({
|
||||
width:
|
||||
config.width || Math.ceil(box.width) || (stage ? stage.width() : 0),
|
||||
height:
|
||||
config.height ||
|
||||
Math.ceil(box.height) ||
|
||||
(stage ? stage.height() : 0),
|
||||
// Clone the required layer from the stage
|
||||
// Set the right details (like removing highlight stroke)
|
||||
// Then render the cloned stage to a bitmap
|
||||
const stageClone = this.cloneStageForRender(stage, config.layerOfInterest);
|
||||
const stageBlob = await stageClone.toBlob({
|
||||
x: x,
|
||||
y: y,
|
||||
width: config.width || Math.ceil(box.width),
|
||||
height: config.height || Math.ceil(box.height),
|
||||
pixelRatio: pixelRatio,
|
||||
});
|
||||
|
||||
const context = offScreenSceneCanvas.getContext();
|
||||
|
||||
const buffer = new OffScreenSceneCanvas({
|
||||
width:
|
||||
offScreenSceneCanvas.width / offScreenSceneCanvas.pixelRatio +
|
||||
Math.abs(x),
|
||||
height:
|
||||
offScreenSceneCanvas.height / offScreenSceneCanvas.pixelRatio +
|
||||
Math.abs(y),
|
||||
pixelRatio: offScreenSceneCanvas.pixelRatio,
|
||||
});
|
||||
|
||||
context.save();
|
||||
|
||||
if (x || y) {
|
||||
context.translate(-1 * x, -1 * y);
|
||||
}
|
||||
container.drawScene(offScreenSceneCanvas, undefined, buffer);
|
||||
// Not a type mistake ... DO NOT FIX
|
||||
// @ts-ignore
|
||||
const offscreenCanvas = offScreenSceneCanvas._canvas as OffscreenCanvas;
|
||||
let result = undefined;
|
||||
}) as Blob;
|
||||
|
||||
// if config.test is true, the result is downloaded to the local files
|
||||
// config.test = true;
|
||||
if (config.test) {
|
||||
const blob = await offscreenCanvas.convertToBlob({
|
||||
quality: config.quality ?? 1.0,
|
||||
type: "image/jpeg",
|
||||
});
|
||||
await FileUtilities.blobToFileJpeg(blob, "1");
|
||||
result = blob;
|
||||
} else {
|
||||
result = offscreenCanvas.transferToImageBitmap();
|
||||
await FileUtilities.blobToFileJpeg(stageBlob, "1");
|
||||
}
|
||||
context.restore();
|
||||
|
||||
const result = await createImageBitmap(stageBlob);
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
@@ -965,8 +979,8 @@ export class RealTimeDrawEngine {
|
||||
|
||||
// Test code
|
||||
if (false) {
|
||||
//this.outputBitmap = bitmap;
|
||||
//this.previewCanvas.image(bitmap);
|
||||
this.outputBitmap = bitmap;
|
||||
this.previewCanvas.image(bitmap);
|
||||
const base64Bitmap = await this.imageBitmapToBase64(bitmap);
|
||||
console.log(this.currentStrength);
|
||||
await this.client?.generateImage({
|
||||
@@ -979,6 +993,7 @@ export class RealTimeDrawEngine {
|
||||
width: 1024,
|
||||
lora_strength: 1.0,
|
||||
});
|
||||
this.isProcessing = false
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1034,6 +1049,19 @@ export class RealTimeDrawEngine {
|
||||
// Add method to create or update background
|
||||
public updateBackground(color: string) {
|
||||
this.captureCanvas.fill(color);
|
||||
this.mediaLayerRef.batchDraw();
|
||||
|
||||
const captureCanvasImage = this.captureCanvas.toDataURL();
|
||||
const imageSource = new Image();
|
||||
imageSource.src = captureCanvasImage;
|
||||
|
||||
imageSource.onload = () => {
|
||||
this.backgroundRasterRect.fill(color);
|
||||
this.backgroundRasterRect.image(imageSource)
|
||||
|
||||
this.mediaLayerRef.batchDraw();
|
||||
this.render();
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { LoadingBar, LoadingBarStatus } from "~/components/ui";
|
||||
import {
|
||||
isLoadingVisible,
|
||||
loadingProgress,
|
||||
loadingText,
|
||||
} from "~/signals/uiEvents/loadingIndicator";
|
||||
|
||||
export const SignaledLoadingIndicator = () => {
|
||||
@@ -10,6 +11,7 @@ export const SignaledLoadingIndicator = () => {
|
||||
useSignalEffect(() => {
|
||||
isLoadingVisible.value;
|
||||
loadingProgress.value;
|
||||
loadingText.value;
|
||||
});
|
||||
|
||||
if (!isLoadingVisible.value) {
|
||||
@@ -47,7 +49,7 @@ export const SignaledLoadingIndicator = () => {
|
||||
className="text-white/90"
|
||||
></path>
|
||||
</svg>
|
||||
Downloading models - {loadingProgress.value}%
|
||||
{loadingText.value} - {loadingProgress.value}%
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,21 @@
|
||||
:root {
|
||||
@apply select-none bg-ui-background font-sans text-white;
|
||||
}
|
||||
|
||||
/* Remove all focus styles */
|
||||
button {
|
||||
@apply outline-none ring-0;
|
||||
-webkit-tap-highlight-color: transparent !important;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
button:focus {
|
||||
@apply outline-none ring-0;
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.pegboard {
|
||||
background-image: radial-gradient(#ffffff25 1px, transparent 0);
|
||||
background-color: #1b1b1f;
|
||||
@@ -187,3 +202,30 @@
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.react-colorful__saturation {
|
||||
margin: 16px 0;
|
||||
border-radius: 10px !important;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.react-colorful__saturation-pointer {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
}
|
||||
|
||||
.react-colorful__hue,
|
||||
.react-colorful__alpha {
|
||||
height: 20px !important;
|
||||
border-radius: 50px !important;
|
||||
}
|
||||
|
||||
.react-colorful__hue {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.react-colorful__hue-pointer,
|
||||
.react-colorful__alpha-pointer {
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,12 @@ export const ToolbarButton = ({
|
||||
className={mergedButtonClasses}
|
||||
disabled={disabled}
|
||||
autoFocus={false}
|
||||
tabIndex={-1}
|
||||
style={{
|
||||
outline: "none",
|
||||
boxShadow: "none",
|
||||
WebkitTapHighlightColor: "transparent",
|
||||
}}
|
||||
{...restButtonProps}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -1,17 +1,43 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { TabSelector, TabItem } from "~/components/ui/TabSelector";
|
||||
import { useComputed } from "@preact/signals-react";
|
||||
import { appMode, dispatchUiEvents } from "~/signals";
|
||||
|
||||
// import { paperWrapperStyles } from "~/components/styles";
|
||||
// import { faPlus, faQuestion } from "@fortawesome/pro-solid-svg-icons";
|
||||
// import { ToolbarButton } from "~/components/features/ToolbarButton";
|
||||
|
||||
export const ToolbarTopLeft = () => {
|
||||
// Use the appMode signal directly with useComputed for reactivity - active tab sets the app mode
|
||||
const activeTab = useComputed(() => appMode.value);
|
||||
|
||||
const tabs: TabItem[] = [
|
||||
{ id: "image", label: "Image" },
|
||||
{ id: "edit", label: "Edit" },
|
||||
// { id: "video", label: "Video" },
|
||||
];
|
||||
|
||||
const handleTabChange = (tabId: string) => {
|
||||
// Update the global signal
|
||||
dispatchUiEvents.changeAppMode(tabId as "image" | "edit");
|
||||
};
|
||||
|
||||
console.log(appMode.value);
|
||||
|
||||
return (
|
||||
<div className={twMerge("z-20 flex h-fit w-fit items-center gap-1")}>
|
||||
<div className={twMerge("z-20 flex h-fit w-fit items-center gap-8 px-4")}>
|
||||
<img
|
||||
src="/brand/mira-logo.png"
|
||||
alt="logo"
|
||||
className="h-[30px] select-none pb-1"
|
||||
/>
|
||||
|
||||
<TabSelector
|
||||
tabs={tabs}
|
||||
activeTab={activeTab.value}
|
||||
onTabChange={handleTabChange}
|
||||
/>
|
||||
|
||||
{/* <ToolbarButton icon={faPlus}>
|
||||
<span className="font-semibold">New Board</span>
|
||||
</ToolbarButton>
|
||||
|
||||
@@ -28,17 +28,17 @@ export const Button = ({
|
||||
function getVariantClassNames(variant: string) {
|
||||
switch (variant) {
|
||||
case "tertiary": {
|
||||
return "bg-tertiary hover:bg-tertiary-400 text-white focus-visible:outline-tertiary-600";
|
||||
return "bg-tertiary hover:bg-tertiary-400 text-white";
|
||||
}
|
||||
case "secondary": {
|
||||
return "bg-ui-controls hover:bg-ui-controls/80 text-white focus-visible:outline-secondary";
|
||||
return "bg-ui-controls hover:bg-ui-controls/80 text-white";
|
||||
}
|
||||
case "action": {
|
||||
return " bg-action hover:bg-action-500 text-white focus-visible:outline-action";
|
||||
return "bg-action hover:bg-action-500 text-white";
|
||||
}
|
||||
case "primary":
|
||||
default: {
|
||||
return "bg-primary hover:bg-primary-600 text-white focus-visible:outline-primary-600";
|
||||
return "bg-primary hover:bg-primary-600 text-white";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,6 @@ export const Button = ({
|
||||
"text-[15px] font-semibold rounded-lg px-3 py-2 shadow-sm",
|
||||
"flex gap-2 items-center justify-center",
|
||||
"transition-all duration-150",
|
||||
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2",
|
||||
getVariantClassNames(propsVariant),
|
||||
propsClassName,
|
||||
disabledClass,
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { MouseEvent, MouseEventHandler, useState } from "react";
|
||||
import { HexColorPicker } from "react-colorful";
|
||||
import { EyeDropper } from "react-eyedrop";
|
||||
import { Button } from "./Button";
|
||||
import { faEyedropper } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
type ColorPickerHTMLAttributes = Omit<React.HTMLAttributes<HTMLDivElement>, "color" | "onChange" | "onChangeCapture">;
|
||||
interface ColorPickerBaseProps<T extends string> extends ColorPickerHTMLAttributes {
|
||||
type ColorPickerHTMLAttributes = Omit<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
"color" | "onChange" | "onChangeCapture"
|
||||
>;
|
||||
interface ColorPickerBaseProps<T extends string>
|
||||
extends ColorPickerHTMLAttributes {
|
||||
color: T;
|
||||
onChange: (newColor: T) => void;
|
||||
}
|
||||
@@ -17,7 +18,7 @@ export const HexEyedropPicker = ({
|
||||
dropperClassName,
|
||||
pickerClassName,
|
||||
onPickerChange,
|
||||
Picker
|
||||
Picker,
|
||||
}: {
|
||||
color: string;
|
||||
onDropperChange?: (newColor: string) => void;
|
||||
@@ -26,9 +27,8 @@ export const HexEyedropPicker = ({
|
||||
onPickerChange: (newColor: string) => void;
|
||||
Picker: (props: Partial<ColorPickerBaseProps<string>>) => JSX.Element;
|
||||
}) => {
|
||||
|
||||
const handleDropperClick = () => {
|
||||
if (!('EyeDropper' in window)) {
|
||||
if (!("EyeDropper" in window)) {
|
||||
// NOTE: This shouldn't happen cuz we're using Chromium
|
||||
console.error("EyeDropper not found in window");
|
||||
return;
|
||||
@@ -46,42 +46,40 @@ export const HexEyedropPicker = ({
|
||||
// User cancelled the picker
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
startEyeDrop().then((newColor) => {
|
||||
if (newColor) {
|
||||
onDropperChange?.(newColor);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const handleOnMouseDown = () => {
|
||||
setIsDragging(true);
|
||||
}
|
||||
const handleOnMouseUp = () => {
|
||||
setIsDragging(false);
|
||||
}
|
||||
// const [isDragging, setIsDragging] = useState(false);
|
||||
// const handleOnMouseDown = () => {
|
||||
// setIsDragging(true);
|
||||
// };
|
||||
// const handleOnMouseUp = () => {
|
||||
// setIsDragging(false);
|
||||
// };
|
||||
|
||||
return (
|
||||
<div className={"relative"}>
|
||||
<Picker
|
||||
className={pickerClassName}
|
||||
color={color}
|
||||
onChange={onPickerChange}
|
||||
onMouseDown={handleOnMouseDown}
|
||||
onMouseUp={handleOnMouseUp}
|
||||
/>
|
||||
<Button
|
||||
className={twMerge([
|
||||
"rounded-full m-1 size-8 text-ui-controls bg-transparent hover:bg-black/70 hover:text-white z-20 shadow-none hover:shadow-xl duration-200",
|
||||
"z-20 size-8 rounded-xl bg-ui-background/30 text-white shadow-none duration-200 hover:bg-white/10 hover:text-white hover:shadow-xl",
|
||||
dropperClassName,
|
||||
"absolute left-0 top-0",
|
||||
isDragging ? "opacity-0 -z-10" : ""
|
||||
])}
|
||||
icon={faEyedropper}
|
||||
onClick={handleDropperClick}
|
||||
/>
|
||||
<Picker
|
||||
className={pickerClassName}
|
||||
color={color}
|
||||
onChange={onPickerChange}
|
||||
// onMouseDown={handleOnMouseDown}
|
||||
// onMouseUp={handleOnMouseUp}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
import { Tab, TabGroup, TabList } from "@headlessui/react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export interface TabItem {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface TabSelectorProps {
|
||||
tabs: TabItem[];
|
||||
activeTab: string;
|
||||
onTabChange: (tabId: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TabSelector: React.FC<TabSelectorProps> = ({
|
||||
tabs,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
className,
|
||||
}) => {
|
||||
// Find the index of the active tab
|
||||
const selectedIndex = tabs.findIndex((tab) => tab.id === activeTab);
|
||||
const tabsRef = useRef<(HTMLElement | null)[]>([]);
|
||||
const [indicatorStyle, setIndicatorStyle] = useState({
|
||||
left: 0,
|
||||
width: 0,
|
||||
});
|
||||
|
||||
// Handle tab change
|
||||
const handleTabChange = (index: number) => {
|
||||
onTabChange(tabs[index].id);
|
||||
};
|
||||
|
||||
// Update the indicator position when the selected tab changes
|
||||
useEffect(() => {
|
||||
if (selectedIndex >= 0 && tabsRef.current[selectedIndex]) {
|
||||
const currentTab = tabsRef.current[selectedIndex];
|
||||
if (currentTab) {
|
||||
setIndicatorStyle({
|
||||
left: currentTab.offsetLeft,
|
||||
width: currentTab.offsetWidth,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [selectedIndex, tabs]);
|
||||
|
||||
return (
|
||||
<TabGroup
|
||||
selectedIndex={selectedIndex}
|
||||
onChange={handleTabChange}
|
||||
className={twMerge("w-full", className)}
|
||||
>
|
||||
<TabList className="glass relative inline-flex min-w-fit overflow-x-auto rounded-lg p-1 shadow-lg">
|
||||
{/* Animated indicator */}
|
||||
<div
|
||||
className="absolute top-1 z-10 h-[calc(100%-8px)] rounded-md border-2 border-primary bg-primary/30 transition-all duration-300 ease-in-out"
|
||||
style={{
|
||||
left: indicatorStyle.left,
|
||||
width: indicatorStyle.width,
|
||||
}}
|
||||
/>
|
||||
|
||||
{tabs.map((tab, index) => (
|
||||
<Tab
|
||||
key={tab.id}
|
||||
ref={(el) => (tabsRef.current[index] = el)}
|
||||
className={({ selected }) =>
|
||||
twMerge(
|
||||
"relative z-20 mx-0.5 min-w-max rounded-md border-2 border-transparent px-4 py-1 text-center font-semibold transition-all duration-300 ease-in-out",
|
||||
selected ? "text-white" : "text-gray-300 hover:text-white",
|
||||
)
|
||||
}
|
||||
>
|
||||
{tab.label}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
</TabGroup>
|
||||
);
|
||||
};
|
||||
@@ -91,7 +91,7 @@ export const ColorPicker = ({
|
||||
if (streamChanges) {
|
||||
onChange(color);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
@@ -124,7 +124,7 @@ export const ColorPicker = ({
|
||||
anchor={anchor}
|
||||
className={twMerge(
|
||||
paperWrapperStyles,
|
||||
"flex flex-col items-center gap-2 overflow-hidden",
|
||||
"flex flex-col items-center gap-2 overflow-hidden p-4",
|
||||
)}
|
||||
style={
|
||||
{
|
||||
@@ -134,13 +134,14 @@ export const ColorPicker = ({
|
||||
static
|
||||
>
|
||||
<HexEyedropPicker
|
||||
pickerClassName="overflow-hidden"
|
||||
color={currColor}
|
||||
onPickerChange={handleColorChange}
|
||||
onDropperChange={handleColorChange}
|
||||
Picker={HexAlphaColorPicker}
|
||||
/>
|
||||
<Input
|
||||
className="mt-2"
|
||||
inputClassName="bg-ui-background/30"
|
||||
style={{ width: "198px" }}
|
||||
icon={faHashtag}
|
||||
value={textInput}
|
||||
|
||||
@@ -11,17 +11,9 @@ import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { paperWrapperStyles } from "~/components/styles";
|
||||
import {
|
||||
BRUSH_MAX_SIZE,
|
||||
BRUSH_MIN_SIZE,
|
||||
paintBrushSize,
|
||||
setPaintBrushSize,
|
||||
} from "~/signals/uiEvents/toolbarMain/paintMode";
|
||||
import { Button } from "../Button";
|
||||
import { Input } from "../Input";
|
||||
import { Slider } from "../Slider";
|
||||
import { EyeDropper } from "react-eyedrop";
|
||||
import { HexEyedropPicker } from "../HexEyedropPicker";
|
||||
import { Input } from "../Input";
|
||||
|
||||
export const BackgroundMenu = ({
|
||||
color: prevColor,
|
||||
|
||||
@@ -5,13 +5,18 @@ import {
|
||||
} from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import { ChangeEvent, useEffect, useState, useRef } from "react";
|
||||
import { HexAlphaColorPicker } from "react-colorful";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { paperWrapperStyles } from "~/components/styles";
|
||||
import { BRUSH_MAX_SIZE, BRUSH_MIN_SIZE, paintBrushSize, setPaintBrushSize } from "~/signals/uiEvents/toolbarMain/paintMode";
|
||||
import {
|
||||
BRUSH_MAX_SIZE,
|
||||
BRUSH_MIN_SIZE,
|
||||
paintBrushSize,
|
||||
setPaintBrushSize,
|
||||
} from "~/signals/uiEvents/toolbarMain/paintMode";
|
||||
import { Button } from "../Button";
|
||||
import { Input } from "../Input";
|
||||
import { Slider } from "../Slider";
|
||||
@@ -52,16 +57,58 @@ export const PaintModeMenu = ({
|
||||
currColor: prevColor,
|
||||
textInput: prevColor.substring(1),
|
||||
});
|
||||
const [isMouseDown, setIsMouseDown] = useState(false);
|
||||
const [isMouseOutside, setIsMouseOutside] = useState(false);
|
||||
const closeRef = useRef<(() => void) | null>(null);
|
||||
const closeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setIsOpen(defaultOpen);
|
||||
}, [defaultOpen]);
|
||||
|
||||
// Global mouse up handler
|
||||
useEffect(() => {
|
||||
const handleGlobalMouseUp = () => {
|
||||
if (isMouseDown) {
|
||||
setIsMouseDown(false);
|
||||
|
||||
// If mouse is outside when released, close the panel after a short delay
|
||||
if (isMouseOutside && closeOnMouseLeave && closeRef.current) {
|
||||
closeTimeoutRef.current = setTimeout(() => {
|
||||
onChange(currColor);
|
||||
setIsOpen(false);
|
||||
closeRef.current?.();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("mouseup", handleGlobalMouseUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mouseup", handleGlobalMouseUp);
|
||||
};
|
||||
}, [isMouseDown, isMouseOutside, closeOnMouseLeave, currColor, onChange]);
|
||||
|
||||
const handleMouseLeave = (close: () => void) => {
|
||||
if (closeOnMouseLeave) {
|
||||
onChange(currColor);
|
||||
setIsOpen(false);
|
||||
close();
|
||||
closeRef.current = close;
|
||||
setIsMouseOutside(true);
|
||||
|
||||
// Only close if mouse is not down and closeOnMouseLeave is true
|
||||
if (closeOnMouseLeave && !isMouseDown) {
|
||||
closeTimeoutRef.current = setTimeout(() => {
|
||||
onChange(currColor);
|
||||
setIsOpen(false);
|
||||
close();
|
||||
}, 200);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsMouseOutside(false);
|
||||
if (closeTimeoutRef.current) {
|
||||
clearTimeout(closeTimeoutRef.current);
|
||||
closeTimeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -92,7 +139,7 @@ export const PaintModeMenu = ({
|
||||
if (streamChanges) {
|
||||
onChange(color);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Rerender the component when these signals change:
|
||||
useSignals();
|
||||
@@ -123,12 +170,16 @@ export const PaintModeMenu = ({
|
||||
)}
|
||||
</PopoverButton>
|
||||
{(open || isOpen) && (
|
||||
<div onMouseLeave={() => handleMouseLeave(close)}>
|
||||
<div
|
||||
onMouseLeave={() => handleMouseLeave(close)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseDown={() => setIsMouseDown(true)}
|
||||
>
|
||||
<PopoverPanel
|
||||
anchor={anchor}
|
||||
className={twMerge(
|
||||
paperWrapperStyles,
|
||||
"flex flex-col items-center gap-2 overflow-hidden",
|
||||
"flex flex-col items-center gap-2 overflow-hidden p-4",
|
||||
)}
|
||||
style={
|
||||
{
|
||||
@@ -138,20 +189,23 @@ export const PaintModeMenu = ({
|
||||
static
|
||||
>
|
||||
<HexEyedropPicker
|
||||
pickerClassName="overflow-hidden"
|
||||
color={currColor}
|
||||
onPickerChange={handleColorChange}
|
||||
onDropperChange={handleColorChange}
|
||||
Picker={HexAlphaColorPicker}
|
||||
/>
|
||||
<Input
|
||||
className="mt-2"
|
||||
inputClassName="bg-ui-background/30"
|
||||
style={{ width: "198px" }}
|
||||
icon={faHashtag}
|
||||
value={textInput}
|
||||
onChange={handleTextInput}
|
||||
/>
|
||||
<div className="flex flex-col w-full mt-2 gap-2">
|
||||
<p className="text-white font-medium text-sm w-full justify-start">Brush Size:</p>
|
||||
<div className="mt-2 flex w-full flex-col gap-2">
|
||||
<p className="w-full justify-start text-sm font-medium text-white">
|
||||
Brush Size:
|
||||
</p>
|
||||
<Slider
|
||||
min={BRUSH_MIN_SIZE}
|
||||
max={BRUSH_MAX_SIZE}
|
||||
|
||||
@@ -8,6 +8,7 @@ export * from "./PanelRight";
|
||||
export * from "./Slider";
|
||||
export * from "./Spinner";
|
||||
export * from "./Switches";
|
||||
export * from "./TabSelector";
|
||||
export * from "./TextArea";
|
||||
export * from "./TextEditor";
|
||||
export * from "./Tooltip";
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { signal, effect } from "@preact/signals-core";
|
||||
|
||||
// this is to control the app mode - BFlat
|
||||
|
||||
// Define the app mode types
|
||||
export type AppModeType = "image" | "edit";
|
||||
|
||||
// Create a signal for the current app mode
|
||||
export const appMode = signal<AppModeType>("image");
|
||||
|
||||
// Create a function to change the app mode
|
||||
export const changeAppMode = (mode: AppModeType) => {
|
||||
appMode.value = mode;
|
||||
};
|
||||
|
||||
// Store callbacks for mode changes
|
||||
const callbacks: ((mode: AppModeType) => void)[] = [];
|
||||
|
||||
// Set up an effect to notify callbacks when the mode changes
|
||||
effect(() => {
|
||||
const currentMode = appMode.value;
|
||||
callbacks.forEach((callback) => callback(currentMode));
|
||||
});
|
||||
|
||||
// Export an event handler for components to use
|
||||
export const appModeEvents = {
|
||||
onChange: (callback: (mode: AppModeType) => void) => {
|
||||
callbacks.push(callback);
|
||||
|
||||
// Return a function to unsubscribe
|
||||
return () => {
|
||||
const index = callbacks.indexOf(callback);
|
||||
if (index !== -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
dispatchers as modelSelectionDispatchers,
|
||||
events as modelSelectionEvents,
|
||||
} from "./modelSelection";
|
||||
import { appMode, changeAppMode, appModeEvents } from "./appMode";
|
||||
|
||||
export const uiEvents = {
|
||||
...addMediaEvents,
|
||||
@@ -49,6 +50,7 @@ export const uiEvents = {
|
||||
promptEvents: promptEvents,
|
||||
loadingIndicator: loadingIndicatorEvents,
|
||||
modelSelection: modelSelectionEvents,
|
||||
appMode: appModeEvents,
|
||||
};
|
||||
export const dispatchUiEvents = {
|
||||
...addMediaDispatchers,
|
||||
@@ -62,4 +64,7 @@ export const dispatchUiEvents = {
|
||||
promptSettings: promptDispatchers,
|
||||
loadingIndicator: loadingIndicatorDispatchers,
|
||||
modelSelection: modelSelectionDispatchers,
|
||||
changeAppMode,
|
||||
};
|
||||
|
||||
export { appMode };
|
||||
|
||||
@@ -28,13 +28,28 @@ const onLoadingProgressChanged = (callback: (progress: number) => void) => {
|
||||
});
|
||||
};
|
||||
|
||||
// Loading text signal
|
||||
export const DEFAULT_LOADING_TEXT = "Downloading";
|
||||
export const loadingText = signal<string>(DEFAULT_LOADING_TEXT);
|
||||
const setLoadingText = (text: string) => {
|
||||
loadingText.value = text;
|
||||
};
|
||||
|
||||
const onLoadingTextChanged = (callback: (text: string) => void) => {
|
||||
effect(() => {
|
||||
callback(loadingText.value);
|
||||
});
|
||||
};
|
||||
|
||||
// EXPORTS
|
||||
export const dispatchers = {
|
||||
setIsLoadingVisible,
|
||||
setLoadingProgress,
|
||||
setLoadingText,
|
||||
};
|
||||
|
||||
export const events = {
|
||||
onIsLoadingVisibleChanged,
|
||||
onLoadingProgressChanged,
|
||||
onLoadingTextChanged,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user