diff --git a/Cargo.lock b/Cargo.lock index 40d66accee..dea359bc41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -653,47 +653,6 @@ dependencies = [ "backtrace", ] -[[package]] -name = "app" -version = "0.1.0" -dependencies = [ - "anyhow", - "assertor", - "base64 0.22.1", - "bytes 1.10.0", - "chrono", - "const_format", - "crockford", - "cudarc", - "directories 6.0.0", - "dirs 6.0.0", - "filesys", - "futures 0.3.29", - "half", - "image", - "libloading 0.8.6", - "log", - "memory_caching", - "ml_models", - "ml_weights_registry", - "once_cell", - "ort", - "primitives", - "rand 0.8.5", - "reqwest 0.12.15", - "serde", - "serde_json", - "serde_yaml", - "serial_test", - "tauri", - "tauri-build", - "tauri-plugin-log", - "tempdir", - "tempfile", - "tokio", - "tokio-util", -] - [[package]] name = "approx" version = "0.5.1" @@ -12092,6 +12051,47 @@ dependencies = [ "windows 0.58.0", ] +[[package]] +name = "tauri-4o" +version = "0.1.0" +dependencies = [ + "anyhow", + "assertor", + "base64 0.22.1", + "bytes 1.10.0", + "chrono", + "const_format", + "crockford", + "cudarc", + "directories 6.0.0", + "dirs 6.0.0", + "filesys", + "futures 0.3.29", + "half", + "image", + "libloading 0.8.6", + "log", + "memory_caching", + "ml_models", + "ml_weights_registry", + "once_cell", + "ort", + "primitives", + "rand 0.8.5", + "reqwest 0.12.15", + "serde", + "serde_json", + "serde_yaml", + "serial_test", + "tauri", + "tauri-build", + "tauri-plugin-log", + "tempdir", + "tempfile", + "tokio", + "tokio-util", +] + [[package]] name = "tauri-build" version = "2.0.5" @@ -12194,6 +12194,47 @@ dependencies = [ "time", ] +[[package]] +name = "tauri-realtime" +version = "0.1.0" +dependencies = [ + "anyhow", + "assertor", + "base64 0.22.1", + "bytes 1.10.0", + "chrono", + "const_format", + "crockford", + "cudarc", + "directories 6.0.0", + "dirs 6.0.0", + "filesys", + "futures 0.3.29", + "half", + "image", + "libloading 0.8.6", + "log", + "memory_caching", + "ml_models", + "ml_weights_registry", + "once_cell", + "ort", + "primitives", + "rand 0.8.5", + "reqwest 0.12.15", + "serde", + "serde_json", + "serde_yaml", + "serial_test", + "tauri", + "tauri-build", + "tauri-plugin-log", + "tempdir", + "tempfile", + "tokio", + "tokio-util", +] + [[package]] name = "tauri-runtime" version = "2.3.0" diff --git a/Cargo.toml b/Cargo.toml index 18c98eb9b9..374b493b9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,9 +22,10 @@ members = [ "crates/cli/production/migration-tool", # Desktop apps - "crates/desktop/src-tauri", "crates/desktop/ml_models", "crates/desktop/ml_weights_registry", + "crates/desktop/tauri-4o", + "crates/desktop/tauri-realtime", # Shared libraries "crates/lib/actix_cors_configs", diff --git a/crates/desktop/src-tauri/.gitignore b/crates/desktop/tauri-4o/.gitignore similarity index 100% rename from crates/desktop/src-tauri/.gitignore rename to crates/desktop/tauri-4o/.gitignore diff --git a/crates/desktop/src-tauri/.taurignore b/crates/desktop/tauri-4o/.taurignore similarity index 100% rename from crates/desktop/src-tauri/.taurignore rename to crates/desktop/tauri-4o/.taurignore diff --git a/crates/desktop/src-tauri/Cargo.toml b/crates/desktop/tauri-4o/Cargo.toml similarity index 99% rename from crates/desktop/src-tauri/Cargo.toml rename to crates/desktop/tauri-4o/Cargo.toml index cb7d638d41..bb9e710db7 100644 --- a/crates/desktop/src-tauri/Cargo.toml +++ b/crates/desktop/tauri-4o/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "app" +name = "tauri-4o" version = "0.1.0" description = "A Tauri App" authors = ["you"] diff --git a/crates/desktop/src-tauri/binary_includes/1024.png b/crates/desktop/tauri-4o/binary_includes/1024.png similarity index 100% rename from crates/desktop/src-tauri/binary_includes/1024.png rename to crates/desktop/tauri-4o/binary_includes/1024.png diff --git a/crates/desktop/src-tauri/build.rs b/crates/desktop/tauri-4o/build.rs similarity index 100% rename from crates/desktop/src-tauri/build.rs rename to crates/desktop/tauri-4o/build.rs diff --git a/crates/desktop/src-tauri/capabilities/default.json b/crates/desktop/tauri-4o/capabilities/default.json similarity index 100% rename from crates/desktop/src-tauri/capabilities/default.json rename to crates/desktop/tauri-4o/capabilities/default.json diff --git a/crates/desktop/src-tauri/icons/128x128.png b/crates/desktop/tauri-4o/icons/128x128.png similarity index 100% rename from crates/desktop/src-tauri/icons/128x128.png rename to crates/desktop/tauri-4o/icons/128x128.png diff --git a/crates/desktop/src-tauri/icons/128x128@2x.png b/crates/desktop/tauri-4o/icons/128x128@2x.png similarity index 100% rename from crates/desktop/src-tauri/icons/128x128@2x.png rename to crates/desktop/tauri-4o/icons/128x128@2x.png diff --git a/crates/desktop/src-tauri/icons/32x32.png b/crates/desktop/tauri-4o/icons/32x32.png similarity index 100% rename from crates/desktop/src-tauri/icons/32x32.png rename to crates/desktop/tauri-4o/icons/32x32.png diff --git a/crates/desktop/src-tauri/icons/Square107x107Logo.png b/crates/desktop/tauri-4o/icons/Square107x107Logo.png similarity index 100% rename from crates/desktop/src-tauri/icons/Square107x107Logo.png rename to crates/desktop/tauri-4o/icons/Square107x107Logo.png diff --git a/crates/desktop/src-tauri/icons/Square142x142Logo.png b/crates/desktop/tauri-4o/icons/Square142x142Logo.png similarity index 100% rename from crates/desktop/src-tauri/icons/Square142x142Logo.png rename to crates/desktop/tauri-4o/icons/Square142x142Logo.png diff --git a/crates/desktop/src-tauri/icons/Square150x150Logo.png b/crates/desktop/tauri-4o/icons/Square150x150Logo.png similarity index 100% rename from crates/desktop/src-tauri/icons/Square150x150Logo.png rename to crates/desktop/tauri-4o/icons/Square150x150Logo.png diff --git a/crates/desktop/src-tauri/icons/Square284x284Logo.png b/crates/desktop/tauri-4o/icons/Square284x284Logo.png similarity index 100% rename from crates/desktop/src-tauri/icons/Square284x284Logo.png rename to crates/desktop/tauri-4o/icons/Square284x284Logo.png diff --git a/crates/desktop/src-tauri/icons/Square30x30Logo.png b/crates/desktop/tauri-4o/icons/Square30x30Logo.png similarity index 100% rename from crates/desktop/src-tauri/icons/Square30x30Logo.png rename to crates/desktop/tauri-4o/icons/Square30x30Logo.png diff --git a/crates/desktop/src-tauri/icons/Square310x310Logo.png b/crates/desktop/tauri-4o/icons/Square310x310Logo.png similarity index 100% rename from crates/desktop/src-tauri/icons/Square310x310Logo.png rename to crates/desktop/tauri-4o/icons/Square310x310Logo.png diff --git a/crates/desktop/src-tauri/icons/Square44x44Logo.png b/crates/desktop/tauri-4o/icons/Square44x44Logo.png similarity index 100% rename from crates/desktop/src-tauri/icons/Square44x44Logo.png rename to crates/desktop/tauri-4o/icons/Square44x44Logo.png diff --git a/crates/desktop/src-tauri/icons/Square71x71Logo.png b/crates/desktop/tauri-4o/icons/Square71x71Logo.png similarity index 100% rename from crates/desktop/src-tauri/icons/Square71x71Logo.png rename to crates/desktop/tauri-4o/icons/Square71x71Logo.png diff --git a/crates/desktop/src-tauri/icons/Square89x89Logo.png b/crates/desktop/tauri-4o/icons/Square89x89Logo.png similarity index 100% rename from crates/desktop/src-tauri/icons/Square89x89Logo.png rename to crates/desktop/tauri-4o/icons/Square89x89Logo.png diff --git a/crates/desktop/src-tauri/icons/StoreLogo.png b/crates/desktop/tauri-4o/icons/StoreLogo.png similarity index 100% rename from crates/desktop/src-tauri/icons/StoreLogo.png rename to crates/desktop/tauri-4o/icons/StoreLogo.png diff --git a/crates/desktop/src-tauri/icons/icon.icns b/crates/desktop/tauri-4o/icons/icon.icns similarity index 100% rename from crates/desktop/src-tauri/icons/icon.icns rename to crates/desktop/tauri-4o/icons/icon.icns diff --git a/crates/desktop/src-tauri/icons/icon.ico b/crates/desktop/tauri-4o/icons/icon.ico similarity index 100% rename from crates/desktop/src-tauri/icons/icon.ico rename to crates/desktop/tauri-4o/icons/icon.ico diff --git a/crates/desktop/src-tauri/icons/icon.png b/crates/desktop/tauri-4o/icons/icon.png similarity index 100% rename from crates/desktop/src-tauri/icons/icon.png rename to crates/desktop/tauri-4o/icons/icon.png diff --git a/crates/desktop/src-tauri/src/endpoints/download_models.rs b/crates/desktop/tauri-4o/src/endpoints/download_models.rs similarity index 100% rename from crates/desktop/src-tauri/src/endpoints/download_models.rs rename to crates/desktop/tauri-4o/src/endpoints/download_models.rs diff --git a/crates/desktop/src-tauri/src/endpoints/flip_image.rs b/crates/desktop/tauri-4o/src/endpoints/flip_image.rs similarity index 100% rename from crates/desktop/src-tauri/src/endpoints/flip_image.rs rename to crates/desktop/tauri-4o/src/endpoints/flip_image.rs diff --git a/crates/desktop/src-tauri/src/endpoints/inpaint_image_endpoint.rs b/crates/desktop/tauri-4o/src/endpoints/inpaint_image_endpoint.rs similarity index 100% rename from crates/desktop/src-tauri/src/endpoints/inpaint_image_endpoint.rs rename to crates/desktop/tauri-4o/src/endpoints/inpaint_image_endpoint.rs diff --git a/crates/desktop/src-tauri/src/endpoints/mod.rs b/crates/desktop/tauri-4o/src/endpoints/mod.rs similarity index 100% rename from crates/desktop/src-tauri/src/endpoints/mod.rs rename to crates/desktop/tauri-4o/src/endpoints/mod.rs diff --git a/crates/desktop/src-tauri/src/endpoints/realtime_image_endpoint.rs b/crates/desktop/tauri-4o/src/endpoints/realtime_image_endpoint.rs similarity index 100% rename from crates/desktop/src-tauri/src/endpoints/realtime_image_endpoint.rs rename to crates/desktop/tauri-4o/src/endpoints/realtime_image_endpoint.rs diff --git a/crates/desktop/src-tauri/src/endpoints/remove_background_endpoint.rs b/crates/desktop/tauri-4o/src/endpoints/remove_background_endpoint.rs similarity index 100% rename from crates/desktop/src-tauri/src/endpoints/remove_background_endpoint.rs rename to crates/desktop/tauri-4o/src/endpoints/remove_background_endpoint.rs diff --git a/crates/desktop/src-tauri/src/endpoints/save_image_endpoint.rs b/crates/desktop/tauri-4o/src/endpoints/save_image_endpoint.rs similarity index 100% rename from crates/desktop/src-tauri/src/endpoints/save_image_endpoint.rs rename to crates/desktop/tauri-4o/src/endpoints/save_image_endpoint.rs diff --git a/crates/desktop/src-tauri/src/endpoints/text_to_image_endpoint.rs b/crates/desktop/tauri-4o/src/endpoints/text_to_image_endpoint.rs similarity index 100% rename from crates/desktop/src-tauri/src/endpoints/text_to_image_endpoint.rs rename to crates/desktop/tauri-4o/src/endpoints/text_to_image_endpoint.rs diff --git a/crates/desktop/src-tauri/src/events/mod.rs b/crates/desktop/tauri-4o/src/events/mod.rs similarity index 100% rename from crates/desktop/src-tauri/src/events/mod.rs rename to crates/desktop/tauri-4o/src/events/mod.rs diff --git a/crates/desktop/src-tauri/src/events/notification_event.rs b/crates/desktop/tauri-4o/src/events/notification_event.rs similarity index 100% rename from crates/desktop/src-tauri/src/events/notification_event.rs rename to crates/desktop/tauri-4o/src/events/notification_event.rs diff --git a/crates/desktop/src-tauri/src/lib.rs b/crates/desktop/tauri-4o/src/lib.rs similarity index 100% rename from crates/desktop/src-tauri/src/lib.rs rename to crates/desktop/tauri-4o/src/lib.rs diff --git a/crates/desktop/src-tauri/src/main.rs b/crates/desktop/tauri-4o/src/main.rs similarity index 100% rename from crates/desktop/src-tauri/src/main.rs rename to crates/desktop/tauri-4o/src/main.rs diff --git a/crates/desktop/src-tauri/src/state/app_config.rs b/crates/desktop/tauri-4o/src/state/app_config.rs similarity index 100% rename from crates/desktop/src-tauri/src/state/app_config.rs rename to crates/desktop/tauri-4o/src/state/app_config.rs diff --git a/crates/desktop/src-tauri/src/state/app_dir.rs b/crates/desktop/tauri-4o/src/state/app_dir.rs similarity index 100% rename from crates/desktop/src-tauri/src/state/app_dir.rs rename to crates/desktop/tauri-4o/src/state/app_dir.rs diff --git a/crates/desktop/src-tauri/src/state/expanduser.rs b/crates/desktop/tauri-4o/src/state/expanduser.rs similarity index 100% rename from crates/desktop/src-tauri/src/state/expanduser.rs rename to crates/desktop/tauri-4o/src/state/expanduser.rs diff --git a/crates/desktop/src-tauri/src/state/mod.rs b/crates/desktop/tauri-4o/src/state/mod.rs similarity index 100% rename from crates/desktop/src-tauri/src/state/mod.rs rename to crates/desktop/tauri-4o/src/state/mod.rs diff --git a/crates/desktop/src-tauri/src/state/os_platform.rs b/crates/desktop/tauri-4o/src/state/os_platform.rs similarity index 100% rename from crates/desktop/src-tauri/src/state/os_platform.rs rename to crates/desktop/tauri-4o/src/state/os_platform.rs diff --git a/crates/desktop/src-tauri/src/state/yaml_config.rs b/crates/desktop/tauri-4o/src/state/yaml_config.rs similarity index 100% rename from crates/desktop/src-tauri/src/state/yaml_config.rs rename to crates/desktop/tauri-4o/src/state/yaml_config.rs diff --git a/crates/desktop/src-tauri/src/stubs/mod.rs b/crates/desktop/tauri-4o/src/stubs/mod.rs similarity index 100% rename from crates/desktop/src-tauri/src/stubs/mod.rs rename to crates/desktop/tauri-4o/src/stubs/mod.rs diff --git a/crates/desktop/src-tauri/src/stubs/model_cache.rs b/crates/desktop/tauri-4o/src/stubs/model_cache.rs similarity index 100% rename from crates/desktop/src-tauri/src/stubs/model_cache.rs rename to crates/desktop/tauri-4o/src/stubs/model_cache.rs diff --git a/crates/desktop/src-tauri/src/stubs/model_config.rs b/crates/desktop/tauri-4o/src/stubs/model_config.rs similarity index 100% rename from crates/desktop/src-tauri/src/stubs/model_config.rs rename to crates/desktop/tauri-4o/src/stubs/model_config.rs diff --git a/crates/desktop/src-tauri/src/stubs/prompt_cache.rs b/crates/desktop/tauri-4o/src/stubs/prompt_cache.rs similarity index 100% rename from crates/desktop/src-tauri/src/stubs/prompt_cache.rs rename to crates/desktop/tauri-4o/src/stubs/prompt_cache.rs diff --git a/crates/desktop/src-tauri/src/threads/downloader_thread.rs b/crates/desktop/tauri-4o/src/threads/downloader_thread.rs similarity index 100% rename from crates/desktop/src-tauri/src/threads/downloader_thread.rs rename to crates/desktop/tauri-4o/src/threads/downloader_thread.rs diff --git a/crates/desktop/src-tauri/src/threads/mod.rs b/crates/desktop/tauri-4o/src/threads/mod.rs similarity index 100% rename from crates/desktop/src-tauri/src/threads/mod.rs rename to crates/desktop/tauri-4o/src/threads/mod.rs diff --git a/crates/desktop/src-tauri/src/transfer/download.rs b/crates/desktop/tauri-4o/src/transfer/download.rs similarity index 100% rename from crates/desktop/src-tauri/src/transfer/download.rs rename to crates/desktop/tauri-4o/src/transfer/download.rs diff --git a/crates/desktop/src-tauri/src/transfer/error.rs b/crates/desktop/tauri-4o/src/transfer/error.rs similarity index 100% rename from crates/desktop/src-tauri/src/transfer/error.rs rename to crates/desktop/tauri-4o/src/transfer/error.rs diff --git a/crates/desktop/src-tauri/src/transfer/mod.rs b/crates/desktop/tauri-4o/src/transfer/mod.rs similarity index 100% rename from crates/desktop/src-tauri/src/transfer/mod.rs rename to crates/desktop/tauri-4o/src/transfer/mod.rs diff --git a/crates/desktop/src-tauri/src/transfer/upload.rs b/crates/desktop/tauri-4o/src/transfer/upload.rs similarity index 100% rename from crates/desktop/src-tauri/src/transfer/upload.rs rename to crates/desktop/tauri-4o/src/transfer/upload.rs diff --git a/crates/desktop/src-tauri/src/transfer/util.rs b/crates/desktop/tauri-4o/src/transfer/util.rs similarity index 100% rename from crates/desktop/src-tauri/src/transfer/util.rs rename to crates/desktop/tauri-4o/src/transfer/util.rs diff --git a/crates/desktop/src-tauri/src/utils/image/decode_base64_image.rs b/crates/desktop/tauri-4o/src/utils/image/decode_base64_image.rs similarity index 100% rename from crates/desktop/src-tauri/src/utils/image/decode_base64_image.rs rename to crates/desktop/tauri-4o/src/utils/image/decode_base64_image.rs diff --git a/crates/desktop/src-tauri/src/utils/image/encode_dynamic_image_base64_png.rs b/crates/desktop/tauri-4o/src/utils/image/encode_dynamic_image_base64_png.rs similarity index 100% rename from crates/desktop/src-tauri/src/utils/image/encode_dynamic_image_base64_png.rs rename to crates/desktop/tauri-4o/src/utils/image/encode_dynamic_image_base64_png.rs diff --git a/crates/desktop/src-tauri/src/utils/image/encode_rgb_image_base64_png.rs b/crates/desktop/tauri-4o/src/utils/image/encode_rgb_image_base64_png.rs similarity index 100% rename from crates/desktop/src-tauri/src/utils/image/encode_rgb_image_base64_png.rs rename to crates/desktop/tauri-4o/src/utils/image/encode_rgb_image_base64_png.rs diff --git a/crates/desktop/src-tauri/src/utils/image/mod.rs b/crates/desktop/tauri-4o/src/utils/image/mod.rs similarity index 100% rename from crates/desktop/src-tauri/src/utils/image/mod.rs rename to crates/desktop/tauri-4o/src/utils/image/mod.rs diff --git a/crates/desktop/src-tauri/src/utils/log_environment_details.rs b/crates/desktop/tauri-4o/src/utils/log_environment_details.rs similarity index 100% rename from crates/desktop/src-tauri/src/utils/log_environment_details.rs rename to crates/desktop/tauri-4o/src/utils/log_environment_details.rs diff --git a/crates/desktop/src-tauri/src/utils/mod.rs b/crates/desktop/tauri-4o/src/utils/mod.rs similarity index 100% rename from crates/desktop/src-tauri/src/utils/mod.rs rename to crates/desktop/tauri-4o/src/utils/mod.rs diff --git a/crates/desktop/src-tauri/tauri.conf.json b/crates/desktop/tauri-4o/tauri.conf.json similarity index 100% rename from crates/desktop/src-tauri/tauri.conf.json rename to crates/desktop/tauri-4o/tauri.conf.json diff --git a/crates/desktop/src-tauri/tests/test_dynamic_library_loading.rs b/crates/desktop/tauri-4o/tests/test_dynamic_library_loading.rs similarity index 100% rename from crates/desktop/src-tauri/tests/test_dynamic_library_loading.rs rename to crates/desktop/tauri-4o/tests/test_dynamic_library_loading.rs diff --git a/crates/desktop/tauri-realtime/.gitignore b/crates/desktop/tauri-realtime/.gitignore new file mode 100644 index 0000000000..502406b4ed --- /dev/null +++ b/crates/desktop/tauri-realtime/.gitignore @@ -0,0 +1,4 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +/gen/schemas diff --git a/crates/desktop/tauri-realtime/.taurignore b/crates/desktop/tauri-realtime/.taurignore new file mode 100644 index 0000000000..ad17b2ca1c --- /dev/null +++ b/crates/desktop/tauri-realtime/.taurignore @@ -0,0 +1,4 @@ +# Don't restart the dev server on Rust changes +crates/ +Cargo.toml +Cargo.lock \ No newline at end of file diff --git a/crates/desktop/tauri-realtime/Cargo.toml b/crates/desktop/tauri-realtime/Cargo.toml new file mode 100644 index 0000000000..7c984d4986 --- /dev/null +++ b/crates/desktop/tauri-realtime/Cargo.toml @@ -0,0 +1,86 @@ +[package] +name = "tauri-realtime" +version = "0.1.0" +description = "A Tauri App" +authors = ["you"] +license = "" +repository = "" +edition = "2021" +rust-version = "1.77.2" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "app_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +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" } +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" +futures = "0.3" +half = { version = "2.3.1", features = ["num-traits", "use-intrinsics", "rand_distr"] } +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" +reqwest = { version = "0.12.12", features = ["stream"], default-features = false } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_yaml = "0.9" +tauri = { version = "2.2.4", features = [] } +tauri-plugin-log = "2" +tempdir = "0.3.7" +tempfile = "3.18.0" +tokio = { version = "1.42", features = ["rt", "rt-multi-thread", "fs"] } +tokio-util = { version = "0.7", features = ["codec"] } + +# tracing-subscriber = { version = "0.3", features = [ "env-filter", "fmt" ] } + +[target.'cfg(not(target_os = "macos"))'.dependencies] +ml_models = { path = "../ml_models" } + +[dev-dependencies] +libloading = "0.8.6" +serial_test = "3.2.0" +assertor = "0.0.3" + +#[target.'cfg(target_os = "macos")'.features] +#default = [] +#ml_models = ["dep:ml_models"] + +[profile.dev] +incremental = true # Compile your binary in smaller steps. + +[profile.release] +codegen-units = 1 # Allows LLVM to perform better optimization. +lto = true # Enables link-time-optimizations. +opt-level = 3 # `s` prioritizes small binary size. Use `3` if you prefer speed. +panic = "abort" # Higher performance by disabling panic handlers. +strip = true # Ensures debug symbols are removed. diff --git a/crates/desktop/tauri-realtime/binary_includes/1024.png b/crates/desktop/tauri-realtime/binary_includes/1024.png new file mode 100644 index 0000000000..9f746687dc Binary files /dev/null and b/crates/desktop/tauri-realtime/binary_includes/1024.png differ diff --git a/crates/desktop/tauri-realtime/build.rs b/crates/desktop/tauri-realtime/build.rs new file mode 100644 index 0000000000..795b9b7c83 --- /dev/null +++ b/crates/desktop/tauri-realtime/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/crates/desktop/tauri-realtime/capabilities/default.json b/crates/desktop/tauri-realtime/capabilities/default.json new file mode 100644 index 0000000000..f3543164cb --- /dev/null +++ b/crates/desktop/tauri-realtime/capabilities/default.json @@ -0,0 +1,12 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "enables the default permissions", + "windows": [ + "main" + ], + "permissions": [ + "core:default", + "log:default" + ] +} \ No newline at end of file diff --git a/crates/desktop/tauri-realtime/icons/128x128.png b/crates/desktop/tauri-realtime/icons/128x128.png new file mode 100644 index 0000000000..77e7d2338e Binary files /dev/null and b/crates/desktop/tauri-realtime/icons/128x128.png differ diff --git a/crates/desktop/tauri-realtime/icons/128x128@2x.png b/crates/desktop/tauri-realtime/icons/128x128@2x.png new file mode 100644 index 0000000000..0f7976f1a0 Binary files /dev/null and b/crates/desktop/tauri-realtime/icons/128x128@2x.png differ diff --git a/crates/desktop/tauri-realtime/icons/32x32.png b/crates/desktop/tauri-realtime/icons/32x32.png new file mode 100644 index 0000000000..98fda06fcb Binary files /dev/null and b/crates/desktop/tauri-realtime/icons/32x32.png differ diff --git a/crates/desktop/tauri-realtime/icons/Square107x107Logo.png b/crates/desktop/tauri-realtime/icons/Square107x107Logo.png new file mode 100644 index 0000000000..f35d84ff13 Binary files /dev/null and b/crates/desktop/tauri-realtime/icons/Square107x107Logo.png differ diff --git a/crates/desktop/tauri-realtime/icons/Square142x142Logo.png b/crates/desktop/tauri-realtime/icons/Square142x142Logo.png new file mode 100644 index 0000000000..1823bb2696 Binary files /dev/null and b/crates/desktop/tauri-realtime/icons/Square142x142Logo.png differ diff --git a/crates/desktop/tauri-realtime/icons/Square150x150Logo.png b/crates/desktop/tauri-realtime/icons/Square150x150Logo.png new file mode 100644 index 0000000000..dc2b22cea5 Binary files /dev/null and b/crates/desktop/tauri-realtime/icons/Square150x150Logo.png differ diff --git a/crates/desktop/tauri-realtime/icons/Square284x284Logo.png b/crates/desktop/tauri-realtime/icons/Square284x284Logo.png new file mode 100644 index 0000000000..0ed3984c5f Binary files /dev/null and b/crates/desktop/tauri-realtime/icons/Square284x284Logo.png differ diff --git a/crates/desktop/tauri-realtime/icons/Square30x30Logo.png b/crates/desktop/tauri-realtime/icons/Square30x30Logo.png new file mode 100644 index 0000000000..60bf0eadf7 Binary files /dev/null and b/crates/desktop/tauri-realtime/icons/Square30x30Logo.png differ diff --git a/crates/desktop/tauri-realtime/icons/Square310x310Logo.png b/crates/desktop/tauri-realtime/icons/Square310x310Logo.png new file mode 100644 index 0000000000..c8ca0ad132 Binary files /dev/null and b/crates/desktop/tauri-realtime/icons/Square310x310Logo.png differ diff --git a/crates/desktop/tauri-realtime/icons/Square44x44Logo.png b/crates/desktop/tauri-realtime/icons/Square44x44Logo.png new file mode 100644 index 0000000000..8756459b63 Binary files /dev/null and b/crates/desktop/tauri-realtime/icons/Square44x44Logo.png differ diff --git a/crates/desktop/tauri-realtime/icons/Square71x71Logo.png b/crates/desktop/tauri-realtime/icons/Square71x71Logo.png new file mode 100644 index 0000000000..2c8023cc82 Binary files /dev/null and b/crates/desktop/tauri-realtime/icons/Square71x71Logo.png differ diff --git a/crates/desktop/tauri-realtime/icons/Square89x89Logo.png b/crates/desktop/tauri-realtime/icons/Square89x89Logo.png new file mode 100644 index 0000000000..2c5e6034ff Binary files /dev/null and b/crates/desktop/tauri-realtime/icons/Square89x89Logo.png differ diff --git a/crates/desktop/tauri-realtime/icons/StoreLogo.png b/crates/desktop/tauri-realtime/icons/StoreLogo.png new file mode 100644 index 0000000000..17d142c0a4 Binary files /dev/null and b/crates/desktop/tauri-realtime/icons/StoreLogo.png differ diff --git a/crates/desktop/tauri-realtime/icons/icon.icns b/crates/desktop/tauri-realtime/icons/icon.icns new file mode 100644 index 0000000000..a2993adc87 Binary files /dev/null and b/crates/desktop/tauri-realtime/icons/icon.icns differ diff --git a/crates/desktop/tauri-realtime/icons/icon.ico b/crates/desktop/tauri-realtime/icons/icon.ico new file mode 100644 index 0000000000..06c23c82fe Binary files /dev/null and b/crates/desktop/tauri-realtime/icons/icon.ico differ diff --git a/crates/desktop/tauri-realtime/icons/icon.png b/crates/desktop/tauri-realtime/icons/icon.png new file mode 100644 index 0000000000..d1756ce45d Binary files /dev/null and b/crates/desktop/tauri-realtime/icons/icon.png differ diff --git a/crates/desktop/tauri-realtime/src/endpoints/download_models.rs b/crates/desktop/tauri-realtime/src/endpoints/download_models.rs new file mode 100644 index 0000000000..ec450baf9d --- /dev/null +++ b/crates/desktop/tauri-realtime/src/endpoints/download_models.rs @@ -0,0 +1,12 @@ +use crate::state::app_dir::AppDataRoot; +use tauri::State; + +#[tauri::command] +pub async fn download_models( + _app_data_root: State<'_, AppDataRoot>, +) -> Result { + + // NB: This endpoint has been removed for now as we now download models in the app lifecycle. + + Ok(("downloaded".to_string())) +} diff --git a/crates/desktop/tauri-realtime/src/endpoints/flip_image.rs b/crates/desktop/tauri-realtime/src/endpoints/flip_image.rs new file mode 100644 index 0000000000..cc6b2a7fea --- /dev/null +++ b/crates/desktop/tauri-realtime/src/endpoints/flip_image.rs @@ -0,0 +1,36 @@ +use base64::prelude::BASE64_STANDARD; +use base64::Engine; +use image::codecs::png::{CompressionType, FilterType, PngEncoder}; +use image::ImageReader; +use std::io::Cursor; + +#[tauri::command] +pub fn flip_image(image: &str) -> Result { + println!("infer_image called; processing image..."); + + let bytes = BASE64_STANDARD.decode(image) + .map_err(|err| format!("Base64 decode error: {}", err))?; + + let image = ImageReader::new(Cursor::new(bytes)) + .with_guessed_format() + .map_err(|err| format!("Image format error: {}", err))? + .decode() + .map_err(|err| format!("Image decode error: {}", err))?; + + let rotated_image = image.rotate180(); + + let mut buffer = Vec::new(); // TODO: Preallocate, better data structure + let mut writer = Cursor::new(&mut buffer); + + let encoder = PngEncoder::new_with_quality + (&mut writer, CompressionType::Fast, FilterType::Adaptive); + + rotated_image.write_with_encoder(encoder) + .map_err(|err| format!("Image encoding error: {}", err))?; + + let encoded = BASE64_STANDARD.encode(&buffer); + + println!("Encoded: {:?}", encoded.split_at(10).0); + + Ok(encoded) +} diff --git a/crates/desktop/tauri-realtime/src/endpoints/inpaint_image_endpoint.rs b/crates/desktop/tauri-realtime/src/endpoints/inpaint_image_endpoint.rs new file mode 100644 index 0000000000..ff2482c971 --- /dev/null +++ b/crates/desktop/tauri-realtime/src/endpoints/inpaint_image_endpoint.rs @@ -0,0 +1,13 @@ +use log::info; + + +#[tauri::command] +pub async fn inpaint_image( + image: &str, + mask: &str, + prompt: String, +) -> Result { + info!("inpaint_endpoint endpoint called."); + + Ok(mask.to_string()) +} diff --git a/crates/desktop/tauri-realtime/src/endpoints/mod.rs b/crates/desktop/tauri-realtime/src/endpoints/mod.rs new file mode 100644 index 0000000000..dcf8a0cc98 --- /dev/null +++ b/crates/desktop/tauri-realtime/src/endpoints/mod.rs @@ -0,0 +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; diff --git a/crates/desktop/tauri-realtime/src/endpoints/realtime_image_endpoint.rs b/crates/desktop/tauri-realtime/src/endpoints/realtime_image_endpoint.rs new file mode 100644 index 0000000000..008252d478 --- /dev/null +++ b/crates/desktop/tauri-realtime/src/endpoints/realtime_image_endpoint.rs @@ -0,0 +1,148 @@ +#[cfg(not(target_os = "macos"))] +use ml_models::ml::stable_diffusion::lcm_pipeline::{lcm_pipeline, Args}; + +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 crate::utils::image::encode_dynamic_image_base64_png::encode_dynamic_image_base64_png; +use crate::utils::image::encode_rgb_image_base64_png::encode_rgb_image_base64_png; +use image::imageops::FilterType; +use image::{DynamicImage, RgbImage}; +use log::{error, info}; +use ml_weights_registry::weights_registry::weights::{CLIP_JSON, LYKON_DEAMSHAPER_7_TEXT_ENCODER_FP16, LYKON_DEAMSHAPER_7_VAE, SDXL_TURBO_CLIP_TEXT_ENCODER, SIMIANLUO_LCM_DREAMSHAPER_V7_UNET}; +use std::path::PathBuf; +use tauri::{AppHandle, State}; + + +const PROMPT_FILENAME : &str = "prompt.txt"; +const PROMPT: &str = "shiba inu, cute dog, detailed, walking in a wooded forest, photorealistic, 8k"; + +const NEGATIVE_PROMPT: &str = "bad quality, bad faces, poor quality, blurry faces, watermark"; + +const RANDOM_SEED: u32 = 42; + + +/// This handler takes an image (as a base64 encoded string) and a prompt and returns +/// an image (as a base64-encoded string). +#[tauri::command] +pub async fn infer_image( + image: &str, + prompt: Option, + strength: Option, + seed: Option, + model_config: State<'_, AppConfig>, + model_cache: State<'_, ModelCache>, + prompt_cache: State<'_, PromptCache>, + app_data_root: State<'_, AppDataRoot>, + app: AppHandle, +) -> Result { + info!("infer_image endpoint called."); + + let prompt = get_prompt_or_fallback(prompt); + + info!("Strength: {:?}; Prompt: {}", strength, prompt); + + let image = decode_base64_image(image) + .map_err(|err| format!("Couldn't hydrate image from base64: {}", err))?; + + // 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, + &app_data_root + ).await; + + image = match result { + Ok(img) => DynamicImage::ImageRgb8(img), + Err(err) => { + error!("There was an error generating the image: {:?}", err); + return Err(format!("There was an error generating the image: {}", err)); + } + }; + } + + let bytes = encode_dynamic_image_base64_png(image) + .map_err(|err| format!("failure to encode image: {:?}", err))?; + + info!("Inference successful; image converted to base64, serving back to browser..."); + + Ok(bytes) +} + +#[cfg(not(target_os = "macos"))] +async fn infer_image_impl( + prompt: &str, + image: DynamicImage, + strength: Option, + seed: Option, + config: &AppConfig, + model_cache: &ModelCache, + prompt_cache: State<'_, PromptCache>, + app_data_root: &AppDataRoot, +) -> Result { + + 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(&LYKON_DEAMSHAPER_7_TEXT_ENCODER_FP16); + let clip_weights_path= weights_dir.weight_path(&SDXL_TURBO_CLIP_TEXT_ENCODER); + + let args = Args { + image: &image, + prompt: prompt.to_string(), + uncond_prompt: NEGATIVE_PROMPT.to_string(), + model_cache, + prompt_cache: &prompt_cache, + img2img_strength: strength, + cfg_scale: config.cfg_scale, + use_flash_attn: true, + model_config: &config.model_config, + maybe_seed: seed, + 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, + }; + + let image = lcm_pipeline(args) + .map_err(|err| format!("failure to encode image: {:?}", err))?; + + Ok(image) +} + +fn get_prompt_or_fallback(user_prompt: Option) -> String { + let user_prompt = user_prompt.map(|prompt| prompt.trim().to_string()) + .filter(|prompt| !prompt.is_empty()); + + if let Some(prompt) = user_prompt { + return prompt; + } + + let prompt_file = PathBuf::from(PROMPT_FILENAME) + .canonicalize() + .unwrap_or_else(|_| PathBuf::from(PROMPT_FILENAME)); + + std::fs::read_to_string(&prompt_file) + .map_err(|err| format!("Failed to read prompt file: {}", err)) + .unwrap_or_else(|_| { + error!("Failed to read prompt file: {:?}", prompt_file); + PROMPT.to_string() + }) + .trim() + .to_string() +} diff --git a/crates/desktop/tauri-realtime/src/endpoints/remove_background_endpoint.rs b/crates/desktop/tauri-realtime/src/endpoints/remove_background_endpoint.rs new file mode 100644 index 0000000000..5c3bfa4eb6 --- /dev/null +++ b/crates/desktop/tauri-realtime/src/endpoints/remove_background_endpoint.rs @@ -0,0 +1,45 @@ +//! Adapted from https://github.com/dnanhkhoa/rust-background-removal + +#[cfg(not(target_os = "macos"))] +use ml_models::ml::background_removal::remove_image_background::remove_image_background; + +use crate::state::app_dir::AppDataRoot; +use crate::utils::image::decode_base64_image::decode_base64_image; +use crate::utils::image::encode_dynamic_image_base64_png::encode_dynamic_image_base64_png; +use image::{imageops, DynamicImage}; +use log::info; +use ml_weights_registry::weights_registry::weights::DIS_MEDIUM_ONNX; +use tauri::State; + +/// This handler removes the background from an image. +#[tauri::command] +pub async fn remove_background( + image: &str, + app_data_root: State<'_, AppDataRoot>, +) -> Result { + info!("remove_background endpoint called."); + + let mut image = decode_base64_image(image) + .map_err(|err| format!("Couldn't hydrate image from base64: {}", err))?; + + #[cfg(not(target_os = "macos"))] + { + image = remove_background_impl(&app_data_root, image).await?; + } + + let image = encode_dynamic_image_base64_png(image) + .map_err(|err| format!("failure to encode image: {:?}", err))?; + + Ok(image) +} + +#[cfg(not(target_os = "macos"))] +async fn remove_background_impl(app_data_root: &AppDataRoot, image: DynamicImage) -> Result { + let model_path = app_data_root.weights_dir().weight_path(&DIS_MEDIUM_ONNX); + + let image = remove_image_background(model_path, image) + .await + .map_err(|err| format!("Couldn't remove image from image: {}", err))?; + + Ok(image) +} diff --git a/crates/desktop/tauri-realtime/src/endpoints/save_image_endpoint.rs b/crates/desktop/tauri-realtime/src/endpoints/save_image_endpoint.rs new file mode 100644 index 0000000000..39312b7d58 --- /dev/null +++ b/crates/desktop/tauri-realtime/src/endpoints/save_image_endpoint.rs @@ -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 { + 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()) +} diff --git a/crates/desktop/tauri-realtime/src/endpoints/text_to_image_endpoint.rs b/crates/desktop/tauri-realtime/src/endpoints/text_to_image_endpoint.rs new file mode 100644 index 0000000000..53ca93157d --- /dev/null +++ b/crates/desktop/tauri-realtime/src/endpoints/text_to_image_endpoint.rs @@ -0,0 +1,114 @@ +#[cfg(not(target_os = "macos"))] +use ml_models::ml::flux::flux_pipeline::{flux_pipeline, FluxModel, FluxPipelineArgs}; + +use crate::state::app_config::AppConfig; +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::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 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. +static IMAGE : Lazy = Lazy::new(||{ + ImageReader::new(Cursor::new(PNG_BYTES)) + .with_guessed_format() + .expect("failed to detect png format") + .decode() + .expect("failed to decode image") +}); + +#[tauri::command] +pub async fn text_to_image( + prompt: String, + model_config: State<'_, AppConfig>, + model_cache: State<'_, ModelCache>, + prompt_cache: State<'_, PromptCache>, + app: AppHandle, + app_data_root: State<'_, AppDataRoot>, +) -> Result { + info!("text_to_image endpoint called."); + + let mut image = IMAGE.resize(512, 512, FilterType::CatmullRom); + + #[cfg(not(target_os = "macos"))] + { + let result = text_to_image_impl( + &prompt, + image, + Some(STRENGTH), + &model_config, + &model_cache, + prompt_cache, + app_data_root.weights_dir(), + app, + ).await; + + image = match result { + Ok(img) => DynamicImage::ImageRgb8(img), + Err(err) => { + error!("There was an error generating the image: {:?}", err); + return Err(format!("There was an error generating the image: {}", err)); + } + }; + } + + let bytes = encode_dynamic_image_base64_png(image) + .map_err(|err| format!("failure to encode image: {:?}", err))?; + + info!("Inference successful; image converted to base64, serving back to browser..."); + + Ok(bytes) +} + +#[cfg(not(target_os = "macos"))] +async fn text_to_image_impl( + prompt: &str, + image: DynamicImage, + strength: Option, + config: &AppConfig, + model_cache: &ModelCache, + prompt_cache: State<'_, PromptCache>, + weights_dir: &AppWeightsDir, + app: AppHandle, +) -> Result { + + let args = FluxPipelineArgs { + image: &image, + prompt: 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, + seed: None, + use_quantized_model: Some(true), + model_config: &config.model_config, + 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 = flux_pipeline(args) + .map_err(|err| format!("failure to encode image: {:?}", err))?; + + Ok(image) +} diff --git a/crates/desktop/tauri-realtime/src/events/mod.rs b/crates/desktop/tauri-realtime/src/events/mod.rs new file mode 100644 index 0000000000..2db7fd03ce --- /dev/null +++ b/crates/desktop/tauri-realtime/src/events/mod.rs @@ -0,0 +1 @@ +pub mod notification_event; \ No newline at end of file diff --git a/crates/desktop/tauri-realtime/src/events/notification_event.rs b/crates/desktop/tauri-realtime/src/events/notification_event.rs new file mode 100644 index 0000000000..5435c91850 --- /dev/null +++ b/crates/desktop/tauri-realtime/src/events/notification_event.rs @@ -0,0 +1,45 @@ +use serde::Serialize; + +#[derive(Clone, Copy, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum NotificationModelType { + ImageSegmentation, + TextEncoder, + TextTokenizer, + Unet, + Vae, + Json, + ClipEncoder, + FluxModel, +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum NotificationEvent<'a> { + ModelDownloadStarted { + model_name: &'a str, + model_type: NotificationModelType, + }, + ModelDownloadProgress { + model_name: &'a str, + model_type: NotificationModelType, + currently_downloaded_bytes: usize, + total_file_bytes: usize, + }, + ModelDownloadComplete { + model_name: &'a str, + model_type: NotificationModelType, + }, + ModelLoading { + model_name: String, + model_type: NotificationModelType, + }, + ModelInference { + model_name: String, + model_type: NotificationModelType, + }, + ModelInferenceComplete { + model_name: String, + model_type: NotificationModelType, + }, +} diff --git a/crates/desktop/tauri-realtime/src/lib.rs b/crates/desktop/tauri-realtime/src/lib.rs new file mode 100644 index 0000000000..e8f7557102 --- /dev/null +++ b/crates/desktop/tauri-realtime/src/lib.rs @@ -0,0 +1,86 @@ +pub mod endpoints; +pub mod events; +pub mod state; +pub mod stubs; +pub mod threads; +pub mod transfer; +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; +use crate::stubs::prompt_cache::PromptCache; +use crate::threads::downloader_thread::downloader_thread; + +use tauri_plugin_log::Target; +use tauri_plugin_log::TargetKind; + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + // NB: Tauri wants to install the logger itself, so we can't rely on the logger crate + // until the tauri runtime begins. + println!("Loading model config..."); + + let config = AppConfig::init() + .expect("config should load"); + + let prompt_cache = PromptCache::with_capacity(8) + .expect("prompt cache should load"); + + println!("Creating model cache..."); + + let model_cache = ModelCache::new(); + + let app_data_root = config.app_data_root.clone(); + let app_data_root2 = config.app_data_root.clone(); + + println!("Initializing backend runtime..."); + + tauri::Builder::default() + .plugin(tauri_plugin_log::Builder::new() + .level(log::LevelFilter::Info) + .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 + // log_environment_details(); + + //if cfg!(debug_assertions) { + // app.handle().plugin( + // tauri_plugin_log::Builder::default() + // .level(log::LevelFilter::Info) + // .build(), + // )?; + //} + let app = app.handle().clone(); + + tauri::async_runtime::spawn(downloader_thread(app_data_root2, app)); + + Ok(()) + }) + .manage(config) + .manage(prompt_cache) + .manage(model_cache) + .manage(app_data_root) + .invoke_handler(tauri::generate_handler![ + download_models, + flip_image, + infer_image, + inpaint_image, + remove_background, + save_image, + text_to_image, + text_to_image, + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/crates/desktop/tauri-realtime/src/main.rs b/crates/desktop/tauri-realtime/src/main.rs new file mode 100644 index 0000000000..bf57eb7c0d --- /dev/null +++ b/crates/desktop/tauri-realtime/src/main.rs @@ -0,0 +1,15 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use std::fs::File; + +fn main() { + //// NB: This is a test to see the behavior of the application with dynamic loading modules. + //// We should be able to execute code before loading CUDA and other dependencies. + //let r = File::create("program_started.txt"); + //match r { + // Err(err) => println!("Failed to create program_started.txt: {}", err), + // Ok(_) => {} + //} + app_lib::run(); +} diff --git a/crates/desktop/tauri-realtime/src/state/app_config.rs b/crates/desktop/tauri-realtime/src/state/app_config.rs new file mode 100644 index 0000000000..fe2e602ddc --- /dev/null +++ b/crates/desktop/tauri-realtime/src/state/app_config.rs @@ -0,0 +1,62 @@ +use crate::state::app_dir::AppDataRoot; +use crate::state::os_platform::OsPlatform; +use crate::state::yaml_config::YamlConfig; +use crate::stubs::model_config::ModelConfig; + +const DEFAULT_SD_IMAGE_WIDTH: usize = 1024; +const DEFAULT_SD_IMAGE_HEIGHT: usize = 1024; + + +/// A central place to configure app-wide details. +pub struct AppConfig { + pub image_height: usize, + pub image_width: usize, + pub scheduler_steps: usize, + pub scheduler_samples: usize, + + pub seed: Option, + + pub cfg_scale: Option, + + /// Root location for application data. + pub app_data_root: AppDataRoot, + + pub model_config: ModelConfig, +} + +impl AppConfig { + + pub fn init() -> anyhow::Result { + let yaml_configs = YamlConfig::load_from_config_file_recursive(); + + println!("Configs: {:?}", yaml_configs); + + let os_platform = OsPlatform::get(); + + let default_data_root = match os_platform { + OsPlatform::Linux => yaml_configs.linux_default_data_path.as_deref(), + OsPlatform::MacOs => yaml_configs.mac_default_data_path.as_deref(), + OsPlatform::Windows => yaml_configs.windows_default_data_path.as_deref(), + }; + + println!("Possible user-defined data root: {:?}", default_data_root); + + let app_data_root = match default_data_root { + None => AppDataRoot::create_default()?, + Some(path) => AppDataRoot::create_existing(path)?, + }; + + println!("App data root: {:?}", app_data_root.path()); + + Ok(Self { + image_height: yaml_configs.image_height.unwrap_or(DEFAULT_SD_IMAGE_HEIGHT), + image_width: yaml_configs.image_width.unwrap_or(DEFAULT_SD_IMAGE_WIDTH), + scheduler_steps: yaml_configs.scheduler_steps.unwrap_or(4), + scheduler_samples: yaml_configs.scheduler_samples.unwrap_or(15), + seed: yaml_configs.seed, + cfg_scale: yaml_configs.cfg_scale, + app_data_root, + model_config: ModelConfig::init()?, + }) + } +} diff --git a/crates/desktop/tauri-realtime/src/state/app_dir.rs b/crates/desktop/tauri-realtime/src/state/app_dir.rs new file mode 100644 index 0000000000..2cbad5d4d5 --- /dev/null +++ b/crates/desktop/tauri-realtime/src/state/app_dir.rs @@ -0,0 +1,220 @@ +use crate::state::expanduser::expanduser; +use crate::state::os_platform::OsPlatform; +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}; + +const DEFAULT_DATA_DIR : &str = "artcraft"; +const ASSETS_SUBDIRECTORY : &str = "assets"; +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 { + path: PathBuf, + assets_dir: AppAssetsDir, + weights_dir: AppWeightsDir, + temp_dir: TemporaryDir, + log_file_name: PathBuf, + log_file_name_string: String, +} + +#[derive(Clone)] +pub struct AppAssetsDir { + path: PathBuf, +} + +#[derive(Clone)] +pub struct AppWeightsDir { + path: PathBuf, +} + +#[derive(Clone)] +pub struct TemporaryDir { + path: PathBuf, +} + +impl AppDataRoot { + pub fn create_default() -> anyhow::Result { + let directory = get_default_data_dir()?; + Self::create_existing(directory) + } + + pub fn create_existing>(dir: P) -> anyhow::Result { + let mut dir = dir.as_ref().to_path_buf(); + + match OsPlatform::get() { + OsPlatform::Linux | OsPlatform::MacOs => { + if let Some(d) = dir.as_os_str().to_str() { + dir = expanduser(d)?; + } + }, + OsPlatform::Windows => {} + } + + if !dir.is_dir() { + println!("Creating directory {:?}", dir); + std::fs::create_dir_all(&dir)?; + } + + match dir.canonicalize() { + Ok(d) => dir = d, + Err(err) => { + println!("Error canonicalizing {:?}: {}", dir, err); + } + } + + let assets_dir = AppAssetsDir::create_existing(dir.join(ASSETS_SUBDIRECTORY))?; + let weights_dir = AppWeightsDir::create_existing(dir.join(WEIGHTS_SUBDIRECTORY))?; + let temp_dir = TemporaryDir::create_existing(dir.join(TEMPORARY_SUBDIRECTORY))?; + 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, + }) + } + + pub fn assets_dir(&self) -> &AppAssetsDir { + &self.assets_dir + } + + pub fn weights_dir(&self) -> &AppWeightsDir { + &self.weights_dir + } + + pub fn temp_dir(&self) -> &TemporaryDir { + &self.temp_dir + } + + pub fn path(&self) -> &Path { + &self.path + } + + 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 { + pub fn create_existing>(dir: P) -> anyhow::Result { + let mut dir = dir.as_ref().to_path_buf(); + match dir.canonicalize() { + Ok(d) => dir = d, + Err(err) => { + println!("Error canonicalizing {:?}: {}", dir, err); + } + } + if !dir.exists() { + println!("Creating directory {:?}", dir); + std::fs::create_dir(&dir)?; + } + Ok(Self { + path: dir, + }) + } + + pub fn path(&self) -> &Path { + &self.path + } + + /// Return the current date asset directory + /// If it doesn't exist, create it. + pub fn make_or_get_current_date_dir(&self) -> anyhow::Result { + let format: DateTime = 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 { + pub fn create_existing>(dir: P) -> anyhow::Result { + let mut dir = dir.as_ref().to_path_buf(); + match dir.canonicalize() { + Ok(d) => dir = d, + Err(err) => { + println!("Error canonicalizing {:?}: {}", dir, err); + } + } + if !dir.exists() { + println!("Creating directory {:?}", dir); + std::fs::create_dir(&dir)?; + } + Ok(Self { + path: dir, + }) + } + + pub fn path(&self) -> &Path { + &self.path + } + + pub fn weight_path(&self, descriptor: &WeightDescriptor) -> PathBuf { + self.path.join(descriptor.filename) + } +} + +impl TemporaryDir { + pub fn create_existing>(dir: P) -> anyhow::Result { + let mut dir = dir.as_ref().to_path_buf(); + match dir.canonicalize() { + Ok(d) => dir = d, + Err(err) => { + println!("Error canonicalizing {:?}: {}", dir, err); + } + } + if !dir.exists() { + println!("Creating directory {:?}", dir); + std::fs::create_dir(&dir)?; + } + Ok(Self { + path: dir, + }) + } + + pub fn with_prefix(&self, prefix: &str) -> anyhow::Result { + let tempdir = TempDir::new_in(&self.path, prefix)?; + Ok(tempdir) + } + + pub fn new_named_temp_file(&self) -> anyhow::Result { + let tempfile = Builder::new() + .prefix("temp_") + .suffix(".bin") + .tempfile_in(&self.path)?; + Ok(tempfile) + } +} + +// eg. /home/bob/artcraft, /Users/bob/artcraft, or C:\Users\Alice\artcraft +fn get_default_data_dir() -> anyhow::Result { + Ok(UserDirs::new() + .ok_or_else(|| anyhow!("could not determine user home directory"))? + .home_dir() + .join(DEFAULT_DATA_DIR)) +} diff --git a/crates/desktop/tauri-realtime/src/state/expanduser.rs b/crates/desktop/tauri-realtime/src/state/expanduser.rs new file mode 100644 index 0000000000..788d3c259b --- /dev/null +++ b/crates/desktop/tauri-realtime/src/state/expanduser.rs @@ -0,0 +1,73 @@ +use once_cell::sync::Lazy; +use std::io; +use std::path::{PathBuf, MAIN_SEPARATOR}; + +static PREFIX: Lazy = Lazy::new(|| format!("~{}", MAIN_SEPARATOR)); + +/// The "expanduser" crate doesn't compile on Windows, so we replace its functionality slightly +pub fn expanduser>(path: P) -> io::Result { + + Ok(match path.as_ref() { + // matches an exact "~" + s if s == "~" => { + home_dir()? + }, + // matches paths that start with `~/` + s if s.starts_with(&*PREFIX) => { + let home = home_dir()?; + home.join(&s[2..]) + }, + // // matches paths that start with `~` but not `~/`, might be a `~username/` path + // s if s.starts_with("~") => { + // let mut parts = s[1..].splitn(2, MAIN_SEPARATOR); + // let user = parts.next() + // .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "malformed path"))?; + // let user = Passwd::from_name(&user) + // .map_err(|_| io::Error::new(io::ErrorKind::Other, "error searching for user"))? + // .ok_or_else(|| io::Error::new(io::ErrorKind::Other, format!("user '{}', does not exist", &user)))?; + // if let Some(ref path) = parts.next() { + // PathBuf::from(user.dir).join(&path) + // } else { + // PathBuf::from(user.dir) + // } + // }, + // nothing to expand, just make a PathBuf + s => PathBuf::from(s) + }) +} + +pub fn home_dir() -> io::Result { + dirs::home_dir() + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "no home directory is set")) +} + +#[cfg(test)] +mod tests { + use crate::state::expanduser::expanduser; + use std::env; + use std::path::PathBuf; + + #[test] + #[serial_test::serial] + fn test_success() { + let old_home = env::var("HOME").expect("no home dir set"); + let new_home = "/home/foo"; + env::set_var("HOME", new_home); + let path = expanduser("~/path/to/directory"); + env::set_var("HOME", old_home); + assert_eq!(path.expect("io error"), PathBuf::from("/home/foo/path/to/directory")); + } + + #[test] + #[serial_test::serial] + fn test_only_tilde() { + let old_home = env::var("HOME").expect("no home dir set"); + let new_home = "/home/foo"; + env::set_var("HOME", new_home); + let pathstr = "~"; + let path = expanduser(pathstr); + env::set_var("HOME", old_home); + assert_eq!(path.expect("io error"), PathBuf::from("/home/foo")); + } + +} \ No newline at end of file diff --git a/crates/desktop/tauri-realtime/src/state/mod.rs b/crates/desktop/tauri-realtime/src/state/mod.rs new file mode 100644 index 0000000000..6038017250 --- /dev/null +++ b/crates/desktop/tauri-realtime/src/state/mod.rs @@ -0,0 +1,5 @@ +pub mod app_config; +pub mod app_dir; +pub mod os_platform; +pub mod yaml_config; +pub mod expanduser; \ No newline at end of file diff --git a/crates/desktop/tauri-realtime/src/state/os_platform.rs b/crates/desktop/tauri-realtime/src/state/os_platform.rs new file mode 100644 index 0000000000..7a4316652c --- /dev/null +++ b/crates/desktop/tauri-realtime/src/state/os_platform.rs @@ -0,0 +1,20 @@ + +#[derive(Copy,Clone,Eq,PartialEq,Debug)] +pub enum OsPlatform { + Linux, + MacOs, + Windows, +} + +impl OsPlatform { + pub fn get() -> OsPlatform { + // Totally scientific. + // https://doc.rust-lang.org/std/env/consts/constant.OS.html + match std::env::consts::OS { + "linux" => OsPlatform::Linux, + "windows" => OsPlatform::Windows, + "macos" | "apple" => OsPlatform::MacOs, + _ => OsPlatform::Linux, + } + } +} diff --git a/crates/desktop/tauri-realtime/src/state/yaml_config.rs b/crates/desktop/tauri-realtime/src/state/yaml_config.rs new file mode 100644 index 0000000000..6a63db99ea --- /dev/null +++ b/crates/desktop/tauri-realtime/src/state/yaml_config.rs @@ -0,0 +1,52 @@ +use filesys::recursively_find_file_by_name::recursively_find_file_by_name; +use std::fs::File; +use std::io::BufReader; +use std::path::Path; + +const CONFIG_FILENAME : &str = "app_config.yaml"; + +/// Configuration read or derived from a YAML config file at startup +/// This is not meant to contain any runtime config values. +/// This may not be exhaustive. +#[derive(serde::Deserialize, serde::Serialize, Clone, Debug, Default)] +pub struct YamlConfig { + pub image_height: Option, + pub image_width: Option, + pub scheduler_steps: Option, + pub scheduler_samples: Option, + pub seed: Option, + pub cfg_scale: Option, + + /// Override the default location to store data on Linux + pub linux_default_data_path: Option, + + /// Override the default location to store data on Windows + pub windows_default_data_path: Option, + + /// Override the default location to store data on Mac + pub mac_default_data_path: Option, +} + +impl YamlConfig { + pub fn read_from_file>(path: P) -> anyhow::Result { + let file = File::open(path)?; + let reader = BufReader::new(file); + let config = serde_yaml::from_reader(reader)?; + Ok(config) + } + + pub fn load_from_config_file_recursive() -> YamlConfig { + let maybe_path = recursively_find_file_by_name(CONFIG_FILENAME, "../../../", 5) + .ok() + .flatten(); + if let Some(path) = maybe_path { + let path = path.canonicalize().unwrap_or(path); + println!("Attempting to load configs from: {:?}", &path); + if let Ok(config) = YamlConfig::read_from_file(path) { + return config; + } + } + println!("Couldn't load config file. Using default configs."); + YamlConfig::default() + } +} diff --git a/crates/desktop/tauri-realtime/src/stubs/mod.rs b/crates/desktop/tauri-realtime/src/stubs/mod.rs new file mode 100644 index 0000000000..62ecaceb95 --- /dev/null +++ b/crates/desktop/tauri-realtime/src/stubs/mod.rs @@ -0,0 +1,3 @@ +pub mod model_cache; +pub mod model_config; +pub mod prompt_cache; \ No newline at end of file diff --git a/crates/desktop/tauri-realtime/src/stubs/model_cache.rs b/crates/desktop/tauri-realtime/src/stubs/model_cache.rs new file mode 100644 index 0000000000..ebd7e163d3 --- /dev/null +++ b/crates/desktop/tauri-realtime/src/stubs/model_cache.rs @@ -0,0 +1,14 @@ +#[cfg(not(target_os = "macos"))] +pub use ml_models::ml::model_cache::ModelCache; + +#[cfg(not(not(target_os = "macos")))] +pub struct ModelCache { + // Intentionally left blank +} + +#[cfg(not(not(target_os = "macos")))] +impl ModelCache { + pub fn new() -> Self { + Self {} + } +} diff --git a/crates/desktop/tauri-realtime/src/stubs/model_config.rs b/crates/desktop/tauri-realtime/src/stubs/model_config.rs new file mode 100644 index 0000000000..d0a6740727 --- /dev/null +++ b/crates/desktop/tauri-realtime/src/stubs/model_config.rs @@ -0,0 +1,14 @@ +#[cfg(not(target_os = "macos"))] +pub use ml_models::ml::model_config::ModelConfig; + +#[cfg(not(not(target_os = "macos")))] +pub struct ModelConfig { + // Intentionally left blank +} + +#[cfg(not(not(target_os = "macos")))] +impl ModelConfig { + pub fn init() -> anyhow::Result { + Ok(Self {}) + } +} diff --git a/crates/desktop/tauri-realtime/src/stubs/prompt_cache.rs b/crates/desktop/tauri-realtime/src/stubs/prompt_cache.rs new file mode 100644 index 0000000000..daf4a4d2db --- /dev/null +++ b/crates/desktop/tauri-realtime/src/stubs/prompt_cache.rs @@ -0,0 +1,15 @@ + +#[cfg(not(target_os = "macos"))] +pub use ml_models::ml::prompt_cache::PromptCache; + +#[cfg(not(not(target_os = "macos")))] +pub struct PromptCache { + // Intentionally left blank +} + +#[cfg(not(not(target_os = "macos")))] +impl PromptCache { + pub fn with_capacity(capacity: usize) -> anyhow::Result { + Ok(Self {}) + } +} diff --git a/crates/desktop/tauri-realtime/src/threads/downloader_thread.rs b/crates/desktop/tauri-realtime/src/threads/downloader_thread.rs new file mode 100644 index 0000000000..66dd60b97b --- /dev/null +++ b/crates/desktop/tauri-realtime/src/threads/downloader_thread.rs @@ -0,0 +1,193 @@ +use crate::events::notification_event::{NotificationEvent, NotificationModelType}; +use crate::state::app_dir::AppDataRoot; +use crate::transfer::download::{download_async, ProgressUpdate}; +use futures::StreamExt; +use futures::stream::FuturesUnordered; +use log::{debug, error, info}; +use ml_weights_registry::weights_registry::weight_descriptor::WeightFunction; +use ml_weights_registry::weights_registry::weights::{CLIP_JSON, DIS_MEDIUM_ONNX, LYKON_DEAMSHAPER_7_TEXT_ENCODER_FP16, LYKON_DEAMSHAPER_7_VAE, SDXL_TURBO_CLIP_TEXT_ENCODER, SDXL_TURBO_CLIP_TEXT_ENCODER_2, SDXL_TURBO_UNET, SDXL_TURBO_VAE, SIMIANLUO_LCM_DREAMSHAPER_V7_UNET}; +use std::collections::VecDeque; +use std::path::PathBuf; +use std::sync::mpsc::Receiver; +use std::sync::{Arc, Mutex}; +use tauri::{AppHandle, Emitter}; +use tempfile::NamedTempFile; +use tokio::task::JoinHandle; +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; +const CHUNK_SIZE : usize = 1024 * 1024; +const PARALLEL_FAILURES : usize = 8; +const MAX_RETRIES : usize = 30; + +struct ModelDownloadStatusChannel { + pub model_name: String, + pub model_type: NotificationModelType, + pub final_file_path: PathBuf, + pub receiver: Receiver, +} + +pub async fn downloader_thread(app_data_root: AppDataRoot, app: AppHandle) -> ! { + + let mut status_check_queue : Arc>> = Arc::new(Mutex::new(Vec::new())); + + { + let status_check_queue2 = status_check_queue.clone(); + let app2 = app.clone(); + + tokio::spawn(download_notify_thread(status_check_queue2, app2)); + } + + let mut download_queue = VecDeque::new(); + + download_queue.push_back(CLIP_JSON); + download_queue.push_back(LYKON_DEAMSHAPER_7_TEXT_ENCODER_FP16); + download_queue.push_back(LYKON_DEAMSHAPER_7_VAE); + download_queue.push_back(SIMIANLUO_LCM_DREAMSHAPER_V7_UNET); + download_queue.push_back(SDXL_TURBO_CLIP_TEXT_ENCODER); // TODO(bt): Why is this still needed? + download_queue.push_back(DIS_MEDIUM_ONNX); + //download_queue.push_back(SDXL_TURBO_CLIP_TEXT_ENCODER_2); // TODO(bt): REMOVE + //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() { + let url = model.r2_download_url.to_string(); + + let filename = app_data_root.weights_dir().weight_path(&model); + + if filename.exists() { + info!("File already exists: {:?}", &filename); + continue; + } + + let headers = None; + + let (tx, rx) = std::sync::mpsc::channel(); + + info!("Download: {:?}", filename); + + let temp_file = match app_data_root.temp_dir().new_named_temp_file() { + Ok(file) => file, + Err(err) => { + error!("Couldn't create a temporary file: {:?}", err); + continue; + } + }; + + let model_type = match model.function { + WeightFunction::TextEncoder => NotificationModelType::TextEncoder, + WeightFunction::TextTokenizer => NotificationModelType::TextTokenizer, + 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 { + model_name: model.name, + model_type, + }); + + if let Err(err) = result { + error!("Could not emit event: {}", err); + } + + let task = tokio::spawn(download_async( + url, + temp_file, + filename.clone(), + MAX_FILES, + CHUNK_SIZE, + PARALLEL_FAILURES, + MAX_RETRIES, + headers, + Some(tx), + )); + + match status_check_queue.lock() { + Err(err) => error!("Could not unlock: {:?}", err), + Ok(mut lock) => { + lock.push(ModelDownloadStatusChannel { + receiver: rx, + final_file_path: filename.clone(), + model_name: model.to_descriptive_name(), + model_type, + }) + } + } + + handles.push(task); + } + + while let Some(result) = handles.next().await { + match result { + Err(err) => error!("Error downloading: {:?}", err), + Ok(_) => {}, + } + } + + loop { + // TODO(bt,2025-03-10): Queue management for new files entering the queue. + tokio::time::sleep(std::time::Duration::from_millis(60_000)).await; + } +} + +pub async fn download_notify_thread(queue: Arc>>, app: AppHandle) -> ! { + loop { + match queue.lock() { + Err(err) => error!("Could not unlock: {:?}", err), + Ok(mut lock) => { + debug!(">>>>> Notifier has {} items", lock.len()); + + let mut reenqueue = Vec::with_capacity(lock.len()); + + for item in (*lock).drain(..) { + if let Ok(update) = item.receiver.try_recv() { + let result = app.emit(NOTIFICATION_CHANNEL_NAME, NotificationEvent::ModelDownloadProgress { + model_name: &item.model_name, + model_type: item.model_type, + currently_downloaded_bytes: update.complete, + total_file_bytes: update.total_length, + }); + + if let Err(err) = result { + error!("Could not emit event: {}", err); + } + } + + if item.final_file_path.exists() { + debug!("No longer sending notifications for {:?}", &item.final_file_path); + + let result = app.emit(NOTIFICATION_CHANNEL_NAME, NotificationEvent::ModelDownloadComplete { + model_name: &item.model_name, + model_type: item.model_type, + }); + + if let Err(err) = result { + error!("Could not emit event: {}", err); + } + } else { + reenqueue.push(item); + } + } + + lock.extend(reenqueue); + } + } + + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + } +} diff --git a/crates/desktop/tauri-realtime/src/threads/mod.rs b/crates/desktop/tauri-realtime/src/threads/mod.rs new file mode 100644 index 0000000000..fb47de2776 --- /dev/null +++ b/crates/desktop/tauri-realtime/src/threads/mod.rs @@ -0,0 +1 @@ +pub mod downloader_thread; \ No newline at end of file diff --git a/crates/desktop/tauri-realtime/src/transfer/download.rs b/crates/desktop/tauri-realtime/src/transfer/download.rs new file mode 100644 index 0000000000..9dd2f1f52e --- /dev/null +++ b/crates/desktop/tauri-realtime/src/transfer/download.rs @@ -0,0 +1,241 @@ +//! Implemented from https://github.com/huggingface/hf_transfer/blob/main/src/lib.rs + +use crate::transfer::error::Error; +use crate::transfer::util::{exponential_backoff, BASE_WAIT_TIME, MAX_WAIT_TIME}; +use futures::stream::FuturesUnordered; +use futures::StreamExt; +use reqwest::header::{ + HeaderMap, HeaderName, HeaderValue, AUTHORIZATION, CONTENT_RANGE, + RANGE, +}; +use reqwest::Url; +use std::collections::HashMap; +use std::fmt::Display; +use std::fs::remove_file; +use std::io::SeekFrom; +use std::path::Path; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::mpsc::Sender; +use std::time::Duration; +use log::info; +use tempfile::NamedTempFile; +use tokio::fs::OpenOptions; +use tokio::io::AsyncWriteExt; +use tokio::io::AsyncSeekExt; +use tokio::sync::Semaphore; +use tokio::time::sleep; + +pub struct ProgressUpdate { + pub complete: usize, + pub total_length: usize, +} + +/// max_files: Number of open file handles, which determines the maximum number of parallel downloads +/// parallel_failures: Number of maximum failures of different chunks in parallel (cannot exceed max_files) +/// max_retries: Number of maximum attempts per chunk. (Retries are exponentially backed off + jitter) +/// +/// The number of threads can be tuned by the environment variable `TOKIO_WORKER_THREADS` as documented in +/// https://docs.rs/tokio/latest/tokio/runtime/struct.Builder.html#method.worker_threads +#[allow(clippy::too_many_arguments)] +pub async fn download_async>( + url: String, + filename: NamedTempFile, + final_filename: P, + max_files: usize, + chunk_size: usize, + parallel_failures: usize, + max_retries: usize, + input_headers: Option>, + maybe_progress_sender: Option>, +) -> Result<(), Error> { + info!(">>> Downloading: {}", filename.as_ref().display()); + + let client = reqwest::Client::builder() + // https://github.com/hyperium/hyper/issues/2136#issuecomment-589488526 + .http2_keep_alive_timeout(Duration::from_secs(15)) + .build()?; + + let mut headers = HeaderMap::new(); + let mut auth_token = None; + if let Some(input_headers) = input_headers { + headers.reserve(input_headers.len()); + for (k, v) in input_headers { + let name: HeaderName = k + .try_into() + .map_err(|err| Error::new_err(format!("Invalid header: {err}")))?; + let value: HeaderValue = AsRef::::as_ref(&v) + .try_into() + .map_err(|err| Error::new_err(format!("Invalid header value: {err}")))?; + if name == AUTHORIZATION { + auth_token = Some(value); + } else { + headers.insert(name, value); + } + } + }; + + let response = if let Some(token) = auth_token.as_ref() { + client.get(&url).header(AUTHORIZATION, token) + } else { + client.get(&url) + } + .headers(headers.clone()) + .header(RANGE, "bytes=0-0") + .send() + .await + .map_err(|err| Error::new_err(format!("Error while downloading: {err}")))? + .error_for_status() + .map_err(|err| Error::new_err(err.to_string()))?; + + // Only call the final redirect URL to avoid overloading the Hub with requests and also + // altering the download count + let redirected_url = response.url(); + if Url::parse(&url) + .map_err(|err| Error::new_err(format!("failed to parse url: {err}")))? + .host() + == redirected_url.host() + { + if let Some(token) = auth_token { + headers.insert(AUTHORIZATION, token); + } + } + + let content_range = response + .headers() + .get(CONTENT_RANGE) + .ok_or(Error::new_err("No content length".to_string()))? + .to_str() + .map_err(|err| Error::new_err(format!("Error while downloading: {err}")))?; + + let size: Vec<&str> = content_range.split('/').collect(); + // Content-Range: bytes 0-0/702517648 + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range + let length: usize = size + .last() + .ok_or(Error::new_err( + "Error while downloading: No size was detected".to_string(), + ))? + .parse() + .map_err(|err| Error::new_err(format!("Error while downloading: {err}")))?; + + + info!("File {:?} is {:?} length", filename.as_ref(), length); + + let mut handles = FuturesUnordered::new(); + let semaphore = Arc::new(Semaphore::new(max_files)); + let parallel_failures_semaphore = Arc::new(Semaphore::new(parallel_failures)); + let max_chunk_complete = Arc::new(AtomicUsize::new(0)); + + for start in (0..length).step_by(chunk_size) { + let url = redirected_url.to_string(); + let filename = filename.as_ref().to_owned(); + let client = client.clone(); + let headers = headers.clone(); + + let stop = std::cmp::min(start + chunk_size - 1, length); + let semaphore = semaphore.clone(); + let parallel_failures_semaphore = parallel_failures_semaphore.clone(); + let maybe_progress = maybe_progress_sender.clone(); + let max_chunk_complete = max_chunk_complete.clone(); + handles.push(tokio::spawn(async move { + let permit = semaphore + .acquire_owned() + .await + .map_err(|err| Error::new_err(format!("Error while downloading: {err}")))?; + let mut chunk = download_chunk(&client, &url, &filename, start, stop, headers.clone()).await; + let mut i = 0; + if parallel_failures > 0 { + while let Err(dlerr) = chunk { + if i >= max_retries { + return Err(Error::new_err(format!( + "Failed after too many retries ({max_retries}): {dlerr}" + ))); + } + let parallel_failure_permit = parallel_failures_semaphore.clone().try_acquire_owned().map_err(|err| { + Error::new_err(format!( + "Failed too many failures in parallel ({parallel_failures}): {dlerr} ({err})" + )) + })?; + + let wait_time = exponential_backoff(BASE_WAIT_TIME, i, MAX_WAIT_TIME); + sleep(Duration::from_millis(wait_time as u64)).await; + + chunk = download_chunk(&client, &url, &filename, start, stop, headers.clone()).await; + i += 1; + drop(parallel_failure_permit); + + if let Some(sender) = maybe_progress.as_ref() { + // NB: Calling fetch_max twice. First call will return the lower value. + let _old = max_chunk_complete.fetch_max(stop, Ordering::SeqCst); + let max_chunk = max_chunk_complete.fetch_max(stop, Ordering::SeqCst); + let progress_update = ProgressUpdate { + complete: max_chunk, + total_length: length, + }; + if let Err(_err) = sender.send(progress_update) { + // Fail silently, not essential to issue progress updates + } + } + } + } + drop(permit); + chunk.map_err(|e| Error::new_err(format!("Downloading error {e}"))).and(Ok(stop - start)) + })); + } + + // Output the chained result + while let Some(result) = handles.next().await { + match result { + Ok(Ok(size)) => { + //if let Some(ref callback) = callback { + // callback.call((size,), None)?; + //} + } + Ok(Err(py_err)) => { + return Err(py_err); + } + Err(err) => { + return Err(Error::new_err(format!( + "Error while downloading: {err}" + ))); + } + } + } + + let file = filename.persist(final_filename) + .map_err(|e| Error::new_err(format!("Could not rename to final filename: {:?}", e)))?; + + file.sync_all()?; + + Ok(()) +} + +async fn download_chunk>( + client: &reqwest::Client, + url: &str, + filename: P, + start: usize, + stop: usize, + headers: HeaderMap, +) -> Result<(), Error> { + // Process each socket concurrently. + let range = format!("bytes={start}-{stop}"); + let mut file = OpenOptions::new() + .write(true) + .truncate(false) + .create(true) + .open(filename) + .await?; + file.seek(SeekFrom::Start(start as u64)).await?; + let response = client + .get(url) + .headers(headers) + .header(RANGE, range) + .send() + .await? + .error_for_status()?; + let content = response.bytes().await?; + file.write_all(&content).await?; + Ok(()) +} diff --git a/crates/desktop/tauri-realtime/src/transfer/error.rs b/crates/desktop/tauri-realtime/src/transfer/error.rs new file mode 100644 index 0000000000..fcad35635c --- /dev/null +++ b/crates/desktop/tauri-realtime/src/transfer/error.rs @@ -0,0 +1,49 @@ +//! Implemented from https://github.com/huggingface/hf_transfer/blob/main/src/lib.rs + +use std::fmt::Display; +use reqwest::header::ToStrError; + +#[derive(Debug)] +pub enum Error { + Io(std::io::Error), + Request(reqwest::Error), + ToStrError(ToStrError), + Misc(String), +} + +impl From for Error { + fn from(value: std::io::Error) -> Self { + Self::Io(value) + } +} + +impl From for Error { + fn from(value: reqwest::Error) -> Self { + Self::Request(value) + } +} + +impl From for Error { + fn from(value: ToStrError) -> Self { + Self::ToStrError(value) + } +} + +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Io(io) => write!(f, "Io: {io}"), + Self::Request(req) => write!(f, "Request: {req}"), + Self::ToStrError(req) => write!(f, "Response non ascii: {req}"), + Self::Misc(err) => write!(f, "Misc error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +impl Error { + pub fn new_err(reason: String) -> Self { + Self::Misc(reason) + } +} diff --git a/crates/desktop/tauri-realtime/src/transfer/mod.rs b/crates/desktop/tauri-realtime/src/transfer/mod.rs new file mode 100644 index 0000000000..cd6facd1b6 --- /dev/null +++ b/crates/desktop/tauri-realtime/src/transfer/mod.rs @@ -0,0 +1,4 @@ +pub mod download; +pub mod upload; +pub mod util; +pub mod error; \ No newline at end of file diff --git a/crates/desktop/tauri-realtime/src/transfer/upload.rs b/crates/desktop/tauri-realtime/src/transfer/upload.rs new file mode 100644 index 0000000000..acd6fd7bb9 --- /dev/null +++ b/crates/desktop/tauri-realtime/src/transfer/upload.rs @@ -0,0 +1,189 @@ +//! Implemented from https://github.com/huggingface/hf_transfer/blob/main/src/lib.rs + +use crate::transfer::error::Error; +use crate::transfer::util::{exponential_backoff, BASE_WAIT_TIME, MAX_WAIT_TIME}; +use futures::stream::FuturesUnordered; +use futures::StreamExt; +use reqwest::header::CONTENT_LENGTH; +use std::collections::HashMap; +use std::fmt::Display; +use std::io::SeekFrom; +use std::sync::Arc; +use std::time::Duration; +use tokio::fs::OpenOptions; +use tokio::io::{AsyncReadExt, AsyncSeekExt}; +use tokio::sync::Semaphore; +use tokio::time::sleep; +use tokio_util::codec::{BytesCodec, FramedRead}; + +/// parts_urls: Dictionary consisting of part numbers as keys and the associated url as values +/// completion_url: The url that should be called when the upload is finished +/// max_files: Number of open file handles, which determines the maximum number of parallel uploads +/// parallel_failures: Number of maximum failures of different chunks in parallel (cannot exceed max_files) +/// max_retries: Number of maximum attempts per chunk. (Retries are exponentially backed off + jitter) +/// +/// The number of threads can be tuned by the environment variable `TOKIO_WORKER_THREADS` as documented in +/// https://docs.rs/tokio/latest/tokio/runtime/struct.Builder.html#method.worker_threads +/// +/// See https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html for more information +/// on the multipart upload +#[allow(clippy::too_many_arguments)] +fn multipart_upload( + file_path: String, + parts_urls: Vec, + chunk_size: u64, + max_files: usize, + parallel_failures: usize, + max_retries: usize, +) -> Result>, Error> { + if parallel_failures > max_files { + return Err(Error::new_err( + "Error parallel_failures cannot be > max_files".to_string(), + )); + } + if (parallel_failures == 0) != (max_retries == 0) { + return Err(Error::new_err( + "For retry mechanism you need to set both `parallel_failures` and `max_retries`" + .to_string(), + )); + } + + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build()? + .block_on(async { + upload_async( + file_path, + parts_urls, + chunk_size, + max_files, + parallel_failures, + max_retries, + ) + .await + }) +} + + + +#[allow(clippy::too_many_arguments)] +async fn upload_async( + file_path: String, + parts_urls: Vec, + chunk_size: u64, + max_files: usize, + parallel_failures: usize, + max_retries: usize, +) -> Result>, Error> { + let client = reqwest::Client::new(); + + let mut handles = FuturesUnordered::new(); + let semaphore = Arc::new(Semaphore::new(max_files)); + let parallel_failures_semaphore = Arc::new(Semaphore::new(parallel_failures)); + + for (part_number, part_url) in parts_urls.iter().enumerate() { + let url = part_url.to_string(); + let path = file_path.to_owned(); + let client = client.clone(); + + let start = (part_number as u64) * chunk_size; + let semaphore = semaphore.clone(); + let parallel_failures_semaphore = parallel_failures_semaphore.clone(); + handles.push(tokio::spawn(async move { + let permit = semaphore + .clone() + .acquire_owned() + .await + .map_err(|err| Error::new_err(format!("Error acquiring semaphore: {err}")))?; + let mut chunk = upload_chunk(&client, &url, &path, start, chunk_size).await; + let mut i = 0; + if parallel_failures > 0 { + while let Err(ul_err) = chunk { + if i >= max_retries { + return Err(Error::new_err(format!( + "Failed after too many retries ({max_retries}): {ul_err}" + ))); + } + + let parallel_failure_permit = parallel_failures_semaphore.clone().try_acquire_owned().map_err(|err| { + Error::new_err(format!( + "Failed too many failures in parallel ({parallel_failures}): {ul_err} ({err})" + )) + })?; + + let wait_time = exponential_backoff(BASE_WAIT_TIME, i, MAX_WAIT_TIME); + sleep(Duration::from_millis(wait_time as u64)).await; + + chunk = upload_chunk(&client, &url, &path, start, chunk_size).await; + i += 1; + drop(parallel_failure_permit); + } + } + drop(permit); + chunk.map_err(|e|{ + //match e { + // Error::Io(io) => Error::new_err(format!("Io error {io}")), + // Error::Request(req) => Error::new_err(format!("Error while sending chunk {req}")), + // Error::ToStrError(req) => Error::new_err(format!("Response header contains non ASCII chars: {req}")), + //} + e + }).map(|chunk| (part_number, chunk, chunk_size)) + })); + } + + let mut results: Vec> = vec![HashMap::default(); parts_urls.len()]; + + while let Some(result) = handles.next().await { + match result { + Ok(Ok((part_number, headers, size))) => { + //if let Some(ref callback) = callback { + // callback.call((size,), None)?; + //} + results[part_number] = headers; + } + Ok(Err(py_err)) => { + return Err(py_err); + } + Err(err) => { + return Err(Error::new_err(format!( + "Error occurred while uploading: {err}" + ))); + } + } + } + + Ok(results) +} + +async fn upload_chunk( + client: &reqwest::Client, + url: &str, + path: &str, + start: u64, + chunk_size: u64, +) -> Result, Error> { + let mut options = OpenOptions::new(); + let mut file = options.read(true).open(path).await?; + let file_size = file.metadata().await?.len(); + let bytes_transferred = std::cmp::min(file_size - start, chunk_size); + + file.seek(SeekFrom::Start(start)).await?; + let chunk = file.take(chunk_size); + + let response = client + .put(url) + .header(CONTENT_LENGTH, bytes_transferred) + .body(reqwest::Body::wrap_stream(FramedRead::new( + chunk, + BytesCodec::new(), + ))) + .send() + .await?; + let response = response.error_for_status()?; + let mut headers = HashMap::new(); + for (name, value) in response.headers().into_iter() { + headers.insert(name.to_string(), value.to_str()?.to_owned()); + } + Ok(headers) +} + diff --git a/crates/desktop/tauri-realtime/src/transfer/util.rs b/crates/desktop/tauri-realtime/src/transfer/util.rs new file mode 100644 index 0000000000..70ee4ce908 --- /dev/null +++ b/crates/desktop/tauri-realtime/src/transfer/util.rs @@ -0,0 +1,15 @@ +//! Implemented from https://github.com/huggingface/hf_transfer/blob/main/src/lib.rs + +use rand::{thread_rng, Rng}; +use std::fmt::Display; + +pub const BASE_WAIT_TIME: usize = 300; +pub const MAX_WAIT_TIME: usize = 10_000; + +fn jitter() -> usize { + thread_rng().gen_range(0..=500) +} + +pub fn exponential_backoff(base_wait_time: usize, n: usize, max: usize) -> usize { + (base_wait_time + n.pow(2) + jitter()).min(max) +} diff --git a/crates/desktop/tauri-realtime/src/utils/image/decode_base64_image.rs b/crates/desktop/tauri-realtime/src/utils/image/decode_base64_image.rs new file mode 100644 index 0000000000..d8ebb2b148 --- /dev/null +++ b/crates/desktop/tauri-realtime/src/utils/image/decode_base64_image.rs @@ -0,0 +1,14 @@ +use base64::prelude::BASE64_STANDARD; +use base64::Engine; +use image::{DynamicImage, ImageReader}; +use std::io::Cursor; + +pub fn decode_base64_image(base64_image: &str) -> anyhow::Result { + let bytes = BASE64_STANDARD.decode(base64_image)?; + + let image = ImageReader::new(Cursor::new(bytes)) + .with_guessed_format()? + .decode()?; + + Ok(image) +} diff --git a/crates/desktop/tauri-realtime/src/utils/image/encode_dynamic_image_base64_png.rs b/crates/desktop/tauri-realtime/src/utils/image/encode_dynamic_image_base64_png.rs new file mode 100644 index 0000000000..cb68e228aa --- /dev/null +++ b/crates/desktop/tauri-realtime/src/utils/image/encode_dynamic_image_base64_png.rs @@ -0,0 +1,14 @@ +use base64::prelude::BASE64_STANDARD; +use base64::Engine; +use image::{DynamicImage, ImageFormat}; +use std::io::Cursor; + +pub fn encode_dynamic_image_base64_png(image: DynamicImage) -> anyhow::Result { + let mut bytes = Vec::with_capacity(1024*1024); + + image.write_to(&mut Cursor::new(&mut bytes), ImageFormat::Png)?; + + let bytes = BASE64_STANDARD.encode(bytes); + + Ok(bytes) +} diff --git a/crates/desktop/tauri-realtime/src/utils/image/encode_rgb_image_base64_png.rs b/crates/desktop/tauri-realtime/src/utils/image/encode_rgb_image_base64_png.rs new file mode 100644 index 0000000000..fcd248eb2f --- /dev/null +++ b/crates/desktop/tauri-realtime/src/utils/image/encode_rgb_image_base64_png.rs @@ -0,0 +1,16 @@ +use base64::prelude::BASE64_STANDARD; +use base64::Engine; +use image::{ImageFormat, RgbImage}; +use std::io::Cursor; + +pub fn encode_rgb_image_base64_png( + image: RgbImage +) -> anyhow::Result { + let mut bytes = Vec::with_capacity(1024*1024); + + image.write_to(&mut Cursor::new(&mut bytes), ImageFormat::Png)?; + + let bytes = BASE64_STANDARD.encode(bytes); + + Ok(bytes) +} diff --git a/crates/desktop/tauri-realtime/src/utils/image/mod.rs b/crates/desktop/tauri-realtime/src/utils/image/mod.rs new file mode 100644 index 0000000000..3b7d7a6b76 --- /dev/null +++ b/crates/desktop/tauri-realtime/src/utils/image/mod.rs @@ -0,0 +1,3 @@ +pub mod decode_base64_image; +pub mod encode_dynamic_image_base64_png; +pub mod encode_rgb_image_base64_png; \ No newline at end of file diff --git a/crates/desktop/tauri-realtime/src/utils/log_environment_details.rs b/crates/desktop/tauri-realtime/src/utils/log_environment_details.rs new file mode 100644 index 0000000000..755894daa5 --- /dev/null +++ b/crates/desktop/tauri-realtime/src/utils/log_environment_details.rs @@ -0,0 +1,11 @@ +use log::info; +//use nvidia_checker::{get_cuda_version, get_cudnn_version, get_kernel_version, get_nvidia_driver_version, get_os_version, get_tensorrt_version}; + +pub fn log_environment_details() { + //info!("NVIDIA driver version: {}", get_nvidia_driver_version(false)); + //info!("CUDA version: {}", get_cuda_version(false)); + //info!("cuDNN version: {}", get_cudnn_version(false)); + //info!("TensorRT version: {}", get_tensorrt_version(false)); + //info!("OS version: {}", get_os_version(false)); + //info!("Kernel version: {}", get_kernel_version(false)); +} diff --git a/crates/desktop/tauri-realtime/src/utils/mod.rs b/crates/desktop/tauri-realtime/src/utils/mod.rs new file mode 100644 index 0000000000..3e475ef135 --- /dev/null +++ b/crates/desktop/tauri-realtime/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod image; +pub mod log_environment_details; \ No newline at end of file diff --git a/crates/desktop/tauri-realtime/tauri.conf.json b/crates/desktop/tauri-realtime/tauri.conf.json new file mode 100644 index 0000000000..f408df7602 --- /dev/null +++ b/crates/desktop/tauri-realtime/tauri.conf.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://schema.tauri.app/config/2.0.0-rc", + "productName": "ArtCraft", + "version": "0.1.0", + "identifier": "com.unique.value.thing.why", + "build": { + "frontendDist": "../../../frontend/realtime/dist", + "devUrl": "http://localhost:5741", + "beforeDevCommand": "npm run dev", + "beforeBuildCommand": "npm run build2" + }, + "app": { + "windows": [ + { + "title": "ArtCraft", + "width": 2400, + "height": 1300, + "resizable": true, + "fullscreen": false + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ] + } +} diff --git a/crates/desktop/tauri-realtime/tests/test_dynamic_library_loading.rs b/crates/desktop/tauri-realtime/tests/test_dynamic_library_loading.rs new file mode 100644 index 0000000000..e1b34faace --- /dev/null +++ b/crates/desktop/tauri-realtime/tests/test_dynamic_library_loading.rs @@ -0,0 +1,7 @@ + +#[test] +fn test_dynamic_library_loading() { + let name = libloading::library_filename("cublas"); + + assert_eq!(name, "libcublas.so"); +} \ No newline at end of file diff --git a/frontend/realtime/package-lock.json b/frontend/realtime/package-lock.json index c574a6953d..41a1bed472 100644 --- a/frontend/realtime/package-lock.json +++ b/frontend/realtime/package-lock.json @@ -4544,8 +4544,6 @@ }, "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "inBundle": true, "license": "MIT", @@ -5286,8 +5284,6 @@ }, "node_modules/npm/node_modules/cross-spawn/node_modules/which": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "inBundle": true, "license": "ISC", @@ -5336,8 +5332,6 @@ }, "node_modules/npm/node_modules/debug/node_modules/ms": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true, "inBundle": true, "license": "MIT" @@ -7164,8 +7158,6 @@ }, "node_modules/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "inBundle": true, "license": "MIT", @@ -7203,8 +7195,6 @@ }, "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "inBundle": true, "license": "MIT", diff --git a/frontend/realtime/yarn.lock b/frontend/realtime/yarn.lock index 6fb4000b26..2b15c1fdc7 100644 --- a/frontend/realtime/yarn.lock +++ b/frontend/realtime/yarn.lock @@ -744,15 +744,10 @@ resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.19.1.tgz" integrity sha512-S45oynt/WH19bHbIXjtli6QmwNYvaz+vtnubvNpNDvUOoA/OWh6j1OikIP3G+v5GHdxyC6EXoChG3HgYGEUfcg== -"@rollup/rollup-linux-x64-gnu@4.24.0": +"@rollup/rollup-darwin-arm64@4.24.0": version "4.24.0" - resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz" - integrity sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A== - -"@rollup/rollup-linux-x64-musl@4.24.0": - version "4.24.0" - resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz" - integrity sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ== + resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz" + integrity sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA== "@sigstore/bundle@^2.3.2": version "2.3.2" @@ -824,15 +819,10 @@ resolved "https://registry.npmjs.org/@tauri-apps/api/-/api-2.2.0.tgz" integrity sha512-R8epOeZl1eJEl603aUMIGb4RXlhPjpgxbGVEaqY+0G5JG9vzV/clNlzTeqc+NLYXVqXcn8mb4c5b9pJIUDEyAg== -"@tauri-apps/cli-linux-x64-gnu@2.4.0": +"@tauri-apps/cli-darwin-arm64@2.4.0": version "2.4.0" - resolved "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.4.0.tgz" - integrity sha512-ebRmV2HLIVms1KlNNueQCp3OrXBv6cimU3mYEh5NbZ8dH88f2sF46dFCyPq8Qr/Zti4qPEaAArVG7RYFjfECPw== - -"@tauri-apps/cli-linux-x64-musl@2.4.0": - version "2.4.0" - resolved "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.0.tgz" - integrity sha512-FOp2cBFyq5LnUr3he95Z99PQm3nCSJv2GZNeH7UqmUpeHwdcYuhBERU7C+8VDJJPR98Q5fkcoV00Pc4nw0v5KQ== + resolved "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.4.0.tgz" + integrity sha512-MVzYrahJBgDyzUJ2gNEU8H+0oCVEucN115+CVorFnidHcJ6DtDRMCaKLkpjOZNfJyag1WQ25fu7imvZSe0mz/g== "@tauri-apps/cli@^2.4.0": version "2.4.0" @@ -2096,6 +2086,11 @@ fs-minipass@^3.0.0, fs-minipass@^3.0.3: dependencies: minipass "^7.0.3" +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + function-bind@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"