Compare commits

...
38 changed files with 1160 additions and 104 deletions
Generated
+157 -89
View File
@@ -463,7 +463,7 @@ dependencies = [
"getrandom 0.2.14",
"once_cell",
"version_check",
"zerocopy 0.7.33",
"zerocopy",
]
[[package]]
@@ -536,7 +536,7 @@ dependencies = [
"easyenv",
"enums",
"errors",
"futures 0.3.29",
"futures 0.3.31",
"hostname 0.4.0",
"log",
"mysql_queries",
@@ -646,9 +646,9 @@ checksum = "70033777eb8b5124a81a1889416543dddef2de240019b674c81285a2635a7e1e"
[[package]]
name = "anyhow"
version = "1.0.97"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
dependencies = [
"backtrace",
]
@@ -817,18 +817,20 @@ dependencies = [
"dirs 6.0.0",
"discord-rich-presence",
"errors",
"fal_client",
"filesys",
"futures 0.3.29",
"futures 0.3.31",
"image",
"indexmap 2.9.0",
"libloading 0.8.6",
"log",
"memory_caching",
"memory_store",
"mimetypes",
"once_cell",
"openai_sora_client",
"primitives",
"rand 0.9.0",
"rand 0.9.1",
"reqwest 0.12.15",
"serde",
"serde_derive",
@@ -1063,7 +1065,7 @@ dependencies = [
"serde_path_to_error",
"serde_qs 0.10.1",
"sha2 0.10.8",
"smart-default",
"smart-default 0.6.0",
"smol_str",
"thiserror 1.0.63",
"tokio",
@@ -1374,7 +1376,7 @@ dependencies = [
"serde_json",
"sqlx",
"tokio",
"toml 0.8.20",
"toml 0.8.22",
"url_config",
"user_traits_component",
"utoipa",
@@ -2052,7 +2054,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02260d489095346e5cafd04dea8e8cb54d1d74fcd759022a9b72986ebe9a1257"
dependencies = [
"serde",
"toml 0.8.20",
"toml 0.8.22",
]
[[package]]
@@ -2155,9 +2157,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.40"
version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
dependencies = [
"android-tzdata",
"iana-time-zone",
@@ -3105,7 +3107,7 @@ dependencies = [
"easyenv",
"enums",
"errors",
"futures 0.3.29",
"futures 0.3.31",
"log",
"mysql_queries",
"serde",
@@ -3936,7 +3938,7 @@ dependencies = [
"cc",
"memchr",
"rustc_version 0.4.0",
"toml 0.8.20",
"toml 0.8.22",
"vswhom",
"winreg 0.52.0",
]
@@ -4024,7 +4026,7 @@ dependencies = [
"sqlx-core",
"sqlx-mysql",
"strum",
"toml 0.8.20",
"toml 0.8.22",
"utoipa",
"workspace-build-acceleration",
]
@@ -4038,7 +4040,7 @@ dependencies = [
"serde_derive",
"serde_json",
"strum",
"toml 0.8.20",
"toml 0.8.22",
"utoipa",
"workspace-build-acceleration",
]
@@ -4114,7 +4116,7 @@ dependencies = [
"elasticsearch_schema",
"enums",
"errors",
"futures 0.3.29",
"futures 0.3.31",
"hostname 0.4.0",
"log",
"mysql_queries",
@@ -4180,6 +4182,17 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "eventsource-stream"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab"
dependencies = [
"futures-core",
"nom",
"pin-project-lite",
]
[[package]]
name = "exr"
version = "1.73.0"
@@ -4201,6 +4214,51 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
[[package]]
name = "fal"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd7dd8565317310f05569296166e2cbdd2bc870173b2b24b77ed681166268d0"
dependencies = [
"base64 0.22.1",
"eventsource-stream",
"fal-derive",
"futures 0.3.31",
"image",
"reqwest 0.12.15",
"serde",
"serde_json",
"smart-default 0.7.1",
"thiserror 1.0.63",
]
[[package]]
name = "fal-derive"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f377f2f18a6bf107f907aa687df6bda2874bd38b25b1531f2a6fb38a9ca643"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "fal_client"
version = "0.0.1"
dependencies = [
"anyhow",
"errors",
"fal",
"file_to_base64_url",
"filesys",
"futures 0.3.31",
"testing",
"tokio",
"url",
"workspace-build-acceleration",
]
[[package]]
name = "fancy-regex"
version = "0.13.0"
@@ -4281,6 +4339,19 @@ dependencies = [
"rustc_version 0.4.0",
]
[[package]]
name = "file_to_base64_url"
version = "0.0.1"
dependencies = [
"anyhow",
"base64 0.22.1",
"errors",
"filesys",
"mimetypes",
"testing",
"workspace-build-acceleration",
]
[[package]]
name = "filesys"
version = "0.0.1"
@@ -4456,9 +4527,9 @@ checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678"
[[package]]
name = "futures"
version = "0.3.29"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
@@ -4471,9 +4542,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
@@ -4481,15 +4552,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
@@ -4510,9 +4581,9 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-lite"
@@ -4544,9 +4615,9 @@ dependencies = [
[[package]]
name = "futures-macro"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
@@ -4555,21 +4626,21 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
@@ -5361,7 +5432,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112fa2f6ad4ab815b9e1b938b4b1e437032d055e2f92ed10fd6ab2e62d02c6b6"
dependencies = [
"dirs 5.0.1",
"futures 0.3.29",
"futures 0.3.31",
"http 1.1.0",
"indicatif",
"log",
@@ -7147,7 +7218,7 @@ dependencies = [
"sqlx",
"tokens",
"tokio",
"toml 0.8.20",
"toml 0.8.22",
"workspace-build-acceleration",
]
@@ -7320,7 +7391,7 @@ dependencies = [
"directories 6.0.0",
"dirs 6.0.0",
"filesys",
"futures 0.3.29",
"futures 0.3.31",
"half",
"hf-hub 0.3.2",
"image",
@@ -7834,7 +7905,7 @@ dependencies = [
"log",
"serde",
"serde_derive",
"toml 0.8.20",
"toml 0.8.22",
]
[[package]]
@@ -8178,7 +8249,7 @@ dependencies = [
"sha2 0.10.8",
"tar",
"thiserror 1.0.63",
"toml 0.8.20",
"toml 0.8.22",
"ureq",
"url",
"uuid 1.8.0",
@@ -8240,7 +8311,7 @@ dependencies = [
"idempotency",
"log",
"once_cell",
"rand 0.9.0",
"rand 0.9.1",
"reqwest 0.12.15",
"serde",
"serde_derive",
@@ -8250,7 +8321,7 @@ dependencies = [
"testing",
"thiserror 2.0.12",
"tokio",
"toml 0.8.20",
"toml 0.8.22",
"workspace-build-acceleration",
]
@@ -9555,13 +9626,12 @@ dependencies = [
[[package]]
name = "rand"
version = "0.9.0"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
"zerocopy 0.8.24",
]
[[package]]
@@ -9791,7 +9861,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6435842fc2fea44b528719eb8c32203bbc1bb2f5b619fbe0c0a3d8350fd8d2a8"
dependencies = [
"bytes 1.10.0",
"futures 0.3.29",
"futures 0.3.31",
"pin-project-lite",
]
@@ -10095,7 +10165,7 @@ dependencies = [
"serde",
"serde_derive",
"sqlx",
"toml 0.8.20",
"toml 0.8.22",
"workspace-build-acceleration",
]
@@ -10284,7 +10354,7 @@ dependencies = [
"base64 0.22.1",
"bytes 1.10.0",
"cfg-if 1.0.0",
"futures 0.3.29",
"futures 0.3.31",
"hex",
"hmac 0.12.1",
"http 1.1.0",
@@ -10977,7 +11047,7 @@ version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9"
dependencies = [
"futures 0.3.29",
"futures 0.3.31",
"log",
"once_cell",
"parking_lot 0.12.2",
@@ -11243,6 +11313,17 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "smart-default"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "smol_str"
version = "0.1.24"
@@ -11670,7 +11751,7 @@ dependencies = [
"errors",
"filesys",
"futures 0.1.31",
"futures 0.3.29",
"futures 0.3.31",
"futures-core",
"futures-util",
"google_sign_in",
@@ -11702,7 +11783,7 @@ dependencies = [
"prompt_engineering",
"pulldown-cmark 0.11.0",
"r2d2_redis",
"rand 0.9.0",
"rand 0.9.1",
"redis_caching",
"redis_common",
"redis_schema",
@@ -11728,7 +11809,7 @@ dependencies = [
"time",
"tokens",
"tokio",
"toml 0.8.20",
"toml 0.8.22",
"tts_common",
"url",
"url_config",
@@ -12393,7 +12474,7 @@ dependencies = [
"cfg-expr",
"heck 0.5.0",
"pkg-config",
"toml 0.8.20",
"toml 0.8.22",
"version-compare",
]
@@ -12538,7 +12619,7 @@ dependencies = [
"serde_json",
"tauri-utils",
"tauri-winres",
"toml 0.8.20",
"toml 0.8.22",
"walkdir",
]
@@ -12596,7 +12677,7 @@ dependencies = [
"serde",
"serde_json",
"tauri-utils",
"toml 0.8.20",
"toml 0.8.22",
"walkdir",
]
@@ -12636,7 +12717,7 @@ dependencies = [
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.12",
"toml 0.8.20",
"toml 0.8.22",
"url",
"uuid 1.8.0",
]
@@ -12783,7 +12864,7 @@ dependencies = [
"serde_with 3.12.0",
"swift-rs",
"thiserror 2.0.12",
"toml 0.8.20",
"toml 0.8.22",
"url",
"urlpattern",
"uuid 1.8.0",
@@ -12797,7 +12878,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56eaa45f707bedf34d19312c26d350bc0f3c59a47e58e8adbeecdc850d2c13a0"
dependencies = [
"embed-resource",
"toml 0.8.20",
"toml 0.8.22",
]
[[package]]
@@ -13123,16 +13204,16 @@ dependencies = [
"sqlx-core",
"sqlx-mysql",
"strum",
"toml 0.8.20",
"toml 0.8.22",
"utoipa",
"workspace-build-acceleration",
]
[[package]]
name = "tokio"
version = "1.43.0"
version = "1.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
dependencies = [
"backtrace",
"bytes 1.10.0",
@@ -13332,21 +13413,21 @@ dependencies = [
[[package]]
name = "toml"
version = "0.8.20"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit 0.22.24",
"toml_edit 0.22.26",
]
[[package]]
name = "toml_datetime"
version = "0.6.8"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
dependencies = [
"serde",
]
@@ -13388,17 +13469,24 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.22.24"
version = "0.22.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
dependencies = [
"indexmap 2.9.0",
"serde",
"serde_spanned",
"toml_datetime",
"toml_write",
"winnow 0.7.6",
]
[[package]]
name = "toml_write"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
[[package]]
name = "tonic"
version = "0.11.0"
@@ -15186,7 +15274,7 @@ dependencies = [
"digest 0.10.7",
"either",
"flate2",
"futures 0.3.29",
"futures 0.3.31",
"futures-channel",
"futures-core",
"futures-executor",
@@ -15504,16 +15592,7 @@ version = "0.7.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "087eca3c1eaf8c47b94d02790dd086cd594b912d2043d4de4bfdd466b3befb7c"
dependencies = [
"zerocopy-derive 0.7.33",
]
[[package]]
name = "zerocopy"
version = "0.8.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879"
dependencies = [
"zerocopy-derive 0.8.24",
"zerocopy-derive",
]
[[package]]
@@ -15527,17 +15606,6 @@ dependencies = [
"syn 2.0.87",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "zerofrom"
version = "0.1.5"
+9 -4
View File
@@ -30,6 +30,7 @@ members = [
"crates/lib/actix_cors_configs",
"crates/lib/actix_helpers",
"crates/lib/api_clients/cloud_storage",
"crates/lib/api_clients/fal_client",
"crates/lib/api_clients/google_drive_common",
"crates/lib/api_clients/google_sign_in",
"crates/lib/api_clients/openai_sora_client",
@@ -54,6 +55,7 @@ members = [
"crates/lib/email_sender",
"crates/lib/errors",
"crates/lib/files/archives",
"crates/lib/files/file_to_base64_url",
"crates/lib/files/filesys",
"crates/lib/files/hashing",
"crates/lib/files/images",
@@ -157,15 +159,16 @@ workspace-build-acceleration = { path = "crates/workspace-build-acceleration" }
# External libs
# Centrally pinned versions so that we can uniformly upgrade/downgrade versions across the workspace
# If a crate needs to use an older/newer version, the dependency should be managed in that crate's own Cargo.toml
anyhow = "1.0.97"
anyhow = "1.0.98"
base64 = "0.22.1"
chrono = { version = "0.4.40", features = ["serde"] }
chrono = { version = "0.4.41", features = ["serde"] }
cookie = "0.18.1"
futures = { version = "0.3.31" }
hex-literal = "1.0.0"
indexmap = "2.9.0"
log = "0.4.27"
once_cell = "1.21.3"
rand = "0.9.0"
rand = "0.9.1"
regex = "1.11.1"
reqwest = { version = "0.12.15", features = ["json", "cookies"] }
serde = { version = "1.0.219", features = ["derive"] }
@@ -175,7 +178,8 @@ serde_yaml = "0.9.1"
sha3 = "0.10.8"
tauri = { version = "2.4.1", features = ["unstable", "devtools"] } # NB: "devtools" prevents App Store distribution
thiserror = "2.0.12"
toml = "0.8.20"
tokio = { version = "1.45.0", features = ["macros"] }
toml = "0.8.22"
url = { version = "2.5.4", features = ["serde"] }
# Internal libs
@@ -183,6 +187,7 @@ crockford = { path = "crates/lib/data/crockford" }
enums = { path = "crates/_types/enums" }
enums_public = { path = "crates/_types/enums_public" }
errors = { path = "crates/lib/errors" }
fal_client = { path = "crates/lib/api_clients/fal_client" }
filesys = { path = "crates/lib/files/filesys" }
idempotency = { path = "crates/lib/idempotency" }
images = { path = "crates/lib/files/images" }
+2
View File
@@ -25,9 +25,11 @@ workspace-build-acceleration = { workspace = true }
# Internal
crockford = { workspace = true }
errors = { workspace = true }
fal_client = { workspace = true }
filesys = { workspace = true }
memory_caching = { workspace = true }
memory_store = { workspace = true }
mimetypes = { workspace = true }
openai_sora_client = { workspace = true }
primitives = { workspace = true }
storyteller_client = { workspace = true }
@@ -73,7 +73,7 @@ impl CommandSuccessResponseWrapper<()> {
/// `Serialize`. Rust's `"specialization"`, which would let us account for this, is an unstable, nightly-only
/// feature. So instead we manually mark all types other than `()` with `SerializeMarker` as a hack to
/// distinguish the two.
trait SerializeMarker : Serialize {}
pub trait SerializeMarker : Serialize {}
// NB: () is Serialize! So we can't broadly implement just `T: Serialize`.
impl From<()> for CommandSuccessResponseWrapper<()> {
@@ -0,0 +1,296 @@
use crate::commands::command_response_wrapper::{CommandErrorResponseWrapper, CommandErrorStatus, CommandResult, SerializeMarker};
use crate::events::sendable_event_trait::SendableEvent;
use crate::events::sora::sora_image_enqueue_failure_event::SoraImageEnqueueFailureEvent;
use crate::events::sora::sora_image_enqueue_success_event::SoraImageEnqueueSuccessEvent;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use crate::state::fal::fal_credential_manager::FalCredentialManager;
use crate::state::sora::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
use crate::state::sora::sora_task_queue::SoraTaskQueue;
use crate::state::storyteller::storyteller_credential_manager::StorytellerCredentialManager;
use crate::utils::get_url_file_extension::get_url_file_extension;
use crate::utils::simple_http_download::simple_http_download;
use crate::utils::simple_http_download_to_tempfile::simple_http_download_to_tempfile;
use anyhow::anyhow;
use base64::prelude::BASE64_STANDARD;
use base64::{DecodeError, Engine};
use errors::{AnyhowError, AnyhowResult};
use fal_client::fal_error_plus::FalErrorPlus;
use fal_client::requests::remove_background_rembg::remove_background_rembg;
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::{DynamicImage, EncodableLayout, ImageReader};
use log::{debug, error, info, warn};
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
use openai_sora_client::credentials::SoraCredentials;
use openai_sora_client::creds::credential_migration::CredentialMigrationRef;
use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_remix_with_session_auto_renew, ImageRemixAutoRenewRequest};
use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest};
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
use openai_sora_client::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
use openai_sora_client::requests::upload::upload_media_from_bytes::sora_media_upload_from_bytes;
use openai_sora_client::requests::upload::upload_media_from_file::sora_media_upload_from_file;
use openai_sora_client::sora_error::SoraError;
use reqwest::Url;
use serde_derive::{Deserialize, Serialize};
use std::fs::read_to_string;
use std::io::{Cursor, Read, Write};
use std::ops::Add;
use std::path::PathBuf;
use std::time::Duration;
use storyteller_client::api_error::ApiError;
use storyteller_client::api_error::ApiError::InternalServerError;
use storyteller_client::media_files::get_media_file::{get_media_file, GetMediaFileSuccessResponse};
use storyteller_client::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
use storyteller_client::utils::api_host::ApiHost;
use tauri::{AppHandle, Emitter, Manager, State};
use tempfile::NamedTempFile;
use filesys::file_read_bytes::file_read_bytes;
use tokens::tokens::media_files::MediaFileToken;
#[derive(Deserialize)]
pub struct FalBackgroundRemovalRequest {
/// Image media file; the image to remove the background from.
pub image_media_token: Option<MediaFileToken>,
/// Base64-encoded image
pub base64_image: Option<String>,
}
#[derive(Serialize)]
pub struct FalBackgroundRemovalSuccessResponse {
/// Result media token
pub media_token: MediaFileToken,
/// Result URL
pub cdn_url: Url,
/// Base64 bytes
pub base64_bytes: String,
}
impl SerializeMarker for FalBackgroundRemovalSuccessResponse {}
#[derive(Serialize, Debug)]
#[serde(rename_all = "snake_case")]
pub enum SoraImageRemixErrorType {
/// Generic server error
ServerError,
/// No Fal API key available
NeedsFalApiKey,
}
#[tauri::command]
pub async fn fal_background_removal_command(
request: FalBackgroundRemovalRequest,
app_data_root: State<'_, AppDataRoot>,
fal_creds_manager: State<'_, FalCredentialManager>,
storyteller_creds_manager: State<'_, StorytellerCredentialManager>,
) -> CommandResult<FalBackgroundRemovalSuccessResponse, SoraImageRemixErrorType, ()> {
info!("fal_background_removal_command called; image media token: {:?}", request.image_media_token);
let has_credentials = fal_creds_manager
.has_apparent_api_token()
.unwrap_or(true); // NB: Failures would be lock issues
if !has_credentials {
warn!("No API key found");
return Err(CommandErrorResponseWrapper {
status: CommandErrorStatus::Unauthorized,
error_message: Some("You need to set a FAL api key".to_string()),
error_type: Some(SoraImageRemixErrorType::NeedsFalApiKey),
error_details: None,
});
}
let result = remove_background(
request,
&app_data_root,
&fal_creds_manager,
&storyteller_creds_manager
).await;
match result {
Err(err) => {
error!("error: {:?}", err);
//let event = SoraImageEnqueueFailureEvent {};
//let result = event.send(&app);
//if let Err(err) = result {
// error!("Failed to emit event: {:?}", err);
//}
let mut status = CommandErrorStatus::ServerError;
let mut error_type = SoraImageRemixErrorType::ServerError;
let mut error_message = "A server error occurred. Please try again. If it continues, please tell our staff about the problem.";
//match (err) {
// _ => {},
//}
Err(CommandErrorResponseWrapper {
status,
error_message: Some(error_message.to_string()),
error_type: Some(error_type),
error_details: None,
})
}
Ok(result) => {
//let event = SoraImageEnqueueSuccessEvent {};
//let result = event.send(&app);
//if let Err(err) = result {
// error!("Failed to emit event: {:?}", err);
//}
Ok(FalBackgroundRemovalSuccessResponse {
media_token: result.0.media_file.token,
cdn_url: result.0.media_file.media_links.cdn_url,
base64_bytes: result.1,
}.into())
}
}
}
#[derive(Debug)]
enum InnerError {
FalError(FalErrorPlus),
AnyhowError(AnyhowError),
StorytellerApiError(ApiError),
DecodeError(DecodeError),
//ImageError(image::ImageError),
IoError(std::io::Error),
}
impl From<AnyhowError> for InnerError {
fn from(value: AnyhowError) -> Self {
Self::AnyhowError(value)
}
}
impl From<FalErrorPlus> for InnerError {
fn from(value: FalErrorPlus) -> Self {
Self::FalError(value)
}
}
impl From<ApiError> for InnerError {
fn from(value: ApiError) -> Self {
Self::StorytellerApiError(value)
}
}
impl From<DecodeError> for InnerError {
fn from(value: DecodeError) -> Self {
Self::DecodeError(value)
}
}
//impl From<image::ImageError> for InnerError {
// fn from(value: image::ImageError) -> Self {
// Self::ImageError(value)
// }
//}
//
impl From<std::io::Error> for InnerError {
fn from(value: std::io::Error) -> Self {
Self::IoError(value)
}
}
pub async fn remove_background(
request: FalBackgroundRemovalRequest,
app_data_root: &AppDataRoot,
fal_creds_manager: &FalCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
) -> Result<(GetMediaFileSuccessResponse, String), InnerError> {
let api_key = fal_creds_manager.get_key_required()?;
let creds = storyteller_creds_manager.get_credentials_required()?;
let mut temp_download;
if let Some(media_token) = request.image_media_token {
temp_download = download_media_file(&app_data_root, &media_token).await?;
} else if let Some(base64_bytes) = request.base64_image {
temp_download = persist_base64(&app_data_root, base64_bytes).await?;
} else {
return Err(InnerError::AnyhowError(anyhow!("No image media token or base64 image provided")));
}
info!("Calling FAL image background removal...");
let filename = temp_download.path().to_path_buf();
let result = remove_background_rembg(filename, &api_key).await?;
let extension_with_dot = get_url_file_extension(&result.image_url)
.map(|ext| format!(".{}", ext))
.unwrap_or_else(|| ".png".to_string());
let mut result_file = app_data_root.temp_dir().new_named_temp_file_with_extension(&extension_with_dot)?;
info!("Downloading file result file...");
simple_http_download_to_tempfile(&result.image_url, &mut result_file).await?;
let result_filename = result_file.path().to_path_buf();
// NB: We couldn't pass the existing file handle as I think the file handle pointer is already at the EOF
let bytes = file_read_bytes(&result_filename)?;
let base64_bytes = BASE64_STANDARD.encode(bytes.as_bytes());
info!("Uploading image media file...");
let upload_result = upload_image_media_file_from_file(
&ApiHost::Storyteller,
Some(&creds),
result_filename
).await?;
// TODO: Don't re-request to simply build MediaLinks (or CDN URL). Get those from the upload API in one turn.
info!("Re-requesting media file...");
let response = get_media_file(&ApiHost::Storyteller, &upload_result.media_file_token).await?;
info!("Uploaded media file: {:?}", response.media_file.token);
Ok((response, base64_bytes))
}
async fn download_media_file(app_data_root: &AppDataRoot, token: &MediaFileToken) -> Result<NamedTempFile, InnerError> {
let response = get_media_file(&ApiHost::Storyteller, token).await?;
let media_file_url = &response.media_file.media_links.cdn_url;
let extension_with_dot = get_url_file_extension(media_file_url)
.map(|ext| format!(".{}", ext))
.unwrap_or_else(|| ".png".to_string());
//let filename = format!("{}{}", response.media_file.token.as_str(), extension_with_dot);
//let filename = app_data_root.downloads_dir().path().join(&filename);
let mut file = app_data_root.temp_dir().new_named_temp_file_with_extension(&extension_with_dot)?;
simple_http_download_to_tempfile(&media_file_url, &mut file).await?;
Ok(file) // NB: Must return TempFile to not drop / delete it
}
async fn persist_base64(app_data_root: &AppDataRoot, base64_image: String) -> Result<NamedTempFile, InnerError> {
let bytes = BASE64_STANDARD.decode(base64_image)?;
let extension = MimetypeInfo::get_for_bytes(&bytes)
.map(|info| info.file_extension())
.flatten()
.map(|ext| ext.extension_with_period().to_string())
.unwrap_or_else(|| ".png".to_string());
let mut file = app_data_root.temp_dir().new_named_temp_file_with_extension(&extension)?;
file.write_all(bytes.as_ref())?;
Ok(file) // NB: Must return TempFile to not drop / delete it
}
@@ -0,0 +1,46 @@
use crate::commands::command_response_wrapper::{CommandResult, SerializeMarker};
use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager;
use crate::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::fal::fal_credential_manager::FalCredentialManager;
use anyhow::anyhow;
use errors::AnyhowResult;
use fal_client::creds::fal_api_key::FalApiKey;
use log::{error, info};
use openai_sora_client::sora_error::SoraError::AnyhowError;
use primitives::traits::trim_or_emptyable::TrimOrEmptyable;
use serde_derive::{Deserialize, Serialize};
use tauri::{AppHandle, State};
#[derive(Serialize)]
pub struct GetFalApiKeyResponse {
pub key: Option<String>,
}
impl SerializeMarker for GetFalApiKeyResponse {}
#[tauri::command]
pub async fn get_fal_api_key_command(
creds_manager: State<'_, FalCredentialManager>,
) -> CommandResult<GetFalApiKeyResponse, (), ()> {
let key = get_key(&creds_manager)
.await
.map_err(|err| {
error!("Error getting API key: {:?}", err);
"error getting key"
})?;
Ok(key.into())
}
async fn get_key(
creds_manager: &FalCredentialManager,
) -> AnyhowResult<GetFalApiKeyResponse> {
let maybe_key = creds_manager.get_key()?
.map(|key| key.0)
.map(|key| key.trim().to_string())
.filter(|key| !key.is_empty());
Ok(GetFalApiKeyResponse { key: maybe_key })
}
@@ -0,0 +1,3 @@
pub mod fal_background_removal_command;
pub mod get_fal_api_key_command;
pub mod set_fal_api_key_command;
@@ -0,0 +1,59 @@
use crate::commands::command_response_wrapper::CommandResult;
use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager;
use crate::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::fal::fal_credential_manager::FalCredentialManager;
use anyhow::anyhow;
use errors::AnyhowResult;
use fal_client::creds::fal_api_key::FalApiKey;
use log::{error, info};
use openai_sora_client::sora_error::SoraError::AnyhowError;
use serde_derive::{Deserialize, Serialize};
use tauri::{AppHandle, State};
/// For now, we'll only update a single preference at a time.
#[derive(Deserialize)]
pub struct SetFalApiKeyRequest{
pub key: Option<String>,
}
#[tauri::command]
pub async fn set_fal_api_key_command(
request: SetFalApiKeyRequest,
creds_manager: State<'_, FalCredentialManager>,
) -> CommandResult<(), (), ()> {
info!("update_app_preferences_command called");
set_key(request, &creds_manager)
.await
.map_err(|err| {
error!("Error getting app preferences: {:?}", err);
"error setting key"
})?;
Ok(().into())
}
async fn set_key(
request: SetFalApiKeyRequest,
creds_manager: &FalCredentialManager,
) -> AnyhowResult<()> {
let key = request.key
.map(|key| key.trim().to_string())
.filter(|key| !key.is_empty())
.map(|key| FalApiKey::from_str(&key));
match key {
None => {
creds_manager.clear_key()?;
creds_manager.purge_api_key_from_disk()?;
}
Some(key) => {
creds_manager.set_key(&key)?;
creds_manager.persist_to_disk()?;
}
}
Ok(())
}
@@ -1,5 +1,6 @@
pub mod app_preferences;
pub mod command_response_wrapper;
pub mod fal;
pub mod flip_image;
pub mod platform_info_command;
pub mod sora;
@@ -91,7 +91,7 @@ pub async fn sora_image_remix_command(
let has_credentials = sora_creds_manager
.has_apparently_complete_credentials()
.unwrap_or(true);
.unwrap_or(true); // NB: Failures would be lock issues
if !has_credentials {
warn!("No apparently completed credentials found");
+14 -1
View File
@@ -9,6 +9,9 @@ use tauri::Manager;
use crate::commands::app_preferences::get_app_preferences_command::get_app_preferences_command;
use crate::commands::app_preferences::update_app_preference_command::update_app_preferences_command;
use crate::commands::fal::fal_background_removal_command::fal_background_removal_command;
use crate::commands::fal::get_fal_api_key_command::get_fal_api_key_command;
use crate::commands::fal::set_fal_api_key_command::set_fal_api_key_command;
use crate::commands::flip_image::flip_image;
use crate::commands::platform_info_command::platform_info_command;
use crate::commands::sora::check_sora_session_command::check_sora_session_command;
@@ -18,6 +21,7 @@ use crate::commands::sora::sora_image_remix_command::sora_image_remix_command;
use crate::commands::sora::sora_logout_command::sora_logout_command;
use crate::state::app_preferences::app_preferences_manager::load_app_preferences_or_default;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::fal::fal_credential_manager::FalCredentialManager;
use crate::state::main_window_position::MainWindowPosition;
use crate::state::main_window_size::MainWindowSize;
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
@@ -47,10 +51,14 @@ pub fn run() {
println!("Attempting to read existing artcraft credentials...");
let storyteller_creds_manager = StorytellerCredentialManager::initialize_from_disk_infallible(&app_data_root);
let storyteller_creds_manager_2 = storyteller_creds_manager.clone();
let storyteller_creds_manager_3 = storyteller_creds_manager.clone();
println!("Attempting to read existing credentials...");
println!("Attempting to read existing sora credentials...");
let sora_creds_manager = SoraCredentialManager::initialize_from_disk_infallible(&app_data_root);
let sora_creds_manager_2 = sora_creds_manager.clone();
println!("Attempting to read existing fal credentials...");
let fal_creds_manager = FalCredentialManager::initialize_from_disk_infallible(&app_data_root);
// Other state
let sora_task_queue = SoraTaskQueue::new();
@@ -134,13 +142,18 @@ pub fn run() {
.manage(app_data_root)
.manage(app_preferences)
.manage(sora_creds_manager)
.manage(fal_creds_manager)
.manage(sora_task_queue)
.manage(storyteller_creds_manager_3)
.invoke_handler(tauri::generate_handler![
check_sora_session_command,
fal_background_removal_command,
flip_image,
get_app_preferences_command,
get_fal_api_key_command,
open_sora_login_command,
platform_info_command,
set_fal_api_key_command,
sora_image_generation_command,
sora_image_remix_command,
sora_logout_command,
@@ -40,4 +40,8 @@ impl AppCredentialsDir {
pub fn get_storyteller_session_cookie_file_path(&self) -> PathBuf {
self.path.join("artcraft_session.txt")
}
pub fn get_fal_api_key_file_path(&self) -> PathBuf {
self.path.join("fal_api_key.txt")
}
}
@@ -35,4 +35,17 @@ impl TemporaryDir {
.tempfile_in(&self.path)?;
Ok(tempfile)
}
pub fn new_named_temp_file_with_extension(&self, extension: &str) -> anyhow::Result<NamedTempFile> {
let extension = if extension.starts_with(".") {
extension.to_string()
} else {
format!(".{}", extension)
};
let tempfile = Builder::new()
.prefix("temp_")
.suffix(&extension)
.tempfile_in(&self.path)?;
Ok(tempfile)
}
}
@@ -0,0 +1,98 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::fal::read_fal_api_key_from_disk::read_fal_api_key_from_disk;
use anyhow::anyhow;
use errors::AnyhowResult;
use fal_client::creds::fal_api_key::FalApiKey;
use memory_store::clone_slot::CloneSlot;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::Path;
use std::{fs, io};
#[derive(Clone)]
pub struct FalCredentialManager {
key: CloneSlot<FalApiKey>,
app_data_root: AppDataRoot,
}
impl FalCredentialManager {
pub fn new(app_data_root: &AppDataRoot) -> Self {
Self {
key: CloneSlot::empty(),
app_data_root: app_data_root.clone(),
}
}
pub fn initialize_from_disk_infallible(app_data_root: &AppDataRoot) -> Self {
let mut holder = CloneSlot::empty();
match read_fal_api_key_from_disk(app_data_root) {
Err(err) => {
log::warn!("Failed to read FAL API key from disk: {:?}", err);
}
Ok(api_key) => {
holder.set_clone(&api_key).expect("Failed to set FAL API key");
}
}
Self {
key: holder,
app_data_root: app_data_root.clone(),
}
}
pub fn set_key(&self, key: &FalApiKey) -> AnyhowResult<()> {
self.key.set_clone(key)
}
pub fn get_key(&self) -> AnyhowResult<Option<FalApiKey>> {
self.key.get_clone()
}
pub fn get_key_required(&self) -> AnyhowResult<FalApiKey> {
self.key.get_clone_required()
}
pub fn clear_key(&self) -> AnyhowResult<()> {
self.key.clear()
}
/// Does not check the token for validity
pub fn has_apparent_api_token(&self) -> AnyhowResult<bool> {
self.key.is_some()
}
pub fn purge_api_key_from_disk(&self) -> Result<(), io::Error> {
let filename = self.app_data_root.credentials_dir().get_fal_api_key_file_path();
if filename.exists() && filename.is_file() {
fs::remove_file(filename)?;
}
Ok(())
}
pub fn persist_to_disk(&self) -> AnyhowResult<()> {
let key = match self.get_key() {
Ok(Some(key)) => key,
Ok(None) => {
self.purge_api_key_from_disk()?;
return Ok(());
},
Err(err) => {
return Err(anyhow!("Failed to get FAL key: {:?}", err));
}
};
let filename = self.app_data_root.credentials_dir().get_fal_api_key_file_path();
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(filename)?;
file.write_all(key.0.as_bytes())?;
file.flush()?;
Ok(())
}
}
@@ -0,0 +1,2 @@
pub mod fal_credential_manager;
pub mod read_fal_api_key_from_disk;
@@ -0,0 +1,19 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use anyhow::anyhow;
use errors::AnyhowResult;
use fal_client::creds::fal_api_key::FalApiKey;
use std::fs::read_to_string;
pub fn read_fal_api_key_from_disk(app_data_root: &AppDataRoot) -> AnyhowResult<FalApiKey> {
let api_key_file = app_data_root.credentials_dir().get_fal_api_key_file_path();
if !api_key_file.exists() {
return Err(anyhow!("API key file does not exist: {:?}", api_key_file));
}
let value = read_to_string(api_key_file)?
.trim()
.to_string();
Ok(FalApiKey::new(value))
}
+1
View File
@@ -2,6 +2,7 @@ pub mod app_preferences;
pub mod app_startup_time;
pub mod data_dir;
pub mod expanduser;
pub mod fal;
pub mod main_window_position;
pub mod main_window_size;
pub mod os_platform;
+1
View File
@@ -5,5 +5,6 @@ pub mod image;
pub mod large_file_transfer;
pub mod object_store;
pub mod simple_http_download;
pub mod simple_http_download_to_tempfile;
pub mod sora;
pub mod webview_unsafe;
@@ -0,0 +1,26 @@
use errors::AnyhowResult;
use reqwest::Client;
use reqwest::Url;
use std::io::Write;
use tempfile::NamedTempFile;
const USER_AGENT: &str = "storyteller-client/1.0";
/// Downloads a (binary) file to a filesystem path. Good for images, etc. Not great for large files.
pub async fn simple_http_download_to_tempfile(url: &Url, temp_file: &mut NamedTempFile) -> AnyhowResult<()> {
let client = Client::builder()
.gzip(true)
.build()?;
let response = client.get(url.clone()) // NB: No IntoUrl for &Url.
.header("User-Agent", USER_AGENT)
.header("Accept", "*/*")
.send()
.await?;
let bytes = response.bytes().await?;
temp_file.write_all(&bytes)?;
Ok(())
}
@@ -0,0 +1,32 @@
[package]
name = "fal_client"
edition = "2021"
version = "0.0.1"
publish = false
[package.metadata.cargo-udeps.ignore]
normal = ["workspace-build-acceleration"]
[lib]
name = "fal_client"
path = "src/lib.rs"
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration = { workspace = true }
# Internal
file_to_base64_url = { path = "../../files/file_to_base64_url" }
errors = { workspace = true }
# External
anyhow = { workspace = true }
fal = { version = "0.3.6", features = ["endpoints"] }
futures = { workspace = true }
tokio = { workspace = true }
url = { workspace = true }
[dev-dependencies]
testing = { path = "../../testing" }
filesys = { path = "../../files/filesys" }
@@ -0,0 +1,13 @@
#[derive(Clone)]
pub struct FalApiKey(pub String);
impl FalApiKey {
pub fn new(api_key: String) -> Self {
Self(api_key)
}
pub fn from_str(api_key: &str) -> Self {
Self(api_key.to_string())
}
}
@@ -0,0 +1 @@
pub mod fal_api_key;
@@ -0,0 +1,43 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
/// Additional errors that aren't included in `fal::FalError`.
#[derive(Debug)]
pub enum FalErrorPlus {
/// An error arising in the `fal` crate.
FalError(fal::FalError),
/// Another error we didn't handle.
AnyhowError(anyhow::Error),
/// URL parse errors.
UrlParseError(url::ParseError),
}
impl Display for FalErrorPlus {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::FalError(err) => write!(f, "FalErrorPlus::FalError: {:?}", err),
Self::AnyhowError(err) => write!(f, "FalErrorPlus::AnyhowError: {:?}", err),
Self::UrlParseError(err) => write!(f, "FalErrorPlus::UrlParseError: {:?}", err),
}
}
}
impl Error for FalErrorPlus {}
impl From<fal::FalError> for FalErrorPlus {
fn from(err: fal::FalError) -> Self {
FalErrorPlus::FalError(err)
}
}
impl From<anyhow::Error> for FalErrorPlus {
fn from(err: anyhow::Error) -> Self {
FalErrorPlus::AnyhowError(err)
}
}
impl From<url::ParseError> for FalErrorPlus {
fn from(err: url::ParseError) -> Self {
FalErrorPlus::UrlParseError(err)
}
}
@@ -0,0 +1,7 @@
//! A wrapper for the "fal" crate that bakes in a few extra recipes and utilities.
extern crate fal;
pub mod creds;
pub mod fal_error_plus;
pub mod requests;
@@ -0,0 +1 @@
pub mod remove_background_rembg;
@@ -0,0 +1,79 @@
use crate::creds::fal_api_key::FalApiKey;
use crate::fal_error_plus::FalErrorPlus;
use fal::endpoints::fal_ai::imageutils::rembg::RemoveBackgroundInput;
use fal::prelude::fal_ai::imageutils::rembg::rembg;
use fal::prelude::*;
use fal_client::file_to_base64_url::file_to_base64_url;
use futures::StreamExt;
use std::path::Path;
use url::Url;
pub struct RemBgArgs<'a, P: AsRef<Path>> {
pub image_path: P,
pub api_key: &'a FalApiKey,
}
pub struct RemBgResponse {
pub image_url: Url,
}
/// remove background using `fal_ai/imageutils/rembg`
pub async fn remove_background_rembg<P: AsRef<Path>>(image_path: P, api_key: &FalApiKey) -> Result<RemBgResponse, FalErrorPlus> {
let image_url = file_to_base64_url(image_path)?;
/*
TODO: Handle error messages -
FalError(FalError(Other("{\"detail\": \"Invalid Key Authorization header format. Expected '<key_id>:<key_secret>'.\"}")))
FalError(FalError(Other("{\"detail\": \"No user found for Key ID and Secret\"}")))
*/
let request = RemoveBackgroundInput {
image_url,
crop_to_bbox: None,
sync_mode: None
};
let result = rembg(request)
.with_api_key(&api_key.0)
.queue()
.await?;
let mut stream = result.stream(true).await?;
while let Some(status) = stream.next().await {
let status = status?;
if status.status == Status::Completed {
break;
}
}
let output = result.response().await?;
let url = Url::parse(&output.image.url)?;
Ok(RemBgResponse {
image_url: url,
})
}
#[cfg(test)]
mod tests {
use crate::creds::fal_api_key::FalApiKey;
use crate::requests::remove_background_rembg::remove_background_rembg;
use errors::AnyhowResult;
use testing::test_file_path::test_file_path;
#[tokio::test]
#[ignore]
async fn test_remove_background_rembg() -> AnyhowResult<()> {
let image = test_file_path("test_data/image/juno.jpg")?;
// TODO: DO NOT COMMIT SECRET !
const KEY : &str = "DO NOT COMMIT";
let api_key = FalApiKey::from_str(KEY);
let result = remove_background_rembg(image, &api_key).await?;
Ok(())
}
}
@@ -31,7 +31,17 @@ impl <T: Clone> CloneSlot<T> {
Ok(store) => Ok(store.clone()),
}
}
pub fn get_clone_required(&self) -> AnyhowResult<T> {
match self.store.read() {
Err(err) => Err(anyhow!("Failed to acquire read lock: {:?}", err)),
Ok(store) => match &*store {
None => Err(anyhow!("Required stored item is not set")),
Some(inner) => Ok(inner.clone()),
}
}
}
pub fn set_clone(&self, value: &T) -> AnyhowResult<()> {
match self.store.write() {
Err(err) => Err(anyhow!("Failed to acquire write lock: {:?}", err)),
@@ -0,0 +1,29 @@
[package]
name = "file_to_base64_url"
edition = "2021"
version = "0.0.1"
publish = false
[package.metadata.cargo-udeps.ignore]
normal = ["workspace-build-acceleration"]
[lib]
name = "fal_client"
path = "src/lib.rs"
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration = { workspace = true }
# Internal
errors = { path = "../../errors" }
# External
anyhow = { workspace = true }
base64 = { workspace = true }
mimetypes = { workspace = true }
[dev-dependencies]
testing = { path = "../../testing" }
filesys = { path = "../../files/filesys" }
@@ -0,0 +1,26 @@
use anyhow::anyhow;
use base64::prelude::{BASE64_STANDARD, BASE64_URL_SAFE, BASE64_URL_SAFE_NO_PAD};
use base64::Engine;
use errors::AnyhowResult;
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
use std::path::Path;
/// Read a file into a base64 URL-safe string.
pub fn file_to_base64_url<P: AsRef<Path>>(path: P) -> AnyhowResult<String> {
let bytes = std::fs::read(path.as_ref())?;
let mime = match MimetypeInfo::get_for_bytes(&bytes) {
Some(mime) => mime,
None => {
return Err(anyhow!("could not determine mimetype"));
},
};
let base64_bytes = BASE64_STANDARD.encode(&bytes);
match mime.mime_type() {
"image/png" => Ok(format!("data:image/png;base64,{}", base64_bytes)),
"image/jpeg" => Ok(format!("data:image/jpeg;base64,{}", base64_bytes)),
_ => Err(anyhow!("unknown or unsupported mime type: {}", mime.mime_type())),
}
}
@@ -0,0 +1 @@
pub mod file_to_base64_url;
@@ -28,6 +28,7 @@ import { invoke } from "@tauri-apps/api/core";
import { DecodeBase64ToImage } from "./utilities/DecodeBase64ToImage";
import { EncodeImageBitmapToBase64 } from "./utilities/EncodeImageBitmapToBase64";
import { FalBackgroundRemoval, FalBackgroundRemovalRequest } from "@storyteller/tauri-api";
@@ -443,6 +444,8 @@ export class Engine {
}
removeBackground() {
console.log("Remove background");
const selectedNodeSet = this.selectionManager.getSelectedNodes();
if (selectedNodeSet.size !== 1) {
@@ -460,15 +463,30 @@ export class Engine {
) => {
const image: HTMLImageElement = selectedNode.kNode.image();
const base64Bitmap = await EncodeImageBitmapToBase64(image);
const base64BitmapResponse = await invoke("remove_background", {
image: base64Bitmap,
});
const createdImage = await DecodeBase64ToImage(
base64BitmapResponse as string,
);
const falRemoveBgRequest: FalBackgroundRemovalRequest = {
base64_image: base64Bitmap,
};
callback(selectedNode, createdImage);
const response = await FalBackgroundRemoval(falRemoveBgRequest);
if (response.status !== "success") {
console.error("Failed to remove background", response);
// TODO
return;
}
console.log("Remove background success", response)
if ("payload" in response) {
const base64BitmapResponse = response.payload?.base64_bytes;
const createdImage = await DecodeBase64ToImage(
base64BitmapResponse as string,
);
callback(selectedNode, createdImage);
}
};
const _promise = removeBgInvocation(
@@ -4,6 +4,7 @@ import { Input } from "@storyteller/ui-input";
import { SoraAccountButton } from "./SoraAccountButton";
import { SoraAccountBlock } from "./SoraAccountBlock";
import { ArtcraftAccountBlock } from "./ArtcraftAccountBlock";
import { FalApiKeyBlock } from "./FalApiKeyBlock";
interface AccountInfo {
username: string;
@@ -59,6 +60,7 @@ export const AccountSettingsPane = ({
<SoraAccountBlock />
<FalApiKeyBlock />
{/*
<hr />
@@ -0,0 +1,69 @@
import { useState, useEffect } from "react";
import { GetFalApiKey, SetFalApiKey } from "@storyteller/tauri-api";
import { Input } from "@storyteller/ui-input";
export const FalApiKeyBlock = () => {
const [falApiKey, setFalApiKey] = useState<string | undefined>(
undefined
);
const [isCheckingFalApiKey, setIsCheckingFalApiKey] =
useState(false);
useEffect(() => {
const fetchKey = async () => {
setIsCheckingFalApiKey(true);
try {
const result = await GetFalApiKey();
if ("payload" in result) {
setFalApiKey(result.payload.key);
}
} catch (e) {
console.error("Error fetching Fal API key", e);
setFalApiKey(undefined);
} finally {
setIsCheckingFalApiKey(false);
}
};
fetchKey();
}, []);
const syncFalApiKey = async (key: string) => {
await SetFalApiKey(key);
setFalApiKey(key);
};
return (
<div className="flex justify-between items-center">
<span>Fal API Key:</span>
<Input
value={falApiKey}
onChange={(e) => syncFalApiKey(e.target.value)}
placeholder="Enter Fal API key"
/>
{/*<Button
variant={
isCheckingArtcraftSession
? "secondary"
: isLoggedIn
? "destructive"
: "primary"
}
className="h-[30px]"
onClick={handleArtcraftButton}
disabled={isCheckingArtcraftSession}
>
{isCheckingArtcraftSession ? (
<FontAwesomeIcon
icon={faSpinnerThird}
className="animate-spin text-sm"
/>
) : isLoggedIn ? (
"Log Out"
) : (
"Log In"
)}
</Button>*/}
</div>
);
};
+3
View File
@@ -5,3 +5,6 @@ export * from "./lib/sora/CheckSoraSession.js";
export * from "./lib/sora/LogoutSoraSession.js";
export * from "./lib/sora/SoraImageRemix.js";
export * from "./lib/sora/useSoraLoginListener.js";
export * from "./lib/fal/FalBackgroundRemoval.js";
export * from "./lib/fal/GetFalApiKey.js";
export * from "./lib/fal/SetFalApiKey.js";
@@ -0,0 +1,39 @@
import { invoke } from "@tauri-apps/api/core";
import { CommandResult } from "../common/CommandStatus";
export enum FalBackgroundRemovalErrorType {
ServerError = "server_error",
NeedsFalApiKey = "needs_fal_api_key",
}
export interface FalBackgroundRemovalRequest {
image_media_token?: string;
base64_image?: string;
}
export interface FalBackgroundRemovalError extends CommandResult {
error_type: FalBackgroundRemovalErrorType;
error_message?: string;
}
export interface FalBackgroundRemovalPayload {
media_token: string;
cdn_url: string;
base64_bytes: string;
}
export interface FalBackgroundRemovalSuccess extends CommandResult {
payload: FalBackgroundRemovalPayload;
}
export type FalBackgroundRemovalResult = FalBackgroundRemovalSuccess | FalBackgroundRemovalError;
export const FalBackgroundRemoval = async (request: FalBackgroundRemovalRequest) : Promise<FalBackgroundRemovalResult> => {
let result = await invoke("fal_background_removal_command", {
request: {
image_media_token: request.image_media_token,
base64_image: request.base64_image,
}
});
return (result as FalBackgroundRemovalResult);
}
@@ -0,0 +1,15 @@
import { invoke } from "@tauri-apps/api/core";
import { CommandResult } from "../common/CommandStatus";
export interface GetFalApiKeySuccessResult extends CommandResult {
payload: {
key: string;
}
}
export type GetFalApiKeyResult = GetFalApiKeySuccessResult | CommandResult;
export const GetFalApiKey = async () : Promise<GetFalApiKeyResult> => {
let result = await invoke("get_fal_api_key_command");
return (result as GetFalApiKeyResult);
}
@@ -0,0 +1,11 @@
import { invoke } from "@tauri-apps/api/core";
import { CommandResult } from "../common/CommandStatus";
export const SetFalApiKey = async (key: string) : Promise<CommandResult> => {
let result = await invoke("set_fal_api_key_command", {
request: {
key: key,
}
});
return (result as CommandResult);
}