mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-29 21:12:52 -04:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f832be22bb | ||
|
|
a7794f6338 | ||
|
|
ef03e292c3 | ||
|
|
0e8c1fa3e7 | ||
|
|
af08834de7 | ||
|
|
e728806c04 | ||
|
|
3e605e7b18 | ||
|
|
d244524abc | ||
|
|
8e455e27a9 | ||
|
|
e0df1644a5 | ||
|
|
736d033ebf | ||
|
|
af3f0baa63 |
Generated
+157
-89
@@ -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
@@ -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" }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -15,7 +15,7 @@ use base64::Engine;
|
||||
use errors::{AnyhowError, AnyhowResult};
|
||||
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
|
||||
use image::{DynamicImage, ImageReader};
|
||||
use log::{debug, error, info};
|
||||
use log::{debug, error, info, warn};
|
||||
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};
|
||||
@@ -29,6 +29,7 @@ use openai_sora_client::sora_error::SoraError;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fs::read_to_string;
|
||||
use std::io::Cursor;
|
||||
use std::ops::Add;
|
||||
use std::time::Duration;
|
||||
use storyteller_client::api_error::ApiError;
|
||||
use storyteller_client::api_error::ApiError::InternalServerError;
|
||||
@@ -64,7 +65,9 @@ pub enum SoraImageRemixErrorType {
|
||||
/// Generic server error
|
||||
ServerError,
|
||||
/// The user is sending too many requests
|
||||
TooManyConcurrentTasks,
|
||||
TooManyConcurrentTasks,
|
||||
/// User is not logged into Sora!
|
||||
SoraLoginRequired,
|
||||
/// The user needs to create a Sora account
|
||||
SoraUsernameNotYetCreated,
|
||||
/// The Sora service is having problems. Try again soon.
|
||||
@@ -86,6 +89,20 @@ pub async fn sora_image_remix_command(
|
||||
|
||||
// TODO(bt,2025-04-24): Better error messages to caller
|
||||
|
||||
let has_credentials = sora_creds_manager
|
||||
.has_apparently_complete_credentials()
|
||||
.unwrap_or(true); // NB: Failures would be lock issues
|
||||
|
||||
if !has_credentials {
|
||||
warn!("No apparently completed credentials found");
|
||||
return Err(CommandErrorResponseWrapper {
|
||||
status: CommandErrorStatus::Unauthorized,
|
||||
error_message: Some("You need to log into Sora to continue. See the settings menu.".to_string()),
|
||||
error_type: Some(SoraImageRemixErrorType::SoraLoginRequired),
|
||||
error_details: None,
|
||||
});
|
||||
}
|
||||
|
||||
let result = generate_image(request, &app_data_root, &sora_creds_manager, &sora_task_queue).await;
|
||||
|
||||
match result {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -54,4 +54,20 @@ impl SoraCredentialHolder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This does not guarantee the credentials are correct, just that they're set.
|
||||
pub fn has_apparently_complete_credentials(&self) -> AnyhowResult<bool> {
|
||||
match self.credentials.read() {
|
||||
Err(err) => Err(anyhow!("Failed to acquire read lock: {:?}", err)),
|
||||
Ok(creds) => match &*creds {
|
||||
None => Ok(false),
|
||||
Some(creds) => {
|
||||
let has_cookies = !creds.cookies.as_str().is_empty();
|
||||
let has_bearer = creds.jwt_bearer_token.is_some();
|
||||
let has_sentinel = creds.sora_sentinel.is_some();
|
||||
Ok(has_cookies && has_bearer && has_sentinel)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,11 @@ impl SoraCredentialManager {
|
||||
self.stats.set_owned(SoraCredentialStats::new())
|
||||
}
|
||||
|
||||
/// This does not guarantee the credentials are correct
|
||||
pub fn has_apparently_complete_credentials(&self) -> AnyhowResult<bool> {
|
||||
self.holder.has_apparently_complete_credentials()
|
||||
}
|
||||
|
||||
pub fn get_credential_stats(&self) -> AnyhowResult<SoraCredentialStats> {
|
||||
self.stats.get_clone()
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -4,10 +4,10 @@
|
||||
"version": "0.0.1",
|
||||
"identifier": "ai.artcraft.app",
|
||||
"build": {
|
||||
"frontendDist": "../../../",
|
||||
"devUrl": "http://localhost:5741",
|
||||
"beforeDevCommand": "nx run editor2d:dev",
|
||||
"beforeBuildCommand": "nx run editor2d:build"
|
||||
"frontendDist": "../../../frontend/apps/editor3d/dist",
|
||||
"devUrl": "http://localhost:5173",
|
||||
"beforeDevCommand": "nx run editor3d:dev",
|
||||
"beforeBuildCommand": "nx run editor3d:build"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
@@ -29,6 +29,7 @@
|
||||
"data:",
|
||||
"https://*.fakeyou.com",
|
||||
"https://*.storyteller.ai",
|
||||
"https://www.google-analytics.com",
|
||||
"https://www.googletagmanager.com",
|
||||
"ipc://localhost",
|
||||
"tauri://localhost"
|
||||
@@ -39,9 +40,11 @@
|
||||
"'unsafe-inline'",
|
||||
"blob:",
|
||||
"data:",
|
||||
"http://ipc.localhost",
|
||||
"https://*.fakeyou.com",
|
||||
"https://*.storyteller.ai",
|
||||
"https://unpkg.com/detect-gpu@5.0.70/",
|
||||
"https://www.google-analytics.com",
|
||||
"https://www.googletagmanager.com",
|
||||
"ipc://localhost",
|
||||
"tauri://localhost"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
{"default":{"identifier":"default","description":"enables the default permissions","local":true,"windows":["main"],"permissions":["core:default","log:default","upload:allow-download","upload:default",{"identifier":"http:default","allow":[{"url":"https://*.storyteller.ai"},{"url":"https://*.fakeyou.com"}],"deny":[{"url":"https://private.tauri.app"}]}]}}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
Binary file not shown.
@@ -1,8 +1,8 @@
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
import Landing from "../pages/landing";
|
||||
import Download from "../pages/download";
|
||||
import Navbar from "../components/navbar";
|
||||
import Footer from "../components/footer";
|
||||
import Landing2 from "../pages/landing2";
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
@@ -10,7 +10,7 @@ export function App() {
|
||||
<Navbar />
|
||||
|
||||
<Routes>
|
||||
<Route path="/" element={<Landing />} />
|
||||
<Route path="/" element={<Landing2 />} />
|
||||
<Route path="/download" element={<Download />} />
|
||||
</Routes>
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { faDiscord } from "@fortawesome/free-brands-svg-icons";
|
||||
import { SOCIAL_LINKS } from "../../config/links";
|
||||
|
||||
interface DiscordButtonProps {
|
||||
className?: string;
|
||||
small?: boolean;
|
||||
}
|
||||
|
||||
export const DiscordButton = ({
|
||||
className = "",
|
||||
small = false,
|
||||
}: DiscordButtonProps) => {
|
||||
const sizeClasses = small
|
||||
? "px-4 py-2 text-md rounded-xl"
|
||||
: "text-lg px-4 py-2 md:px-8 md:py-4 md:text-2xl rounded-2xl";
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={`relative z-10 ${sizeClasses} font-bold transition-all duration-300 shadow-lg hover:shadow-blue-500/25 ${className}`}
|
||||
icon={faDiscord}
|
||||
as="link"
|
||||
href={SOCIAL_LINKS.DISCORD}
|
||||
target="_blank"
|
||||
>
|
||||
Join Discord
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./discord-button";
|
||||
@@ -1,18 +1,15 @@
|
||||
import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
} from "@headlessui/react";
|
||||
import { Disclosure, DisclosureButton } from "@headlessui/react";
|
||||
import { faBars } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
// import { Button } from "@storyteller/ui-button";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { useEffect, useState } from "react";
|
||||
import { DiscordButton } from "../discord-button";
|
||||
|
||||
const navigation = [
|
||||
{ name: "Home", href: "/", current: true },
|
||||
{ name: "Download", href: "/download", current: false },
|
||||
];
|
||||
// const navigation = [
|
||||
// { name: "Home", href: "/", current: true },
|
||||
// { name: "Download", href: "/download", current: false },
|
||||
// ];
|
||||
|
||||
export default function Navbar() {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
@@ -30,11 +27,13 @@ export default function Navbar() {
|
||||
<Disclosure
|
||||
as="nav"
|
||||
className={twMerge(
|
||||
"z-20 fixed top-0 left-0 w-full transition-colors duration-200",
|
||||
scrolled ? "bg-[#1b1b1f]/70 backdrop-blur-lg" : "bg-transparent"
|
||||
"z-20 fixed top-0 left-0 w-full transition-colors duration-200 bg-transparent",
|
||||
scrolled
|
||||
? "bg-[#1b1b1f]/70 backdrop-blur-lg lg:bg-transparent lg:backdrop-blur-none"
|
||||
: "bg-transparent"
|
||||
)}
|
||||
>
|
||||
<div className="mx-auto max-w-[1920px] sm:px-6 lg:px-8 px-6 md:px-16 xl:px-32">
|
||||
<div className="mx-auto max-w-screen sm:px-6 lg:px-8 px-6 md:px-16 xl:px-4">
|
||||
<div className="flex h-16 justify-between">
|
||||
<div className="flex">
|
||||
<div className="flex shrink-0 items-center">
|
||||
@@ -46,7 +45,7 @@ export default function Navbar() {
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div className="hidden md:ml-7 md:flex md:items-center md:space-x-3">
|
||||
{/* <div className="hidden md:ml-7 md:flex md:items-center md:space-x-3">
|
||||
{navigation.map((item) => (
|
||||
<a
|
||||
key={item.name}
|
||||
@@ -62,27 +61,24 @@ export default function Navbar() {
|
||||
{item.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="hidden md:ml-4 md:flex md:shrink-0 md:items-center">
|
||||
<Button as="link" href="/download">
|
||||
{/* <Button as="link" href="/download">
|
||||
Download
|
||||
</Button>
|
||||
</Button> */}
|
||||
<DiscordButton small />
|
||||
</div>
|
||||
<div className="mr-2 -ml-2 flex items-center md:hidden">
|
||||
<div className="-ml-2 flex items-center md:hidden">
|
||||
{/* Mobile menu button */}
|
||||
<DisclosureButton className="group relative inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-700 hover:text-white focus:ring-2 focus:ring-white focus:outline-hidden focus:ring-inset">
|
||||
<span className="absolute -inset-0.5" />
|
||||
<span className="sr-only">Open main menu</span>
|
||||
<FontAwesomeIcon icon={faBars} />
|
||||
</DisclosureButton>
|
||||
<DiscordButton className="text-sm" small />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DisclosurePanel className="md:hidden">
|
||||
{/* <DisclosurePanel className="md:hidden">
|
||||
<div className="space-y-1 px-2 pt-2 pb-3 sm:px-3">
|
||||
{navigation.map((item) => (
|
||||
<DisclosureButton
|
||||
@@ -101,7 +97,7 @@ export default function Navbar() {
|
||||
</DisclosureButton>
|
||||
))}
|
||||
</div>
|
||||
</DisclosurePanel>
|
||||
</DisclosurePanel> */}
|
||||
</Disclosure>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./landing";
|
||||
export * from "./landing2";
|
||||
export * from "./download";
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./landing2";
|
||||
@@ -0,0 +1,240 @@
|
||||
import { DiscordButton } from "../../components/discord-button";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
const fadeUpVariants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.6,
|
||||
delay: 0.2,
|
||||
ease: "easeOut",
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Landing = () => {
|
||||
const videos = [
|
||||
{
|
||||
src: "https://www.youtube.com/embed/H4NFXGMuwpY?si=U2_m5Ic1YBn6176D",
|
||||
},
|
||||
{
|
||||
src: "https://www.youtube.com/embed/pGn-1BKo3nY?si=q4dm0ICb6wF1JW8N",
|
||||
},
|
||||
{
|
||||
src: "https://www.youtube.com/embed/7x7IZkHiGD8?si=tL8nK4CULigpfHQR",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="relative min-h-screen bg-[#101014] text-white bg-dots">
|
||||
<div className="dotted-pattern absolute inset-0 z-[0] opacity-50" />
|
||||
{/* Hero Section */}
|
||||
<div className="relative flex overflow-hidden xl:items-center xl:pt-0 mb-0">
|
||||
<motion.div
|
||||
className="w-full flex flex-col items-center justify-center text-center pt-36 px-10"
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
variants={fadeUpVariants}
|
||||
>
|
||||
<motion.div className="relative" variants={fadeUpVariants}>
|
||||
<h1 className="relative mb-6 md:mb-10 font-extrabold text-4xl sm:text-[3rem] md:text-[4rem] lg:text-[6rem] !leading-none">
|
||||
<span className="text-primary">“</span>
|
||||
Is this really AI?
|
||||
<span className="text-primary">”</span>
|
||||
</h1>
|
||||
<span className="absolute -right-5 bottom-1 md:-right-14 md:bottom-14 text-xl md:text-3xl font-bold opacity-40 italic">
|
||||
— You
|
||||
</span>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Hero Section */}
|
||||
<div className="relative z-10 mx-auto max-w-[100rem] px-4 md:px-16 lg:px-12 xl:px-32 lg:mb-32">
|
||||
<motion.div
|
||||
className="items-center gap-16 mb-12 p-4 bg-white/10 backdrop-blur-md rounded-[40px]"
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
variants={fadeUpVariants}
|
||||
>
|
||||
<div className="rounded-[24px] overflow-hidden bg-[#1C1C20]">
|
||||
<video
|
||||
muted
|
||||
autoPlay
|
||||
loop
|
||||
playsInline
|
||||
className="w-full aspect-[16/9] object-cover"
|
||||
>
|
||||
<source src="/videos/hero-video.mp4" type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="flex justify-center"
|
||||
variants={fadeUpVariants}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
>
|
||||
<DiscordButton />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-hidden py-24 sm:py-32">
|
||||
<motion.div
|
||||
className="mx-auto max-w-[88rem] md:px-6 lg:px-8"
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
variants={fadeUpVariants}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-20 lg:grid-cols-2 lg:items-center">
|
||||
<motion.div
|
||||
className="px-6 lg:px-0 lg:pt-4 lg:pr-4"
|
||||
variants={fadeUpVariants}
|
||||
>
|
||||
<div className="max-w-2xl lg:mx-0 mb-10 md:mb-16">
|
||||
<h2 className="relative font-extrabold text-4xl md:text-5xl lg:text-6xl xl:text-[4.8rem] !leading-tight">
|
||||
Our tool lets you design, control, and achieve{" "}
|
||||
<span className="text-primary">great consistency.</span>
|
||||
</h2>
|
||||
</div>
|
||||
<DiscordButton />
|
||||
</motion.div>
|
||||
<div className="sm:px-6 lg:px-0">
|
||||
<div className="relative isolate overflow-hidden bg-indigo-500 sm:mx-auto sm:max-w-2xl sm:rounded-3xl sm:pr-0 lg:mx-0 lg:max-w-none">
|
||||
<div className="mx-auto max-w-2xl sm:mx-0 sm:max-w-none border-[16px] border-white/10 rounded-3xl overflow-hidden">
|
||||
<video
|
||||
src="/videos/artcraft-3d-demo.mp4"
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
className="aspect-square w-full max-w-none rounded-tl-xl bg-gray-800 ring-1 ring-white/10 object-cover h-full rounded-lg overflow-hidden"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-hidden md:py-24 sm:py-32">
|
||||
<motion.div
|
||||
className="mx-auto max-w-[88rem] md:px-6 lg:px-8"
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
variants={fadeUpVariants}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-20 lg:grid-cols-2 lg:items-center">
|
||||
<div className="sm:px-6 lg:px-0 order-2 lg:order-1">
|
||||
<motion.div
|
||||
className="relative isolate overflow-hidden bg-indigo-500 sm:mx-auto sm:max-w-2xl sm:rounded-3xl sm:pr-0 lg:mx-0 lg:max-w-none"
|
||||
variants={fadeUpVariants}
|
||||
>
|
||||
<div className="mx-auto max-w-2xl sm:mx-0 sm:max-w-none border-[16px] border-white/10 rounded-3xl overflow-hidden">
|
||||
<video
|
||||
src="/videos/artcraft-canvas-demo.mp4"
|
||||
autoPlay
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
className="aspect-square w-full max-w-none rounded-tl-xl bg-gray-800 ring-1 ring-white/10 object-cover h-full rounded-lg overflow-hidden"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
<motion.div
|
||||
className="px-6 lg:px-0 lg:pt-4 lg:pr-4 order-1 lg:order-2"
|
||||
variants={fadeUpVariants}
|
||||
>
|
||||
<div className=" max-w-2xl lg:mx-0 mb-10 md:mb-16">
|
||||
<h2 className="relative font-extrabold text-4xl md:text-5xl lg:text-6xl xl:text-[4.8rem] !leading-tight">
|
||||
<span className="text-primary">Patented</span> Brain to Art
|
||||
Interface
|
||||
</h2>
|
||||
</div>
|
||||
<DiscordButton />
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 mx-auto max-w-screen px-4 py-32 sm:px-24 lg:px-32">
|
||||
{/* Videos Section */}
|
||||
<motion.div
|
||||
className="space-y-12"
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
variants={fadeUpVariants}
|
||||
>
|
||||
<div className="text-center">
|
||||
<h1 className="md:mb-16 text-4xl md:text-7xl font-extrabold">
|
||||
Made using{" "}
|
||||
<span className="relative after:absolute after:bottom-0 after:left-0 after:w-full after:h-[5px] md:after:h-[12px] after:bg-primary/60 after:mb-1">
|
||||
ArtCraft
|
||||
</span>
|
||||
</h1>
|
||||
{/* <p className="text-lg text-gray-300 max-w-2xl mx-auto">
|
||||
See examples of artwork created using ArtCraft.
|
||||
</p> */}
|
||||
</div>
|
||||
<div className="grid gap-8 xl:grid-cols-3">
|
||||
{videos.map((video, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
className="group relative rounded-2xl bg-white/10 backdrop-blur-md p-1.5 transition-all duration-300 hover:bg-white/20 hover:scale-[1.02] hover:shadow-2xl hover:shadow-primary/20"
|
||||
variants={fadeUpVariants}
|
||||
>
|
||||
<div className="aspect-video overflow-hidden rounded-xl">
|
||||
<iframe
|
||||
width="100%"
|
||||
height="100%"
|
||||
src={video.src}
|
||||
title="YouTube video player"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
allowFullScreen
|
||||
className="rounded-xl transition-transform duration-300 group-hover:scale-[1.02]"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute inset-0 rounded-2xl border border-white/10 group-hover:border-white/20 transition-colors duration-300 pointer-events-none" />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 mx-auto max-w-[1920px] px-4 md:pt-32 md:pb-56 sm:px-24 lg:px-32">
|
||||
{/* Discord CTA Section */}
|
||||
<motion.div
|
||||
className="flex flex-col items-center justify-center text-center p-12 py-20 rounded-[40px] bg-primary/30 backdrop-blur-md border-[6px] border-primary/60"
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
variants={fadeUpVariants}
|
||||
>
|
||||
<h2 className="relative mb-12 font-extrabold text-4xl lg:text-6xl max-w-3xl !leading-tight">
|
||||
Why haven't you joined our Discord yet?
|
||||
</h2>
|
||||
{/* <p className="text-lg text-gray-400 mb-8 max-w-2xl">
|
||||
Connect with other creators, share your work, get feedback, and stay
|
||||
updated with the latest features and updates.
|
||||
</p> */}
|
||||
<DiscordButton />
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Landing;
|
||||
@@ -51,6 +51,9 @@
|
||||
},
|
||||
{
|
||||
"path": "../../libs/tauri-utils/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../editor3d"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
{
|
||||
"path": "../../libs/tauri-utils"
|
||||
},
|
||||
{
|
||||
"path": "../editor3d"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import { Environment } from "~/configs";
|
||||
|
||||
export enum FrontendEnvironmentType {
|
||||
Dev,
|
||||
DevProxy,
|
||||
Staging,
|
||||
Production,
|
||||
}
|
||||
|
||||
export class FrontendEnvironment {
|
||||
private static instance: FrontendEnvironment;
|
||||
private readonly environment: FrontendEnvironmentType;
|
||||
private readonly isLocalDev: boolean;
|
||||
|
||||
public static getInstance(): FrontendEnvironment {
|
||||
if (FrontendEnvironment.instance !== undefined) {
|
||||
return FrontendEnvironment.instance;
|
||||
}
|
||||
|
||||
if (typeof document === "undefined") {
|
||||
// TODO(bt,2025-02-10): This is digusting. Vite or whatever
|
||||
// doesn't have 'document' defined early in preloading execution.
|
||||
// This non-cached codepath is dangerous, because who knows what
|
||||
// happens downstream. Fix this ASAP.
|
||||
return new FrontendEnvironment(FrontendEnvironmentType.Production, false);
|
||||
}
|
||||
|
||||
const isLocalDev =
|
||||
document.location.host.includes("localhost") ||
|
||||
document.location.host.startsWith("dev.") ||
|
||||
document.location.host.startsWith("development.");
|
||||
|
||||
let environmentType = FrontendEnvironmentType.Production;
|
||||
|
||||
if (isLocalDev) {
|
||||
environmentType = FrontendEnvironmentType.Dev;
|
||||
}
|
||||
|
||||
if (document.location.host.includes("dev") &&
|
||||
document.location.hostname.includes("proxy")) {
|
||||
environmentType = FrontendEnvironmentType.DevProxy;
|
||||
}
|
||||
|
||||
const instance = new FrontendEnvironment(environmentType, isLocalDev);
|
||||
FrontendEnvironment.instance = instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
public getFrontendEnvironmentType(): FrontendEnvironmentType {
|
||||
return this.environment;
|
||||
}
|
||||
|
||||
public getIsLocalDev(): boolean {
|
||||
return this.isLocalDev;
|
||||
}
|
||||
|
||||
private constructor(environment: FrontendEnvironmentType, isLocalDev: boolean) {
|
||||
this.environment = environment;
|
||||
this.isLocalDev = isLocalDev;
|
||||
}
|
||||
}
|
||||
|
||||
export function GetFrontendEnvironment() : FrontendEnvironment {
|
||||
return FrontendEnvironment.getInstance();
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GetFrontendEnvironment } from "~/Classes/GetFrontendEnvironment";
|
||||
// TODO(bt,2025-05-15): Remove this code
|
||||
|
||||
/**
|
||||
* Return the CDN origin (scheme + host + optional port, but no path)
|
||||
@@ -15,16 +15,9 @@ class CdnHostHelper {
|
||||
return CdnHostHelper.instance;
|
||||
}
|
||||
|
||||
const isLocalDev = GetFrontendEnvironment().getIsLocalDev();
|
||||
|
||||
let origin;
|
||||
if (isLocalDev) {
|
||||
//origin = 'https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev';
|
||||
origin = 'https://cdn-2.fakeyou.com';
|
||||
} else {
|
||||
origin = 'https://cdn-2.fakeyou.com';
|
||||
}
|
||||
const origin = 'https://cdn-2.fakeyou.com';
|
||||
const instance = new CdnHostHelper(origin);
|
||||
|
||||
CdnHostHelper.instance = instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Signal } from "@preact/signals-react";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { setLogoutStates } from "~/signals/authentication/utilities";
|
||||
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
|
||||
import { useAppUiContext } from "~/pages/Page2d/contextSignals/appUi";
|
||||
|
||||
function isEditorPath(path: string) {
|
||||
if (path === "/") return true;
|
||||
@@ -34,11 +35,16 @@ interface Props {
|
||||
}
|
||||
|
||||
const appTabs: TabItem[] = [
|
||||
{ id: "3D", label: "3D" },
|
||||
{ id: "2D", label: "2D" },
|
||||
{ id: "3D", label: "3D" },
|
||||
];
|
||||
|
||||
export const TopBar = ({ pageName, appTabIdSignal, setAppTabId, is3DInitSignal }: Props) => {
|
||||
export const TopBar = ({
|
||||
pageName,
|
||||
appTabIdSignal,
|
||||
setAppTabId,
|
||||
is3DInitSignal,
|
||||
}: Props) => {
|
||||
useSignals();
|
||||
|
||||
const currentLocation = getCurrentLocationWithoutParams(
|
||||
@@ -55,7 +61,7 @@ export const TopBar = ({ pageName, appTabIdSignal, setAppTabId, is3DInitSignal }
|
||||
engine3D?.unmountEngine();
|
||||
}
|
||||
setAppTabId(tabId);
|
||||
}
|
||||
};
|
||||
|
||||
const currentAppTabId = appTabIdSignal.value;
|
||||
const is3DInit = is3DInitSignal.value;
|
||||
@@ -143,6 +149,39 @@ export const TopBar = ({ pageName, appTabIdSignal, setAppTabId, is3DInitSignal }
|
||||
activeTab={activeLibraryTab}
|
||||
onTabChange={setActiveLibraryTab}
|
||||
onDownloadClicked={downloadFileFromUrl}
|
||||
onAddToSceneClicked={async (url) => {
|
||||
console.log("add to scene", url);
|
||||
// Download the image from the URL and convert to File
|
||||
const downloadImage = async (url: string): Promise<File | null> => {
|
||||
try {
|
||||
// Fetch the image
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch image: ${response.status}`);
|
||||
}
|
||||
|
||||
// Get the blob data
|
||||
const blob = await response.blob();
|
||||
|
||||
// Extract filename from URL or use a default name
|
||||
const filename = url.split("/").pop() || "image.png";
|
||||
|
||||
// Create a File object from the blob
|
||||
const file = new File([blob], filename, { type: blob.type });
|
||||
|
||||
return file;
|
||||
} catch (error) {
|
||||
console.error("Error downloading image:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const file = await downloadImage(url);
|
||||
if (file) {
|
||||
console.log("file", file);
|
||||
} else {
|
||||
console.error("Failed to download image");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
+1
-21
@@ -29,6 +29,7 @@ import { PaintModeMenu } from "../../../components/ui/ToolbarMain/PaintModeMenu"
|
||||
// import { EraseModeMenu } from "../../../components/ui/ToolbarMain/EraseModeMenu";
|
||||
// import { BackgroundMenu } from "../../../components/ui/ToolbarMain/BackgroundMenu";
|
||||
// import { bgColor } from "../../../signals/uiEvents/toolbarMain/backgroundMenu";
|
||||
import { useAppUiContext } from "~/pages/Page2d/contextSignals/appUi";
|
||||
|
||||
export const ToolbarMain = ({
|
||||
disabled = false,
|
||||
@@ -147,9 +148,6 @@ export const ToolbarMain = ({
|
||||
<ToolbarButton icon={faImage} buttonProps={buttonProps.ADD_IMAGE}>
|
||||
<span className="text-[16px]">Add Image</span>
|
||||
</ToolbarButton>
|
||||
{/* <ToolbarButton icon={faFilm} buttonProps={buttonProps.ADD_VIDEO}>
|
||||
Add Video
|
||||
</ToolbarButton> */}
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
</div>
|
||||
@@ -191,24 +189,6 @@ export const ToolbarMain = ({
|
||||
tooltip="Paint Brush"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Conditionally show eraser menu on mode selection */}
|
||||
{/* {buttonProps.ERASER.active ? (
|
||||
<EraseModeMenu
|
||||
faIcon={faEraser}
|
||||
borderStyle="border-2 bg-primary/30 border-2 border-primary hover:bg-primary/30 text-white"
|
||||
defaultOpen={true}
|
||||
anchor="right"
|
||||
anchorGap={12}
|
||||
closeOnMouseLeave={true}
|
||||
/>
|
||||
) : (
|
||||
<ToolbarButton
|
||||
icon={faEraser}
|
||||
buttonProps={buttonProps.ERASER}
|
||||
tooltip="Eraser"
|
||||
/>
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import * as THREE from "three";
|
||||
import { GLTFLoader, GLTF } from "three/addons/loaders/GLTFLoader.js";
|
||||
import { MMDLoader } from "three/addons/loaders/MMDLoader.js";
|
||||
|
||||
import environmentVariables from "~/Classes/EnvironmentVariables";
|
||||
|
||||
import { Font } from "three/examples/jsm/loaders/FontLoader.js";
|
||||
import { generateUUID } from "three/src/math/MathUtils.js";
|
||||
import { LoadingPlaceHolderManager } from "./placeholder_manager";
|
||||
@@ -13,8 +10,6 @@ import { ChromaKeyMaterial } from "./chromakey";
|
||||
import { TimeLine } from "./timeline";
|
||||
import { ClipGroup, ClipType } from "~/enums";
|
||||
import { ClipUI } from "../clips/clip_ui";
|
||||
import { GetCdnOrigin } from "~/api/GetCdnOrigin";
|
||||
import { GetFrontendEnvironment } from "~/Classes/GetFrontendEnvironment";
|
||||
import { gridVisibility } from "../signals/engine";
|
||||
import { InfiniteGridHelper } from "./InfiniteGridHelper";
|
||||
import { cameras, selectedCameraId } from "../signals/camera";
|
||||
@@ -382,10 +377,7 @@ class Scene {
|
||||
cameraConfig.position.z,
|
||||
);
|
||||
|
||||
// TODO(bt,2025-02-10): Make the local dev assets more configurable or seedable.
|
||||
const camera_id = GetFrontendEnvironment().getIsLocalDev()
|
||||
? "m_cxh4asqhapdz10j880755dg4yevshb" // Local development
|
||||
: "m_cxh4asqhapdz10j880755dg4yevshb"; // Production
|
||||
const camera_id = "m_cxh4asqhapdz10j880755dg4yevshb";
|
||||
|
||||
this.loadGlbWithPlaceholder(
|
||||
camera_id,
|
||||
|
||||
+1
-4
@@ -1,5 +1,4 @@
|
||||
import { signal } from "@preact/signals-core";
|
||||
import { GetFrontendEnvironment } from "~/Classes/GetFrontendEnvironment";
|
||||
import { AssetType } from "~/enums";
|
||||
import { MediaItem } from "~/pages/PageEnigma/models";
|
||||
|
||||
@@ -26,9 +25,7 @@ export const demoAnimationItems = signal<MediaItem[]>([
|
||||
},
|
||||
{
|
||||
version: 1,
|
||||
media_id: GetFrontendEnvironment().getIsLocalDev() ?
|
||||
"m_z0wj1rckcsea1sb0ht27m41g2w4zss" : // Local development
|
||||
"m_s7d4ems68sb2vqj4pdje1rc15q3ycp", // Production
|
||||
media_id: "m_s7d4ems68sb2vqj4pdje1rc15q3ycp",
|
||||
type: AssetType.ANIMATION,
|
||||
length: 100,
|
||||
name: "Walk",
|
||||
|
||||
+1
-4
@@ -1,14 +1,11 @@
|
||||
import { signal } from "@preact/signals-core";
|
||||
import { MediaItem } from "~/pages/PageEnigma/models";
|
||||
import { AssetType } from "~/enums";
|
||||
import { GetFrontendEnvironment } from "~/Classes/GetFrontendEnvironment";
|
||||
|
||||
export const demoCharacterItems = signal<MediaItem[]>([
|
||||
{
|
||||
version: 1,
|
||||
media_id: GetFrontendEnvironment().getIsLocalDev() ?
|
||||
"m_8dvne30awgsyrb45xvha9f23gtgg8w" : // Local development
|
||||
"m_gff67btr810vg3ng9szj85zskztcgy", // Production
|
||||
media_id: "m_gff67btr810vg3ng9szj85zskztcgy",
|
||||
type: AssetType.CHARACTER,
|
||||
name: "Story Girl",
|
||||
thumbnail: "/resources/characters/img13.png",
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
// import { faEnvelope, faKey, faUser } from "@fortawesome/pro-solid-svg-icons";
|
||||
// import { Button, H1, Input, Link, P } from "~/components";
|
||||
// import { addToast, authentication, signUp } from "~/signals";
|
||||
// import { FormEvent, useRef, useState } from "react";
|
||||
// import { AUTH_STATUS, ToastTypes } from "~/enums";
|
||||
// import { useSignalEffect, useSignals } from "@preact/signals-react/runtime";
|
||||
// import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
|
||||
// const regEx =
|
||||
// /^(([^<>()[\].,;:\s@"]+(.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i;
|
||||
|
||||
// export default function SignUpScreen() {
|
||||
// useSignals();
|
||||
// const navigate = useNavigate();
|
||||
// const formRef = useRef<HTMLFormElement | null>(null);
|
||||
// const [usernameError, setUsernameError] = useState("");
|
||||
// const [emailError, setEmailError] = useState("");
|
||||
// const [passwordError, setPasswordError] = useState("");
|
||||
// const [passwordConfirmationError, setPasswordConfirmationError] =
|
||||
// useState("");
|
||||
// const [searchParams] = useSearchParams();
|
||||
// const { status: authStatus } = authentication;
|
||||
|
||||
// const handleOnSubmit = (ev: FormEvent<HTMLFormElement>) => {
|
||||
// ev.preventDefault();
|
||||
// if (formRef.current) {
|
||||
// const form = new FormData(formRef.current);
|
||||
// const email = form.get("email")?.toString();
|
||||
// const password = form.get("password")?.toString();
|
||||
// const passwordConfirmation = form
|
||||
// .get("password-confirmation")
|
||||
// ?.toString();
|
||||
// const username = form.get("username")?.toString();
|
||||
// setUsernameError("");
|
||||
// if (!username || username.length < 6) {
|
||||
// setUsernameError("Username must be at least 6 characters long");
|
||||
// }
|
||||
// setEmailError("");
|
||||
// if (!email || !email.match(regEx)) {
|
||||
// setEmailError("Email must be a valid email address");
|
||||
// }
|
||||
// setPasswordError("");
|
||||
// if (!password || password.length < 8) {
|
||||
// setPasswordError("Password must be at least 8 characters long");
|
||||
// }
|
||||
// setPasswordConfirmationError("");
|
||||
// if (!passwordError && passwordConfirmation !== password) {
|
||||
// setPasswordConfirmationError("Passwords do not match");
|
||||
// }
|
||||
|
||||
// if (
|
||||
// !usernameError &&
|
||||
// !passwordError &&
|
||||
// !emailError &&
|
||||
// !passwordConfirmationError &&
|
||||
// signUp
|
||||
// ) {
|
||||
// signUp({
|
||||
// username: username!,
|
||||
// email: email!,
|
||||
// password: password!,
|
||||
// passwordConfirmation: passwordConfirmation!,
|
||||
// }).then((error) => {
|
||||
// console.log("Error occured", error);
|
||||
// if (error) {
|
||||
// addToast(ToastTypes.ERROR, error);
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }; // end handleOnSubmit
|
||||
|
||||
// useSignalEffect(() => {
|
||||
// const redirectPath = searchParams.get("redirect");
|
||||
// if (authStatus.value === AUTH_STATUS.LOGGED_IN) {
|
||||
// navigate(redirectPath ? redirectPath : "/");
|
||||
// return;
|
||||
// }
|
||||
// if (authStatus.value === AUTH_STATUS.NO_ACCESS) {
|
||||
// window.open("https://storyteller.ai/", "_self");
|
||||
// return;
|
||||
// }
|
||||
// });
|
||||
|
||||
// return (
|
||||
// <div
|
||||
// className="fixed w-full overflow-scroll"
|
||||
// style={{ height: "calc(100% - 72px)" }}
|
||||
// >
|
||||
// <div className="mx-auto my-6 w-10/12 max-w-2xl">
|
||||
// <H1 className="text-center">Sign Up to Storyteller</H1>
|
||||
// </div>
|
||||
// <div className="mx-auto my-6 w-10/12 max-w-2xl rounded-lg border border-ui-panel-border bg-ui-panel p-6">
|
||||
// <form ref={formRef} onSubmit={handleOnSubmit}>
|
||||
// <Input
|
||||
// label="Username"
|
||||
// icon={faUser}
|
||||
// placeholder="Username"
|
||||
// name="username"
|
||||
// errorMessage={usernameError}
|
||||
// required
|
||||
// />
|
||||
// <br />
|
||||
// <Input
|
||||
// type="email"
|
||||
// label="Email"
|
||||
// icon={faEnvelope}
|
||||
// placeholder="Email"
|
||||
// name="email"
|
||||
// errorMessage={emailError}
|
||||
// required
|
||||
// />
|
||||
// <br />
|
||||
// <Input
|
||||
// type="password"
|
||||
// label="Password"
|
||||
// icon={faKey}
|
||||
// name="password"
|
||||
// placeholder="Password"
|
||||
// autoComplete="current-password"
|
||||
// errorMessage={passwordError}
|
||||
// required
|
||||
// />
|
||||
// <br />
|
||||
// <Input
|
||||
// type="password"
|
||||
// label="Password Confrimation"
|
||||
// icon={faKey}
|
||||
// placeholder="Password Confirmation"
|
||||
// name="password-confirmation"
|
||||
// errorMessage={passwordConfirmationError}
|
||||
// required
|
||||
// />
|
||||
// <br />
|
||||
// <br />
|
||||
// <Button>Sign up</Button>
|
||||
// <div className="flex gap-2">
|
||||
// <P>Back to home</P>
|
||||
// <Link to="/">Back</Link>
|
||||
// </div>
|
||||
// <br />
|
||||
// <div className="flex gap-2">
|
||||
// <P>Already have an account?</P>
|
||||
// <Link to="/login">Log in instead</Link>
|
||||
// </div>
|
||||
// </form>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
@@ -84,6 +84,9 @@
|
||||
},
|
||||
{
|
||||
"path": "../../libs/tauri-utils"
|
||||
},
|
||||
{
|
||||
"path": "../../libs/build-env"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,45 +1,4 @@
|
||||
// import { GetFrontendEnvironment } from "~/Classes/GetFrontendEnvironment";
|
||||
|
||||
/**
|
||||
* Return the CDN origin (scheme + host + optional port, but no path)
|
||||
* See: https://web.dev/articles/url-parts
|
||||
*
|
||||
* TODO(bt,2025-02-10): Make this easier to configure in development
|
||||
*/
|
||||
// class CdnHostHelper {
|
||||
// private static instance: CdnHostHelper;
|
||||
// private readonly origin: string;
|
||||
|
||||
// public static getInstance(): CdnHostHelper {
|
||||
// if (CdnHostHelper.instance !== undefined) {
|
||||
// return CdnHostHelper.instance;
|
||||
// }
|
||||
|
||||
// const isLocalDev = GetFrontendEnvironment().getIsLocalDev();
|
||||
|
||||
// let origin;
|
||||
// if (isLocalDev) {
|
||||
// //origin = 'https://pub-c8a4a5bdbdb048f286b77bdf9f786ff2.r2.dev';
|
||||
// origin = "https://cdn-2.fakeyou.com";
|
||||
// } else {
|
||||
// origin = "https://cdn-2.fakeyou.com";
|
||||
// }
|
||||
// const instance = new CdnHostHelper(origin);
|
||||
// CdnHostHelper.instance = instance;
|
||||
// return instance;
|
||||
// }
|
||||
|
||||
// public getOrigin(): string {
|
||||
// return this.origin;
|
||||
// }
|
||||
|
||||
// private constructor(origin: string) {
|
||||
// this.origin = origin;
|
||||
// }
|
||||
// }
|
||||
|
||||
// TODO(bt,2025-02-10): Consolidate all of this logic.
|
||||
// TODO(bt,2025-05-15): Remove this code
|
||||
export function GetCdnOrigin(): string {
|
||||
// return CdnHostHelper.getInstance().getOrigin();
|
||||
return "https://cdn-2.fakeyou.com";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@nx/react/babel",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": []
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# @frontend/build-env
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test @frontend/build-env` to execute the unit tests via [Vitest](https://vitest.dev/).
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@frontend/build-env",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"development": "./src/index.ts",
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
export * from './lib/BuildEnv';
|
||||
export * from './lib/BuildEnvType';
|
||||
@@ -0,0 +1,30 @@
|
||||
import { BuildEnvType, GetBuildEnvType } from "./BuildEnvType";
|
||||
|
||||
export class BuildEnv {
|
||||
private static instance: BuildEnv;
|
||||
private readonly envType: BuildEnvType;
|
||||
|
||||
public static getInstance(): BuildEnv {
|
||||
if (BuildEnv.instance !== undefined) {
|
||||
return BuildEnv.instance;
|
||||
}
|
||||
|
||||
const envType = GetBuildEnvType();
|
||||
const instance = new BuildEnv(envType);
|
||||
BuildEnv.instance = instance;
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public getType(): BuildEnvType {
|
||||
return this.envType;
|
||||
}
|
||||
|
||||
private constructor(envType: BuildEnvType) {
|
||||
this.envType = envType;
|
||||
}
|
||||
}
|
||||
|
||||
export function GetBuildEnv() : BuildEnv {
|
||||
return BuildEnv.getInstance();
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
|
||||
/**
|
||||
* This is cached whenever vite/remix launches or CI/CD bakes in constants.
|
||||
* It's fed from an environment variable.
|
||||
*/
|
||||
export enum BuildEnvType {
|
||||
Dev,
|
||||
DevProxy,
|
||||
Staging,
|
||||
Production,
|
||||
}
|
||||
|
||||
// Use CI/CD to replace this value with one of the BuildEnvType values.
|
||||
const BUILD_ENV_TYPE = '%BUILD_ENV_TYPE_VALUE%';
|
||||
|
||||
// Attempt to read the environment type.
|
||||
export function GetBuildEnvType() : BuildEnvType {
|
||||
let maybeEnvType : BuildEnvType | undefined = undefined;
|
||||
|
||||
if (!BUILD_ENV_TYPE.startsWith("%") && !BUILD_ENV_TYPE.endsWith("%")) {
|
||||
maybeEnvType = stringToEnvType(BUILD_ENV_TYPE);
|
||||
if (maybeEnvType !== undefined) {
|
||||
return maybeEnvType;
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.env.BUILD_ENV_TYPE !== undefined) {
|
||||
console.log(`Reading environment variable 'BUILD_ENV_TYPE' (= ${import.meta.env.BUILD_ENV_TYPE}) to determine build environment.`)
|
||||
maybeEnvType = stringToEnvType(import.meta.env.BUILD_ENV_TYPE);
|
||||
if (maybeEnvType !== undefined) {
|
||||
return maybeEnvType;
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.env.VITE_ENVIRONMENT_TYPE !== undefined) {
|
||||
console.log(`Reading legacy environment variable 'VITE_ENVIRONMENT_TYPE' (= ${import.meta.env.VITE_ENVIRONMENT_TYPE}) to determine build environment.`)
|
||||
maybeEnvType = stringToEnvType(import.meta.env.VITE_ENVIRONMENT_TYPE);
|
||||
if (maybeEnvType !== undefined) {
|
||||
return maybeEnvType;
|
||||
}
|
||||
}
|
||||
|
||||
// Always default to production.
|
||||
return BuildEnvType.Production;
|
||||
}
|
||||
|
||||
function stringToEnvType(str: string): BuildEnvType | undefined {
|
||||
switch (str.toLocaleLowerCase()) {
|
||||
case "dev":
|
||||
case "development":
|
||||
return BuildEnvType.Dev;
|
||||
case "dev-proxy":
|
||||
case "devproxy":
|
||||
return BuildEnvType.DevProxy;
|
||||
case "stage":
|
||||
case "staging":
|
||||
return BuildEnvType.Staging;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
],
|
||||
"extends": "../../tsconfig.base.json"
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"types": [
|
||||
"node",
|
||||
"@nx/react/typings/cssmodule.d.ts",
|
||||
"@nx/react/typings/image.d.ts",
|
||||
"vite/client"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"jsx": "react-jsx",
|
||||
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo"
|
||||
},
|
||||
"exclude": [
|
||||
"out-tsc",
|
||||
"dist",
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.test.tsx",
|
||||
"**/*.spec.js",
|
||||
"**/*.test.js",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.test.jsx",
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.js",
|
||||
"src/**/*.jsx",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/vitest",
|
||||
"types": [
|
||||
"vitest/globals",
|
||||
"vitest/importMeta",
|
||||
"vite/client",
|
||||
"node",
|
||||
"vitest"
|
||||
],
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/// <reference types='vitest' />
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import * as path from 'path';
|
||||
|
||||
export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../node_modules/.vite/libs/build-env',
|
||||
plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
|
||||
// Uncomment this if you are using workers.
|
||||
// worker: {
|
||||
// plugins: [ nxViteTsPaths() ],
|
||||
// },
|
||||
// Configuration for building your library.
|
||||
// See: https://vitejs.dev/guide/build.html#library-mode
|
||||
build: {
|
||||
outDir: './dist',
|
||||
emptyOutDir: true,
|
||||
reportCompressedSize: true,
|
||||
commonjsOptions: {
|
||||
transformMixedEsModules: true,
|
||||
},
|
||||
lib: {
|
||||
// Could also be a dictionary or array of multiple entry points.
|
||||
entry: 'src/index.ts',
|
||||
name: '@frontend/build-env',
|
||||
fileName: 'index',
|
||||
// Change this to the formats you want to support.
|
||||
// Don't forget to update your package.json as well.
|
||||
formats: ['es' as const]
|
||||
},
|
||||
rollupOptions: {
|
||||
// External packages that should not be bundled into your library.
|
||||
external: ['react','react-dom','react/jsx-runtime']
|
||||
},
|
||||
},
|
||||
test: {
|
||||
watch: false,
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
reporters: ['default'],
|
||||
coverage: {
|
||||
reportsDirectory: './test-output/vitest/coverage',
|
||||
provider: 'v8' as const,
|
||||
}
|
||||
},
|
||||
}));
|
||||
@@ -40,6 +40,7 @@ interface GalleryModalProps {
|
||||
activeTab: string;
|
||||
onTabChange: (tabId: string) => void;
|
||||
onDownloadClicked?: (url: string) => Promise<void>;
|
||||
onAddToSceneClicked?: (url: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const GalleryModal = React.memo(
|
||||
@@ -55,6 +56,7 @@ export const GalleryModal = React.memo(
|
||||
activeTab,
|
||||
onTabChange,
|
||||
onDownloadClicked,
|
||||
onAddToSceneClicked,
|
||||
}: GalleryModalProps) => {
|
||||
const [groupedItems, setGroupedItems] = useState<GroupedItems>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -342,6 +344,7 @@ export const GalleryModal = React.memo(
|
||||
createdAt={lightboxImage?.createdAt}
|
||||
downloadUrl={imageUrl}
|
||||
onDownloadClicked={onDownloadClicked}
|
||||
onAddToSceneClicked={onAddToSceneClicked}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -16,6 +16,7 @@ interface LightboxModalProps {
|
||||
additionalInfo?: React.ReactNode;
|
||||
downloadUrl?: string;
|
||||
onDownloadClicked?: (url: string) => Promise<void>;
|
||||
onAddToSceneClicked?: (url: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function LightboxModal({
|
||||
@@ -29,10 +30,12 @@ export function LightboxModal({
|
||||
additionalInfo,
|
||||
downloadUrl,
|
||||
onDownloadClicked,
|
||||
onAddToSceneClicked,
|
||||
}: LightboxModalProps) {
|
||||
return createPortal(
|
||||
<Transition appear show={isOpen}>
|
||||
<div className="fixed inset-0 z-[100]">
|
||||
{/* backdrop */}
|
||||
<TransitionChild
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
@@ -46,6 +49,8 @@ export function LightboxModal({
|
||||
onClick={onClose}
|
||||
/>
|
||||
</TransitionChild>
|
||||
|
||||
{/* dialog container */}
|
||||
<div
|
||||
className="fixed inset-0 flex items-center justify-center p-4"
|
||||
onClick={onClose}
|
||||
@@ -62,11 +67,15 @@ export function LightboxModal({
|
||||
className="relative h-[90vh] w-[80vw] rounded-xl bg-[#2C2C2C]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* close */}
|
||||
<CloseButton
|
||||
onClick={onClose}
|
||||
className="absolute right-4 top-4 z-10"
|
||||
/>
|
||||
|
||||
{/* content grid */}
|
||||
<div className="grid h-full grid-cols-3 gap-6">
|
||||
{/* image panel */}
|
||||
<div className="col-span-2 flex h-full items-center justify-center overflow-hidden rounded-l-xl bg-black/40">
|
||||
{!imageUrl ? (
|
||||
<div className="flex h-full w-full items-center justify-center bg-gray-800">
|
||||
@@ -81,6 +90,8 @@ export function LightboxModal({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* info + actions */}
|
||||
<div className="flex h-full flex-col py-5 pe-5">
|
||||
<div className="flex-1 space-y-4">
|
||||
<div className="text-xl font-medium">
|
||||
@@ -94,30 +105,44 @@ export function LightboxModal({
|
||||
)}
|
||||
{additionalInfo}
|
||||
</div>
|
||||
{downloadUrl && (
|
||||
<div className="flex justify-end">
|
||||
{onDownloadClicked ? (
|
||||
|
||||
{/* buttons with spacing */}
|
||||
{(onAddToSceneClicked && downloadUrl) || downloadUrl ? (
|
||||
<div className="mt-15 mb-15 flex justify-end gap-2">
|
||||
{onAddToSceneClicked && downloadUrl && (
|
||||
<Button
|
||||
icon={faDownToLine}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await onDownloadClicked(downloadUrl);
|
||||
await onAddToSceneClicked(downloadUrl);
|
||||
}}
|
||||
>
|
||||
Download
|
||||
Add to Current Scene
|
||||
</Button>
|
||||
) : (
|
||||
<a
|
||||
href={downloadUrl}
|
||||
download
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="no-underline"
|
||||
>
|
||||
<Button icon={faDownToLine}>Download</Button>
|
||||
</a>
|
||||
)}
|
||||
|
||||
{downloadUrl &&
|
||||
(onDownloadClicked ? (
|
||||
<Button
|
||||
icon={faDownToLine}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await onDownloadClicked(downloadUrl);
|
||||
}}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
) : (
|
||||
<a
|
||||
href={downloadUrl}
|
||||
download
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="no-underline"
|
||||
>
|
||||
<Button icon={faDownToLine}>Download</Button>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,7 @@ export function LoginModal({
|
||||
return loggedIn;
|
||||
};
|
||||
|
||||
useSoraLoginListener((payload:any) => {
|
||||
useSoraLoginListener((payload: any) => {
|
||||
console.log("Login success!", payload);
|
||||
if (isLoggedInArtCraft) {
|
||||
setStep(4);
|
||||
@@ -73,6 +73,13 @@ export function LoginModal({
|
||||
// Sora session exists, but not logged in to ArtCraft
|
||||
setIsOpen(true);
|
||||
setStep(3);
|
||||
setIsLoggedInArtCraft(false);
|
||||
} else if (!soraSessionExists && loggedIn) {
|
||||
// No Sora session, but logged in to ArtCraft
|
||||
setIsOpen(true);
|
||||
setStep(2);
|
||||
setIsLoggedInArtCraft(true);
|
||||
// setIsOpen(false);
|
||||
} else if (!soraSessionExists) {
|
||||
// No Sora session, start at step 1
|
||||
setIsOpen(true);
|
||||
@@ -411,4 +418,4 @@ export function LoginModal({
|
||||
);
|
||||
}
|
||||
|
||||
export default LoginModal;
|
||||
export default LoginModal;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { toast } from "sonner";
|
||||
import { toast } from "@storyteller/ui-toaster";
|
||||
import {
|
||||
JobContextType,
|
||||
MaybeCanvasRenderBitmapType,
|
||||
@@ -32,6 +32,8 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { IsDesktopApp } from "@storyteller/tauri-utils";
|
||||
import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal";
|
||||
import { PromptsApi } from "@storyteller/api";
|
||||
import { CommandSuccessStatus, GetAppPreferences, SoraImageRemix, SoraImageRemixErrorType } from "@storyteller/tauri-api";
|
||||
import { SoundRegistry } from "@storyteller/soundboard";
|
||||
|
||||
interface ReferenceImage {
|
||||
id: string;
|
||||
@@ -271,19 +273,73 @@ export const PromptBox2D = ({
|
||||
console.log("useSystemPrompt", useSystemPrompt);
|
||||
console.log("Snapshot media token:", snapshotMediaToken.data);
|
||||
|
||||
const generateResponse = await invoke("sora_image_remix_command", {
|
||||
request: {
|
||||
snapshot_media_token: snapshotMediaToken.data,
|
||||
disable_system_prompt: !useSystemPrompt,
|
||||
prompt: prompt,
|
||||
maybe_additional_images: referenceImages.map(
|
||||
(image) => image.mediaToken
|
||||
),
|
||||
maybe_number_of_samples: 1,
|
||||
},
|
||||
//const generateResponse = await invoke("sora_image_remix_command", {
|
||||
// request: {
|
||||
// snapshot_media_token: snapshotMediaToken.data,
|
||||
// disable_system_prompt: !useSystemPrompt,
|
||||
// prompt: prompt,
|
||||
// maybe_additional_images: referenceImages.map(
|
||||
// (image) => image.mediaToken
|
||||
// ),
|
||||
// maybe_number_of_samples: 1,
|
||||
// },
|
||||
//});
|
||||
//console.log("Generate response:", generateResponse);
|
||||
//toast.success("Please wait while we process your image.");
|
||||
|
||||
const generateResponse = await SoraImageRemix({
|
||||
snapshot_media_token: snapshotMediaToken.data,
|
||||
disable_system_prompt: !useSystemPrompt,
|
||||
prompt: prompt,
|
||||
maybe_additional_images: referenceImages.map(
|
||||
(image) => image.mediaToken,
|
||||
),
|
||||
maybe_number_of_samples: 1,
|
||||
});
|
||||
console.log("Generate response:", generateResponse);
|
||||
toast.success("Please wait while we process your image.");
|
||||
|
||||
console.log("generateResponse", generateResponse);
|
||||
|
||||
if (generateResponse.status === CommandSuccessStatus.Success) {
|
||||
const prefs = await GetAppPreferences();
|
||||
const soundName = prefs.preferences?.enqueue_success_sound;
|
||||
if (soundName !== undefined) {
|
||||
const registry = SoundRegistry.getInstance();
|
||||
registry.playSound(soundName);
|
||||
}
|
||||
toast.success("Image added to queue");
|
||||
|
||||
} else {
|
||||
const prefs = await GetAppPreferences();
|
||||
const soundName = prefs.preferences?.enqueue_failure_sound;
|
||||
if (soundName !== undefined) {
|
||||
const registry = SoundRegistry.getInstance();
|
||||
registry.playSound(soundName);
|
||||
}
|
||||
|
||||
let errorMessage = "Failed to enqueue image";
|
||||
|
||||
if ("error_type" in generateResponse) {
|
||||
switch (generateResponse.error_type) {
|
||||
case SoraImageRemixErrorType.SoraLoginRequired:
|
||||
errorMessage = "You need to log into Sora to continue. See the settings menu.";
|
||||
break;
|
||||
case SoraImageRemixErrorType.ServerError:
|
||||
errorMessage = "Server error. Failed to enqueue image.";
|
||||
break;
|
||||
case SoraImageRemixErrorType.TooManyConcurrentTasks:
|
||||
errorMessage = "You have too many Sora image generation tasks running. Please wait a moment.";
|
||||
break;
|
||||
case SoraImageRemixErrorType.SoraUsernameNotYetCreated:
|
||||
errorMessage = "Your Sora username is not yet created. Please visit the Sora.com website to create it first.";
|
||||
break;
|
||||
case SoraImageRemixErrorType.SoraIsHavingProblems:
|
||||
errorMessage = "Sora is having problems. Please try again later.";
|
||||
break;
|
||||
}
|
||||
}
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const handleWebEnqueue = async () => {
|
||||
|
||||
@@ -446,6 +446,9 @@ export const PromptBox3D = ({
|
||||
|
||||
if ("error_type" in generateResponse) {
|
||||
switch (generateResponse.error_type) {
|
||||
case SoraImageRemixErrorType.SoraLoginRequired:
|
||||
errorMessage = "You need to log into Sora to continue. See the settings menu.";
|
||||
break;
|
||||
case SoraImageRemixErrorType.ServerError:
|
||||
errorMessage = "Server error. Failed to enqueue image.";
|
||||
break;
|
||||
|
||||
+2
@@ -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 />
|
||||
|
||||
+69
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -26,8 +26,9 @@ export interface SoraImageRemixSuccess extends CommandResult {
|
||||
export enum SoraImageRemixErrorType {
|
||||
ServerError = "server_error",
|
||||
TooManyConcurrentTasks = "too_many_concurrent_tasks",
|
||||
SoraUsernameNotYetCreated = "sora_username_not_yet_created",
|
||||
SoraIsHavingProblems = "sora_is_having_problems",
|
||||
SoraLoginRequired = "sora_login_required",
|
||||
SoraUsernameNotYetCreated = "sora_username_not_yet_created",
|
||||
}
|
||||
|
||||
export interface SoraImageRemixError extends CommandResult {
|
||||
|
||||
Generated
+48
@@ -16,6 +16,7 @@
|
||||
"@headlessui/react": "^2.2.2",
|
||||
"@preact/signals-react": "^3.0.1",
|
||||
"@remix-run/serve": "^2.15.0",
|
||||
"framer-motion": "^12.11.0",
|
||||
"pnpm": "^10.8.1",
|
||||
"react": "18.3.1",
|
||||
"react-device-detect": "^2.2.3",
|
||||
@@ -2186,6 +2187,10 @@
|
||||
"@tauri-apps/plugin-upload": "2.2.1"
|
||||
}
|
||||
},
|
||||
"libs/build-env": {
|
||||
"name": "@frontend/build-env",
|
||||
"version": "0.0.1"
|
||||
},
|
||||
"libs/common": {
|
||||
"name": "@storyteller/common",
|
||||
"version": "0.0.1"
|
||||
@@ -5019,6 +5024,10 @@
|
||||
"resolved": "apps/artcraft-website",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@frontend/build-env": {
|
||||
"resolved": "libs/build-env",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@frontend/login": {
|
||||
"resolved": "libs/login",
|
||||
"link": true
|
||||
@@ -18610,6 +18619,32 @@
|
||||
"url": "https://github.com/sponsors/rawify"
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "12.11.0",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.11.0.tgz",
|
||||
"integrity": "sha512-BaBPmkhaC2l0n619Kt1nQaxSdUdyyz5V1Z7EKJ1CcraOTZitgVx0RTbL8lmg2XesaFi6o8MPBIhkWDIvzDpGaQ==",
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.11.0",
|
||||
"motion-utils": "^12.9.4",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
@@ -22987,6 +23022,19 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.11.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.11.0.tgz",
|
||||
"integrity": "sha512-CItkGYJenn5ZsbzTX0D9mE0UWdjdd9r535FrxEXhzR8Kwa9I2dLr1uhEJgQPWbgaIJ6i0sNFnf2T9NvVDWQVBw==",
|
||||
"dependencies": {
|
||||
"motion-utils": "^12.9.4"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "12.9.4",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.9.4.tgz",
|
||||
"integrity": "sha512-BW3I65zeM76CMsfh3kHid9ansEJk9Qvl+K5cu4DVHKGsI52n76OJ4z2CUJUV+Mn3uEP9k1JJA3tClG0ggSrRcg=="
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"@headlessui/react": "^2.2.2",
|
||||
"@preact/signals-react": "^3.0.1",
|
||||
"@remix-run/serve": "^2.15.0",
|
||||
"framer-motion": "^12.11.0",
|
||||
"pnpm": "^10.8.1",
|
||||
"react": "18.3.1",
|
||||
"react-device-detect": "^2.2.3",
|
||||
|
||||
@@ -89,6 +89,9 @@
|
||||
},
|
||||
{
|
||||
"path": "./libs/components/switch"
|
||||
},
|
||||
{
|
||||
"path": "./libs/build-env"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user