Compare commits

...
Author SHA1 Message Date
Michael Chung 1ce9c8bed8 Added reference images to the generation
enqueueImageGeneration now accepts additionalImages
enqueueImageGeneration needs testing.
Polling for jobs doesn't work,
most api calls don't work and need integration testing.
2025-04-03 17:26:52 -04:00
Michael Chung 25c15bffd8 Merge branch 'main' into feature/gen 2025-04-03 12:37:31 -04:00
bflatastic 8ff91690c1 bring back objects, skybox, creatures tabs 2025-04-03 23:24:14 +07:00
BFlat 97bbe0fe07 add image ui (#471) 2025-04-03 23:01:02 +07:00
Michael Chung 2d361c6636 Merge branch 'main' into feature/gen 2025-04-03 11:55:38 -04:00
BFlat 9361634d83 final fix ratio default to 3:2 (#469) 2025-04-03 12:41:40 +07:00
bflatastic 7cf0363555 fixed ratio in render 3:2 2025-04-03 12:32:45 +07:00
bflatastic c2ee3b0ce1 render default to 3:2 ratio 2025-04-03 12:30:57 +07:00
BFlat ca6fc13171 aspect ratio update (#468)
- added 3:2, 2:3 ratios (defaulted to 3:2 [1200 x 800 px])
- added letterbox accordingly
- aspect ratio buttons to show icons for each ratio
- popover to close when item selected
2025-04-03 12:23:54 +07:00
Michael Chung 534afbbbf9 Wire screenshot upload and generate to the engine. 2025-04-02 21:36:42 -04:00
Brandon Thomas cb1ea1f828 fix CDN in local development 2025-04-02 18:48:44 -04:00
bflatastic 010e3be5dc make letterbox always visible 2025-04-02 22:59:59 +07:00
bflatastic d8dab1a66e fix trimming 2 2025-04-02 22:42:28 +07:00
bflatastic 97b236d04d paste trim fix 2025-04-02 22:40:56 +07:00
bflatastic 15fe59cc19 added enqueueing state to generate button, limited textarea height and updates 2025-04-02 22:36:53 +07:00
BFlat 89bec31540 removed video controls and add prompt box (#467) 2025-04-02 22:01:19 +07:00
ArEnScandMichael Chung 7568a0ef86 Fixes for the new 3d editor (#465)
Removed the login page and timeline and confusing UI components.
Extended scene to the bottom of the screen.

Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-04-01 19:11:42 -04:00
Brandon Thomas 6180222e15 netlify.toml for editor2d 2025-04-01 00:48:53 -04:00
Brandon Thomas 6d45ce29e2 Adding copy of realtime as editor2d for web deployment 2025-04-01 00:44:28 -04:00
Brandon Thomas 1da56d6d22 netlify build outputs dir 2025-04-01 00:06:22 -04:00
Brandon Thomas 91a1c00442 reserved names 2025-04-01 00:00:52 -04:00
Brandon Thomas 3db942f1f6 artcraft cors 2025-03-31 23:58:48 -04:00
Brandon Thomas 2e56291649 unblock rust docker builds 2025-03-31 21:41:54 -04:00
Brandon Thomas b31badc6d1 Editor3D - this directory is a fork of "studio gen 2" by Tanish.
We'll be using this for our image generation roadmap:

https://docs.google.com/document/d/1O9YI_iLwXkl2wAJJ23dFp3eHR-JzhlRHCBPD8psG-1E/edit

The original Studio Gen 2 is in the `frontend/studio-gen-2` directory.

The original Studio Gen 1 is in the `frontend/studio-gen-1` directory
2025-03-31 13:28:59 -04:00
Brandon Thomas ed399fcb21 rename storyteller animator to "studio-gen-2" 2025-03-31 13:25:51 -04:00
Brandon Thomas d858c9394b work in progress background removal adjustment 2025-03-28 01:33:52 -04:00
Brandon Thomas a19a6599ec rename model 2025-03-27 23:02:51 -04:00
Brandon Thomas 57c15f7c9f rename realtime to sketch 2025-03-27 22:41:40 -04:00
Brandon Thomas aa25ae8c87 load flux models from R2 2025-03-27 21:04:39 -04:00
Brandon Thomas c27055761e fix 2025-03-26 20:35:50 -04:00
Brandon Thomas abf3024892 add to downloaders 2025-03-26 20:35:11 -04:00
Brandon Thomas 2182d4dde1 lots of new models in R2 2025-03-26 20:32:05 -04:00
Brandon Thomas b632b927a3 save images to file 2025-03-26 13:12:54 -04:00
Brandon Thomas 4e2be12064 flux cherry picked in 2025-03-26 02:37:04 -04:00
Brandon Thomas 29b33cd327 save image handler 2025-03-25 22:43:25 -04:00
Brandon Thomas 032d0e1eae remove lock button 2025-03-25 20:05:49 -04:00
Brandon Thomas a3ddffeeee get rid of "transform" / "move" context menu 2025-03-25 19:54:51 -04:00
Brandon Thomas 28a5ff2f9f remove background from preview images 2025-03-25 19:43:48 -04:00
Brandon Thomas 1028e04e02 turn lcm println!() to debug!() level 2025-03-25 17:45:29 -04:00
Brandon Thomas 49c010db59 temporarily remove references to flux (temp fix) 2025-03-25 17:34:33 -04:00
Kasisnu 48ff60d3b8 super basic flux (#463) 2025-03-25 17:05:50 -04:00
Brandon Thomas 9c5b993c4f read seed from config too 2025-03-25 12:34:33 -04:00
Brandon Thomas a4aecc4d38 send seed as a parameter 2025-03-25 12:21:47 -04:00
Brandon Thomas 4d73230e84 add logging 2025-03-25 10:44:11 -04:00
Brandon Thomas a5a09f3e64 fixup inpaint stub 2025-03-25 01:44:20 -04:00
49894bd9b3 Generate Mode (#461)
Co-authored-by: Tanish Manku <-h>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
2025-03-25 01:38:56 -04:00
3137 changed files with 153119 additions and 1465 deletions
Generated
+107 -64
View File
@@ -559,15 +559,15 @@ checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_log-sys"
version = "0.3.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ecc8056bf6ab9892dcd53216c83d1597487d7dacac16c8df6b877d127df9937"
checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d"
[[package]]
name = "android_logger"
version = "0.14.1"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b07e8e73d720a1f2e4b6014766e6039fd2e96a4fa44e2a78d0e1fa2ff49826"
checksum = "f6f39be698127218cca460cb624878c9aa4e2b47dba3b277963d2bf00bad263b"
dependencies = [
"android_log-sys",
"env_filter",
@@ -661,7 +661,10 @@ dependencies = [
"assertor",
"base64 0.22.1",
"bytes 1.10.0",
"chrono",
"const_format",
"crockford",
"cudarc",
"directories 6.0.0",
"dirs 6.0.0",
"filesys",
@@ -674,6 +677,7 @@ dependencies = [
"ml_models",
"ml_weights_registry",
"once_cell",
"ort",
"primitives",
"rand 0.8.5",
"reqwest 0.12.12",
@@ -1373,7 +1377,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
dependencies = [
"objc2",
"objc2 0.5.2",
]
[[package]]
@@ -1835,6 +1839,7 @@ dependencies = [
"fancy-regex",
"half",
"intel-mkl-src",
"log",
"num-traits",
"rand 0.8.5",
"rayon",
@@ -1971,9 +1976,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.39"
version = "0.4.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
dependencies = [
"android-tzdata",
"iana-time-zone",
@@ -1981,7 +1986,7 @@ dependencies = [
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets 0.52.6",
"windows-link",
]
[[package]]
@@ -3949,9 +3954,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fdeflate"
version = "0.3.1"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64d6dafc854908ff5da46ff3f8f473c6984119a2876a383a860246dd7841a868"
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
dependencies = [
"simd-adler32",
]
@@ -5458,9 +5463,9 @@ dependencies = [
[[package]]
name = "image"
version = "0.25.5"
version = "0.25.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b"
checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a"
dependencies = [
"bytemuck",
"byteorder-lite",
@@ -5489,6 +5494,16 @@ dependencies = [
"quick-error",
]
[[package]]
name = "image_algorithms"
version = "0.0.1"
dependencies = [
"errors",
"image",
"my-workspace-hack",
"testing",
]
[[package]]
name = "imageproc"
version = "0.24.0"
@@ -6307,9 +6322,9 @@ checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e"
[[package]]
name = "log"
version = "0.4.21"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
dependencies = [
"value-bag",
]
@@ -6729,6 +6744,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
@@ -6817,6 +6833,7 @@ dependencies = [
"half",
"hf-hub 0.3.2",
"image",
"image_algorithms",
"libloading 0.8.6",
"log",
"memory_caching",
@@ -6921,9 +6938,9 @@ dependencies = [
"dpi",
"gtk",
"keyboard-types",
"objc2",
"objc2 0.5.2",
"objc2-app-kit",
"objc2-foundation",
"objc2-foundation 0.2.2",
"once_cell",
"png",
"serde",
@@ -7449,6 +7466,15 @@ dependencies = [
"objc2-encode",
]
[[package]]
name = "objc2"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3531f65190d9cff863b77a99857e74c314dd16bf56c538c4b57c7cbc3f3a6e59"
dependencies = [
"objc2-encode",
]
[[package]]
name = "objc2-app-kit"
version = "0.2.2"
@@ -7458,10 +7484,10 @@ dependencies = [
"bitflags 2.5.0",
"block2",
"libc",
"objc2",
"objc2 0.5.2",
"objc2-core-data",
"objc2-core-image",
"objc2-foundation",
"objc2-foundation 0.2.2",
"objc2-quartz-core",
]
@@ -7473,9 +7499,9 @@ checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
dependencies = [
"bitflags 2.5.0",
"block2",
"objc2",
"objc2 0.5.2",
"objc2-core-location",
"objc2-foundation",
"objc2-foundation 0.2.2",
]
[[package]]
@@ -7485,8 +7511,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
dependencies = [
"block2",
"objc2",
"objc2-foundation",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
[[package]]
@@ -7497,8 +7523,8 @@ checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
dependencies = [
"bitflags 2.5.0",
"block2",
"objc2",
"objc2-foundation",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
[[package]]
@@ -7508,8 +7534,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
dependencies = [
"block2",
"objc2",
"objc2-foundation",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-metal",
]
@@ -7520,9 +7546,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
dependencies = [
"block2",
"objc2",
"objc2 0.5.2",
"objc2-contacts",
"objc2-foundation",
"objc2-foundation 0.2.2",
]
[[package]]
@@ -7540,7 +7566,17 @@ dependencies = [
"bitflags 2.5.0",
"block2",
"libc",
"objc2",
"objc2 0.5.2",
]
[[package]]
name = "objc2-foundation"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998"
dependencies = [
"bitflags 2.5.0",
"objc2 0.6.0",
]
[[package]]
@@ -7550,9 +7586,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
dependencies = [
"block2",
"objc2",
"objc2 0.5.2",
"objc2-app-kit",
"objc2-foundation",
"objc2-foundation 0.2.2",
]
[[package]]
@@ -7563,8 +7599,8 @@ checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
dependencies = [
"bitflags 2.5.0",
"block2",
"objc2",
"objc2-foundation",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
[[package]]
@@ -7575,8 +7611,8 @@ checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
dependencies = [
"bitflags 2.5.0",
"block2",
"objc2",
"objc2-foundation",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-metal",
]
@@ -7586,8 +7622,8 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
dependencies = [
"objc2",
"objc2-foundation",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
[[package]]
@@ -7598,12 +7634,12 @@ checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
dependencies = [
"bitflags 2.5.0",
"block2",
"objc2",
"objc2 0.5.2",
"objc2-cloud-kit",
"objc2-core-data",
"objc2-core-image",
"objc2-core-location",
"objc2-foundation",
"objc2-foundation 0.2.2",
"objc2-link-presentation",
"objc2-quartz-core",
"objc2-symbols",
@@ -7618,8 +7654,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
dependencies = [
"block2",
"objc2",
"objc2-foundation",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
[[package]]
@@ -7630,9 +7666,9 @@ checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
dependencies = [
"bitflags 2.5.0",
"block2",
"objc2",
"objc2 0.5.2",
"objc2-core-location",
"objc2-foundation",
"objc2-foundation 0.2.2",
]
[[package]]
@@ -7643,9 +7679,9 @@ checksum = "68bc69301064cebefc6c4c90ce9cba69225239e4b8ff99d445a2b5563797da65"
dependencies = [
"bitflags 2.5.0",
"block2",
"objc2",
"objc2 0.5.2",
"objc2-app-kit",
"objc2-foundation",
"objc2-foundation 0.2.2",
]
[[package]]
@@ -7935,6 +7971,7 @@ dependencies = [
"half",
"lazy_static",
"libc",
"libloading 0.7.4",
"ndarray",
"tar",
"thiserror 1.0.63",
@@ -8419,15 +8456,15 @@ dependencies = [
[[package]]
name = "png"
version = "0.17.10"
version = "0.17.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64"
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
dependencies = [
"bitflags 1.3.2",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide 0.7.2",
"miniz_oxide 0.8.4",
]
[[package]]
@@ -10635,8 +10672,8 @@ dependencies = [
"foreign-types 0.5.0",
"js-sys",
"log",
"objc2",
"objc2-foundation",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-quartz-core",
"raw-window-handle",
"redox_syscall 0.5.2",
@@ -11808,9 +11845,9 @@ dependencies = [
"log",
"mime",
"muda",
"objc2",
"objc2 0.5.2",
"objc2-app-kit",
"objc2-foundation",
"objc2-foundation 0.2.2",
"percent-encoding",
"plist",
"raw-window-handle",
@@ -11918,16 +11955,16 @@ dependencies = [
[[package]]
name = "tauri-plugin-log"
version = "2.2.1"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "367a28a5e0ca39eac98005699466e8906edc4a2a8f8e13a5f1a71dc0bea6c677"
checksum = "2341d5b9bc5318c8e34f35a569140c78337241aa9c14091550b424c49f0314e0"
dependencies = [
"android_logger",
"byte-unit",
"fern",
"log",
"objc2",
"objc2-foundation",
"objc2 0.6.0",
"objc2-foundation 0.3.0",
"serde",
"serde_json",
"serde_repr",
@@ -11967,9 +12004,9 @@ dependencies = [
"http 1.1.0",
"jni",
"log",
"objc2",
"objc2 0.5.2",
"objc2-app-kit",
"objc2-foundation",
"objc2-foundation 0.2.2",
"percent-encoding",
"raw-window-handle",
"softbuffer",
@@ -12779,9 +12816,9 @@ dependencies = [
"dirs 5.0.1",
"libappindicator",
"muda",
"objc2",
"objc2 0.5.2",
"objc2-app-kit",
"objc2-foundation",
"objc2-foundation 0.2.2",
"once_cell",
"png",
"serde",
@@ -13752,9 +13789,9 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ea403deff7b51fff19e261330f71608ff2cdef5721d72b64180bb95be7c4150"
dependencies = [
"objc2",
"objc2 0.5.2",
"objc2-app-kit",
"objc2-foundation",
"objc2-foundation 0.2.2",
"raw-window-handle",
"windows-sys 0.59.0",
"windows-version",
@@ -13853,6 +13890,12 @@ dependencies = [
"syn 2.0.87",
]
[[package]]
name = "windows-link"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-registry"
version = "0.2.0"
@@ -14247,9 +14290,9 @@ dependencies = [
"kuchikiki",
"libc",
"ndk 0.9.0",
"objc2",
"objc2 0.5.2",
"objc2-app-kit",
"objc2-foundation",
"objc2-foundation 0.2.2",
"objc2-ui-kit",
"objc2-web-kit",
"once_cell",
+1
View File
@@ -58,6 +58,7 @@ members = [
"crates/lib/files/mimetypes",
"crates/lib/files/videos",
"crates/lib/idempotency",
"crates/lib/image_algorithms",
"crates/lib/jobs_common",
"crates/lib/logging",
"crates/lib/markdown",
+1
View File
@@ -20,6 +20,7 @@ normal = ["my-workspace-hack"]
# Internal
filesys = { path = "../../lib/files/filesys" }
image_algorithms = { path = "../../lib/image_algorithms" }
memory_caching = { path = "../../lib/caching/memory_caching" }
ml_weights_registry = { path = "../ml_weights_registry" }
primitives = { path = "../../lib/data/primitives" }
@@ -3,6 +3,7 @@ use image::{imageops, DynamicImage};
use ndarray::{Array, CowArray};
use ort::Value;
use std::path::Path;
use image_algorithms::remove_image_border::remove_image_border;
pub async fn remove_image_background<P: AsRef<Path>>(model_path: P, image: DynamicImage) -> anyhow::Result<DynamicImage> {
// NB: Real time needed cuDNN 9, but this uses cuDNN 8:
@@ -11,6 +12,10 @@ pub async fn remove_image_background<P: AsRef<Path>>(model_path: P, image: Dynam
// update-alternatives: using /usr/include/x86_64-linux-gnu/cudnn_v8.h to provide /usr/include/cudnn.h (libcudnn) in manual mode
let session = onnx_session(model_path.as_ref())?;
let image = process_dynamic_image(&session, image)?;
// Crop transparent border cruft away.
let image = remove_image_border(&image)?;
Ok(image)
}
@@ -0,0 +1,297 @@
use std::path::Path;
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
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::prompt_cache::PromptCache;
use candle_nn::VarBuilder;
use crate::ml::model_config::ModelConfig;
use anyhow::{anyhow, Error as E, Result};
use candle_core::{DType, IndexOp, Tensor, D, Device, Module};
use candle_transformers::models::stable_diffusion::vae::{AutoEncoderKL, DiagonalGaussianDistribution};
use candle_transformers::models::stable_diffusion::lcm::LCMScheduler;
use image::DynamicImage;
use log::{debug, info};
use rand::Rng;
use tauri::{AppHandle, Emitter};
use candle_transformers::models::{clip, t5, flux};
use tokenizers::Tokenizer;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FluxModel {
Schnell,
Dev,
}
pub struct FluxPipelineArgs<'a> {
pub image: &'a DynamicImage,
pub prompt: String,
pub model_cache: &'a ModelCache,
pub prompt_cache: &'a PromptCache,
pub app: &'a AppHandle,
pub flux_model: FluxModel,
pub active_blocks: Option<usize>,
pub use_cuda_stream: Option<bool>,
pub prefetch_next_batch: Option<bool>,
pub use_full_gpu: Option<bool>,
pub img2img_strength: Option<f64>,
pub seed: Option<u64>,
pub use_quantized_model: Option<bool>,
pub model_config: &'a ModelConfig,
pub flux_dev_or_schnell_path: &'a Path,
pub flux_schnell_quantized_gguf: &'a Path,
pub flux_autoencoder_path: &'a Path,
pub google_t5_model_path: &'a Path,
pub google_t5_config_path: &'a Path,
pub lmz_t5_tokenizer_path: &'a Path,
pub openai_clip_model_path: &'a Path,
pub openai_clip_tokenizer_json_path: &'a Path,
}
pub fn flux_pipeline(args: FluxPipelineArgs<'_>) -> Result<RgbImage> {
let FluxPipelineArgs {
prompt,
model_cache,
prompt_cache,
app,
image,
flux_model,
active_blocks,
use_cuda_stream,
prefetch_next_batch,
use_full_gpu,
img2img_strength,
seed,
use_quantized_model,
model_config ,
flux_dev_or_schnell_path,
flux_schnell_quantized_gguf,
flux_autoencoder_path,
google_t5_model_path,
google_t5_config_path,
lmz_t5_tokenizer_path,
openai_clip_model_path,
openai_clip_tokenizer_json_path,
} = args;
if use_quantized_model.unwrap_or(false) {
#[cfg(feature = "cuda")]
candle_core::quantized::cuda::set_force_dmmv(false);
}
let height = 1024;
let width = 1024;
let quantized = use_quantized_model.unwrap_or(false);
let device = model_config.device.clone();
let dtype = device.bf16_default_to_f32();
info!("Starting image generation with FLUX model:");
info!(" Model: {:?}", flux_model);
info!(" Prompt: {}", prompt);
info!(" Dimensions: {}x{}", width, height);
info!(" Device: {:?}", model_config.device);
let seed_value = seed.unwrap_or_else(|| rand::thread_rng().gen());
device.set_seed(seed_value)?;
info!("Using seed: {}", seed_value);
// Setup FLUX model configuration
let flux_config = match flux_model {
FluxModel::Dev => flux::model::Config::dev(),
FluxModel::Schnell => flux::model::Config::schnell(),
};
// Get T5 embeddings
let t5_emb = {
info!("Generating T5 embeddings for prompt...");
let vb = unsafe { VarBuilder::from_mmaped_safetensors(&[google_t5_model_path], dtype , &device)? };
let config = std::fs::read_to_string(google_t5_config_path)?;
let config: t5::Config = serde_json::from_str(&config)?;
let mut model = t5::T5EncoderModel::load(vb, &config)?;
let tokenizer = Tokenizer::from_file(lmz_t5_tokenizer_path).map_err(E::msg)?;
let mut tokens = tokenizer.encode(prompt.as_str(), true).map_err(E::msg)?.get_ids().to_vec();
tokens.resize(256, 0);
let input_token_ids = Tensor::new(&tokens[..], &device)?.unsqueeze(0)?;
model.forward(&input_token_ids)?
};
info!("T5 embeddings generated");
// Get CLIP embeddings
let clip_emb = {
info!("Generating CLIP embeddings for prompt...");
let vb =
unsafe { VarBuilder::from_mmaped_safetensors(&[openai_clip_model_path], dtype, &device)? };
// https://huggingface.co/openai/clip-vit-large-patch14/blob/main/config.json
let config = clip::text_model::ClipTextConfig {
vocab_size: 49408,
projection_dim: 768,
activation: clip::text_model::Activation::QuickGelu,
intermediate_size: 3072,
embed_dim: 768,
max_position_embeddings: 77,
pad_with: None,
num_hidden_layers: 12,
num_attention_heads: 12,
};
let model =
clip::text_model::ClipTextTransformer::new(vb.pp("text_model"), &config)?;
let tokenizer = Tokenizer::from_file(openai_clip_tokenizer_json_path).map_err(E::msg)?;
let tokens = tokenizer
.encode(prompt.as_str(), true)
.map_err(E::msg)?
.get_ids()
.to_vec();
let input_token_ids = Tensor::new(&tokens[..], &device)?.unsqueeze(0)?;
model.forward(&input_token_ids)?
};
info!("CLIP embeddings generated");
// Process input image to tensor
info!("Converting input image to tensor...");
let img_tensor = dynamic_image_to_tensor(image, &device, dtype)?;
info!("Image tensor shape: {:?}", img_tensor.shape());
// Create initial noise - this is important to match the expected format
info!("Generating initial noise for FLUX...");
let initial_tensor = flux::sampling::get_noise(1, height, width, &device)?.to_dtype(dtype)?;
info!("Initial tensor shape: {:?}", initial_tensor.shape());
// Blend initial noise with input image if img2img_strength is specified
let initial_tensor = if let Some(strength) = img2img_strength {
if strength > 0.0 && strength < 1.0 {
info!("Using img2img with strength: {}", strength);
// Reshape the input tensor to match the noise tensor dimensions
let adjusted_img = if img_tensor.shape() != initial_tensor.shape() {
info!("Reshaping input image to match noise dimensions...");
// You may need more complex reshaping logic here depending on your use case
let (_, c, h, w) = initial_tensor.dims4()?;
img_tensor.upsample_nearest2d(h, w)?
} else {
img_tensor
};
// Blend the noise and the image according to strength
let strength_tensor = Tensor::new(strength as f32, &device)?;
let inverse_strength = Tensor::new(1.0 - strength as f32, &device)?;
let weighted_noise = (&initial_tensor * &strength_tensor)?;
let weighted_img = (&adjusted_img * &inverse_strength)?;
(&weighted_noise + &weighted_img)?
} else {
initial_tensor
}
} else {
initial_tensor
};
info!("Final initial tensor shape: {:?}", initial_tensor.shape());
// Create FLUX state
info!("Creating FLUX state...");
let state = if quantized {
flux::sampling::State::new(
&t5_emb.to_dtype(DType::F32)?,
&clip_emb.to_dtype(DType::F32)?,
&initial_tensor.to_dtype(DType::F32)?,
)?
} else {
flux::sampling::State::new(&t5_emb, &clip_emb, &initial_tensor)?
};
info!("{state:?}");
// Set appropriate timesteps based on model
let timesteps = match flux_model {
FluxModel::Dev => flux::sampling::get_schedule(28, Some((state.img.dim(1)?, 0.5, 1.15))),
FluxModel::Schnell => flux::sampling::get_schedule(4, None),
};
info!("Starting FLUX denoising with timesteps: {:?}", timesteps);
// Actual model loading and inference
let img_latent = if quantized {
// Quantized model path
info!("Loading quantized FLUX model...");
let model_file = match flux_model {
FluxModel::Schnell => flux_schnell_quantized_gguf,
FluxModel::Dev => return Err(anyhow!("Quantized model not available for FLUX.1-dev")),
};
info!("Loading quantized model from {}", model_file.to_str().unwrap());
let vb = candle_transformers::quantized_var_builder::VarBuilder::from_gguf(model_file, &device)?;
let mut model = flux::quantized_model::Flux::new(&flux_config, vb)?;
info!("Quantized model loaded on {:?}", device);
// Run denoising
info!("Running denoising...");
let img_latent = flux::sampling::denoise(&mut model, &state.img, &state.img_ids, &state.txt, &state.txt_ids, &state.vec, &timesteps, 4.)?.to_dtype(dtype)?;
info!("Denoising completed");
img_latent
} else {
// Full precision model
info!("Loading FLUX model...");
let mut model = if use_full_gpu.unwrap_or(false) {
// Load entire model on GPU
info!("Loading entire model directly to GPU...");
let gpu_vb = unsafe { VarBuilder::from_mmaped_safetensors(&[flux_dev_or_schnell_path], dtype, &device)? };
let mut model = flux::model::Flux::new(&flux_config, gpu_vb)?;
model.set_use_device_management(false);
model
} else {
// Use memory-efficient approach with blocks on CPU
info!("Using memory-efficient approach with blocks on CPU...");
let cpu_device = candle_core::Device::Cpu;
let cpu_vb = unsafe { VarBuilder::from_mmaped_safetensors(&[flux_dev_or_schnell_path], dtype, &cpu_device)? };
let mut model = flux::model::Flux::new_with_gpu_core(&flux_config, cpu_vb, &device)?;
// Set active blocks
let active_blocks_count = active_blocks.unwrap_or(1);
model.set_max_active_blocks(active_blocks_count);
info!("Using {} active blocks on GPU", active_blocks_count);
// Enable prefetching if requested
if prefetch_next_batch.unwrap_or(false) {
model.set_prefetch_next_batch(true);
info!("Prefetching enabled - will overlap transfers with computation");
}
model
};
// Run denoising
flux::sampling::denoise(&mut model, &state.img, &state.img_ids, &state.txt, &state.txt_ids, &state.vec, &timesteps, 4.)?
};
info!("FLUX denoising completed, unpacking latent image...");
let img_unpacked = flux::sampling::unpack(&img_latent, height, width)?;
// Load autoencoder for final decoding
info!("Loading autoencoder for final decoding...");
let ae_config = match flux_model {
FluxModel::Dev => flux::autoencoder::Config::dev(),
FluxModel::Schnell => flux::autoencoder::Config::schnell(),
};
let vb = unsafe { VarBuilder::from_mmaped_safetensors(&[flux_autoencoder_path], dtype, &device)? };
let ae_model = flux::autoencoder::AutoEncoder::new(&ae_config, vb)?;
info!("Decoding final image...");
let img = ae_model.decode(&img_unpacked)?;
// Convert tensor to image
info!("Converting tensor to image...");
let img = ((img.clamp(-1f32, 1f32)? + 1.0)? * 127.5)?.to_dtype(candle_core::DType::U8)?;
// Convert to RGB image
let image = tensor_to_image_buffer(&img.i(0)?)?;
info!("FLUX pipeline completed successfully");
Ok(image)
}
@@ -0,0 +1 @@
pub mod flux_pipeline;
+1
View File
@@ -1,4 +1,5 @@
pub mod background_removal;
pub mod flux;
pub mod image;
pub mod model_cache;
pub mod model_config;
@@ -20,12 +20,14 @@ use crate::ml::stable_diffusion::get_vae_scale::get_vae_scale;
use crate::ml::stable_diffusion::infer_clip_text_embeddings::{infer_clip_text_embeddings, ClipArgs};
use crate::ml::stable_diffusion::remap_lcm_strength_range::remap_lcm_strength_range;
use image::DynamicImage;
use log::info;
use log::{debug, error, info};
use ml_weights_registry::weights_registry::weights::{LYKON_DEAMSHAPER_7_TEXT_ENCODER_FP16, LYKON_DEAMSHAPER_7_VAE, SIMIANLUO_LCM_DREAMSHAPER_V7_UNET};
use rand::Rng;
const DEFAULT_STRENGTH : f64 = 75.0;
const DEFAULT_SEED : u64 = 42;
pub struct Args<'a, P1: AsRef<Path>, P2: AsRef<Path>, P3: AsRef<Path>, P4: AsRef<Path>> {
pub image: &'a DynamicImage,
pub prompt: String,
@@ -85,10 +87,9 @@ pub fn lcm_pipeline<P1, P2, P3, P4>(args: Args<'_, P1, P2, P3, P4>) -> Result<Rg
// Use LCM Scheduler instead of Euler Ancestral for better speed and quality
let mut scheduler = LCMScheduler::new(scheduler_steps, img2img_strength, LCMSchedulerConfig::default())?;
let seed = maybe_seed.unwrap_or_else(|| rand::thread_rng().gen());
//let seed = maybe_seed.unwrap_or_else(|| rand::thread_rng().gen());
let seed = maybe_seed.unwrap_or(DEFAULT_SEED);
let seed = 42;
device.set_seed(seed)?;
let guidance_scale = match cfg_scale {
@@ -129,13 +130,13 @@ pub fn lcm_pipeline<P1, P2, P3, P4>(args: Args<'_, P1, P2, P3, P4>) -> Result<Rg
tensor
};
info!("Text embeddings shape: {:?}", text_embeddings.shape());
debug!("Text embeddings shape: {:?}", text_embeddings.shape());
info!("Loading input image into tensor...");
debug!("Loading input image into tensor...");
let input_image = dynamic_image_to_tensor(image, &device, dtype)?;
info!("Reference image shape: {:?}", input_image.shape());
debug!("Reference image shape: {:?}", input_image.shape());
let vae_scale = get_vae_scale(sd_version);
@@ -161,7 +162,7 @@ pub fn lcm_pipeline<P1, P2, P3, P4>(args: Args<'_, P1, P2, P3, P4>) -> Result<Rg
let unet = match maybe_unet {
Some(unet) => unet,
None => {
info!("No unet found in cache; loading...");
info!("Building UNET model from file {:?}...", unet_path.as_ref());
let in_channels = match sd_version {
StableDiffusionVersion::XlInpaint | StableDiffusionVersion::V2Inpaint | StableDiffusionVersion::V1_5Inpaint => 9,
@@ -217,16 +218,17 @@ pub fn lcm_pipeline<P1, P2, P3, P4>(args: Args<'_, P1, P2, P3, P4>) -> Result<Rg
let guidance_scale_embedding = scheduler.get_guidance_scale_embedding(guidance_scale, embedding_dim, &device, dtype)?;
// Generate latents from input image using the LCM approach
info!("Generating latents from input image...");
debug!("Generating latents from input image...");
let init_latents = (init_latent_dist.sample()? * vae_scale)?;
info!("Initial latents shape: {:?}", init_latents.shape());
debug!("Initial latents shape: {:?}", init_latents.shape());
// Calculate timesteps for LCM with proper img2img handling
let timesteps = LCMScheduler::get_timesteps_for_steps(scheduler_steps, img2img_strength);
let t_start = timesteps[0]; // First denoising step
// Add noise at the appropriate timestep for img2img
info!("Adding noise to latents for t_start={}", t_start);
debug!("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)?;
@@ -244,7 +246,7 @@ pub fn lcm_pipeline<P1, P2, P3, P4>(args: Args<'_, P1, P2, P3, P4>) -> Result<Rg
let mut noise_pred = match unet.forward_with_guidance(&latent_model_input, timestep as f64, &text_embeddings, Some(&guidance_scale_embedding)) {
Ok(pred) => pred,
Err(e) => {
info!("UNet inference failed with error: {}", e);
error!("UNet inference failed with error: {}", e);
return Err(anyhow::anyhow!("UNet inference failed: {}", e));
},
};
@@ -259,21 +261,19 @@ pub fn lcm_pipeline<P1, P2, P3, P4>(args: Args<'_, P1, P2, P3, P4>) -> Result<Rg
latents = scheduler.step(&noise_pred, timestep, &latents, timestep_index, scheduler_steps)?;
}
info!("Diffusion process completed, decoding image...");
debug!("Diffusion process completed, decoding image...");
let image = vae.decode(&(latents / vae_scale)?)?;
info!("VAE decode completed");
info!("Scaling image back");
debug!("Scaling image back");
let image = ((image / 2.)? + 0.5)?;
info!("Normalized image values");
debug!("Normalized image values");
let image = (image.clamp(0f32, 1.)? * 255.)?;
info!("Convert to int8");
debug!("Convert to int8");
let image = image.to_dtype(DType::U8)?;
info!("Converted to 8-bit format");
debug!("Converted to 8-bit format");
let image = tensor_to_image_buffer(&image.i(0)?)?;
@@ -59,14 +59,6 @@ pub fn stable_diffusion_pipeline<P1, P2, P3, P4>(args: Args<'_, P1, P2, P3, P4>)
clip_weights_path,
} = args;
println!("Starting image generation with the following configuration:");
println!(" Model: {:?}", sd_version);
println!(" Prompt: {}", prompt);
println!(" Steps: {}", scheduler_steps);
println!(" Device: {:?}", device);
println!("Model dimensions: {}x{}", sd_config.width, sd_config.height);
// TODO(bt,2025-02-18): The scheduler is `EulerAncestralDiscreteScheduler`, but we may want to port an LCM scheduler.
// This is a target for performance improvement
// See: https://github.com/huggingface/candle/issues/1331
@@ -0,0 +1,80 @@
use crate::weights_registry::weight_descriptor::{WeightDescriptor, WeightFunction};
use crate::weights_registry::weight_descriptor_builder::weight;
/// https://huggingface.co/black-forest-labs/FLUX.1-dev
pub const FLUX_DEV : WeightDescriptor = weight!(
"Flux-1 Dev",
"flux1-dev.safetensors",
WeightFunction::ImageGeneration
);
// TODO(bt): I think the autoencoder for Dev and Schnell are the same.
// They have the same checksum unless I mixed up the files when downloading.
/// https://huggingface.co/black-forest-labs/FLUX.1-dev
pub const FLUX_DEV_AUTOENCODER : WeightDescriptor = weight!(
"Flux-1 Dev (Autoencoder)",
"flux1-dev.ae.safetensors",
WeightFunction::ImageGeneration
);
/// https://huggingface.co/black-forest-labs/FLUX.1-schnell
pub const FLUX_SCHNELL : WeightDescriptor = weight!(
"Flux-1 Schnell",
"flux1-schnell.safetensors",
WeightFunction::ImageGeneration
);
/// https://huggingface.co/black-forest-labs/FLUX.1-schnell
pub const FLUX_SCHNELL_AUTOENCODER : WeightDescriptor = weight!(
"Flux-1 Schnell (Autoencoder)",
"flux1-schnell.ae.safetensors",
WeightFunction::ImageGeneration
);
/// https://huggingface.co/lmz/candle-flux
pub const LMZ_CANDLE_FLUX_SCHNELL_QUANTIZED_GGUF : WeightDescriptor = weight!(
"LMZ Flux Schnell Quantized",
"lmz_candle_flux_flux1-schnell.gguf",
WeightFunction::ImageGeneration
);
/// https://huggingface.co/google/t5-v1_1-xxl/tree/main
/// NB: I couldn't find the safetensors file here
/// NB: This should live under "refs/pr/2"
pub const GOOGLE_T5_V1_1_XXL_MODEL : WeightDescriptor = weight!(
"Google T5 v1.1 (XXL)",
"google_t5_v1_1_xxl_model.safetensors",
WeightFunction::TextEncoder
);
/// https://huggingface.co/google/t5-v1_1-xxl/tree/main
/// NB: I couldn't find the safetensors file here
/// NB: This should live under "refs/pr/2"
pub const GOOGLE_T5_V1_1_XXL_CONFIG : WeightDescriptor = weight!(
"Google T5 v1.1 (XXL) Config",
"google_t5_v1_1_xxl_config.json",
WeightFunction::TextEncoder
);
/// https://huggingface.co/lmz/mt5-tokenizers
pub const LMZ_T5_TOKENIZER_JSON : WeightDescriptor = weight!(
"LMZ T5 Tokenizer",
"lmz_mt5_tokenizers_t5-v1_1-xxl.tokenizer.json",
WeightFunction::TextTokenizer
);
/// https://huggingface.co/openai/clip-vit-large-patch14/tree/main
pub const OPENAI_CLIP_VIT_P14_MODEL : WeightDescriptor = weight!(
"OpenAI CLIP VIT (patch 14) model",
"openai_clip-vit-large-patch14.model.safetensors",
WeightFunction::TextTokenizer
);
/// https://huggingface.co/openai/clip-vit-large-patch14/tree/main
pub const OPENAI_CLIP_VIT_P14_TOKENIZER_JSON: WeightDescriptor = weight!(
"OpenAI CLIP VIT (patch 14) tokenizer",
"openai_clip-vit-large-patch14.tokenizer.json",
WeightFunction::TextTokenizer
);
@@ -1,3 +1,4 @@
mod weight_descriptor_builder;
pub mod flux_weights;
pub mod weight_descriptor;
pub mod weights;
@@ -5,6 +5,7 @@
#[derive(Clone, Copy)]
pub enum WeightFunction {
ImageGeneration,
ImageSegmentation,
TextEncoder,
TextTokenizer,
@@ -29,6 +30,7 @@ pub struct WeightDescriptor {
impl WeightFunction {
pub fn as_descriptive_str(&self) -> &'static str {
match self {
WeightFunction::ImageGeneration => "image generation",
WeightFunction::ImageSegmentation => "image segmentation",
WeightFunction::TextEncoder => "text encoder",
WeightFunction::TextTokenizer => "text tokenizer",
+6 -1
View File
@@ -21,6 +21,7 @@ tauri-build = { version = "2.0.4", features = [] }
[dependencies]
# Internal
crockford = { path = "../../lib/data/crockford" }
filesys = { path = "../../lib/files/filesys" }
memory_caching = { path = "../../lib/caching/memory_caching" }
ml_weights_registry = { path = "../ml_weights_registry" }
@@ -29,12 +30,16 @@ primitives = { path = "../../lib/data/primitives" }
# Internal ML
# TODO: Only on Windows and Linux
#ml_models = { path = "../ml_models", optional = true }
# Bypass cudarc issues on Windows https://github.com/coreylowman/cudarc/issues/332
cudarc = { version = "0.13.5", features = ["std", "cublas", "cublaslt", "curand", "driver", "nvrtc", "f16", "cuda-version-from-build-system", "dynamic-linking"], default-features=false }
# External
#openssl = { version = "0.10", features = ["vendored"] }
# NB(bt,2025-03-10): default-features = false on Windows while I debug openssl / rustls
anyhow = "1.0"
base64 = "0.22.1"
bytes = "1.10"
chrono = "0.4.40"
const_format = "0.2.34"
directories = "6.0.0"
dirs = "6.0.0"
@@ -43,8 +48,8 @@ half = { version = "2.3.1", features = ["num-traits", "use-intrinsics", "rand_di
image = "0.25.5"
log = "0.4"
once_cell = "1.20.3"
ort = { version = "1.16.2", features = ["cuda", "load-dynamic"] }
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"
@@ -0,0 +1,13 @@
use log::info;
#[tauri::command]
pub async fn inpaint_image(
image: &str,
mask: &str,
prompt: String,
) -> Result<String, String> {
info!("inpaint_endpoint endpoint called.");
Ok(mask.to_string())
}
@@ -1,5 +1,7 @@
pub mod download_models;
pub mod flip_image;
pub mod inpaint_image_endpoint;
pub mod realtime_image_endpoint;
pub mod remove_background_endpoint;
pub mod save_image_endpoint;
pub mod text_to_image_endpoint;
@@ -31,6 +31,7 @@ pub async fn infer_image(
image: &str,
prompt: Option<String>,
strength: Option<f64>,
seed: Option<u64>,
model_config: State<'_, AppConfig>,
model_cache: State<'_, ModelCache>,
prompt_cache: State<'_, PromptCache>,
@@ -49,12 +50,15 @@ pub async fn infer_image(
// TODO(bt,2025-02-17): Running out of vram with full image buffer size
let mut image = image.resize(512, 512, FilterType::CatmullRom);
let seed = seed.or(model_config.seed);
#[cfg(not(target_os = "macos"))]
{
let result = infer_image_impl(
&prompt,
image,
strength,
seed,
&model_config,
&model_cache,
prompt_cache,
@@ -83,6 +87,7 @@ async fn infer_image_impl(
prompt: &str,
image: DynamicImage,
strength: Option<f64>,
seed: Option<u64>,
config: &AppConfig,
model_cache: &ModelCache,
prompt_cache: State<'_, PromptCache>,
@@ -106,7 +111,7 @@ async fn infer_image_impl(
cfg_scale: config.cfg_scale,
use_flash_attn: true,
model_config: &config.model_config,
maybe_seed: None,
maybe_seed: seed,
scheduler_steps: config.scheduler_steps,
vae_path: &vae_path,
unet_path: &unet_path,
@@ -0,0 +1,38 @@
use crate::state::app_config::AppConfig;
use crate::state::app_dir::AppDataRoot;
use crate::stubs::model_cache::ModelCache;
use crate::stubs::prompt_cache::PromptCache;
use crate::utils::image::decode_base64_image::decode_base64_image;
use chrono::Local;
use crockford::crockford_entropy_lower;
use image::{ImageFormat, RgbImage};
use log::info;
use tauri::{AppHandle, State};
const FILENAME_LENGTH : usize = 10;
/// This handler takes an image (as a base64 encoded string) and saves it.
#[tauri::command]
pub async fn save_image(
image: &str,
app_data_root: State<'_, AppDataRoot>,
) -> Result<String, String> {
info!("save_image endpoint called.");
let image = decode_base64_image(image)
.map_err(|err| format!("Couldn't hydrate image from base64: {}", err))?;
let assets_dir = app_data_root.assets_dir();
let date_save_dir = assets_dir.make_or_get_current_date_dir()
.map_err(|err| format!("Couldn't create assets save directory: {}", err))?;
let entropy = crockford_entropy_lower(FILENAME_LENGTH); // TODO: These are ugly. A monotonic counter would be better.
let filename = format!("{}.png", entropy);
let filename = date_save_dir.join(filename);
image.save_with_format(filename, ImageFormat::Png)
.map_err(|err| format!("Couldn't save image: {}", err))?;
Ok("".to_string())
}
@@ -1,23 +1,21 @@
#[cfg(not(target_os = "macos"))]
use ml_models::ml::stable_diffusion::lcm_pipeline::{lcm_pipeline, Args};
use ml_models::ml::flux::flux_pipeline::{flux_pipeline, FluxModel, FluxPipelineArgs};
use crate::state::app_config::AppConfig;
use crate::state::app_dir::AppDataRoot;
use crate::state::app_dir::{AppDataRoot, AppWeightsDir};
use crate::stubs::model_cache::ModelCache;
use crate::stubs::prompt_cache::PromptCache;
use crate::utils::image::encode_dynamic_image_base64_png::encode_dynamic_image_base64_png;
use image::imageops::FilterType;
use image::{DynamicImage, ImageReader, RgbImage};
use log::{error, info};
use ml_weights_registry::weights_registry::weights::{CLIP_JSON, LYKON_DEAMSHAPER_7_VAE, SDXL_TURBO_CLIP_TEXT_ENCODER, SIMIANLUO_LCM_DREAMSHAPER_V7_UNET};
use ml_weights_registry::weights_registry::flux_weights::{FLUX_DEV, FLUX_SCHNELL, FLUX_SCHNELL_AUTOENCODER, GOOGLE_T5_V1_1_XXL_CONFIG, GOOGLE_T5_V1_1_XXL_MODEL, LMZ_CANDLE_FLUX_SCHNELL_QUANTIZED_GGUF, LMZ_T5_TOKENIZER_JSON, OPENAI_CLIP_VIT_P14_MODEL, OPENAI_CLIP_VIT_P14_TOKENIZER_JSON};
use once_cell::sync::Lazy;
use std::io::Cursor;
use tauri::{AppHandle, State};
const RANDOM_SEED: u32 = 42;
const STRENGTH: f64 = 1.0;
const STRENGTH: f64 = 100.0;
const PNG_BYTES : &[u8] = include_bytes!("../../binary_includes/1024.png");
// NB: We're just hacking a temporary "text to image" test endpoint leveraging the existing image to image modality.
@@ -35,8 +33,8 @@ pub async fn text_to_image(
model_config: State<'_, AppConfig>,
model_cache: State<'_, ModelCache>,
prompt_cache: State<'_, PromptCache>,
app_data_root: State<'_, AppDataRoot>,
app: AppHandle,
app_data_root: State<'_, AppDataRoot>,
) -> Result<String, String> {
info!("text_to_image endpoint called.");
@@ -51,8 +49,8 @@ pub async fn text_to_image(
&model_config,
&model_cache,
prompt_cache,
app_data_root.weights_dir(),
app,
&app_data_root
).await;
image = match result {
@@ -80,35 +78,36 @@ async fn text_to_image_impl(
config: &AppConfig,
model_cache: &ModelCache,
prompt_cache: State<'_, PromptCache>,
weights_dir: &AppWeightsDir,
app: AppHandle,
app_data_root: &AppDataRoot,
) -> Result<RgbImage, String> {
let weights_dir = app_data_root.weights_dir();
let vae_path = weights_dir.weight_path(&LYKON_DEAMSHAPER_7_VAE);
let unet_path = weights_dir.weight_path(&SIMIANLUO_LCM_DREAMSHAPER_V7_UNET);
let clip_json_path = weights_dir.weight_path(&CLIP_JSON);
let clip_weights_path= weights_dir.weight_path(&SDXL_TURBO_CLIP_TEXT_ENCODER);
let args = Args {
let args = FluxPipelineArgs {
image: &image,
prompt: prompt.to_string(),
uncond_prompt: "".to_string(),
model_cache,
prompt_cache: &prompt_cache,
app: &app,
flux_model: FluxModel::Schnell,
active_blocks: None,
use_cuda_stream: Some(true),
prefetch_next_batch: Some(false),
use_full_gpu: Some(true),
img2img_strength: strength,
cfg_scale: config.cfg_scale,
use_flash_attn: true,
seed: None,
use_quantized_model: Some(true),
model_config: &config.model_config,
maybe_seed: None,
scheduler_steps: config.scheduler_steps,
vae_path: &vae_path,
unet_path: &unet_path,
clip_json_path: &clip_json_path,
clip_weights_path: &clip_weights_path,
flux_dev_or_schnell_path: &weights_dir.weight_path(&FLUX_SCHNELL),
flux_schnell_quantized_gguf: &weights_dir.weight_path(&LMZ_CANDLE_FLUX_SCHNELL_QUANTIZED_GGUF),
flux_autoencoder_path: &weights_dir.weight_path(&FLUX_SCHNELL_AUTOENCODER),
google_t5_model_path: &weights_dir.weight_path(&GOOGLE_T5_V1_1_XXL_MODEL),
google_t5_config_path: &weights_dir.weight_path(&GOOGLE_T5_V1_1_XXL_CONFIG),
lmz_t5_tokenizer_path: &weights_dir.weight_path(&LMZ_T5_TOKENIZER_JSON),
openai_clip_model_path: &weights_dir.weight_path(&OPENAI_CLIP_VIT_P14_MODEL),
openai_clip_tokenizer_json_path: &weights_dir.weight_path(&OPENAI_CLIP_VIT_P14_TOKENIZER_JSON),
};
let image = lcm_pipeline(args)
let image = flux_pipeline(args)
.map_err(|err| format!("failure to encode image: {:?}", err))?;
Ok(image)
@@ -8,6 +8,9 @@ pub enum NotificationModelType {
TextTokenizer,
Unet,
Vae,
Json,
ClipEncoder,
FluxModel,
}
#[derive(Clone, Serialize)]
@@ -26,5 +29,17 @@ pub enum NotificationEvent<'a> {
ModelDownloadComplete {
model_name: &'a str,
model_type: NotificationModelType,
}
},
ModelLoading {
model_name: String,
model_type: NotificationModelType,
},
ModelInference {
model_name: String,
model_type: NotificationModelType,
},
ModelInferenceComplete {
model_name: String,
model_type: NotificationModelType,
},
}
+9 -2
View File
@@ -8,8 +8,10 @@ pub mod utils;
use crate::endpoints::download_models::download_models;
use crate::endpoints::flip_image::flip_image;
use crate::endpoints::inpaint_image_endpoint::inpaint_image;
use crate::endpoints::realtime_image_endpoint::infer_image;
use crate::endpoints::remove_background_endpoint::remove_background;
use crate::endpoints::save_image_endpoint::save_image;
use crate::endpoints::text_to_image_endpoint::text_to_image;
use crate::state::app_config::AppConfig;
use crate::stubs::model_cache::ModelCache;
@@ -28,7 +30,6 @@ pub fn run() {
let config = AppConfig::init()
.expect("config should load");
let prompt_cache = PromptCache::with_capacity(8)
.expect("prompt cache should load");
@@ -44,7 +45,10 @@ pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_log::Builder::new()
.level(log::LevelFilter::Info)
.targets(vec![Target::new(TargetKind::Stdout)])
.targets(vec![
Target::new(TargetKind::Stdout),
Target::new(TargetKind::LogDir { file_name: Some(app_data_root.log_file_name_str().to_string()) }),
])
.build())
.setup(|app| {
// TODO(bt): This is broken on windows
@@ -71,7 +75,10 @@ pub fn run() {
download_models,
flip_image,
infer_image,
inpaint_image,
remove_background,
save_image,
text_to_image,
text_to_image,
])
.run(tauri::generate_context!())
@@ -4,6 +4,7 @@ use anyhow::anyhow;
use directories::UserDirs;
use ml_weights_registry::weights_registry::weight_descriptor::WeightDescriptor;
use std::path::{Path, PathBuf};
use chrono::{DateTime, Local};
use tempdir::TempDir;
use tempfile::{Builder, NamedTempFile};
@@ -13,6 +14,9 @@ const WEIGHTS_SUBDIRECTORY : &str = "weights";
const TEMPORARY_SUBDIRECTORY : &str = "temp";
/// Note: Tauri appends ".log" to the end of the filename.
const LOG_FILE_NAME : &str = "application_debug";
/// The path to the application data directory, which includes "asset" and "weights" data.
#[derive(Clone)]
pub struct AppDataRoot {
@@ -20,6 +24,8 @@ pub struct AppDataRoot {
assets_dir: AppAssetsDir,
weights_dir: AppWeightsDir,
temp_dir: TemporaryDir,
log_file_name: PathBuf,
log_file_name_string: String,
}
#[derive(Clone)]
@@ -70,12 +76,19 @@ impl AppDataRoot {
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))?;
let log_file_name = dir.join(LOG_FILE_NAME);
let log_file_name_string = log_file_name
.to_str()
.ok_or(anyhow!("couldn't convert log path to str"))?
.to_string();
Ok(Self {
path: dir,
assets_dir,
weights_dir,
temp_dir,
log_file_name,
log_file_name_string,
})
}
@@ -94,6 +107,14 @@ impl AppDataRoot {
pub fn path(&self) -> &Path {
&self.path
}
pub fn log_file_name(&self) -> &Path {
&self.log_file_name
}
pub fn log_file_name_str(&self) -> &str {
&self.log_file_name_string
}
}
impl AppAssetsDir {
@@ -117,6 +138,18 @@ impl AppAssetsDir {
pub fn path(&self) -> &Path {
&self.path
}
/// Return the current date asset directory
/// If it doesn't exist, create it.
pub fn make_or_get_current_date_dir(&self) -> anyhow::Result<PathBuf> {
let format: DateTime<Local> = Local::now();
let date_directory_name = format.format("%Y-%d-%m").to_string();
let full_path = self.path.join(date_directory_name);
if !full_path.exists() {
std::fs::create_dir(&full_path)?;
}
Ok(full_path)
}
}
impl AppWeightsDir {
@@ -13,6 +13,7 @@ use std::sync::{Arc, Mutex};
use tauri::{AppHandle, Emitter};
use tempfile::NamedTempFile;
use tokio::task::JoinHandle;
use ml_weights_registry::weights_registry::flux_weights::{FLUX_SCHNELL, FLUX_SCHNELL_AUTOENCODER, GOOGLE_T5_V1_1_XXL_CONFIG, GOOGLE_T5_V1_1_XXL_MODEL, LMZ_CANDLE_FLUX_SCHNELL_QUANTIZED_GGUF, LMZ_T5_TOKENIZER_JSON, OPENAI_CLIP_VIT_P14_MODEL, OPENAI_CLIP_VIT_P14_TOKENIZER_JSON};
const NOTIFICATION_CHANNEL_NAME : &str = "notification";
const MAX_FILES : usize = 8;
@@ -50,6 +51,16 @@ pub async fn downloader_thread(app_data_root: AppDataRoot, app: AppHandle) -> !
//download_queue.push_back(SDXL_TURBO_VAE); // TODO(bt): REMOVE
//download_queue.push_back(SDXL_TURBO_UNET); // TODO(bt): REMOVE
// Flux:
download_queue.push_back(FLUX_SCHNELL);
download_queue.push_back(FLUX_SCHNELL_AUTOENCODER);
download_queue.push_back(LMZ_CANDLE_FLUX_SCHNELL_QUANTIZED_GGUF);
download_queue.push_back(GOOGLE_T5_V1_1_XXL_MODEL);
download_queue.push_back(GOOGLE_T5_V1_1_XXL_CONFIG);
download_queue.push_back(LMZ_T5_TOKENIZER_JSON);
download_queue.push_back(OPENAI_CLIP_VIT_P14_MODEL);
download_queue.push_back(OPENAI_CLIP_VIT_P14_TOKENIZER_JSON);
let mut handles = FuturesUnordered::new();
while let Some(model) = download_queue.pop_front() {
@@ -82,6 +93,7 @@ pub async fn downloader_thread(app_data_root: AppDataRoot, app: AppHandle) -> !
WeightFunction::Unet => NotificationModelType::Unet,
WeightFunction::Vae => NotificationModelType::Vae,
WeightFunction::ImageSegmentation => NotificationModelType::ImageSegmentation,
WeightFunction::ImageGeneration => NotificationModelType::ImageSegmentation, // TODO FIX
};
let result = app.emit(NOTIFICATION_CHANNEL_NAME, NotificationEvent::ModelDownloadStarted {
@@ -0,0 +1,13 @@
use actix_cors::Cors;
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
pub fn add_artcraft_2d(cors: Cors, _is_production: bool) -> Cors {
cors
// Hypothetical domains
.allowed_origin("https://2d.storyteller.ai")
.allowed_origin("https://2d.getartcraft.com")
// Netlify project
.allowed_origin_fn(|origin, _req_head| {
netlify_branch_domain_matches(origin, "storyteller-2d.netlify.app")
})
}
@@ -0,0 +1,13 @@
use actix_cors::Cors;
use crate::util::netlify_branch_domain_matches::netlify_branch_domain_matches;
pub fn add_artcraft_3d(cors: Cors, _is_production: bool) -> Cors {
cors
// Hypothetical domains
.allowed_origin("https://3d.storyteller.ai")
.allowed_origin("https://3d.getartcraft.com")
// Netlify project
.allowed_origin_fn(|origin, _req_head| {
netlify_branch_domain_matches(origin, "storyteller-3d.netlify.app")
})
}
@@ -1,3 +1,5 @@
pub (crate) mod artcraft_2d;
pub (crate) mod artcraft_3d;
pub (crate) mod development_only;
pub (crate) mod fakeyou;
pub (crate) mod legacy;
+6 -1
View File
@@ -2,7 +2,8 @@ use actix_cors::Cors;
use log::info;
use reusable_types::server_environment::ServerEnvironment;
use crate::configs::artcraft_2d::add_artcraft_2d;
use crate::configs::artcraft_3d::add_artcraft_3d;
use crate::configs::development_only::add_development_only;
use crate::configs::fakeyou::{add_fakeyou, add_fakeyou_dev_proxy};
use crate::configs::legacy::{add_legacy_storyteller_stream, add_legacy_trumped, add_legacy_vocodes, add_power_stream};
@@ -40,6 +41,10 @@ fn do_build_cors_config(is_production: bool) -> Cors {
cors = add_storyteller_board(cors, is_production);
cors = add_storyteller_render(cors, is_production);
// Artcraft
cors = add_artcraft_2d(cors, is_production);
cors = add_artcraft_3d(cors, is_production);
// Legacy
cors = add_legacy_trumped(cors, is_production);
cors = add_power_stream(cors, is_production);
+30
View File
@@ -0,0 +1,30 @@
[package]
name = "image_algorithms"
edition = "2018"
version = "0.0.1"
authors = [
"Brandon Thomas <bt@brand.io>",
"Brandon Thomas <echelon@gmail.com>",
]
publish = false
[lib]
name = "image_algorithms"
path = "src/lib.rs"
[package.metadata.cargo-udeps.ignore]
normal = ["my-workspace-hack"]
[dependencies]
# Workspace hack (faster compile times)
my-workspace-hack = { version = "0.1", path = "../../my-workspace-hack" }
# Internal
errors = { path = "../errors" }
# External
image = "0.25.6"
[dev-dependencies]
testing = { path = "../testing" }
+1
View File
@@ -0,0 +1 @@
pub mod remove_image_border;
@@ -0,0 +1,78 @@
use image::{DynamicImage, Pixel};
use errors::AnyhowResult;
pub fn remove_image_border(image: &DynamicImage) -> AnyhowResult<DynamicImage> {
let mut image = image.to_rgba8();
let mut top = 0u32;
let mut bottom = image.height();
let mut left = 0u32;
let mut right = image.width();
// TODO: Iterate with binary search - it'll probably be much faster.
for (i, mut row) in image.rows().enumerate() {
let count_non_transparent = row.filter(|pixel| pixel.0[3] != 0).count();
if count_non_transparent < 5 {
top = i as u32;
} else {
break;
}
}
for (i, mut row) in image.rows().enumerate().rev() {
//let all_transparent = row.all(|pixel| pixel.0[3] == 0);
let count_non_transparent = row.filter(|pixel| pixel.0[3] != 0).count();
//if all_transparent {
if count_non_transparent < 5 {
bottom = i as u32;
} else {
break;
}
}
for (x, y, pixel) in image.enumerate_pixels() {
}
for (y, mut row) in image.enumerate_rows() {
for (x, y, pixel) in row {
}
}
let image = DynamicImage::ImageRgba8(image);
let width = image.width();
let height = bottom - top;
println!("top: {}, bottom: {}", top, bottom);
println!("orig width: {}, height: {}", image.width(), image.height());
println!("new width: {}, height: {}", width, height);
let image = image.crop_imm(0, top, width, height);
Ok(image)
}
#[cfg(test)]
mod tests {
use image::{ImageFormat, ImageReader};
use errors::AnyhowResult;
use testing::test_file_path::test_file_path;
use crate::remove_image_border::remove_image_border;
#[ignore] // NB: Not going to check these files into the repo. Just for manual testing.
#[test]
fn test_crop_image() -> AnyhowResult<()> {
let filename = test_file_path("test_data/image/mario.png")?;
let image = ImageReader::open(filename)?.decode()?;
let image = remove_image_border(&image)?;
image.save_with_format("testing_output.png", ImageFormat::Png)?;
Ok(())
}
}
+248 -219
View File
@@ -15,252 +15,281 @@ use tokenizers::Tokenizer;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Args {
/// The prompt to be used for image generation.
#[arg(long, default_value = "A rusty robot walking on a beach")]
prompt: String,
/// The prompt to be used for image generation.
#[arg(long, default_value = "A rusty robot walking on a beach")]
prompt: String,
/// Run on CPU rather than on GPU.
#[arg(long)]
cpu: bool,
/// Run on CPU rather than on GPU.
#[arg(long)]
cpu: bool,
/// Use the quantized model.
#[arg(long)]
quantized: bool,
/// Use the quantized model.
#[arg(long)]
quantized: bool,
/// Enable tracing (generates a trace-timestamp.json file).
#[arg(long)]
tracing: bool,
/// Enable tracing (generates a trace-timestamp.json file).
#[arg(long)]
tracing: bool,
/// The height in pixels of the generated image.
#[arg(long)]
height: Option<usize>,
/// The height in pixels of the generated image.
#[arg(long)]
height: Option<usize>,
/// The width in pixels of the generated image.
#[arg(long)]
width: Option<usize>,
/// The width in pixels of the generated image.
#[arg(long)]
width: Option<usize>,
#[arg(long)]
decode_only: Option<String>,
#[arg(long)]
decode_only: Option<String>,
#[arg(long, value_enum, default_value = "schnell")]
model: Model,
#[arg(long, value_enum, default_value = "schnell")]
model: Model,
/// Use the slower kernels.
#[arg(long)]
use_dmmv: bool,
/// Use the slower kernels.
#[arg(long)]
use_dmmv: bool,
/// The seed to use when generating random samples.
#[arg(long)]
seed: Option<u64>,
/// The seed to use when generating random samples.
#[arg(long)]
seed: Option<u64>,
#[arg(long)]
cache_path: Option<String>,
/// Number of blocks to keep active on GPU (default: 1).
/// Only relevant when NOT using --use_full_gpu.
/// Higher values use more memory but may improve performance.
#[arg(long, default_value = "1")]
active_blocks: usize,
/// Use CUDA stream for faster device transfers (default: true).
/// Only relevant when running on GPU.
#[arg(long, default_value = "true")]
use_cuda_stream: bool,
/// Enable prefetching of the next batch of blocks while processing the current batch.
/// Only relevant when NOT using --use_full_gpu.
/// This can improve performance by overlapping memory transfers and computation.
#[arg(long, default_value = "false")]
prefetch_next_batch: bool,
/// Load entire model on GPU instead of using memory-efficient active blocks approach.
/// This uses more GPU memory but avoids CPU-GPU transfers completely.
/// When this flag is enabled, the --active_blocks and --prefetch_next_batch flags have no effect.
#[arg(long, default_value = "false")]
use_full_gpu: bool,
}
#[derive(Debug, Clone, Copy, clap::ValueEnum, PartialEq, Eq)]
enum Model {
Schnell,
Dev,
Schnell,
Dev,
}
fn run(args: Args) -> Result<()> {
use tracing_chrome::ChromeLayerBuilder;
use tracing_subscriber::prelude::*;
use tracing_chrome::ChromeLayerBuilder;
use tracing_subscriber::prelude::*;
let Args {
prompt,
cpu,
height,
width,
tracing,
decode_only,
model,
quantized,
..
} = args;
let width = width.unwrap_or(1360);
let height = height.unwrap_or(768);
let Args { prompt, cpu, height, width, tracing, decode_only, model, quantized, active_blocks, use_cuda_stream, prefetch_next_batch, use_full_gpu, .. } = args;
let width = width.unwrap_or(1360);
let height = height.unwrap_or(768);
let _guard = if tracing {
let (chrome_layer, guard) = ChromeLayerBuilder::new().build();
tracing_subscriber::registry().with(chrome_layer).init();
Some(guard)
} else {
None
let _guard = if tracing {
let (chrome_layer, guard) = ChromeLayerBuilder::new().build();
tracing_subscriber::registry().with(chrome_layer).init();
Some(guard)
} else {
None
};
let api = match args.cache_path.as_ref() {
Some(path) => hf_hub::api::sync::ApiBuilder::from_cache(hf_hub::Cache::new(path.to_string().into())).build().map_err(anyhow::Error::msg)?,
None => {
let cache = hf_hub::Cache::from_env();
hf_hub::api::sync::ApiBuilder::from_cache(cache).build().map_err(anyhow::Error::msg)?
},
};
let bf_repo = {
let name = match model {
Model::Dev => "black-forest-labs/FLUX.1-dev",
Model::Schnell => "black-forest-labs/FLUX.1-schnell",
};
api.repo(hf_hub::Repo::model(name.to_string()))
};
let api = hf_hub::api::sync::Api::new()?;
let bf_repo = {
let name = match model {
Model::Dev => "black-forest-labs/FLUX.1-dev",
Model::Schnell => "black-forest-labs/FLUX.1-schnell",
};
api.repo(hf_hub::Repo::model(name.to_string()))
};
let device = candle_examples::device(cpu)?;
if let Some(seed) = args.seed {
device.set_seed(seed)?;
// Use CUDA with stream for faster device transfers when not in CPU mode
let device = if cpu {
candle::Device::Cpu
} else {
#[cfg(feature = "cuda")]
{
if use_cuda_stream {
println!("Using CUDA with dedicated stream for faster device transfers");
// Use device ordinal 0 (first GPU)
candle::Device::new_cuda_with_stream(0)?
} else {
println!("Using standard CUDA device (no dedicated stream)");
candle::Device::new_cuda(0)?
}
}
let dtype = device.bf16_default_to_f32();
let img = match decode_only {
None => {
let t5_emb = {
let repo = api.repo(hf_hub::Repo::with_revision(
"google/t5-v1_1-xxl".to_string(),
hf_hub::RepoType::Model,
"refs/pr/2".to_string(),
));
let model_file = repo.get("model.safetensors")?;
let vb =
unsafe { VarBuilder::from_mmaped_safetensors(&[model_file], dtype, &device)? };
let config_filename = repo.get("config.json")?;
let config = std::fs::read_to_string(config_filename)?;
let config: t5::Config = serde_json::from_str(&config)?;
let mut model = t5::T5EncoderModel::load(vb, &config)?;
let tokenizer_filename = api
.model("lmz/mt5-tokenizers".to_string())
.get("t5-v1_1-xxl.tokenizer.json")?;
let tokenizer = Tokenizer::from_file(tokenizer_filename).map_err(E::msg)?;
let mut tokens = tokenizer
.encode(prompt.as_str(), true)
.map_err(E::msg)?
.get_ids()
.to_vec();
tokens.resize(256, 0);
let input_token_ids = Tensor::new(&tokens[..], &device)?.unsqueeze(0)?;
println!("{input_token_ids}");
model.forward(&input_token_ids)?
};
println!("T5\n{t5_emb}");
let clip_emb = {
let repo = api.repo(hf_hub::Repo::model(
"openai/clip-vit-large-patch14".to_string(),
));
let model_file = repo.get("model.safetensors")?;
let vb =
unsafe { VarBuilder::from_mmaped_safetensors(&[model_file], dtype, &device)? };
// https://huggingface.co/openai/clip-vit-large-patch14/blob/main/config.json
let config = clip::text_model::ClipTextConfig {
vocab_size: 49408,
projection_dim: 768,
activation: clip::text_model::Activation::QuickGelu,
intermediate_size: 3072,
embed_dim: 768,
max_position_embeddings: 77,
pad_with: None,
num_hidden_layers: 12,
num_attention_heads: 12,
};
let model =
clip::text_model::ClipTextTransformer::new(vb.pp("text_model"), &config)?;
let tokenizer_filename = repo.get("tokenizer.json")?;
let tokenizer = Tokenizer::from_file(tokenizer_filename).map_err(E::msg)?;
let tokens = tokenizer
.encode(prompt.as_str(), true)
.map_err(E::msg)?
.get_ids()
.to_vec();
let input_token_ids = Tensor::new(&tokens[..], &device)?.unsqueeze(0)?;
println!("{input_token_ids}");
model.forward(&input_token_ids)?
};
println!("CLIP\n{clip_emb}");
let img = {
let cfg = match model {
Model::Dev => flux::model::Config::dev(),
Model::Schnell => flux::model::Config::schnell(),
};
let img = flux::sampling::get_noise(1, height, width, &device)?.to_dtype(dtype)?;
let state = if quantized {
flux::sampling::State::new(
&t5_emb.to_dtype(candle::DType::F32)?,
&clip_emb.to_dtype(candle::DType::F32)?,
&img.to_dtype(candle::DType::F32)?,
)?
} else {
flux::sampling::State::new(&t5_emb, &clip_emb, &img)?
};
let timesteps = match model {
Model::Dev => {
flux::sampling::get_schedule(50, Some((state.img.dim(1)?, 0.5, 1.15)))
}
Model::Schnell => flux::sampling::get_schedule(4, None),
};
println!("{state:?}");
println!("{timesteps:?}");
if quantized {
let model_file = match model {
Model::Schnell => api
.repo(hf_hub::Repo::model("lmz/candle-flux".to_string()))
.get("flux1-schnell.gguf")?,
Model::Dev => todo!(),
};
let vb = candle_transformers::quantized_var_builder::VarBuilder::from_gguf(
model_file, &device,
)?;
#[cfg(not(feature = "cuda"))]
{
println!("CUDA not available, using CPU");
candle::Device::Cpu
}
};
let model = flux::quantized_model::Flux::new(&cfg, vb)?;
flux::sampling::denoise(
&model,
&state.img,
&state.img_ids,
&state.txt,
&state.txt_ids,
&state.vec,
&timesteps,
4.,
)?
.to_dtype(dtype)?
} else {
let model_file = match model {
Model::Schnell => bf_repo.get("flux1-schnell.safetensors")?,
Model::Dev => bf_repo.get("flux1-dev.safetensors")?,
};
let vb = unsafe {
VarBuilder::from_mmaped_safetensors(&[model_file], dtype, &device)?
};
let model = flux::model::Flux::new(&cfg, vb)?;
flux::sampling::denoise(
&model,
&state.img,
&state.img_ids,
&state.txt,
&state.txt_ids,
&state.vec,
&timesteps,
4.,
)?
}
};
flux::sampling::unpack(&img, height, width)?
}
Some(file) => {
let mut st = candle::safetensors::load(file, &device)?;
st.remove("img").unwrap().to_dtype(dtype)?
}
};
println!("latent img\n{img}");
let img = {
let model_file = bf_repo.get("ae.safetensors")?;
if let Some(seed) = args.seed {
device.set_seed(seed)?;
}
let start_time = std::time::Instant::now();
let dtype = device.bf16_default_to_f32();
let img = match decode_only {
None => {
let t5_emb = {
let repo = api.repo(hf_hub::Repo::with_revision("google/t5-v1_1-xxl".to_string(), hf_hub::RepoType::Model, "refs/pr/2".to_string()));
let model_file = repo.get("model.safetensors")?;
let vb = unsafe { VarBuilder::from_mmaped_safetensors(&[model_file], dtype, &device)? };
let config_filename = repo.get("config.json")?;
let config = std::fs::read_to_string(config_filename)?;
let config: t5::Config = serde_json::from_str(&config)?;
let mut model = t5::T5EncoderModel::load(vb, &config)?;
let tokenizer_filename = api.model("lmz/mt5-tokenizers".to_string()).get("t5-v1_1-xxl.tokenizer.json")?;
let tokenizer = Tokenizer::from_file(tokenizer_filename).map_err(E::msg)?;
let mut tokens = tokenizer.encode(prompt.as_str(), true).map_err(E::msg)?.get_ids().to_vec();
tokens.resize(256, 0);
let input_token_ids = Tensor::new(&tokens[..], &device)?.unsqueeze(0)?;
println!("{input_token_ids}");
model.forward(&input_token_ids)?
};
println!("T5\n{t5_emb}");
let clip_emb = {
let repo = api.repo(hf_hub::Repo::model("openai/clip-vit-large-patch14".to_string()));
let model_file = repo.get("model.safetensors")?;
let vb = unsafe { VarBuilder::from_mmaped_safetensors(&[model_file], dtype, &device)? };
// https://huggingface.co/openai/clip-vit-large-patch14/blob/main/config.json
let config = clip::text_model::ClipTextConfig { vocab_size: 49408, projection_dim: 768, activation: clip::text_model::Activation::QuickGelu, intermediate_size: 3072, embed_dim: 768, max_position_embeddings: 77, pad_with: None, num_hidden_layers: 12, num_attention_heads: 12 };
let model = clip::text_model::ClipTextTransformer::new(vb.pp("text_model"), &config)?;
let tokenizer_filename = repo.get("tokenizer.json")?;
let tokenizer = Tokenizer::from_file(tokenizer_filename).map_err(E::msg)?;
let tokens = tokenizer.encode(prompt.as_str(), true).map_err(E::msg)?.get_ids().to_vec();
let input_token_ids = Tensor::new(&tokens[..], &device)?.unsqueeze(0)?;
println!("{input_token_ids}");
model.forward(&input_token_ids)?
};
println!("CLIP\n{clip_emb}");
let img = {
let cfg = match model {
Model::Dev => flux::autoencoder::Config::dev(),
Model::Schnell => flux::autoencoder::Config::schnell(),
Model::Dev => flux::model::Config::dev(),
Model::Schnell => flux::model::Config::schnell(),
};
let model = flux::autoencoder::AutoEncoder::new(&cfg, vb)?;
model.decode(&img)?
let img = flux::sampling::get_noise(1, height, width, &device)?.to_dtype(dtype)?;
let state = if quantized { flux::sampling::State::new(&t5_emb.to_dtype(candle::DType::F32)?, &clip_emb.to_dtype(candle::DType::F32)?, &img.to_dtype(candle::DType::F32)?)? } else { flux::sampling::State::new(&t5_emb, &clip_emb, &img)? };
let timesteps = match model {
Model::Dev => flux::sampling::get_schedule(28, Some((state.img.dim(1)?, 0.5, 1.15))),
Model::Schnell => flux::sampling::get_schedule(4, None),
};
println!("{state:?}");
println!("{timesteps:?}");
if quantized {
// Note about quantized model memory management
let model_file = match model {
Model::Schnell => api.repo(hf_hub::Repo::model("lmz/candle-flux".to_string())).get("flux1-schnell.gguf")?,
Model::Dev => todo!(),
};
let vb = candle_transformers::quantized_var_builder::VarBuilder::from_gguf(model_file, &device)?;
let mut model = flux::quantized_model::Flux::new(&cfg, vb)?;
// Quantized models are always fully loaded to the selected device
if !cpu {
println!("Quantized model fully loaded to GPU with optimized memory layout");
}
flux::sampling::denoise(&mut model, &state.img, &state.img_ids, &state.txt, &state.txt_ids, &state.vec, &timesteps, 4.)?.to_dtype(dtype)?
} else {
let model_file = match model {
Model::Schnell => bf_repo.get("flux1-schnell.safetensors")?,
Model::Dev => bf_repo.get("flux1-dev.safetensors")?,
};
let mut model = if use_full_gpu {
// When use_full_gpu is true, load everything directly to GPU
println!("Loading entire model directly to GPU...");
let gpu_vb = unsafe { VarBuilder::from_mmaped_safetensors(&[model_file], dtype, &device)? };
let mut model = flux::model::Flux::new(&cfg, gpu_vb)?;
model.set_use_device_management(false);
println!("Model loaded entirely on GPU");
model
} else {
// Use memory-efficient approach with blocks on CPU
// Create a CPU device for initially loading all model weights
let cpu_device = candle::Device::Cpu;
// Load the entire model to CPU first (uses less memory)
println!("Loading model to CPU to minimize GPU memory usage...");
let cpu_vb = unsafe { VarBuilder::from_mmaped_safetensors(&[model_file], dtype, &cpu_device)? };
// Create the model with CPU weights, then selectively move core components to GPU
let mut model = flux::model::Flux::new_with_gpu_core(&cfg, cpu_vb, &device)?;
println!("Model loaded (core components on GPU, blocks on CPU for reduced memory usage)");
// Set the number of active blocks to keep on GPU
model.set_max_active_blocks(active_blocks);
println!("Using {} active blocks on GPU", active_blocks);
// Enable prefetching if requested
if prefetch_next_batch {
model.set_prefetch_next_batch(true);
println!("Prefetching enabled - will overlap transfers with computation");
}
model
};
// Print final memory management approach being used
if use_full_gpu {
println!("Using full GPU memory - entire model on GPU");
} else {
println!("Using device management - blocks will be transferred between CPU and GPU as needed");
}
flux::sampling::denoise(&mut model, &state.img, &state.img_ids, &state.txt, &state.txt_ids, &state.vec, &timesteps, 4.)?
}
};
flux::sampling::unpack(&img, height, width)?
},
Some(file) => {
let mut st = candle::safetensors::load(file, &device)?;
st.remove("img").unwrap().to_dtype(dtype)?
},
};
println!("latent img\n{img}");
let end_time = std::time::Instant::now();
println!("Time taken to decode: {:?}", end_time.duration_since(start_time));
let img = {
let model_file = bf_repo.get("ae.safetensors")?;
let vb = unsafe { VarBuilder::from_mmaped_safetensors(&[model_file], dtype, &device)? };
let cfg = match model {
Model::Dev => flux::autoencoder::Config::dev(),
Model::Schnell => flux::autoencoder::Config::schnell(),
};
println!("img\n{img}");
let img = ((img.clamp(-1f32, 1f32)? + 1.0)? * 127.5)?.to_dtype(candle::DType::U8)?;
let filename = match args.seed {
None => "out.jpg".to_string(),
Some(s) => format!("out-{s}.jpg"),
};
candle_examples::save_image(&img.i(0)?, filename)?;
Ok(())
let model = flux::autoencoder::AutoEncoder::new(&cfg, vb)?;
model.decode(&img)?
};
println!("img\n{img}");
let img = ((img.clamp(-1f32, 1f32)? + 1.0)? * 127.5)?.to_dtype(candle::DType::U8)?;
let filename = match args.seed {
None => "out.jpg".to_string(),
Some(s) => format!("out-{s}.jpg"),
};
candle_examples::save_image(&img.i(0)?, filename)?;
Ok(())
}
fn main() -> Result<()> {
let args = Args::parse();
#[cfg(feature = "cuda")]
candle::quantized::cuda::set_force_dmmv(args.use_dmmv);
run(args)
let args = Args::parse();
#[cfg(feature = "cuda")]
candle::quantized::cuda::set_force_dmmv(args.use_dmmv);
run(args)
}
+19 -1
View File
@@ -28,7 +28,8 @@
//! ```
//!
//! [`Layer Normalization`]: https://arxiv.org/abs/1607.06450
use candle::{DType, Module, Result, Tensor, D};
use candle::{DType, Module, Result, Tensor, D, Device};
use crate::offload::DeviceTransferable;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LayerNormConfig {
@@ -201,3 +202,20 @@ pub fn rms_norm(size: usize, eps: f64, vb: crate::VarBuilder) -> Result<RmsNorm>
};
Ok(RmsNorm(layer_norm(size, config, vb)?))
}
impl DeviceTransferable for LayerNorm {
fn to_device(&self, device: &Device) -> Result<Self> {
Ok(Self {
weight: self.weight.to_device(device)?,
bias: self.bias.as_ref().map(|b| b.to_device(device)).transpose()?,
remove_mean: self.remove_mean,
eps: self.eps,
})
}
}
impl DeviceTransferable for RmsNorm {
fn to_device(&self, device: &Device) -> Result<Self> {
Ok(Self(self.0.to_device(device)?))
}
}
+2
View File
@@ -27,6 +27,7 @@ pub mod kv_cache;
pub mod layer_norm;
pub mod linear;
pub mod loss;
pub mod offload;
pub mod ops;
pub mod optim;
pub mod rnn;
@@ -58,3 +59,4 @@ pub use var_builder::VarBuilder;
pub use var_map::VarMap;
pub use candle::{Module, ModuleT};
pub use offload::DeviceTransferable;
+12 -1
View File
@@ -17,7 +17,9 @@
//! assert_eq!(ys.to_vec2::<f32>()?, &[[210.0, 430.0, 650.0]]);
//! # Ok(()) }
//! ```
use candle::{Result, Tensor};
use candle::{Result, Tensor, Device};
use crate::offload::DeviceTransferable;
#[derive(Clone, Debug)]
pub struct Linear {
@@ -88,3 +90,12 @@ pub fn linear_b(
linear_no_bias(in_dim, out_dim, vb)
}
}
impl DeviceTransferable for Linear {
fn to_device(&self, device: &Device) -> Result<Self> {
Ok(Self {
weight: self.weight.to_device(device)?,
bias: self.bias.as_ref().map(|b| b.to_device(device)).transpose()?,
})
}
}
+18
View File
@@ -0,0 +1,18 @@
use candle::{Device, Result};
pub trait DeviceTransferable {
/// Move the module to the specified device
///
/// # Arguments
///
/// * `device` - The target device to move the module to
///
/// # Returns
///
/// A new instance of the module with all tensors moved to the target device,
/// or an error if the module cannot be moved.
fn to_device(&self, _device: &Device) -> Result<Self> where Self: Sized {
// Default implementation that rejects moving - components can override this
candle::bail!("to_device not implemented for this type")
}
}
+2 -1
View File
@@ -9,7 +9,9 @@ candle = { path = "../candle-core", package = "candle-core" }
candle-flash-attn = { path = "../candle-flash-attn", optional = true }
candle-nn = { path = "../candle-nn", package = "candle-nn" }
fancy-regex = "0.13.0"
half = { version = "2.3.1", features = ["num-traits", "use-intrinsics", "rand_distr"] }
intel-mkl-src = { version = "0.8.1", features = ["mkl-static-lp64-iomp"], optional = true }
log = "0.4.27"
num-traits = "0.2.15"
rand = "0.8.5"
rayon = "1.7.0"
@@ -17,7 +19,6 @@ 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 = []
@@ -24,9 +24,39 @@
use candle::{Result, Tensor};
pub trait WithForward {
/// The forward pass for the Flux model.
///
/// # Arguments
///
/// * `img` - The input image tensor
/// * `img_ids` - The image position ids
/// * `txt` - The text tensor
/// * `txt_ids` - The text position ids
/// * `timesteps` - The timesteps tensor
/// * `y` - The conditioning vector
/// * `guidance` - An optional guidance vector
///
/// # Returns
///
/// The result of the forward pass
///
/// # Note on Memory Optimization
///
/// To optimize memory usage, we could implement a mechanism to move model blocks
/// to CPU when not in use. This would require:
///
/// 1. Adding `to_device` methods to all model blocks (DoubleStreamBlock, SingleStreamBlock, etc.)
/// 2. Implementing these methods by moving all tensors inside each block to the target device
/// 3. Modifying the forward method to:
/// - Move each block to GPU when needed
/// - Process the block
/// - Move the block back to CPU when done
///
/// This pattern could significantly reduce GPU memory usage for large models by
/// keeping only the currently active block in GPU memory.
#[allow(clippy::too_many_arguments)]
fn forward(
&self,
&mut self,
img: &Tensor,
img_ids: &Tensor,
txt: &Tensor,
File diff suppressed because it is too large Load Diff
@@ -419,7 +419,7 @@ impl Flux {
impl super::WithForward for Flux {
#[allow(clippy::too_many_arguments)]
fn forward(
&self,
&mut self,
img: &Tensor,
img_ids: &Tensor,
txt: &Tensor,
@@ -93,7 +93,7 @@ pub fn unpack(xs: &Tensor, height: usize, width: usize) -> Result<Tensor> {
#[allow(clippy::too_many_arguments)]
pub fn denoise<M: super::WithForward>(
model: &M,
model: &mut M,
img: &Tensor,
img_ids: &Tensor,
txt: &Tensor,
@@ -1,6 +1,9 @@
use super::schedulers::{betas_for_alpha_bar, BetaSchedule, PredictionType};
use candle::{DType, Device, IndexOp, Result, Tensor};
use log::debug;
const FINAL_ALPHA_CUMPROD: f64 = 0.9991499781608582;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LCMVarianceType {
FixedSmall,
@@ -87,7 +90,7 @@ impl LCMScheduler {
let timesteps = LCMScheduler::get_timesteps_for_steps(inference_steps, strength);
let step_ratio = config.train_timesteps / inference_steps;
println!("Using LCM timesteps: {:?}", timesteps);
debug!("Using LCM timesteps: {:?}", timesteps);
Ok(Self {
alphas_cumprod,
@@ -143,11 +146,11 @@ impl LCMScheduler {
let scaled_timestep_squared = scaled_timestep.powi(2);
let denominator = scaled_timestep_squared + sigma_squared;
println!(
debug!(
"Boundary condition for timestep {}, scaled timestep {}",
timestep, scaled_timestep
);
println!(
debug!(
" Intermediate values: sigma_data^2: {:.20}, scaled_timestep^2: {:.20}, denominator: {:.20}",
sigma_squared, scaled_timestep_squared, denominator
);
@@ -155,7 +158,7 @@ impl LCMScheduler {
let c_skip = sigma_squared / denominator;
let c_out = scaled_timestep / (scaled_timestep_squared + sigma_squared).sqrt();
println!(
debug!(
" Final computed values: c_skip: {:.20}, c_out: {:.20}",
c_skip, c_out
);
@@ -172,14 +175,14 @@ impl LCMScheduler {
) -> Result<Tensor> {
// Scale by exactly 1000.0 to match diffusers
let w = guidance_scale * 1000.0;
println!(
debug!(
"Creating guidance embedding with dim={}, scaled value={}",
embedding_dim, w
);
// Calculate the half dimension
let half_dim = embedding_dim / 2;
println!("Half dimension: {}", half_dim);
debug!("Half dimension: {}", half_dim);
let log_10000 = (10000.0f32).ln();
let freq_factor = log_10000 / ((half_dim - 1) as f32); // pre-compute division
@@ -191,7 +194,7 @@ impl LCMScheduler {
freqs.push(freq);
}
println!(
debug!(
"First few frequencies: {:?}",
&freqs.iter().take(5).collect::<Vec<_>>()
);
@@ -204,39 +207,39 @@ impl LCMScheduler {
emb_data.push(w_val * freq);
}
println!(
debug!(
"First few scaled frequencies: {:?}",
&emb_data.iter().take(5).collect::<Vec<_>>()
);
// Create the embedding tensor
let emb = Tensor::new(emb_data.as_slice(), device)?.to_dtype(dtype)?;
println!("Embedding tensor shape: {:?}", emb.shape());
debug!("Embedding tensor shape: {:?}", emb.shape());
// Reshape for correct sin/cos application
let emb = emb.reshape((1, half_dim))?;
println!("Reshaped embedding tensor: {:?}", emb.shape());
debug!("Reshaped embedding tensor: {:?}", emb.shape());
// Compute sin and cos components
let sin_emb = emb.sin()?;
println!("Sin embedding shape: {:?}", sin_emb.shape());
debug!("Sin embedding shape: {:?}", sin_emb.shape());
let cos_emb = emb.cos()?;
println!("Cos embedding shape: {:?}", cos_emb.shape());
debug!("Cos embedding shape: {:?}", cos_emb.shape());
// Concatenate sin and cos (along dimension 1)
let result = Tensor::cat(&[sin_emb, cos_emb], 1)?;
println!("After concatenation shape: {:?}", result.shape());
debug!("After concatenation shape: {:?}", result.shape());
// If odd dimension, pad with zero
let result = if embedding_dim % 2 == 1 {
println!("Adding zero padding for odd dimension");
debug!("Adding zero padding for odd dimension");
let zero = Tensor::zeros((1, 1), dtype, device)?;
Tensor::cat(&[result, zero], 1)?
} else {
result
};
println!("Final guidance embedding shape: {:?}", result.shape());
debug!("Final guidance embedding shape: {:?}", result.shape());
Ok(result)
}
@@ -288,12 +291,12 @@ impl LCMScheduler {
};
let timesteps = linspace.iter().map(|&f| f as usize).rev().collect();
println!("timesteps: {:?}", timesteps);
debug!("timesteps: {:?}", timesteps);
timesteps
}
};
println!("all_timesteps: {:?}", all_timesteps);
debug!("all_timesteps: {:?}", all_timesteps);
// Compute `t_start` based on `strength`
let num_train_timesteps = 1000;
@@ -303,7 +306,7 @@ impl LCMScheduler {
.position(|&t| t <= init_timestep)
.unwrap_or(0);
println!(
debug!(
"get_timesteps: t_start: {} init_timestep: {} strength: {}",
t_start, init_timestep, strength
);
@@ -327,7 +330,7 @@ impl LCMScheduler {
0
};
println!(
debug!(
"LCM step: timestep={}, prev_timestep={}, step_index={}",
timestep, prev_timestep, step_index
);
@@ -357,15 +360,15 @@ impl LCMScheduler {
let denoised = ((c_out * pred_original_sample )? + (c_skip * sample)?)?;
let pred_prev_sample = if step_index != self.timesteps.len() - 1 {
println!("Adding noise at intermediate step {}", timestep);
debug!("Adding noise at intermediate step {}", timestep);
let noise = Tensor::randn_like(&denoised, 0.0, 1.0)?;
((alpha_prod_t_prev.sqrt() * denoised)? + (beta_prod_t_prev.sqrt() * noise)?)?
} else {
println!("No noise for final timestep {}", timestep);
debug!("No noise for final timestep {}", timestep);
denoised
};
println!(
debug!(
"Denoised sample shape: {:?}, first few values: {}",
pred_prev_sample.shape(),
pred_prev_sample
@@ -388,7 +391,7 @@ impl LCMScheduler {
// self.timesteps[step_index + 1]
// };
// println!(
// debug!(
// "LCM step: timestep={}, prev_timestep={}",
// timestep, prev_timestep
// );
@@ -401,7 +404,7 @@ impl LCMScheduler {
// self.alphas_cumprod[prev_timestep]
// };
// println!(
// debug!(
// "LCM scheduler step: timestep={}, step_index={}, sigma={}",
// timestep,
// step_index,
@@ -433,13 +436,13 @@ impl LCMScheduler {
// pred_original_sample = pred_original_sample.clamp(-1f32, 1f32)?;
// }
// println!("boundary: Pred original sample first few values: {}",
// debug!("boundary: Pred original sample first few values: {}",
// pred_original_sample.i(0)?.i(0)?.narrow(0, 0, 5)?.narrow(1, 0, 5)?);
// // Denoise model output using boundary conditions
// let denoised = ((pred_original_sample * c_out)? + (sample * c_skip)?)?;
// println!("boundary: Denoised first few values: {}",
// debug!("boundary: Denoised first few values: {}",
// denoised.i(0)?.i(0)?.narrow(0, 0, 5)?.narrow(1, 0, 5)?);
// // LCM doesn't add noise during inference
@@ -452,11 +455,11 @@ impl LCMScheduler {
// // let pred_prev_sample = pred_prev_sample.clamp(-1f32, 1f32)?;
// println!(
// debug!(
// "LCM scheduler pred_original shape: {:?}",
// pred_prev_sample.shape()
// );
// println!(
// debug!(
// "LCM scheduler pred_original first few values: {}",
// pred_prev_sample
// .i(0)?
@@ -469,7 +472,7 @@ impl LCMScheduler {
// }
pub fn add_noise(&self, original: &Tensor, noise: Tensor, timestep: usize) -> Result<Tensor> {
println!("Adding noise at timestep {}", timestep);
debug!("Adding noise at timestep {}", timestep);
let timestep = if timestep >= self.alphas_cumprod.len() {
self.alphas_cumprod.len() - 1 // Ensure valid range
} else {
@@ -491,7 +494,7 @@ impl LCMScheduler {
// let alpha_cumprod = self.alphas_cumprod[timestep];
// // Print the alpha value to debug
// println!(
// debug!(
// "Adding noise with timestep={}, alpha={}, sqrt(alpha)={}",
// timestep,
// alpha_cumprod,
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
backup
.venv
# Editor directories and files
~
.vscode
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
**/*.onnx
+8
View File
@@ -0,0 +1,8 @@
<html>
<head>
<title>Not Found</title>
</head>
<body>
<h1> This Page Was Not Found </h1>
</body>
</html>
+39
View File
@@ -0,0 +1,39 @@
# Memeboard
## Installation & Dev
```sh
npm install
npm run dev
```
or
```sh
yarn # this installs the node packages
yarn dev # this runs the vite dev server
```
You may need to turn off browser security to get around CORS restrictions:
### Windows 11
from your powershell
```sh
# Chrome
Start "C:\Program Files\Google\Chrome\Application\chrome.exe" -ArgumentList '--user-data-dir="C://chrome-dev-disabled-security" --disable-web-security --disable-site-isolation-trials'
```
### on MAC
form your terminal
```sh
# Chrome
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --disable-web-security --user-data-dir=~/chrome
```
### Inspecting the Shared Workers
on chrome, type `chrome://inspect/#workers` into the url field
+3
View File
@@ -0,0 +1,3 @@
/*
Access-Control-Allow-Origin: *
+11
View File
@@ -0,0 +1,11 @@
# Board Netlify Redirects File
# Netlify docs:
# The redirects engine will process the first matching rule it
# finds, reading from top to bottom. Rules in the _redirects file
# are always processed first, followed by rules in the Netlify
# configuration file.
/landing /index.html 200!
/login /index.html 200!
/m_* /index.html 200!
+44
View File
@@ -0,0 +1,44 @@
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
import react from "eslint-plugin-react";
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [
js.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
...tseslint.configs.stylisticTypeChecked,
],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
react,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
...react.configs.recommended.rules,
...react.configs["jsx-runtime"].rules,
// TODO WIL: make no-reaching rule for imports
// "import/no-internal-modules": [
// "error",
// {
// allow: [],
// forbid: ["**/uiAccess/*"],
// },
// ],
},
},
);
+24
View File
@@ -0,0 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-ico" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Syne:wght@600&display=swap"
rel="stylesheet"
/>
<title>Board | Storyteller.AI</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/app/index.tsx"></script>
</body>
</html>
+34
View File
@@ -0,0 +1,34 @@
[build]
command = "./script/netlify_build.sh"
publish = "dist/"
#[build.environment]
#REACT_APP_PUBLIC_POSTHOG_KEY = "phc_jBFgac0mVALAFk3negnSfYCcHgvkT00yBLQDmCDYNBb"
#REACT_APP_PUBLIC_POSTHOG_UI = "https://us.i.posthog.com"
#[dev]
#command = "npm run dev"
#framework = "vite"
# Set immutable caching for static files, because they have fingerprinted filenames
[[headers]]
for = "/build/*"
[headers.values]
"Cache-Control" = "public, max-age=31560000, immutable"
# [[redirects]]
# from = "/ingest/static/*"
# to = "https://us-assets.i.posthog.com/static/:splat"
# host = "us-assets.i.posthog.com"
# status = 200
# force = true
#
# [[redirects]]
# from = "/ingest/*"
# to = "https://us.i.posthog.com/:splat"
# host = "us.i.posthog.com"
# status = 200
# force = true
+9348
View File
File diff suppressed because it is too large Load Diff
+66
View File
@@ -0,0 +1,66 @@
{
"name": "storyteller-board",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "echo 'realtime' && vite",
"build": "tsc -b && vite build",
"build2": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@devnomic/marquee": "^1.0.3",
"@fortawesome/free-brands-svg-icons": "^6.6.0",
"@headlessui/react": "^2.1.3",
"@preact/signals-react": "^2.2.0",
"@tauri-apps/api": "^2.2.0",
"@techstark/opencv-js": "^4.10.0-release.1",
"@zip.js/zip.js": "^2.7.52",
"konva": "^9.3.14",
"onnxruntime-web": "^1.19.2",
"posthog-js": "^1.174.3",
"react": "^18.3.1",
"react-colorful": "^5.6.1",
"react-dom": "^18.3.1",
"react-drag-drop-files": "^2.3.10",
"react-easy-crop": "^5.1.0",
"react-router-dom": "^6.26.1",
"sonner": "^2.0.1",
"tailwind-merge": "^2.5.2",
"uuid": "^10.0.0",
"yarn": "^1.22.22"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/pro-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
"@preact/signals-react-transform": "^0.4.0",
"@tauri-apps/cli": "^2.4.0",
"@types/deep-equal": "^1.0.4",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.20",
"deep-equal": "^2.2.3",
"eslint": "^9.9.0",
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
"i": "^0.3.7",
"npm": "^10.8.3",
"postcss": "^8.4.41",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.6",
"tailwindcss": "^3.4.10",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.1",
"vite": "^5.4.1",
"vite-plugin-static-copy": "^1.0.6",
"vite-tsconfig-paths": "^5.0.1"
}
}
@@ -0,0 +1,272 @@
import asyncio
import base64
import json
import logging
import pathlib
import time
from io import BytesIO
from typing import Dict, Any, Optional, Tuple, Callable, List
from dataclasses import dataclass
from dataclasses_json import dataclass_json
from enum import Enum, auto
import base64
from PIL import Image
import io
import torch
from diffusers import AutoPipelineForImage2Image, LCMScheduler
from diffusers.utils import make_image_grid, load_image
from sd_embed.embedding_funcs import get_weighted_text_embeddings_sdxl
import torch
import websockets
from PIL import Image
from diffusers import (
StableDiffusionXLPipeline,
StableDiffusionXLImg2ImgPipeline,
AutoPipelineForImage2Image,
LCMScheduler,
UNet2DConditionModel,
)
import torch
import torchvision.transforms as transforms
import gc
transform = transforms.Compose([
transforms.ToTensor(), # This converts PIL Image to tensor and scales to [0, 1]
])
@dataclass_json
@dataclass
class ProgressMessage:
message: str
progress: float
type: str = "progress"
error: Optional[str] = None
@dataclass_json
@dataclass
class LoadModelRequest:
model_path: str
lora_path: Optional[str] = None
command: str = "load_model"
@dataclass_json
@dataclass
class GenerateRequest:
image: str
prompt: str
command: str = "generate"
strength: float = 0.6
guidance_scale: float = 2.0
num_inference_steps: int = 4
height: int = 1024
width: int = 1024
lora_strength: float = 1.0
@dataclass_json
@dataclass
class GenerateResponse:
image: str
type: str = "result"
def save_image(image: Image.Image, base_path: str = "outputs") -> str:
"""Save a PIL Image to disk with a timestamp-based filename."""
try:
# Create outputs directory if it doesn't exist
output_dir = pathlib.Path(base_path)
output_dir.mkdir(parents=True, exist_ok=True)
# Generate filename with timestamp
timestamp = time.strftime("%Y%m%d-%H%M%S")
filename = f"generated-{timestamp}.png"
filepath = output_dir / filename
# Save the image
print(f"Saving image to: {filepath}")
image.save(str(filepath), format="PNG")
print(f"Successfully saved image to: {filepath}")
return str(filepath)
except Exception as e:
print(f"Error saving image: {e}")
return ""
async def send_progress(websocket, message: str, progress: float = 0.0, error: str = None):
"""Send progress updates to the client."""
progress_msg = ProgressMessage(
message=message,
progress=progress,
error=error
)
await websocket.send(progress_msg.to_json())
async def load_model(model_path: str, lora_path: str = None) -> StableDiffusionXLImg2ImgPipeline:
"""Load the model with progress reporting."""
try:
# Load base SDXL pipeline
pipe = StableDiffusionXLPipeline.from_single_file(
model_path,
torch_dtype=torch.float16,
use_safetensors=True
).to("cuda")
# Convert to img2img pipeline
pipe = StableDiffusionXLImg2ImgPipeline(**pipe.components)
# Set scheduler
pipe.scheduler = LCMScheduler.from_config(pipe.scheduler.config)
# Load LCM-LoRA weights if provided
if lora_path:
pipe.load_lora_weights(
"F:/ComfyUI_windows_portable_nvidia/ComfyUI_windows_portable/ComfyUI/models/loras/LCM_LoRA_Weights_SDXL.safetensors",
adapter_name="lcm"
)
pipe.load_lora_weights(lora_path, adapter_name="lora")
pipe.set_adapters(["lcm", "lora"], adapter_weights=[1.0, 1.0])
#pipe.set_adapters(["lcm"], adapter_weights=[1.0])
#pipe.unet = torch.compile(pipe.unet, mode="reduce-overhead", fullgraph=True)
return pipe
except Exception as e:
raise Exception(f"Error loading model: {str(e)}")
#https://huggingface.co/blog/simple_sdxl_optimizations
async def generate_image(
pipe: StableDiffusionXLImg2ImgPipeline,
request: GenerateRequest
) -> str:
"""Generate image and return base64 string."""
try:
# Clear CUDA cache before processing
torch.cuda.empty_cache()
image_stream = io.BytesIO(base64.b64decode(request.image))
init_image = Image.open(image_stream).convert('RGB')
init_image = init_image.resize((request.width, request.height)) # width, height
init_image = transform(init_image).to('cuda')
init_image = init_image.unsqueeze(0)
(
prompt_embeds
, prompt_neg_embeds
, pooled_prompt_embeds
, negative_pooled_prompt_embeds
) = get_weighted_text_embeddings_sdxl(
pipe,
prompt = request.prompt,
neg_prompt = "bad quality, poorly Rendered face, poorly drawn face, poorly facial details, poorly drawn hands, poorly rendered hands, low resolution, Images cut out at the top, left, right, bottom, bad composition, mutated body parts, blurry image, disfigured, oversaturated, bad anatomy, deformed body features"
)
# Generate image
with torch.inference_mode():
output_image = pipe(
prompt_embeds = prompt_embeds,
negative_prompt_embeds = prompt_neg_embeds,
pooled_prompt_embeds = pooled_prompt_embeds,
negative_pooled_prompt_embeds = negative_pooled_prompt_embeds,
image=init_image,
num_inference_steps=request.num_inference_steps,
guidance_scale=request.guidance_scale,
strength=request.strength,
generator=torch.manual_seed(0),
height=request.height, width=request.width,
).images[0]
# Clean up CUDA tensors
del init_image, prompt_embeds, prompt_neg_embeds,pooled_prompt_embeds, negative_pooled_prompt_embeds
torch.cuda.empty_cache()
gc.collect()
# Convert to base64
buffered = BytesIO()
output_image.save(buffered, format="PNG")
buffered.seek(0)
img_str = base64.b64encode(buffered.getvalue()).decode()
return f"data:image/png;base64,{img_str}"
except Exception as e:
raise Exception(f"Error generating image: {str(e)} at line {e.__traceback__.tb_lineno}")
async def handle_client(websocket):
"""Handle WebSocket client connection."""
pipe = None
try:
async for message in websocket:
try:
# Clear memory before processing new request
torch.cuda.empty_cache()
gc.collect()
data = json.loads(message)
command = data.get("command")
if command == "load_model":
await send_progress(websocket, "Loading model...", 0.0)
request = LoadModelRequest.from_json(message)
if pipe is not None:
# Clean up existing model
del pipe
torch.cuda.empty_cache()
gc.collect()
pipe = await load_model(request.model_path, request.lora_path)
await send_progress(websocket, "Model loaded successfully", 1.0)
elif command == "generate":
if not pipe:
raise Exception("Model not loaded. Please load model first.")
await send_progress(websocket, "Generating image...", 0.0)
request = GenerateRequest.from_json(message)
# Use a timeout for generation to prevent hanging
try:
result = await asyncio.wait_for(
generate_image(pipe=pipe, request=request),
timeout=10.0 # 30 second timeout
)
response = GenerateResponse(image=result)
await websocket.send(response.to_json())
except asyncio.TimeoutError:
# Force cleanup if generation times out
torch.cuda.empty_cache()
gc.collect()
raise Exception("Generation timed out")
else:
raise Exception(f"Unknown command: {command}")
except Exception as e:
await send_progress(websocket, "Error processing request", 0.0, str(e))
# Clean up after error
torch.cuda.empty_cache()
gc.collect()
except websockets.exceptions.ConnectionClosed:
print("Client connection closed")
except Exception as e:
await send_progress(websocket, "Fatal error", 0.0, str(e))
finally:
# Clean up resources when client disconnects
if pipe is not None:
del pipe
torch.cuda.empty_cache()
gc.collect()
async def server():
"""Start WebSocket server."""
async with websockets.serve(
handle_client,
"localhost",
8765,
max_size=None,
ping_interval=None, # Disable ping/pong to prevent timeouts
max_queue=10 # Limit pending connections
):
print("Server started on ws://localhost:8765")
await asyncio.Future() # run forever
if __name__ == "__main__":
# test() # Comment out test function
asyncio.run(server())
@@ -0,0 +1,101 @@
import asyncio
import base64
import json
import logging
import pathlib
import time
from io import BytesIO
from typing import Dict, Any, Optional, Tuple, Callable, List
import torch
from diffusers import AutoPipelineForImage2Image, LCMScheduler
from diffusers.utils import make_image_grid, load_image
import torch
import websockets
from PIL import Image
from diffusers import (
StableDiffusionXLPipeline,
StableDiffusionXLImg2ImgPipeline,
AutoPipelineForImage2Image,
LCMScheduler,
UNet2DConditionModel,
)
def save_image(image: Image.Image, base_path: str = "outputs") -> str:
"""Save a PIL Image to disk with a timestamp-based filename."""
try:
# Create outputs directory if it doesn't exist
output_dir = pathlib.Path(base_path)
output_dir.mkdir(parents=True, exist_ok=True)
# Generate filename with timestamp
timestamp = time.strftime("%Y%m%d-%H%M%S")
filename = f"generated-{timestamp}.png"
filepath = output_dir / filename
# Save the image
print(f"Saving image to: {filepath}")
image.save(str(filepath), format="PNG")
print(f"Successfully saved image to: {filepath}")
return str(filepath)
except Exception as e:
print(f"Error saving image: {e}")
return ""
def test():
# Load base SDXL pipeline from .safetensors file
pipe = StableDiffusionXLPipeline.from_single_file(
f"C:/Users/Tensor/Downloads/animagineXL40_v4Opt.safetensors",
torch_dtype=torch.float16,
use_safetensors=True,
).to("cuda")
# Convert to img2img pipeline
pipe = StableDiffusionXLImg2ImgPipeline(**pipe.components)
# Set scheduler
pipe.scheduler = LCMScheduler.from_config(pipe.scheduler.config)
# Load LCM-LoRA weights
pipe.load_lora_weights(
"F:/ComfyUI_windows_portable_nvidia/ComfyUI_windows_portable/ComfyUI/models/loras/LCM_LoRA_Weights_SDXL.safetensors",
adapter_name="lcm"
)
pipe.set_adapters(["lcm"], adapter_weights=[1.0])
# prepare image
url = "https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/7462a0ac-637d-467b-baa9-34c5f54696b4/original=true,quality=90/60277788.jpeg"
init_image = load_image(url)
prompt = "safe_pos, 1girl, solo, solo focus, 8k, masterpiece, hires, absurdres, splash art, gradient, looking at viewer, medium breasts, vibrant yellow hair, purple hair tips, (shoulder freckles:1.2), nude, athletic, confident, hand on waist, extremely long hair, dancing, delicate gold jewelry, thin gold bangles, belly button chain, action shot,, head tilt, hair censor, spotlight, tottotonero"
# pass prompt and image to pipeline
generator = torch.manual_seed(0)
output_image = pipe(
prompt,
image=init_image,
num_inference_steps=4,
guidance_scale=2,
strength=0.6,
generator=generator
).images[0]
# Save both input and output images
save_image(init_image, "outputs/input")
output_path = save_image(output_image, "outputs/output")
print(f"Output image saved to: {output_path}")
# Create and save the grid
grid = make_image_grid([init_image, output_image], rows=1, cols=2)
grid_path = save_image(grid, "outputs/grid")
print(f"Grid image saved to: {grid_path}")
async def start_server():
async with websockets.serve(
"localhost",
8765 # Increase to 10MB
):
await asyncio.Future() # run forever
if __name__ == "__main__":
test()
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Before

Width:  |  Height:  |  Size: 701 B

After

Width:  |  Height:  |  Size: 701 B

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 KiB

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Before

Width:  |  Height:  |  Size: 615 B

After

Width:  |  Height:  |  Size: 615 B

Some files were not shown because too many files have changed in this diff Show More