Compare commits

...
Author SHA1 Message Date
Michael Chung 1d4fd6379b Add icons for all platforms for the desktop app artcraft 2025-05-02 10:18:16 -04:00
Michael Chung 431f2efcf0 Fix login and signup back button links 2025-05-01 23:30:05 -04:00
ArEnScandMichael Chung 7c64da88da Navigation Fixes (#576)
Added Navigation

---------

Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-05-01 22:52:46 -04:00
ArEnScandMichael Chung 93eb78da91 When the desktop app is running, the profile dropdown should not be w… (#575)
…hite.

Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-05-01 22:32:55 -04:00
Brandon Thomas f96e79ff49 use "@storyteller/api" for snapshots and prompting 2025-05-01 22:20:21 -04:00
Brandon Thomas 39e6f4511f use new media files client for importing objects in 3D 2025-05-01 21:41:31 -04:00
Brandon Thomas 4c0d062d76 production build + production origin host fix 2025-05-01 20:57:59 -04:00
Michael Chung 7da40d5177 Moved all the prompting specific code into a separate file lib/PromptsApi.ts 2025-05-01 19:27:49 -04:00
Brandon Thomas f06b5098e2 remove dead package from package lock 2025-05-01 18:52:29 -04:00
Tanish Manku eac503af46 Use shared lightbox component in 3D 2025-05-01 15:48:59 -07:00
Tanish Manku 16932374f5 Some project config improvements (nx + tsconfig) (#561) 2025-05-01 15:09:46 -07:00
ArEnScandMichael Chung 35940a4a63 Fix lightbox modal referrer policy (#574)
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-05-01 18:03:33 -04:00
ArEnScandMichael Chung 55543b3735 Launch editor3d in the background and delete myapp (#573)
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-05-01 17:52:31 -04:00
Brandon Thomas 3d44e6e29e "fix cors" 2025-05-01 17:44:39 -04:00
Brandon Thomas 3244b802fa cors headers for vite dev 2025-05-01 17:30:05 -04:00
Brandon Thomas 5e7a5d2cff fixing cors mixed with other refactoring 2025-05-01 17:18:39 -04:00
ArEnScandMichael Chung ecb73e4f80 Gallery fixes using thumbnail template. (#572)
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-05-01 16:45:15 -04:00
ArEnScandMichael Chung 39092fa866 Fix image gallery data mapping (#571)
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-05-01 16:36:16 -04:00
ArEnScandMichael Chung ca80275d28 Fix image gallery data mapping (#570)
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-05-01 16:35:58 -04:00
Brandon Thomas 8c192b17f1 (messy) upload sora media files end-to-end 2025-05-01 16:30:44 -04:00
Brandon Thomas 40da6f6500 (messy) uploading to storyteller backend works, sans creds 2025-05-01 16:30:44 -04:00
ArEnScandMichael Chung 2a3982aa5a Feature/fix desktop 3d run (#569)
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-05-01 15:43:12 -04:00
Brandon Thomas a1c2b4f5c8 revert script, add copy for michael 2025-05-01 15:17:32 -04:00
ArEnScandMichael Chung 213d3f7bd2 Feature/fix desktop 2d run (#568)
added a quick runner in 2d editor and 3d editor

---------

Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-05-01 15:11:23 -04:00
Brandon Thomas 0788e98b54 pass storyteller creds manager 2025-05-01 15:08:31 -04:00
Brandon Thomas 65c8ddfa91 sync creds now 2025-05-01 15:08:31 -04:00
ArEnScandMichael Chung a60e20c0b4 Added run run_desktop_3d (#567)
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-05-01 14:16:49 -04:00
Brandon Thomas 54d417f4c5 use vendored tauri-plugin-http from tauri app 2025-05-01 14:02:05 -04:00
Brandon Thomas 98a74e3a96 Begin vendoring in tauri-plugin-http @ 144461bd (see notes)
From fork of plugins here:
https://github.com/tauri-apps/tauri-plugin-http

SHA: 144461bd068300f7436db25c6c7ad952dbefdb21

The original upstream is here:

https://github.com/tauri-apps/plugins-workspace
2025-05-01 13:50:05 -04:00
Michael Chung b537223ffa Fix issue with the 3d editor developer experience kill dev server 2025-05-01 13:47:03 -04:00
Brandon Thomas 8001f24195 fix request proxying 2025-05-01 13:35:04 -04:00
BFlat 778a4e72c8 posthog fix editor 3d? (#566) 2025-05-01 23:54:54 +07:00
bflatastic 083680232c editor2d posthog api key as env variable 2025-05-01 23:29:41 +07:00
Brandon Thomas b1b44ceb60 proxy requests through tauri to avoid 3rd party cookie ban (#565) 2025-05-01 04:04:06 -04:00
Brandon Thomas d988acbf6e document in code 2025-05-01 02:16:21 -04:00
Brandon Thomas 6e05b7a1b5 this seemingly avoids the startup race condition (see details)
There's an issue when the "main window thread" inquires about the
webview cookies shortly after app startup. When it attempts to dump the
cookies within a few milliseconds of app launch, it deadlocks and the
app never launches correctly. The entire webview goes blank and the app
freezes.

The timed waiting seems to fix this entirely.
2025-05-01 02:13:27 -04:00
Brandon Thomas 8a364605cf bug is still present, might be a race condition 2025-05-01 01:10:45 -04:00
Brandon Thomas 30a8e72efe comment out broken thread 2025-05-01 00:32:55 -04:00
Brandon Thomas ff1a83898b correct persistence behavior for window / storyteller cookies ? 2025-05-01 00:01:21 -04:00
Brandon Thomas f1b9299e46 memory store utilties to reuse 2025-04-30 23:43:24 -04:00
Brandon Thomas 2eefaaf7a1 storyteller cookie saving; temporarily broke window size persist 2025-04-30 22:31:20 -04:00
Brandon Thomas a21b4a4275 storyteller credentials store 2025-04-30 22:31:20 -04:00
ArEnScandMichael Chung e4c561eb41 Fixing imports and removing unused code and working on scene management. (#563)
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-04-30 22:01:12 -04:00
Tanish Manku 420a23d359 Disable pointer lock controls if we detect safari webkit (#562) 2025-04-30 18:10:18 -07:00
Brandon Thomas 9cd3b493c6 command with info on platform 2025-04-30 20:13:12 -04:00
Tanish Manku bbd5953000 Use arrow keys to rotate cam (#560) 2025-04-30 16:41:08 -07:00
Brandon Thomas b346750968 intellij default project settings 2025-04-30 18:42:13 -04:00
Brandon Thomas e5749f0e77 storyteller api credentials WIP, but need to switch gears 2025-04-30 18:32:52 -04:00
bflatastic 826cf00cbb new videos for 2d and 3d 2025-04-30 16:52:55 +07:00
bflatastic 6b7d034068 added tauri2.conf.json 2025-04-30 15:39:18 +07:00
Brandon Thomas 7bd3f6f8c6 remove core-foundation for breaking windows 2025-04-30 03:42:32 -04:00
bflatastic 0c550fcb1a fixed empty snapshot data string 2025-04-30 14:11:41 +07:00
Brandon Thomas 3b2b7ede18 open sora login dialog on 3d 2025-04-30 03:10:29 -04:00
Brandon Thomas 782b68e9a2 move credentials dir 2025-04-30 02:58:57 -04:00
Brandon Thomas 5c23568646 refactor directory support 2025-04-30 02:41:51 -04:00
Brandon Thomas 43e781571f save window size between runs (super annoying to not have this) 2025-04-29 23:38:32 -04:00
Brandon Thomas 84361eb93e polling works 2025-04-29 21:59:00 -04:00
Brandon Thomas 38ab84d52e rename script 2025-04-29 20:07:06 -04:00
Brandon Thomas 82e87fa883 much better way to launch tauri dev (this was annoying to discover) 2025-04-29 20:04:56 -04:00
Brandon Thomas de5daa3f92 it's hard to build tauri in dev mode without it stepping on our feet 2025-04-29 19:37:19 -04:00
Brandon Thomas fc0fba7573 task draining / filtering 2025-04-29 17:34:49 -04:00
Brandon Thomas b199738ee0 auto-healing task polling, beginnings of a task polling thread 2025-04-29 16:29:08 -04:00
Tanish MankuandTanish Manku aa76113e8c Fix 3D performance decay after generation (#559)
Use 3JS size api instead of direct domElement access (avoids implicit
pixel density scaling)

Co-authored-by: Tanish Manku <-h>
2025-04-29 01:29:24 -07:00
Brandon Thomas bfe02a7880 dispatch to tauri or web on 3D 2025-04-29 00:29:36 -04:00
Brandon Thomas ac81de6a48 artcraft desktop: report discord status 2025-04-28 23:13:26 -04:00
Brandon Thomas 2c2a63e80b nit: fix metadata 2025-04-28 21:55:23 -04:00
Michael Chung 3521f73142 Fix ux issue with prompt box when enqueueing 2025-04-28 20:58:55 -04:00
ArEnScandMichael Chung 88fd732eec Ok fixed some minor issues. (#558)
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-04-28 20:02:36 -04:00
Brandon Thomas 490915a3e7 artcraft website build configs 2025-04-28 18:24:36 -04:00
bflatastic 1802b0939e fix object highlight, camera speed, after delete character hide and toggle out of FK mode 2025-04-28 22:53:33 +07:00
bflatastic 36b6aa78f8 fix visible fps lag on higher refresh rate screens 2025-04-28 22:28:04 +07:00
bflatastic 499c07548c bug fix: undefined fetch on page load 2025-04-28 22:07:06 +07:00
Kasisnu 65580ec502 small tweaks to imagen callback 2025-04-28 20:15:30 +05:30
bflatastic 5d5b782760 only update camera when there's change 2025-04-28 21:36:35 +07:00
BFlat 09d0ae263f freecam camera smoothing (#556) 2025-04-28 15:39:59 +07:00
Kasisnu c3509d770f updated sqlx migrations 2025-04-28 13:11:38 +05:30
bflatastic ee1043f92c add button styling fix 2025-04-28 14:30:51 +07:00
ArEnScandMichael Chung e88b6ce39b Feature/UI sign in (#555)
Co-authored-by: Michael Chung <michaelchung@michaels-mbp-2.home>
2025-04-27 22:42:34 -04:00
Brandon Thomas 0e535e52ad update job state from endpoint 2025-04-27 02:41:00 -04:00
Brandon Thomas aa594b773e upload media files from gpt-image-1 2025-04-27 02:17:53 -04:00
Brandon Thomas ac209f8f4f builder for media file records 2025-04-27 00:19:54 -04:00
Brandon Thomas 66abfd18ba simple image medadata functions 2025-04-26 21:42:38 -04:00
Brandon Thomas 79cf730270 bundled mimetype / file extension detection 2025-04-26 20:44:31 -04:00
Brandon Thomas 698351f2a3 stub endpoint for updating gpt image job statuses 2025-04-26 03:20:45 -04:00
Brandon Thomas fc4eaff7f6 info -> debug logging 2025-04-25 04:30:00 -04:00
Brandon Thomas dadf14dbf4 accept reference images 2025-04-25 03:52:44 -04:00
Brandon Thomas ff695d7af6 more logging 2025-04-25 03:45:12 -04:00
Brandon Thomas a5e9bf2262 more logging 2025-04-25 02:39:23 -04:00
Brandon Thomas b49da80762 stop logging this in production 2025-04-25 02:19:24 -04:00
Brandon Thomas a0db62f0fd don't use this thing 2025-04-24 23:00:22 -04:00
Brandon Thomas ebc0f1ae42 auto-healing credentials in all pathways 2025-04-24 22:56:28 -04:00
Brandon Thomas dc5f5c5906 auto-renewing image gen 2025-04-24 20:20:20 -04:00
Brandon Thomas 98b7042192 much better session behavior on desktop app (large refactor) (#553) 2025-04-24 19:16:45 -04:00
bflatastic 76db001f59 updated button component to support href links 2025-04-24 23:11:16 +07:00
bflatastic 60637394d6 artcraft website: discord CTA and videos section from youtube 2025-04-24 23:02:27 +07:00
BFlatandTanish Manku f3b5a77475 working shared gallery component for both 2d and 3d (#551)
shared gallery component for 2d and 3d editor.
prompt box reference images are working too.

---------

Co-authored-by: Tanish Manku <-h>
2025-04-24 20:40:01 +07:00
Tanish MankuandTanish Manku d3e1d7fab9 Fix api lib (ApiManager) imports (#552)
Co-authored-by: Tanish Manku <-h>
2025-04-24 00:10:55 -07:00
369 changed files with 10974 additions and 5257 deletions
+13
View File
@@ -0,0 +1,13 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<codeStyleSettings language="Rust">
<option name="RIGHT_MARGIN" value="180" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_PARAMETERS_IN_CALLS" value="false" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>
+5
View File
@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nUPDATE generic_inference_jobs\nSET\n status = \"complete_success\",\n on_success_result_entity_type = ?,\n on_success_result_entity_token = ?,\n failure_reason = NULL,\n internal_debugging_failure_reason = NULL,\n success_execution_millis = ?,\n success_inference_execution_millis = ?,\n retry_at = NULL,\n successfully_completed_at = NOW()\nWHERE token = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "2bd7a5b886c9624ae48533ddac409cfca0668f30ab1f7016c412876b5904168b"
}
Generated
+144 -14
View File
@@ -840,11 +840,14 @@ version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc2d0cfb2a7388d34f590e76686704c494ed7aaceed62ee1ba35cbf363abc2a5"
dependencies = [
"brotli 3.5.0",
"flate2",
"futures-core",
"memchr",
"pin-project-lite",
"tokio",
"zstd 0.13.1",
"zstd-safe 7.1.0",
]
[[package]]
@@ -1921,7 +1924,7 @@ checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
dependencies = [
"byteorder",
"fnv",
"uuid",
"uuid 1.8.0",
]
[[package]]
@@ -2877,6 +2880,12 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]]
name = "data-url"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
[[package]]
name = "datetimes"
version = "0.0.1"
@@ -3258,6 +3267,20 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "discord-rich-presence"
version = "0.2.5"
source = "git+https://github.com/vionya/discord-rich-presence#9c6cabe04a70cfcc822affd1bf8be51321f8a821"
dependencies = [
"log",
"serde",
"serde_derive",
"serde_json",
"serde_repr",
"thiserror 2.0.12",
"uuid 0.8.2",
]
[[package]]
name = "dispatch"
version = "0.2.0"
@@ -5325,6 +5348,7 @@ dependencies = [
"hyper 1.6.0",
"hyper-util",
"rustls 0.23.13",
"rustls-native-certs",
"rustls-pki-types",
"tokio",
"tokio-rustls",
@@ -5547,7 +5571,7 @@ dependencies = [
name = "idempotency"
version = "0.0.1"
dependencies = [
"uuid",
"uuid 1.8.0",
"workspace-build-acceleration",
]
@@ -5653,8 +5677,10 @@ dependencies = [
name = "images"
version = "0.0.1"
dependencies = [
"anyhow",
"errors",
"image",
"mimetypes",
"testing",
"workspace-build-acceleration",
]
@@ -6768,6 +6794,15 @@ dependencies = [
"workspace-build-acceleration",
]
[[package]]
name = "memory_store"
version = "0.0.1"
dependencies = [
"anyhow",
"errors",
"workspace-build-acceleration",
]
[[package]]
name = "metal"
version = "0.27.0"
@@ -6868,7 +6903,9 @@ dependencies = [
name = "mimetypes"
version = "0.0.1"
dependencies = [
"infer 0.16.0",
"anyhow",
"infer 0.19.0",
"testing",
"workspace-build-acceleration",
]
@@ -7793,7 +7830,7 @@ dependencies = [
"toml 0.8.20",
"ureq",
"url",
"uuid",
"uuid 1.8.0",
"walkdir",
]
@@ -7844,12 +7881,14 @@ dependencies = [
"anyhow",
"base64 0.22.1",
"chrono",
"cookie 0.18.1",
"crockford",
"errors",
"filesys",
"hex-literal",
"idempotency",
"log",
"once_cell",
"rand 0.9.0",
"reqwest 0.12.15",
"serde",
@@ -9618,6 +9657,7 @@ dependencies = [
"pin-project-lite",
"quinn",
"rustls 0.23.13",
"rustls-native-certs",
"rustls-pemfile 2.1.2",
"rustls-pki-types",
"serde",
@@ -9628,6 +9668,7 @@ dependencies = [
"tokio",
"tokio-native-tls",
"tokio-rustls",
"tokio-socks",
"tokio-util",
"tower 0.5.2",
"tower-service",
@@ -9698,7 +9739,7 @@ dependencies = [
"rkyv_derive",
"seahash",
"tinyvec",
"uuid",
"uuid 1.8.0",
]
[[package]]
@@ -9955,6 +9996,19 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a"
dependencies = [
"openssl-probe",
"rustls-pemfile 2.1.2",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.4"
@@ -10099,7 +10153,7 @@ dependencies = [
"serde",
"serde_json",
"url",
"uuid",
"uuid 1.8.0",
]
[[package]]
@@ -10633,6 +10687,7 @@ dependencies = [
"log",
"openai_sora_client",
"r2d2_redis",
"reqwest 0.12.15",
"workspace-build-acceleration",
]
@@ -11188,6 +11243,7 @@ dependencies = [
"hostname 0.4.0",
"http_server_common",
"idempotency",
"images",
"infer 0.16.0",
"language-tags",
"lexical-sort",
@@ -11245,7 +11301,7 @@ dependencies = [
"user_traits_component",
"utoipa",
"utoipa-swagger-ui",
"uuid",
"uuid 1.8.0",
"videos",
"workspace-build-acceleration",
"zip 0.6.6",
@@ -11258,9 +11314,11 @@ dependencies = [
"anyhow",
"backoff",
"chrono",
"cookie 0.18.1",
"enums",
"enums_public",
"errors",
"idempotency",
"log",
"reqwest 0.12.15",
"serde",
@@ -11269,6 +11327,7 @@ dependencies = [
"tokens",
"tokio",
"url",
"uuid 1.8.0",
"workspace-build-acceleration",
]
@@ -12039,13 +12098,16 @@ dependencies = [
"crockford",
"directories 6.0.0",
"dirs 6.0.0",
"discord-rich-presence",
"errors",
"filesys",
"futures 0.3.29",
"image",
"indexmap 2.9.0",
"libloading 0.8.6",
"log",
"memory_caching",
"memory_store",
"once_cell",
"openai_sora_client",
"primitives",
@@ -12060,6 +12122,7 @@ dependencies = [
"subprocess_common",
"tauri",
"tauri-build",
"tauri-plugin-http",
"tauri-plugin-log",
"tempdir",
"tempfile",
@@ -12114,7 +12177,7 @@ dependencies = [
"thiserror 2.0.12",
"time",
"url",
"uuid",
"uuid 1.8.0",
"walkdir",
]
@@ -12134,9 +12197,9 @@ dependencies = [
[[package]]
name = "tauri-plugin"
version = "2.1.1"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4190775d6ff73fe66d9af44c012739a2659720efd9c0e1e56a918678038699d"
checksum = "37a5ebe6a610d1b78a94650896e6f7c9796323f408800cef436e0fa0539de601"
dependencies = [
"anyhow",
"glob",
@@ -12149,6 +12212,52 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-fs"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88371e340ad2f07409a3b68294abe73f20bc9c1bc1b631a31dc37a3d0161f682"
dependencies = [
"anyhow",
"dunce",
"glob",
"percent-encoding",
"schemars",
"serde",
"serde_json",
"serde_repr",
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.12",
"toml 0.8.20",
"url",
"uuid 1.8.0",
]
[[package]]
name = "tauri-plugin-http"
version = "2.4.3"
dependencies = [
"bytes 1.10.0",
"cookie_store",
"data-url",
"http 1.1.0",
"regex",
"reqwest 0.12.15",
"schemars",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror 2.0.12",
"tokio",
"tracing",
"url",
"urlpattern",
]
[[package]]
name = "tauri-plugin-log"
version = "2.3.1"
@@ -12261,9 +12370,9 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "2.3.1"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f037e66c7638cc0a2213f61566932b9a06882b8346486579c90e4b019bac447"
checksum = "b2900399c239a471bcff7f15c4399eb1a8c4fe511ba2853e07c996d771a5e0a4"
dependencies = [
"anyhow",
"brotli 7.0.0",
@@ -12293,7 +12402,7 @@ dependencies = [
"toml 0.8.20",
"url",
"urlpattern",
"uuid",
"uuid 1.8.0",
"walkdir",
]
@@ -12746,6 +12855,18 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-socks"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f"
dependencies = [
"either",
"futures-util",
"thiserror 1.0.63",
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.16"
@@ -13537,6 +13658,15 @@ dependencies = [
"zip 1.1.4",
]
[[package]]
name = "uuid"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [
"getrandom 0.2.14",
]
[[package]]
name = "uuid"
version = "1.8.0"
@@ -14698,7 +14828,7 @@ dependencies = [
"unicode-normalization",
"ureq",
"url",
"uuid",
"uuid 1.8.0",
"void",
"winapi 0.3.9",
"windows-sys 0.48.0",
+13
View File
@@ -39,6 +39,7 @@ members = [
"crates/lib/bootstrap",
"crates/lib/bucket_paths",
"crates/lib/caching/memory_caching",
"crates/lib/caching/memory_store",
"crates/lib/caching/redis_caching",
"crates/lib/concurrency",
"crates/lib/cookies",
@@ -117,6 +118,7 @@ members = [
"crates/vendor/candle/candle-transformers",
"crates/vendor/candle/tensor-tools",
"crates/vendor/limitation",
"crates/vendor/tauri-plugin-http",
# NB(bt,2023-12-12): Newrelic keeps breaking with upgrades. Kill for now.
#"crates/vendor/newrelic-telemetry-sdk-rust",
@@ -147,6 +149,10 @@ publish = false
# - tauri-runtime-wry
# - tauri-utils
#
# Broken on Windows:
# - core-foundation
# - core-foundation-sys
#
workspace-build-acceleration = { path = "crates/workspace-build-acceleration" }
# External libs
@@ -155,7 +161,9 @@ workspace-build-acceleration = { path = "crates/workspace-build-acceleration" }
anyhow = "1.0.97"
base64 = "0.22.1"
chrono = { version = "0.4.40", features = ["serde"] }
cookie = "0.18.1"
hex-literal = "1.0.0"
indexmap = "2.9.0"
log = "0.4.27"
once_cell = "1.21.3"
rand = "0.9.0"
@@ -166,6 +174,7 @@ serde_derive = "1.0.219"
serde_json = "1.0.140"
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"
url = { version = "2.5.4", features = ["serde"] }
@@ -176,7 +185,11 @@ enums = { path = "crates/_types/enums" }
enums_public = { path = "crates/_types/enums_public" }
errors = { path = "crates/lib/errors" }
filesys = { path = "crates/lib/files/filesys" }
idempotency = { path = "crates/lib/idempotency" }
images = { path = "crates/lib/files/images" }
memory_caching = { path = "crates/lib/caching/memory_caching" }
memory_store = { path = "crates/lib/caching/memory_store" }
mimetypes = { path = "crates/lib/files/mimetypes" }
openai_sora_client = { path = "crates/lib/api_clients/openai_sora_client" }
primitives = { path = "crates/lib/data/primitives" }
shared_service_components = { path = "crates/lib/shared_service_components" }
@@ -28,11 +28,12 @@ pub enum MediaFileOriginCategory {
DeviceApi,
/// From Storyteller Studio Engine
#[deprecated(note="This db field should only denote file provenance, not the product.")]
#[serde(rename = "studio")]
StorytellerStudio,
/// From Storyteller Studio Engine
#[deprecated(note = "Use StorytellerStudio instead")]
#[deprecated(note = "DO NOT USE. Originally deprecated in favor of `StorytellerStudio`.")]
StoryEngine,
}
@@ -39,6 +39,10 @@ pub enum MediaFileOriginProductCategory {
#[serde(rename = "vst")]
VideoStyleTransfer,
/// Image Studio
#[serde(rename = "image_studio")]
ImageStudio,
/// Storyteller Studio
#[serde(rename = "studio")]
StorytellerStudio,
@@ -87,6 +91,7 @@ impl MediaFileOriginProductCategory {
Self::FaceFusion => "face_fusion",
Self::FaceMirror => "face_mirror",
Self::VideoStyleTransfer => "vst",
Self::ImageStudio => "image_studio",
Self::StorytellerStudio => "studio",
Self::TextToSpeech => "tts",
Self::VoiceConversion => "voice_conversion",
@@ -105,6 +110,7 @@ impl MediaFileOriginProductCategory {
"face_fusion" => Ok(Self::FaceFusion),
"face_mirror" => Ok(Self::FaceMirror),
"vst" => Ok(Self::VideoStyleTransfer),
"image_studio" => Ok(Self::ImageStudio),
"studio" => Ok(Self::StorytellerStudio),
"tts" => Ok(Self::TextToSpeech),
"voice_conversion" => Ok(Self::VoiceConversion),
@@ -126,6 +132,7 @@ impl MediaFileOriginProductCategory {
Self::FaceFusion,
Self::FaceMirror,
Self::VideoStyleTransfer,
Self::ImageStudio,
Self::StorytellerStudio,
Self::TextToSpeech,
Self::VoiceConversion,
@@ -153,6 +160,7 @@ mod tests {
assert_serialization(MediaFileOriginProductCategory::FaceFusion, "face_fusion");
assert_serialization(MediaFileOriginProductCategory::FaceMirror, "face_mirror");
assert_serialization(MediaFileOriginProductCategory::VideoStyleTransfer, "vst");
assert_serialization(MediaFileOriginProductCategory::ImageStudio, "image_studio");
assert_serialization(MediaFileOriginProductCategory::StorytellerStudio, "studio");
assert_serialization(MediaFileOriginProductCategory::TextToSpeech, "tts");
assert_serialization(MediaFileOriginProductCategory::VoiceConversion, "voice_conversion");
@@ -170,6 +178,7 @@ mod tests {
assert_eq!(MediaFileOriginProductCategory::FaceFusion.to_str(), "face_fusion");
assert_eq!(MediaFileOriginProductCategory::FaceMirror.to_str(), "face_mirror");
assert_eq!(MediaFileOriginProductCategory::VideoStyleTransfer.to_str(), "vst");
assert_eq!(MediaFileOriginProductCategory::ImageStudio.to_str(), "image_studio");
assert_eq!(MediaFileOriginProductCategory::StorytellerStudio.to_str(), "studio");
assert_eq!(MediaFileOriginProductCategory::TextToSpeech.to_str(), "tts");
assert_eq!(MediaFileOriginProductCategory::VoiceConversion.to_str(), "voice_conversion");
@@ -187,6 +196,7 @@ mod tests {
assert_eq!(MediaFileOriginProductCategory::from_str("face_fusion").unwrap(), MediaFileOriginProductCategory::FaceFusion);
assert_eq!(MediaFileOriginProductCategory::from_str("face_mirror").unwrap(), MediaFileOriginProductCategory::FaceMirror);
assert_eq!(MediaFileOriginProductCategory::from_str("vst").unwrap(), MediaFileOriginProductCategory::VideoStyleTransfer);
assert_eq!(MediaFileOriginProductCategory::from_str("image_studio").unwrap(), MediaFileOriginProductCategory::ImageStudio);
assert_eq!(MediaFileOriginProductCategory::from_str("studio").unwrap(), MediaFileOriginProductCategory::StorytellerStudio);
assert_eq!(MediaFileOriginProductCategory::from_str("tts").unwrap(), MediaFileOriginProductCategory::TextToSpeech);
assert_eq!(MediaFileOriginProductCategory::from_str("voice_conversion").unwrap(), MediaFileOriginProductCategory::VoiceConversion);
@@ -200,12 +210,13 @@ mod tests {
#[test]
fn all_variants() {
let mut variants = MediaFileOriginProductCategory::all_variants();
assert_eq!(variants.len(), 13);
assert_eq!(variants.len(), 14);
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::Unknown));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::FaceAnimator));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::FaceFusion));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::FaceMirror));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::VideoStyleTransfer));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::ImageStudio));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::StorytellerStudio));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::TextToSpeech));
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::VoiceConversion));
+12 -5
View File
@@ -27,6 +27,7 @@ crockford = { workspace = true }
errors = { workspace = true }
filesys = { workspace = true }
memory_caching = { workspace = true }
memory_store = { workspace = true }
openai_sora_client = { workspace = true }
primitives = { workspace = true }
storyteller_client = { workspace = true }
@@ -34,10 +35,8 @@ subprocess_common = { workspace = true }
tokens = { workspace = true }
# External
#openssl = { version = "0.10", features = ["vendored"] }
# NB(bt,2025-03-10): default-features = false on Windows while I debug openssl / rustls
anyhow = { workspace = true }
base64 = "0.22.1"
base64 = { workspace = true }
bytes = "1.10"
chrono = { workspace = true }
const_format = "0.2.34"
@@ -45,21 +44,29 @@ directories = "6.0.0"
dirs = "6.0.0"
futures = "0.3"
image = "0.25.5"
indexmap = { workspace = true }
log = { workspace = true }
once_cell = { workspace = true }
rand = { workspace = true }
reqwest = { workspace = true, features = ["multipart", "stream"], default-features = false }
reqwest = { workspace = true, features = ["multipart", "stream"], default-features = false } # NB(bt,2025-03-10): default-features = false on Windows while I debug openssl / rustls
serde = { workspace = true }
serde_derive = { workspace = true }
serde_json = { workspace = true }
serde_yaml = { workspace = true }
tauri = { version = "2.4.1", features = ["unstable"] }
tauri = { workspace = true }
tauri-plugin-log = "2.3.1"
tempdir = "0.3.7"
tempfile = "3.18.0"
tokio = { version = "1.42", features = ["rt", "rt-multi-thread", "fs"] }
tokio-util = { version = "0.7", features = ["codec"] }
# NB: We need to vendor this in so that we can access the cookies inside via the private API
tauri-plugin-http = { path = "../../vendor/tauri-plugin-http" }
# NB: The version of the `discord-rich-presence` crate (0.2.5) last published has non-Send/Sync error
# types and can't do threading. We need to wait for a new version to unpin from Git.
discord-rich-presence = { git = "https://github.com/vionya/discord-rich-presence" }
[dev-dependencies]
libloading = "0.8.6"
serial_test = "3.2.0"
@@ -7,6 +7,14 @@
],
"permissions": [
"core:default",
"log:default"
"log:default",
{
"identifier": "http:default",
"allow": [
{ "url": "https://*.storyteller.ai" },
{ "url": "https://*.fakeyou.com" }
],
"deny": [{ "url": "https://private.tauri.app" }]
}
]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1023 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

@@ -1,2 +1,3 @@
pub mod flip_image;
pub mod platform_info_command;
pub mod sora;
@@ -0,0 +1,63 @@
use crate::state::os_platform::OsPlatform;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::ImageReader;
use log::info;
use serde_derive::Serialize;
use std::io::Cursor;
#[derive(Debug, Serialize)]
pub struct PlatformInfoResponse {
pub os_platform: DetectedOs,
pub webview_runtime: WebviewRuntime,
}
#[derive(Copy, Clone, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DetectedOs {
Windows,
#[serde(rename = "macos")]
MacOS,
Linux,
Unknown,
}
#[derive(Copy, Clone, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum WebviewRuntime {
#[serde(rename = "webkit_safari")]
WebkitSafari,
#[serde(rename = "webkit_gtk")]
WebkitGtk,
#[serde(rename = "edge_webview_2")]
EdgeWebview2,
Unknown,
}
#[tauri::command]
pub fn platform_info_command() -> Result<PlatformInfoResponse, String> {
info!("platform_info_command called...");
let os_platform = match OsPlatform::maybe_get() {
Some(OsPlatform::Linux) => DetectedOs::Linux,
Some(OsPlatform::MacOs) => DetectedOs::MacOS,
Some(OsPlatform::Windows) => DetectedOs::Windows,
None => DetectedOs::Unknown,
};
// These are the webviews that Tauri uses on each OS:
// https://tauri.app/reference/webview-versions/
let webview_runtime = match os_platform {
DetectedOs::Windows => WebviewRuntime::EdgeWebview2,
DetectedOs::MacOS => WebviewRuntime::WebkitSafari,
DetectedOs::Linux => WebviewRuntime::WebkitGtk,
DetectedOs::Unknown => WebviewRuntime::Unknown,
};
Ok(PlatformInfoResponse {
os_platform,
webview_runtime,
})
}
@@ -1,4 +1,4 @@
use crate::state::app_dir::AppDataRoot;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::sora::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
use base64::prelude::BASE64_STANDARD;
@@ -8,10 +8,10 @@ use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::{DynamicImage, ImageReader};
use log::{error, info};
use openai_sora_client::credentials::SoraCredentials;
use openai_sora_client::creds::credential_migration::CredentialMigrationRef;
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::SoraMediaUploadRequest;
use std::fs::read_to_string;
use std::io::Cursor;
use tauri::{AppHandle, Manager, State};
@@ -43,14 +43,12 @@ pub async fn generate_image(
sora_creds_holder: &SoraCredentialHolder,
) -> AnyhowResult<()> {
let sora_credentials = read_sora_credentials_from_disk(app_data_root)
let creds = read_sora_credentials_from_disk(app_data_root)
.map_err(|err| {
error!("Failed to read Sora credentials from disk: {:?}", err);
err
})?;
//let sora_credentials = sora_creds_holder.get_credentials()?;
let mut sora_media_tokens = vec![];
if let Some(image) = maybe_image {
@@ -58,7 +56,7 @@ pub async fn generate_image(
let filename = "image.png".to_string();
let response = sora_media_upload_from_bytes(image_bytes, filename, &sora_credentials)
let response = sora_media_upload_from_bytes(image_bytes, filename, CredentialMigrationRef::New(&creds))
.await
.map_err(|err| {
error!("Failed to upload image to Sora: {:?}", err);
@@ -73,7 +71,7 @@ pub async fn generate_image(
num_images: NumImages::One,
image_size: ImageSize::Square,
sora_media_tokens: sora_media_tokens.clone(),
credentials: &sora_credentials,
credentials: CredentialMigrationRef::New(&creds),
}).await
.map_err(|err| {
error!("Failed to call Sora image generation: {:?}", err);
@@ -1,7 +1,9 @@
use crate::state::app_dir::AppDataRoot;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::data_dir::trait_data_subdir::DataSubdir;
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::utils::get_url_file_extension::get_url_file_extension;
use crate::utils::simple_http_download::simple_http_download;
use base64::prelude::BASE64_STANDARD;
@@ -11,11 +13,14 @@ use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::{DynamicImage, ImageReader};
use log::{debug, error, info};
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::sentinel_refresh::refresh_sentinel;
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, SoraMediaUploadRequest};
use openai_sora_client::requests::upload::upload_media_from_file::sora_media_upload_from_file;
use serde_derive::Deserialize;
use std::fs::read_to_string;
use std::io::Cursor;
@@ -47,10 +52,15 @@ pub async fn sora_image_remix_command(
request: SoraImageRemixCommand,
app_data_root: State<'_, AppDataRoot>,
sora_creds_manager: State<'_, SoraCredentialManager>,
sora_task_queue: State<'_, SoraTaskQueue>,
) -> Result<String, String> {
info!("image_generation_command called; processing image...");
info!("image_generation_command called; scene media token: {:?}, additional images: {:?}",
request.snapshot_media_token, request.maybe_additional_images);
generate_image(request, &app_data_root, &sora_creds_manager)
// TODO(bt,2025-04-24): Better error messages to caller
generate_image(request, &app_data_root, &sora_creds_manager, &sora_task_queue)
.await
.map_err(|err| {
error!("error: {:?}", err);
@@ -64,6 +74,7 @@ pub async fn generate_image(
request: SoraImageRemixCommand,
app_data_root: &AppDataRoot,
sora_creds_manager: &SoraCredentialManager,
sora_task_queue: &SoraTaskQueue,
) -> AnyhowResult<()> {
let response = get_media_file(&ApiHost::Storyteller, &request.snapshot_media_token).await?;
@@ -80,78 +91,63 @@ pub async fn generate_image(
let files_to_upload = vec![filename];
// TODO(bt,2025-04-21): Read from in-memory cache instead, but allow for desktop replacement.
let mut sora_creds = read_sora_credentials_from_disk(app_data_root)
.map_err(|err| {
error!("Failed to read Sora credentials from disk: {:?}", err);
err
})?;
let mut creds = sora_creds_manager.get_credentials_required()?;
let no_sentinel = !app_data_root.get_sora_sentinel_file_path().is_file()
|| sora_creds.sentinel.is_none();
let credential_updated = maybe_upgrade_or_renew_session(&mut creds)
.await
.map_err(|err| {
error!("Failed to upgrade or renew session: {:?}", err);
err
})?;
if no_sentinel {
info!("Refreshing credentials...");
sora_creds = sora_creds_manager.call_sentinel_refresh()
.await
.map_err(|err| {
error!("Failed to refresh: {:?}", err);
err
})?;
// TODO: Write to disk.
if credential_updated {
info!("Storing updated credentials");
sora_creds_manager.set_credentials(&creds)?;
}
let mut sora_media_tokens = Vec::with_capacity(files_to_upload.len());
for file_path in files_to_upload {
let sora_upload_response = sora_media_upload_from_file(file_path, &sora_creds)
.await?;
for (i, file_path) in files_to_upload.iter().enumerate() {
info!("Uploading image {} of {}...", (i+1), files_to_upload.len());
sora_media_tokens.push(sora_upload_response.id);
let (response, maybe_new_credentials) =
image_upload_from_file_with_session_auto_renew(ImageUploadFromFileAutoRenewRequest {
file_path,
credentials: &creds,
}).await?;
if let Some(new_creds) = maybe_new_credentials {
info!("Storing updated credentials.");
sora_creds_manager.set_credentials(&new_creds)?;
creds = new_creds;
}
sora_media_tokens.push(response.id);
}
info!("Calling image generation...");
// TODO(bt,2025-04-21): Download media tokens.
// Note: This is incredibly inefficient. We should keep a local cache.
// Also, if they've already been uploaded to OpenAI, we shouldn't continue to re-upload.
let mut response = sora_image_gen_remix(SoraImageGenRemixRequest {
prompt: request.prompt.to_string(),
num_images: NumImages::One,
image_size: ImageSize::Square,
sora_media_tokens: sora_media_tokens.clone(),
credentials: &sora_creds,
}).await;
let (response, maybe_new_creds) =
image_remix_with_session_auto_renew(ImageRemixAutoRenewRequest {
prompt: request.prompt.to_string(),
num_images: NumImages::One,
image_size: ImageSize::Square,
sora_media_tokens: sora_media_tokens.clone(),
credentials: &creds,
}).await?;
if let Err(err) = &response {
error!("Error in generating image: {:?}", err);
sora_creds = sora_creds_manager.call_sentinel_refresh()
.await
.map_err(|err| {
error!("Failed to refresh: {:?}", err);
err
})?;
info!("Retrying request...");
response = sora_image_gen_remix(SoraImageGenRemixRequest {
prompt: request.prompt.to_string(),
num_images: NumImages::One,
image_size: ImageSize::Square,
sora_media_tokens: sora_media_tokens.clone(),
credentials: &sora_creds,
}).await;
if let Some(new_creds) = maybe_new_creds {
info!("Storing updated credentials.");
sora_creds_manager.set_credentials(&new_creds)?;
}
let response = response
.map_err(|err| {
error!("Failed to call Sora image generation: {:?}", err);
err
})?;
info!("New Sora Task ID: {:?} ", response.task_id);
println!(">> TASK ID: {:?} ", response.task_id);
sora_task_queue.insert(&response.task_id)?;
Ok(())
}
+51 -6
View File
@@ -4,20 +4,30 @@ pub mod state;
pub mod threads;
pub mod utils;
use tauri::Manager;
use crate::commands::flip_image::flip_image;
use crate::commands::platform_info_command::platform_info_command;
use crate::commands::sora::open_sora_login_command::open_sora_login_command;
use crate::commands::sora::sora_image_generation_command::sora_image_generation_command;
use crate::commands::sora::sora_image_remix_command::sora_image_remix_command;
use crate::state::app_config::AppConfig;
use crate::state::main_window_size::MainWindowSize;
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::threads::discord_presence_thread::discord_presence_thread;
use crate::threads::main_window_thread::main_window_thread::main_window_thread;
use crate::threads::sora_session_login_thread::sora_session_login_thread;
use crate::threads::sora_task_polling_thread::sora_task_polling_thread;
use tauri_plugin_http;
use tauri_plugin_log::Target;
use tauri_plugin_log::TargetKind;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
// NB: Tauri wants to install the logger itself, so we can't rely on the logger crate
// NB: Tauri wants to install the logger itself, so we can't rely on the logger crate
// until the tauri runtime begins.
println!("Loading model config...");
@@ -25,15 +35,24 @@ pub fn run() {
.expect("config should load");
let app_data_root = config.app_data_root.clone();
let app_data_root2 = config.app_data_root.clone();
let app_data_root_2 = config.app_data_root.clone();
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();
println!("Attempting to read existing credentials...");
let sora_creds_manager = SoraCredentialManager::initialize_from_disk_infallible(&app_data_root);
let sora_creds_manager2 = sora_creds_manager.clone();
let sora_creds_manager_2 = sora_creds_manager.clone();
// Other state
let sora_task_queue = SoraTaskQueue::new();
let sora_task_queue_2 = sora_task_queue.clone();
println!("Initializing backend runtime...");
tauri::Builder::default()
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_log::Builder::new()
.level(log::LevelFilter::Info)
.targets(vec![
@@ -43,7 +62,7 @@ pub fn run() {
}),
])
.build())
.setup(|app| {
.setup(move |app| {
// TODO(bt): This is broken on windows
// log_environment_details();
@@ -56,16 +75,42 @@ pub fn run() {
//}
let app = app.handle().clone();
tauri::async_runtime::spawn(sora_session_login_thread(app, app_data_root2, sora_creds_manager2));
let app_2 = app.clone();
let app_3 = app.clone();
let app_data_root_3 = app_data_root_2.clone();
let app_data_root_4 = app_data_root_2.clone();
let sora_creds_manager_3 = sora_creds_manager_2.clone();
let storyteller_creds_manager_3 = storyteller_creds_manager.clone();
match MainWindowSize::from_filesystem_configs(&app_data_root_3) {
Ok(None) => {}
Ok(Some(size)) => {
println!("Resizing window to: {:?}", size);
let result = size.apply_to_main_window(&app);
if let Err(err) = result {
eprintln!("Could not set window size: {:?}", err);
}
}
Err(err) => {
eprintln!("Failed to read window size from disk: {:?}", err);
}
}
tauri::async_runtime::spawn(sora_session_login_thread(app_2, app_data_root_2, sora_creds_manager_2));
tauri::async_runtime::spawn(main_window_thread(app_3, app_data_root_3, storyteller_creds_manager_2));
tauri::async_runtime::spawn(sora_task_polling_thread(app_data_root_4, sora_creds_manager_3, storyteller_creds_manager_3, sora_task_queue_2));
tauri::async_runtime::spawn(discord_presence_thread());
Ok(())
})
.manage(config)
.manage(app_data_root)
.manage(config)
.manage(sora_creds_manager)
.manage(sora_task_queue)
.invoke_handler(tauri::generate_handler![
flip_image,
open_sora_login_command,
platform_info_command,
sora_image_generation_command,
sora_image_remix_command,
])
@@ -1,4 +1,4 @@
use crate::state::app_dir::AppDataRoot;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::os_platform::OsPlatform;
use crate::state::yaml_config::YamlConfig;
@@ -1,264 +0,0 @@
use crate::state::expanduser::expanduser;
use crate::state::os_platform::OsPlatform;
use anyhow::anyhow;
use directories::UserDirs;
use std::path::{Path, PathBuf};
use chrono::{DateTime, Local};
use tempdir::TempDir;
use tempfile::{Builder, NamedTempFile};
const DEFAULT_DATA_DIR : &str = "artcraft";
const ASSETS_SUBDIRECTORY : &str = "assets";
const DOWNLOADS_SUBDIRECTORY : &str = "downloads";
const WEIGHTS_SUBDIRECTORY : &str = "weights";
const TEMPORARY_SUBDIRECTORY : &str = "temp";
/// Note: Tauri appends ".log" to the end of the filename.
const LOG_FILE_NAME : &str = "application_debug";
/// The path to the application data directory, which includes "asset" and "weights" data.
#[derive(Clone)]
pub struct AppDataRoot {
path: PathBuf,
assets_dir: AppAssetsDir,
downloads_dir: AppDownloadsDir,
weights_dir: AppWeightsDir,
temp_dir: TemporaryDir,
log_file_name: PathBuf,
log_file_name_string: String,
}
#[derive(Clone)]
pub struct AppAssetsDir {
path: PathBuf,
}
#[derive(Clone)]
pub struct AppDownloadsDir {
path: PathBuf,
}
#[derive(Clone)]
pub struct AppWeightsDir {
path: PathBuf,
}
#[derive(Clone)]
pub struct TemporaryDir {
path: PathBuf,
}
impl AppDataRoot {
pub fn create_default() -> anyhow::Result<Self> {
let directory = get_default_data_dir()?;
Self::create_existing(directory)
}
pub fn create_existing<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
let mut dir = dir.as_ref().to_path_buf();
match OsPlatform::get() {
OsPlatform::Linux | OsPlatform::MacOs => {
if let Some(d) = dir.as_os_str().to_str() {
dir = expanduser(d)?;
}
},
OsPlatform::Windows => {}
}
if !dir.is_dir() {
println!("Creating directory {:?}", dir);
std::fs::create_dir_all(&dir)?;
}
match dir.canonicalize() {
Ok(d) => dir = d,
Err(err) => {
println!("Error canonicalizing {:?}: {}", dir, err);
}
}
let assets_dir = AppAssetsDir::create_existing(dir.join(ASSETS_SUBDIRECTORY))?;
let downloads_dir = AppDownloadsDir::create_existing(dir.join(DOWNLOADS_SUBDIRECTORY))?;
let weights_dir = AppWeightsDir::create_existing(dir.join(WEIGHTS_SUBDIRECTORY))?;
let temp_dir = TemporaryDir::create_existing(dir.join(TEMPORARY_SUBDIRECTORY))?;
let log_file_name = dir.join(LOG_FILE_NAME);
let log_file_name_string = log_file_name
.to_str()
.ok_or(anyhow!("couldn't convert log path to str"))?
.to_string();
Ok(Self {
path: dir,
assets_dir,
downloads_dir,
weights_dir,
temp_dir,
log_file_name,
log_file_name_string,
})
}
pub fn assets_dir(&self) -> &AppAssetsDir {
&self.assets_dir
}
pub fn downloads_dir(&self) -> &AppDownloadsDir {
&self.downloads_dir
}
pub fn weights_dir(&self) -> &AppWeightsDir {
&self.weights_dir
}
pub fn temp_dir(&self) -> &TemporaryDir {
&self.temp_dir
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn log_file_name(&self) -> &Path {
&self.log_file_name
}
pub fn log_file_name_str(&self) -> &str {
&self.log_file_name_string
}
pub fn get_sora_cookie_file_path(&self) -> PathBuf {
self.path.join("sora_cookies.txt")
}
pub fn get_sora_bearer_token_file_path(&self) -> PathBuf {
self.path.join("sora_bearer_token.txt")
}
pub fn get_sora_sentinel_file_path(&self) -> PathBuf {
self.path.join("sora_sentinel.txt")
}
}
impl AppAssetsDir {
pub fn create_existing<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
let mut dir = dir.as_ref().to_path_buf();
match dir.canonicalize() {
Ok(d) => dir = d,
Err(err) => {
println!("Error canonicalizing {:?}: {}", dir, err);
}
}
if !dir.exists() {
println!("Creating directory {:?}", dir);
std::fs::create_dir(&dir)?;
}
Ok(Self {
path: dir,
})
}
pub fn path(&self) -> &Path {
&self.path
}
/// Return the current date asset directory
/// If it doesn't exist, create it.
pub fn make_or_get_current_date_dir(&self) -> anyhow::Result<PathBuf> {
let format: DateTime<Local> = Local::now();
let date_directory_name = format.format("%Y-%d-%m").to_string();
let full_path = self.path.join(date_directory_name);
if !full_path.exists() {
std::fs::create_dir(&full_path)?;
}
Ok(full_path)
}
}
impl AppDownloadsDir {
pub fn create_existing<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
let mut dir = dir.as_ref().to_path_buf();
match dir.canonicalize() {
Ok(d) => dir = d,
Err(err) => {
println!("Error canonicalizing {:?}: {}", dir, err);
}
}
if !dir.exists() {
println!("Creating directory {:?}", dir);
std::fs::create_dir(&dir)?;
}
Ok(Self {
path: dir,
})
}
pub fn path(&self) -> &Path {
&self.path
}
}
impl AppWeightsDir {
pub fn create_existing<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
let mut dir = dir.as_ref().to_path_buf();
match dir.canonicalize() {
Ok(d) => dir = d,
Err(err) => {
println!("Error canonicalizing {:?}: {}", dir, err);
}
}
if !dir.exists() {
println!("Creating directory {:?}", dir);
std::fs::create_dir(&dir)?;
}
Ok(Self {
path: dir,
})
}
pub fn path(&self) -> &Path {
&self.path
}
}
impl TemporaryDir {
pub fn create_existing<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
let mut dir = dir.as_ref().to_path_buf();
match dir.canonicalize() {
Ok(d) => dir = d,
Err(err) => {
println!("Error canonicalizing {:?}: {}", dir, err);
}
}
if !dir.exists() {
println!("Creating directory {:?}", dir);
std::fs::create_dir(&dir)?;
}
Ok(Self {
path: dir,
})
}
pub fn with_prefix(&self, prefix: &str) -> anyhow::Result<TempDir> {
let tempdir = TempDir::new_in(&self.path, prefix)?;
Ok(tempdir)
}
pub fn new_named_temp_file(&self) -> anyhow::Result<NamedTempFile> {
let tempfile = Builder::new()
.prefix("temp_")
.suffix(".bin")
.tempfile_in(&self.path)?;
Ok(tempfile)
}
}
// eg. /home/bob/artcraft, /Users/bob/artcraft, or C:\Users\Alice\artcraft
fn get_default_data_dir() -> anyhow::Result<PathBuf> {
Ok(UserDirs::new()
.ok_or_else(|| anyhow!("could not determine user home directory"))?
.home_dir()
.join(DEFAULT_DATA_DIR))
}
@@ -0,0 +1,22 @@
use chrono::{DateTime, TimeDelta, Utc};
pub struct AppStartupTime {
startup_time: DateTime<Utc>,
}
impl AppStartupTime {
pub fn new() -> Self {
Self {
startup_time: Utc::now(),
}
}
pub fn get_startup_time(&self) -> DateTime<Utc> {
self.startup_time
}
pub fn time_delta_since(&self) -> TimeDelta {
let now = Utc::now();
now.signed_duration_since(self.startup_time)
}
}
@@ -0,0 +1,36 @@
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use chrono::{DateTime, Local};
use std::path::{Path, PathBuf};
#[derive(Clone)]
pub struct AppAssetsDir {
path: PathBuf,
}
impl DataSubdir for AppAssetsDir {
const DIRECTORY_NAME: &'static str = "assets";
fn new_from<P: AsRef<Path>>(dir: P) -> Self {
Self {
path: dir.as_ref().to_path_buf(),
}
}
fn path(&self) -> &Path {
&self.path
}
}
impl AppAssetsDir {
/// Return the current date asset directory
/// If it doesn't exist, create it.
pub fn make_or_get_current_date_dir(&self) -> anyhow::Result<PathBuf> {
let format: DateTime<Local> = Local::now();
let date_directory_name = format.format("%Y-%d-%m").to_string();
let full_path = self.path.join(date_directory_name);
if !full_path.exists() {
std::fs::create_dir(&full_path)?;
}
Ok(full_path)
}
}
@@ -0,0 +1,43 @@
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use std::path::{Path, PathBuf};
#[derive(Clone)]
pub struct AppCredentialsDir {
path: PathBuf,
}
impl DataSubdir for AppCredentialsDir {
const DIRECTORY_NAME: &'static str = "credentials";
fn new_from<P: AsRef<Path>> (dir: P) -> Self {
Self {
path: dir.as_ref().to_path_buf(),
}
}
fn path(&self) -> &Path {
&self.path
}
}
impl AppCredentialsDir {
pub fn get_sora_cookie_file_path(&self) -> PathBuf {
self.path.join("sora_cookies.txt")
}
pub fn get_sora_bearer_token_file_path(&self) -> PathBuf {
self.path.join("sora_bearer_token.txt")
}
pub fn get_sora_sentinel_file_path(&self) -> PathBuf {
self.path.join("sora_sentinel.txt")
}
pub fn get_storyteller_avt_cookie_file_path(&self) -> PathBuf {
self.path.join("artcraft_avt.txt")
}
pub fn get_storyteller_session_cookie_file_path(&self) -> PathBuf {
self.path.join("artcraft_session.txt")
}
}
@@ -0,0 +1,140 @@
use crate::state::data_dir::app_assets_dir::AppAssetsDir;
use crate::state::data_dir::app_credentials_dir::AppCredentialsDir;
use crate::state::data_dir::app_downloads_dir::AppDownloadsDir;
use crate::state::data_dir::app_weights_dir::AppWeightsDir;
use crate::state::data_dir::temporary_dir::TemporaryDir;
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use crate::state::expanduser::expanduser;
use crate::state::os_platform::OsPlatform;
use anyhow::anyhow;
use directories::UserDirs;
use std::path::{Path, PathBuf};
const DEFAULT_DATA_DIR : &str = "Artcraft";
/// Note: Tauri appends ".log" to the end of the filename.
const LOG_FILE_NAME : &str = "application_debug";
/// The path to the application data directory, which includes "asset" and "weights" data.
#[derive(Clone)]
pub struct AppDataRoot {
path: PathBuf,
assets_dir: AppAssetsDir,
credentials_dir: AppCredentialsDir,
downloads_dir: AppDownloadsDir,
weights_dir: AppWeightsDir,
temp_dir: TemporaryDir,
log_file_name: PathBuf,
log_file_name_string: String,
}
impl AppDataRoot {
pub fn create_default() -> anyhow::Result<Self> {
let directory = get_default_data_dir()?;
Self::create_existing(directory)
}
pub fn create_existing<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
let mut dir = dir.as_ref().to_path_buf();
match OsPlatform::get() {
OsPlatform::Linux | OsPlatform::MacOs => {
if let Some(d) = dir.as_os_str().to_str() {
dir = expanduser(d)?;
}
},
OsPlatform::Windows => {}
}
if !dir.is_dir() {
println!("Creating directory {:?}", dir);
std::fs::create_dir_all(&dir)?;
}
match dir.canonicalize() {
Ok(d) => dir = d,
Err(err) => {
println!("Error canonicalizing {:?}: {}", dir, err);
}
}
let assets_dir = AppAssetsDir::get_or_create_in_root_dir(&dir)?;
let downloads_dir = AppDownloadsDir::get_or_create_in_root_dir(&dir)?;
let credentials_dir = AppCredentialsDir::get_or_create_in_root_dir(&dir)?;
let weights_dir = AppWeightsDir::get_or_create_in_root_dir(&dir)?;
let temp_dir = TemporaryDir::get_or_create_in_root_dir(&dir)?;
let log_file_name = dir.join(LOG_FILE_NAME);
let log_file_name_string = log_file_name
.to_str()
.ok_or(anyhow!("couldn't convert log path to str"))?
.to_string();
Ok(Self {
path: dir,
assets_dir,
credentials_dir,
downloads_dir,
weights_dir,
temp_dir,
log_file_name,
log_file_name_string,
})
}
pub fn assets_dir(&self) -> &AppAssetsDir {
&self.assets_dir
}
pub fn credentials_dir(&self) -> &AppCredentialsDir {
&self.credentials_dir
}
pub fn downloads_dir(&self) -> &AppDownloadsDir {
&self.downloads_dir
}
pub fn weights_dir(&self) -> &AppWeightsDir {
&self.weights_dir
}
pub fn temp_dir(&self) -> &TemporaryDir {
&self.temp_dir
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn log_file_name(&self) -> &Path {
&self.log_file_name
}
pub fn log_file_name_str(&self) -> &str {
&self.log_file_name_string
}
pub fn get_sora_cookie_file_path(&self) -> PathBuf {
self.credentials_dir().get_sora_cookie_file_path()
}
pub fn get_sora_bearer_token_file_path(&self) -> PathBuf {
self.credentials_dir().get_sora_bearer_token_file_path()
}
pub fn get_sora_sentinel_file_path(&self) -> PathBuf {
self.credentials_dir().get_sora_sentinel_file_path()
}
pub fn get_window_size_config_file(&self) -> PathBuf {
self.path.join("window_size.json")
}
}
// eg. /home/bob/artcraft, /Users/bob/artcraft, or C:\Users\Alice\artcraft
fn get_default_data_dir() -> anyhow::Result<PathBuf> {
Ok(UserDirs::new()
.ok_or_else(|| anyhow!("could not determine user home directory"))?
.home_dir()
.join(DEFAULT_DATA_DIR))
}
@@ -0,0 +1,21 @@
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use std::path::{Path, PathBuf};
#[derive(Clone)]
pub struct AppDownloadsDir {
path: PathBuf,
}
impl DataSubdir for AppDownloadsDir {
const DIRECTORY_NAME: &'static str = "downloads";
fn new_from<P: AsRef<Path>>(dir: P) -> Self {
Self {
path: dir.as_ref().to_path_buf(),
}
}
fn path(&self) -> &Path {
&self.path
}
}
@@ -0,0 +1,21 @@
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use std::path::{Path, PathBuf};
#[derive(Clone)]
pub struct AppWeightsDir {
path: PathBuf,
}
impl DataSubdir for AppWeightsDir {
const DIRECTORY_NAME: &'static str = "weights";
fn new_from<P: AsRef<Path>>(dir: P) -> Self {
Self {
path: dir.as_ref().to_path_buf(),
}
}
fn path(&self) -> &Path {
&self.path
}
}
@@ -0,0 +1,7 @@
pub mod app_assets_dir;
pub mod app_credentials_dir;
pub mod app_data_root;
pub mod app_downloads_dir;
pub mod app_weights_dir;
pub mod temporary_dir;
pub mod trait_data_subdir;
@@ -0,0 +1,38 @@
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use std::path::{Path, PathBuf};
use tempdir::TempDir;
use tempfile::{Builder, NamedTempFile};
#[derive(Clone)]
pub struct TemporaryDir {
path: PathBuf,
}
impl DataSubdir for TemporaryDir {
const DIRECTORY_NAME: &'static str = "temp";
fn new_from<P: AsRef<Path>>(dir: P) -> Self {
Self {
path: dir.as_ref().to_path_buf(),
}
}
fn path(&self) -> &Path {
&self.path
}
}
impl TemporaryDir {
pub fn with_prefix(&self, prefix: &str) -> anyhow::Result<TempDir> {
let tempdir = TempDir::new_in(&self.path, prefix)?;
Ok(tempdir)
}
pub fn new_named_temp_file(&self) -> anyhow::Result<NamedTempFile> {
let tempfile = Builder::new()
.prefix("temp_")
.suffix(".bin")
.tempfile_in(&self.path)?;
Ok(tempfile)
}
}
@@ -0,0 +1,31 @@
use std::path::Path;
pub trait DataSubdir : Sized {
const DIRECTORY_NAME: &'static str;
fn new_from<P: AsRef<Path>>(dir: P) -> Self;
fn path(&self) -> &Path;
fn get_or_create_in_root_dir<P: AsRef<Path>>(root_dir: P) -> anyhow::Result<Self> {
Self::get_or_create_dir(root_dir.as_ref().join(Self::DIRECTORY_NAME))
}
fn get_or_create_dir<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
let mut dir = dir.as_ref().to_path_buf();
match dir.canonicalize() {
Ok(d) => dir = d,
Err(err) => {
println!("Error canonicalizing {:?}: {}", dir, err);
}
}
if !dir.exists() {
println!("Creating directory {:?}", dir);
std::fs::create_dir(&dir)?;
}
Ok(Self::new_from(dir))
}
}
@@ -0,0 +1,74 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use errors::AnyhowResult;
use serde_derive::{Deserialize, Serialize};
use std::fs::OpenOptions;
use std::io::Write;
use tauri::{AppHandle, Manager, PhysicalSize, Window};
const MAIN_WINDOW_NAME: &str = "main";
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct MainWindowSize {
pub width: u32,
pub height: u32,
}
impl MainWindowSize {
pub fn new(width: u32, height: u32) -> Self {
Self { width, height }
}
pub fn from_main_window(app: &AppHandle) -> AnyhowResult<Self> {
let windows = app.windows();
windows.get(MAIN_WINDOW_NAME)
.map(|window| Self::from_window(window))
.unwrap_or_else(|| Err(anyhow::anyhow!("Main window not found")))
}
pub fn from_window(window: &Window) -> AnyhowResult<Self> {
let size = window.inner_size()?;
Ok(Self {
width: size.width,
height: size.height,
})
}
pub fn from_filesystem_configs(app_data_root: &AppDataRoot) -> AnyhowResult<Option<Self>> {
let filename = app_data_root.get_window_size_config_file();
if !filename.exists() {
return Ok(None);
}
let contents = std::fs::read_to_string(filename)?;
let size: MainWindowSize = serde_json::from_str(&contents)?;
Ok(Some(size))
}
pub fn apply_to_main_window(&self, app: &AppHandle) -> AnyhowResult<()> {
let windows = app.windows();
let window = windows.get(MAIN_WINDOW_NAME)
.ok_or_else(|| anyhow::anyhow!("Main window not found"))?;
window.set_size(PhysicalSize::new(self.width, self.height))?;
Ok(())
}
pub fn persist_to_filesystem(&self, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
let filename = app_data_root.get_window_size_config_file();
let json = serde_json::to_string(self)?;
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(filename)?;
file.write_all(json.as_bytes())?;
file.flush()?;
Ok(())
}
pub fn to_physical_size(&self) -> PhysicalSize<u32> {
PhysicalSize::new(self.width, self.height)
}
pub fn matches_physical_size(&self, size: &PhysicalSize<u32>) -> bool {
self.width == size.width && self.height == size.height
}
}
@@ -1,6 +1,9 @@
pub mod app_config;
pub mod app_dir;
pub mod app_startup_time;
pub mod data_dir;
pub mod expanduser;
pub mod main_window_size;
pub mod os_platform;
pub mod sora;
pub mod storyteller;
pub mod yaml_config;
@@ -7,14 +7,18 @@ pub enum OsPlatform {
}
impl OsPlatform {
pub fn get() -> OsPlatform {
pub fn maybe_get() -> Option<Self> {
// Totally scientific.
// https://doc.rust-lang.org/std/env/consts/constant.OS.html
match std::env::consts::OS {
"linux" => OsPlatform::Linux,
"windows" => OsPlatform::Windows,
"macos" | "apple" => OsPlatform::MacOs,
_ => OsPlatform::Linux,
"linux" => Some(Self::Linux),
"windows" => Some(Self::Windows),
"macos" | "apple" => Some(Self::MacOs),
_ => None,
}
}
pub fn get() -> Self {
Self::maybe_get().unwrap_or(Self::Linux)
}
}
@@ -1,3 +1,5 @@
pub mod read_sora_credentials_from_disk;
pub mod read_sora_credentials_from_disk_with_upgrade;
pub mod sora_credential_holder;
pub mod sora_credential_manager;
pub mod sora_credential_manager;
pub mod sora_task_queue;
@@ -1,36 +1,48 @@
use std::fs::read_to_string;
use crate::state::data_dir::app_data_root::AppDataRoot;
use anyhow::anyhow;
use errors::AnyhowResult;
use openai_sora_client::credentials::SoraCredentials;
use crate::state::app_dir::AppDataRoot;
use openai_sora_client::creds::sora_cookies::SoraCookies;
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
use openai_sora_client::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
use openai_sora_client::creds::sora_sentinel::SoraSentinel;
use std::fs::read_to_string;
pub fn read_sora_credentials_from_disk(app_data_root: &AppDataRoot) -> AnyhowResult<SoraCredentials> {
pub fn read_sora_credentials_from_disk(app_data_root: &AppDataRoot) -> AnyhowResult<SoraCredentialSet> {
let cookie_file = app_data_root.get_sora_cookie_file_path();
let bearer_file = app_data_root.get_sora_bearer_token_file_path();
let sentinel_file = app_data_root.get_sora_sentinel_file_path();
let cookie = read_to_string(cookie_file)?
if !cookie_file.exists() {
return Err(anyhow!("Cookie file does not exist: {:?}", cookie_file));
}
let value = read_to_string(cookie_file)?
.trim()
.to_string();
let bearer = read_to_string(bearer_file)?
.trim()
.to_string();
let cookie = SoraCookies::new(value);
if sentinel_file.exists() && sentinel_file.is_file() {
let sentinel = read_to_string(sentinel_file)?
let mut bearer = None;
let mut sentinel = None;
if bearer_file.exists() {
let value = read_to_string(&bearer_file)?
.trim()
.to_string();
bearer = Some(SoraJwtBearerToken::new(value)?);
}
if sentinel_file.exists() {
let value = read_to_string(&sentinel_file)?
.trim()
.to_string();
return Ok(SoraCredentials {
bearer_token: bearer,
cookie,
sentinel: Some(sentinel),
});
sentinel = Some(SoraSentinel::new(value));
}
Ok(SoraCredentials {
bearer_token: bearer,
Ok(SoraCredentialSet::initialize(
cookie,
sentinel: None,
})
bearer,
sentinel,
))
}
@@ -0,0 +1,92 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use anyhow::anyhow;
use errors::AnyhowResult;
use log::{info, warn};
use openai_sora_client::creds::sora_cookies::SoraCookies;
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
use openai_sora_client::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
use openai_sora_client::creds::sora_sentinel::SoraSentinel;
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use std::fs::{read_to_string, OpenOptions};
use std::io::Write;
pub async fn read_sora_credentials_from_disk_with_upgrade(app_data_root: &AppDataRoot) -> AnyhowResult<SoraCredentialSet> {
let cookie_file = app_data_root.get_sora_cookie_file_path();
let bearer_file = app_data_root.get_sora_bearer_token_file_path();
let sentinel_file = app_data_root.get_sora_sentinel_file_path();
if !cookie_file.exists() {
return Err(anyhow!("Cookie file does not exist: {:?}", cookie_file));
}
let value = read_to_string(cookie_file)?
.trim()
.to_string();
let cookie = SoraCookies::new(value);
let mut bearer = None;
let mut sentinel = None;
if bearer_file.exists() {
let value = read_to_string(&bearer_file)?
.trim()
.to_string();
bearer = Some(SoraJwtBearerToken::new(value)?);
}
if sentinel_file.exists() {
let value = read_to_string(&sentinel_file)?
.trim()
.to_string();
sentinel = Some(SoraSentinel::new(value));
}
let mut credentials = SoraCredentialSet::initialize(
cookie,
bearer,
sentinel,
);
let response = maybe_upgrade_or_renew_session(&mut credentials).await;
if let Err(err) = response {
// NB: Make this portion infallible. Don't die on setup. We can recover downstream.
warn!("Failed to upgrade or renew session: {:?}", err);
}
if !bearer_file.exists() {
if let Some(bearer) = &credentials.jwt_bearer_token {
info!("Persisting bearer token value...");
let value = bearer.token_str();
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&bearer_file)?;
file.write_all(value.as_bytes())?;
file.flush()?;
}
}
if !sentinel_file.exists() {
if let Some(sentinel) = &credentials.sora_sentinel {
info!("Persisting sentinel value...");
let value = sentinel.get_sentinel();
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&sentinel_file)?;
file.write_all(value.as_bytes())?;
file.flush()?;
}
}
Ok(credentials)
}
@@ -1,23 +1,22 @@
use std::ops::Deref;
use std::sync::{Arc, RwLock};
use anyhow::anyhow;
use errors::AnyhowResult;
use openai_sora_client::credentials::SoraCredentials;
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
use std::sync::{Arc, RwLock};
/// Hold credentials for the application.
#[derive(Clone)]
pub struct SoraCredentialHolder {
credentials: Arc<RwLock<Option<SoraCredentials>>>
credentials: Arc<RwLock<Option<SoraCredentialSet>>>,
}
impl SoraCredentialHolder {
pub fn new() -> Self {
Self {
credentials: Arc::new(RwLock::new(None))
credentials: Arc::new(RwLock::new(None)),
}
}
pub fn set_credentials(&self, credentials: &SoraCredentials) -> AnyhowResult<()> {
pub fn set_credentials(&self, credentials: &SoraCredentialSet) -> AnyhowResult<()> {
match self.credentials.write() {
Err(err) => Err(anyhow!("Failed to acquire write lock: {:?}", err)),
Ok(mut creds) => {
@@ -37,7 +36,7 @@ impl SoraCredentialHolder {
}
}
pub fn get_credentials(&self) -> AnyhowResult<Option<SoraCredentials>> {
pub fn get_credentials(&self) -> AnyhowResult<Option<SoraCredentialSet>> {
match self.credentials.read() {
Err(err) => Err(anyhow!("Failed to acquire read lock: {:?}", err)),
Ok(creds) => {
@@ -46,10 +45,10 @@ impl SoraCredentialHolder {
}
}
pub fn get_credentials_required(&self) -> AnyhowResult<SoraCredentials> {
pub fn get_credentials_required(&self) -> AnyhowResult<SoraCredentialSet> {
match self.credentials.read() {
Err(err) => Err(anyhow!("Failed to acquire read lock: {:?}", err)),
Ok(creds) => match creds.deref() {
Ok(creds) => match &*creds {
None => Err(anyhow!("Credentials not set")),
Some(creds) => Ok(creds.clone()),
}
@@ -1,14 +1,16 @@
use crate::state::app_dir::AppDataRoot;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::sora::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
use anyhow::anyhow;
use errors::AnyhowResult;
use log::{error, info, warn};
use openai_sora_client::credentials::SoraCredentials;
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
use openai_sora_client::creds::sora_sentinel::SoraSentinel;
use openai_sora_client::requests::sentinel_refresh::generate::token::generate_token;
use openai_sora_client::requests::sentinel_refresh::refresh_sentinel;
use std::fs::OpenOptions;
use std::io::Write;
use openai_sora_client::creds::sora_cookies::SoraCookies;
use openai_sora_client::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
#[derive(Clone)]
pub struct SoraCredentialManager {
@@ -34,30 +36,50 @@ impl SoraCredentialManager {
}
}
pub fn set_credentials(&self, creds: &SoraCredentials) -> AnyhowResult<()> {
pub fn set_credentials(&self, creds: &SoraCredentialSet) -> AnyhowResult<()> {
self.holder.set_credentials(creds)
}
pub fn clear_credentials(&self) -> AnyhowResult<()> {
self.holder.clear_credentials()
self.holder.clear_credentials()?;
Ok(())
}
pub fn get_credentials(&self) -> AnyhowResult<Option<SoraCredentials>> {
pub fn get_credentials(&self) -> AnyhowResult<Option<SoraCredentialSet>> {
self.holder.get_credentials()
}
pub fn get_credentials_required(&self) -> AnyhowResult<SoraCredentials> {
pub fn get_credentials_required(&self) -> AnyhowResult<SoraCredentialSet> {
self.holder.get_credentials_required()
}
pub fn reset_from_disk(&self) -> AnyhowResult<SoraCredentials> {
pub fn reset_from_disk(&self) -> AnyhowResult<SoraCredentialSet> {
let creds = read_sora_credentials_from_disk(&self.app_data_root)?;
self.holder.set_credentials(&creds)?;
Ok(creds)
}
pub fn persist_all_to_disk(&self) -> AnyhowResult<()> {
let credentials = self.holder.get_credentials_required()?;
info!("Persisting cookies to disk...");
persist_cookies_to_disk(&credentials.cookies, &self.app_data_root)?;
if let Some(bearer) = &credentials.jwt_bearer_token {
info!("Persisting JWT to disk...");
persist_jwt_bearer_to_disk(bearer, &self.app_data_root)?;
}
if let Some(sentinel) = &credentials.sora_sentinel {
info!("Persisting sentinel to disk...");
persist_sentinel_to_disk(sentinel, &self.app_data_root)?;
}
Ok(())
}
/// Refresh the sentinel token from Sora's API
pub async fn call_sentinel_refresh(&self) -> AnyhowResult<SoraCredentials> {
pub async fn call_sentinel_refresh(&self) -> AnyhowResult<SoraCredentialSet> {
// NB(bt,2025-04-21): Technically we don't need credentials to get a sentinel.
let mut creds = self.holder.get_credentials_required()?;
@@ -72,7 +94,7 @@ impl SoraCredentialManager {
info!("Token obtained.");
creds.sentinel = Some(sentinel.clone());
creds.sora_sentinel = Some(SoraSentinel::new(sentinel.clone()));
self.holder.set_credentials(&creds)?;
@@ -88,3 +110,48 @@ impl SoraCredentialManager {
Ok(creds)
}
}
fn persist_cookies_to_disk(cookies: &SoraCookies, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
let filename = app_data_root.get_sora_cookie_file_path();
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(filename)?;
file.write_all(cookies.as_bytes())?;
file.flush()?;
Ok(())
}
fn persist_jwt_bearer_to_disk(bearer: &SoraJwtBearerToken, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
let filename = app_data_root.get_sora_bearer_token_file_path();
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(filename)?;
file.write_all(bearer.as_bytes())?;
file.flush()?;
Ok(())
}
fn persist_sentinel_to_disk(sentinel: &SoraSentinel, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
let filename = app_data_root.get_sora_sentinel_file_path();
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(filename)?;
file.write_all(sentinel.as_bytes())?;
file.flush()?;
Ok(())
}
@@ -0,0 +1,231 @@
use std::collections::HashSet;
use chrono::{DateTime, Utc};
use errors::AnyhowResult;
use indexmap::IndexMap;
use openai_sora_client::requests::image_gen::image_gen_status::TaskId;
use std::sync::{Arc, RwLock};
#[derive(Clone)]
pub struct TaskDetails {
pub enqueue_time: DateTime<Utc>
}
impl TaskDetails {
pub fn new() -> Self {
Self {
enqueue_time: Utc::now()
}
}
}
#[derive(Clone)]
pub struct TaskIdAndDetails {
pub task_id: TaskId,
pub details: TaskDetails,
}
#[derive(Clone)]
pub struct SoraTaskQueue {
// Insertion-order map of task IDs to task details
queue: Arc<RwLock<IndexMap<TaskId, TaskDetails>>>
}
impl SoraTaskQueue {
pub fn new() -> Self {
Self {
queue: Arc::new(RwLock::new(IndexMap::new()))
}
}
/// Determine if queue is empty
/// This is an O(1) operation.
pub fn is_empty(&self) -> AnyhowResult<bool> {
match self.queue.read() {
Err(err) => {
Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err))
}
Ok(lock) => Ok(lock.is_empty())
}
}
/// Get length of set
/// This is an O(1) operation.
pub fn len(&self) -> AnyhowResult<usize> {
match self.queue.read() {
Err(err) => {
Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err))
}
Ok(lock) => Ok(lock.len())
}
}
/// Determine if the key is in the queue
/// This is an O(1) operation.
pub fn contains_key(&self, task_id: &TaskId) -> AnyhowResult<bool> {
match self.queue.read() {
Err(err) => {
Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err))
}
Ok(lock) => Ok(lock.contains_key(task_id))
}
}
/// Get the first enqueued task
/// This is an O(1) operation.
pub fn first(&self) -> AnyhowResult<Option<TaskIdAndDetails>> {
match self.queue.read() {
Err(err) => {
Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err))
}
Ok(lock) => match lock.first() {
None => Ok(None),
Some((task_id, task_details)) => Ok(Some(TaskIdAndDetails {
task_id: task_id.clone(),
details: task_details.clone(),
}))
}
}
}
/// Get the last enqueued task
/// This is an O(1) operation.
pub fn last(&self) -> AnyhowResult<Option<TaskIdAndDetails>> {
match self.queue.read() {
Err(err) => {
Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err))
}
Ok(lock) => match lock.last() {
None => Ok(None),
Some((task_id, task_details)) => Ok(Some(TaskIdAndDetails {
task_id: task_id.clone(),
details: task_details.clone(),
}))
}
}
}
/// Insert a new task. If a task already exists, we update its value.
/// This is an O(1) operation.
pub fn insert(&self, task_id: &TaskId) -> AnyhowResult<Option<TaskDetails>> {
match self.queue.write() {
Err(err) => {
Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err))
}
Ok(mut lock) => {
// NB: Probably fine to compute TaskDetails here for now. We may eventually expose this if we include additional info.
let task_details = TaskDetails::new();
Ok(lock.insert(task_id.clone(), task_details))
}
}
}
/// Remove a task by ID.
/// This is an O(n) operation.
pub fn remove(&self, task_id: &TaskId) -> AnyhowResult<Option<TaskDetails>> {
match self.queue.write() {
Err(err) => {
Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err))
}
Ok(mut lock) => {
Ok(lock.shift_remove(task_id))
}
}
}
/// Remove a list of tasks by ID.
/// This is an O(n) operation.
pub fn remove_list(&self, task_ids: &[&TaskId]) -> AnyhowResult<()> {
match self.queue.write() {
Err(err) => {
Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err))
}
Ok(mut lock) => {
// TODO: Cleaner way to do this?
let mut set : HashSet<&TaskId, _> = HashSet::with_capacity(task_ids.len());
set.extend(task_ids);
lock.retain(|k, v| !set.contains(k));
Ok(())
}
}
}
}
#[cfg(test)]
mod tests {
use errors::AnyhowResult;
use openai_sora_client::requests::image_gen::image_gen_status::TaskId;
use crate::state::sora::sora_task_queue::SoraTaskQueue;
#[test]
fn remove_list() -> AnyhowResult<()> {
let task_queue = SoraTaskQueue::new();
assert!(task_queue.is_empty()?);
assert_eq!(task_queue.len()?, 0);
task_queue.insert(&TaskId("a".to_string()))?;
task_queue.insert(&TaskId("b".to_string()))?;
task_queue.insert(&TaskId("c".to_string()))?;
task_queue.insert(&TaskId("d".to_string()))?;
task_queue.insert(&TaskId("e".to_string()))?;
task_queue.insert(&TaskId("f".to_string()))?;
assert!(!task_queue.is_empty()?);
assert_eq!(task_queue.len()?, 6);
task_queue.insert(&TaskId("a".to_string()))?;
task_queue.insert(&TaskId("b".to_string()))?;
task_queue.insert(&TaskId("c".to_string()))?;
task_queue.insert(&TaskId("d".to_string()))?;
task_queue.insert(&TaskId("e".to_string()))?;
task_queue.insert(&TaskId("f".to_string()))?;
assert!(!task_queue.is_empty()?);
assert_eq!(task_queue.len()?, 6);
task_queue.remove_list(&[
&TaskId("a".to_string()),
&TaskId("c".to_string()),
&TaskId("e".to_string()),
])?;
assert!(!task_queue.is_empty()?);
assert_eq!(task_queue.len()?, 3);
assert!(task_queue.contains_key(&TaskId("b".to_string()))?);
assert!(task_queue.contains_key(&TaskId("d".to_string()))?);
assert!(task_queue.contains_key(&TaskId("f".to_string()))?);
assert!(!task_queue.contains_key(&TaskId("a".to_string()))?);
assert!(!task_queue.contains_key(&TaskId("c".to_string()))?);
assert!(!task_queue.contains_key(&TaskId("e".to_string()))?);
task_queue.remove_list(&[
&TaskId("foo".to_string()),
&TaskId("bar".to_string()),
&TaskId("baz".to_string()),
])?;
assert!(!task_queue.is_empty()?);
assert_eq!(task_queue.len()?, 3);
assert!(task_queue.contains_key(&TaskId("b".to_string()))?);
assert!(task_queue.contains_key(&TaskId("d".to_string()))?);
assert!(task_queue.contains_key(&TaskId("f".to_string()))?);
task_queue.remove_list(&[
&TaskId("b".to_string()),
&TaskId("d".to_string()),
&TaskId("f".to_string()),
])?;
assert!(task_queue.is_empty()?);
assert_eq!(task_queue.len()?, 0);
assert!(!task_queue.contains_key(&TaskId("b".to_string()))?);
assert!(!task_queue.contains_key(&TaskId("d".to_string()))?);
assert!(!task_queue.contains_key(&TaskId("f".to_string()))?);
Ok(())
}
}
@@ -0,0 +1,3 @@
pub mod read_storyteller_credentials_from_disk;
pub mod storyteller_credential_holder;
pub mod storyteller_credential_manager;
@@ -0,0 +1,39 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use errors::AnyhowResult;
use std::fs::read_to_string;
use storyteller_client::credentials::storyteller_avt_cookie::StorytellerAvtCookie;
use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use storyteller_client::credentials::storyteller_session_cookie::StorytellerSessionCookie;
pub fn read_storyteller_credentials_from_disk(app_data_root: &AppDataRoot) -> AnyhowResult<StorytellerCredentialSet> {
let creds_dir= app_data_root.credentials_dir();
let mut avt = None;
{
let avt_file = creds_dir.get_storyteller_avt_cookie_file_path();
if avt_file.exists() {
let contents = read_to_string(&avt_file)?
.trim()
.to_string();
avt = Some(StorytellerAvtCookie::new(contents));
}
}
let mut session = None;
{
let session_file = creds_dir.get_storyteller_session_cookie_file_path();
if session_file.exists() {
let contents = read_to_string(&session_file)?
.trim()
.to_string();
session = Some(StorytellerSessionCookie::new(contents));
}
}
Ok(StorytellerCredentialSet::initialize(
avt,
session,
))
}
@@ -0,0 +1,57 @@
use anyhow::anyhow;
use errors::AnyhowResult;
use std::sync::{Arc, RwLock};
use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
/// Hold credentials for the application.
#[derive(Clone)]
pub struct StorytellerCredentialHolder {
credentials: Arc<RwLock<Option<StorytellerCredentialSet>>>,
}
impl StorytellerCredentialHolder {
pub fn new() -> Self {
Self {
credentials: Arc::new(RwLock::new(None)),
}
}
pub fn set_credentials(&self, credentials: &StorytellerCredentialSet) -> AnyhowResult<()> {
match self.credentials.write() {
Err(err) => Err(anyhow!("Failed to acquire write lock: {:?}", err)),
Ok(mut creds) => {
*creds = Some(credentials.clone());
Ok(())
}
}
}
pub fn clear_credentials(&self) -> AnyhowResult<()> {
match self.credentials.write() {
Err(err) => Err(anyhow!("Failed to acquire write lock: {:?}", err)),
Ok(mut creds) => {
*creds = None;
Ok(())
}
}
}
pub fn get_credentials(&self) -> AnyhowResult<Option<StorytellerCredentialSet>> {
match self.credentials.read() {
Err(err) => Err(anyhow!("Failed to acquire read lock: {:?}", err)),
Ok(creds) => {
Ok(creds.clone())
}
}
}
pub fn get_credentials_required(&self) -> AnyhowResult<StorytellerCredentialSet> {
match self.credentials.read() {
Err(err) => Err(anyhow!("Failed to acquire read lock: {:?}", err)),
Ok(creds) => match &*creds {
None => Err(anyhow!("Credentials not set")),
Some(creds) => Ok(creds.clone()),
}
}
}
}
@@ -0,0 +1,109 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
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::storyteller::read_storyteller_credentials_from_disk::read_storyteller_credentials_from_disk;
use crate::state::storyteller::storyteller_credential_holder::StorytellerCredentialHolder;
use anyhow::anyhow;
use errors::AnyhowResult;
use log::{error, info, warn};
use std::fs::OpenOptions;
use std::io::Write;
use storyteller_client::credentials::storyteller_avt_cookie::StorytellerAvtCookie;
use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use storyteller_client::credentials::storyteller_session_cookie::StorytellerSessionCookie;
#[derive(Clone)]
pub struct StorytellerCredentialManager {
holder: StorytellerCredentialHolder,
app_data_root: AppDataRoot,
}
impl StorytellerCredentialManager {
pub fn initialize_from_disk_infallible(app_data_root: &AppDataRoot) -> Self {
let holder = StorytellerCredentialHolder::new();
match read_storyteller_credentials_from_disk(app_data_root) {
Err(err) => warn!("Failed to read credentials from disk: {:?}", err),
Ok(creds) => {
holder.set_credentials(&creds).expect("Failed to set credentials");
}
}
Self {
holder,
app_data_root: app_data_root.clone(),
}
}
pub fn set_credentials(&self, creds: &StorytellerCredentialSet) -> AnyhowResult<()> {
self.holder.set_credentials(creds)
}
pub fn clear_credentials(&self) -> AnyhowResult<()> {
self.holder.clear_credentials()?;
Ok(())
}
pub fn get_credentials(&self) -> AnyhowResult<Option<StorytellerCredentialSet>> {
self.holder.get_credentials()
}
pub fn get_credentials_required(&self) -> AnyhowResult<StorytellerCredentialSet> {
self.holder.get_credentials_required()
}
pub fn reset_from_disk(&self) -> AnyhowResult<StorytellerCredentialSet> {
let creds = read_storyteller_credentials_from_disk(&self.app_data_root)?;
self.holder.set_credentials(&creds)?;
Ok(creds)
}
pub fn persist_all_to_disk(&self) -> AnyhowResult<()> {
let credentials = self.holder.get_credentials_required()?;
info!("Persisting avt cookie to disk...");
if let Some(avt) = credentials.avt.as_ref() {
persist_avt_to_disk(&avt, &self.app_data_root)?;
}
info!("Persisting session cookie to disk...");
if let Some(session) = credentials.session.as_ref() {
persist_session_to_disk(&session, &self.app_data_root)?;
}
Ok(())
}
}
fn persist_session_to_disk(session: &StorytellerSessionCookie, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
let creds_dir = app_data_root.credentials_dir();
let filename = creds_dir.get_storyteller_session_cookie_file_path();
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(filename)?;
file.write_all(session.as_bytes())?;
file.flush()?;
Ok(())
}
fn persist_avt_to_disk(avt: &StorytellerAvtCookie, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
let creds_dir = app_data_root.credentials_dir();
let filename = creds_dir.get_storyteller_avt_cookie_file_path();
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(filename)?;
file.write_all(avt.as_bytes())?;
file.flush()?;
Ok(())
}
@@ -0,0 +1,42 @@
use anyhow::anyhow;
use discord_rich_presence::{DiscordIpc, DiscordIpcClient};
use errors::AnyhowResult;
use log::info;
/// Our Discord App ID. Not sure if this is a secret.
const DISCORD_APP_ID : &str = "1366596912593113138";
pub async fn discord_presence_thread() -> ! {
loop {
let client = DiscordIpcClient::new(DISCORD_APP_ID);
let _err = discord_main_loop(client).await;
tokio::time::sleep(std::time::Duration::from_millis(10_000)).await;
}
}
async fn discord_main_loop(mut client: DiscordIpcClient) -> AnyhowResult<()> {
info!("Connecting Discord IPC client");
client.connect()
.map_err(|err| anyhow!("Error connecting to Discord IPC: {:?}", err))?;
loop {
info!("Notifying discord...");
let assets = discord_rich_presence::activity::Assets::new()
.large_image("https://storyteller.ai/android-chrome-512x512.png")
.large_text("https://GetArtCraft.com");
//.small_image("https://storyteller.ai/android-chrome-192x192.png")
//.small_text("small text");
client.set_activity(discord_rich_presence::activity::Activity::new()
//.state("Anyone can make arts!")
.state("GetArtCraft.com")
.details("Making Arts with ArtCraft")
.activity_type(discord_rich_presence::activity::ActivityType::Playing)
.assets(assets))
.map_err(|err| anyhow!("Error setting Discord activity: {:?}", err))?;
tokio::time::sleep(std::time::Duration::from_millis(30_000)).await;
}
}
@@ -0,0 +1,62 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::main_window_size::MainWindowSize;
use crate::state::storyteller::storyteller_credential_manager::StorytellerCredentialManager;
use crate::threads::main_window_thread::persist_storyteller_cookies_task::persist_storyteller_cookies_task;
use crate::threads::main_window_thread::persist_window_resize_task::persist_window_resize_task;
use errors::AnyhowResult;
use log::{error, info};
use memory_store::clone_slot::CloneSlot;
use tauri::{AppHandle, Manager, Webview, Window};
use crate::state::app_startup_time::AppStartupTime;
const MAIN_WINDOW_NAME : &str = "main";
pub async fn main_window_thread(
app: AppHandle,
app_data_root: AppDataRoot,
storyteller_creds_manager: StorytellerCredentialManager,
) -> ! {
// TODO: Move these into some kind of dependency injection framework
let window_size_slot: CloneSlot<MainWindowSize> = CloneSlot::empty();
let app_startup_time = AppStartupTime::new();
loop {
for (window_name, window) in app.windows() {
if window_name == MAIN_WINDOW_NAME {
let result = handle_main_window(
&app,
&window,
&app_data_root,
&storyteller_creds_manager,
&window_size_slot,
&app_startup_time,
).await;
if let Err(err) = result {
error!("Error handling main window: {:?}", err);
}
}
}
tokio::time::sleep(std::time::Duration::from_millis(1_000)).await;
}
}
pub async fn handle_main_window(
app: &AppHandle,
window: &Window,
app_data_root: &AppDataRoot,
storyteller_creds_manager: &StorytellerCredentialManager,
window_size_slot: &CloneSlot<MainWindowSize>,
app_startup_time: &AppStartupTime,
) -> AnyhowResult<()> {
loop {
log_errors(persist_window_resize_task(window, app_data_root, window_size_slot).await);
log_errors(persist_storyteller_cookies_task(app, storyteller_creds_manager, app_startup_time).await);
tokio::time::sleep(std::time::Duration::from_millis(1_000)).await;
}
}
pub fn log_errors<T>(result: AnyhowResult<T>) {
if let Err(err) = result {
error!("Error persisting window size: {:?}", err);
}
}
@@ -0,0 +1,3 @@
pub mod main_window_thread;
pub mod persist_storyteller_cookies_task;
pub mod persist_window_resize_task;
@@ -0,0 +1,114 @@
use crate::state::app_startup_time::AppStartupTime;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::storyteller::storyteller_credential_manager::StorytellerCredentialManager;
use crate::threads::sora_session_login_thread::LOGIN_WINDOW_NAME;
use anyhow::anyhow;
use chrono::TimeDelta;
use errors::AnyhowResult;
use log::{debug, error, info};
use once_cell::sync::Lazy;
use reqwest::Url;
use std::fs;
use std::sync::LockResult;
use storyteller_client::credentials::storyteller_avt_cookie::StorytellerAvtCookie;
use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use storyteller_client::credentials::storyteller_session_cookie::StorytellerSessionCookie;
use tauri::{AppHandle, Manager, Webview, Window};
use tauri_plugin_http::reqwest_cookie_store::CookieStore;
use tauri_plugin_http::Http;
const MAIN_WEBVIEW_NAME: &str = "main";
const STORYTELLER_ROOT_COOKIE_URL_STR: &str = "https://api.storyteller.ai/";
static STORYTELLER_ROOT_COOKIE_URL: Lazy<Url> = Lazy::new(|| {
Url::parse(STORYTELLER_ROOT_COOKIE_URL_STR).expect("URL should parse")
});
const AVT_COOKIE_NAME : &str = "visitor";
const SESSION_COOKIE_NAME : &str = "session";
/// There's some kind of race condition that we need to wait for before inspecting cookies.
const RACE_CONDITION_WAIT_TIME: TimeDelta = TimeDelta::milliseconds(5000);
pub async fn persist_storyteller_cookies_task(
app: &AppHandle,
storyteller_credential_manager: &StorytellerCredentialManager,
app_startup_time: &AppStartupTime,
) -> AnyhowResult<()> {
//if app_startup_time.time_delta_since() < RACE_CONDITION_WAIT_TIME {
// // NB: There's an issue when the "main window thread" inquires about the
// // webview cookies shortly after app startup. When it attempts to dump the
// // cookies within a few milliseconds of app launch, it deadlocks and the
// // app never launches correctly. The entire webview goes blank and the app
// // freezes. This hack seems to fix the problem.
// return Ok(())
//}
let maybe_http = app.try_state::<Http>();
let http = match maybe_http {
None => {
error!("No HTTP plugin found");
return Err(anyhow!("No HTTP plugin found"));
}
Some(http) => http,
};
match http.cookies_jar.store.lock() {
Err(err) => {
error!("Failed to lock cookie jar: {:?}", err);
return Err(anyhow!("Failed to lock cookie jar"));
}
Ok(cookie_jar) => {
sync_tauri_credentials(&cookie_jar, storyteller_credential_manager)?;
}
}
Ok(())
}
fn sync_tauri_credentials(
cookie_store: &CookieStore,
storyteller_credential_manager: &StorytellerCredentialManager,
) -> AnyhowResult<()> {
let current_http_plugin_credentials = get_credentials_from_cookie_store(cookie_store)?;
let mut replace_credentials = true;
let maybe_old_credentials = storyteller_credential_manager.get_credentials()?;
if let Some(old_credentials) = maybe_old_credentials {
if old_credentials.equals(&current_http_plugin_credentials) {
replace_credentials = false;
}
}
if replace_credentials {
info!("Syncing ArtCraft credentials ...");
storyteller_credential_manager.set_credentials(&current_http_plugin_credentials)?;
// NB: tauri-plugin-http stores the credentials on disk, so we can defer to that for now.
//storyteller_credential_manager.persist_all_to_disk()?;
}
Ok(())
}
pub fn get_credentials_from_cookie_store(cookie_jar: &CookieStore) -> AnyhowResult<StorytellerCredentialSet> {
let mut avt_cookie = None;
let mut session_cookie = None;
for cookie in cookie_jar.iter_unexpired() {
if let Some(avt) = StorytellerAvtCookie::maybe_from_cookie(&cookie) {
avt_cookie = Some(avt);
} else if let Some(session) = StorytellerSessionCookie::maybe_from_cookie(&cookie) {
session_cookie = Some(session);
}
}
Ok(StorytellerCredentialSet::initialize(
avt_cookie,
session_cookie,
))
}
@@ -0,0 +1,32 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::main_window_size::MainWindowSize;
use errors::AnyhowResult;
use log::info;
use memory_store::clone_slot::CloneSlot;
use tauri::Window;
pub async fn persist_window_resize_task(
window: &Window,
app_data_root: &AppDataRoot,
window_size_slot: &CloneSlot<MainWindowSize>,
) -> AnyhowResult<()> {
let current_window_size = MainWindowSize::from_window(window)?;
let current_physical_size = window.inner_size()?;
let mut save_size_to_disk = true;
if let Ok(Some(old_size)) = window_size_slot.get_clone() {
if old_size.matches_physical_size(&current_physical_size) {
save_size_to_disk = false;
}
}
if save_size_to_disk {
info!("Saving window size configs to disk...");
current_window_size.persist_to_filesystem(app_data_root)?;
window_size_slot.set_clone(&current_window_size)?;
}
Ok(())
}
@@ -1 +1,4 @@
pub mod sora_session_login_thread;
pub mod discord_presence_thread;
pub mod main_window_thread;
pub mod sora_session_login_thread;
pub mod sora_task_polling_thread;
@@ -1,15 +1,21 @@
use crate::state::app_dir::AppDataRoot;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
use crate::utils::cookies::sora_webview_cookies::get_all_sora_cookies_as_string;
use crate::utils::sora::initialize_sora_jwt_bearer_token::initialize_sora_jwt_bearer_token;
use crate::utils::sora_webview_cookies::get_all_sora_cookies_as_string;
use anyhow::anyhow;
use chrono::{DateTime, NaiveDateTime, TimeDelta};
use errors::AnyhowResult;
use log::{error, info};
use log::{error, info, warn};
use once_cell::sync::Lazy;
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use openai_sora_client::utils::has_session_cookie::{has_session_cookie, SessionCookiePresence};
use reqwest::Url;
use std::fs::OpenOptions;
use std::fs;
use std::fs::{read_to_string, OpenOptions};
use std::io::Write;
use std::ops::Sub;
use tauri::{AppHandle, Manager, Webview};
pub const LOGIN_WINDOW_NAME: &str = "login_window";
@@ -34,7 +40,11 @@ pub async fn sora_session_login_thread(
loop {
for (window_name, webview) in app.webviews() {
if window_name == LOGIN_WINDOW_NAME {
let result = check_login_window(&webview, &app_data_root).await;
let result = check_login_window(
&webview,
&app_data_root,
&sora_creds_manager,
).await;
if let Err(err) = result {
error!("Error checking login window: {:?}", err);
}
@@ -45,10 +55,14 @@ pub async fn sora_session_login_thread(
}
}
async fn check_login_window(webview: &Webview, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
async fn check_login_window(
webview: &Webview,
app_data_root: &AppDataRoot,
sora_credential_manager: &SoraCredentialManager,
) -> AnyhowResult<()> {
clear_browsing_data_on_test_domain(webview)?;
//keep_on_task(webview)?;
extract_cookies_to_file(webview, app_data_root)?;
extract_cookies_to_file(webview, app_data_root, sora_credential_manager).await?;
//initialize_sora_jwt_bearer_token(app_data_root).await?; // TODO: This only runs once. We need better management.
Ok(())
}
@@ -92,19 +106,67 @@ fn clear_browsing_data_on_test_domain(webview: &Webview) -> AnyhowResult<()> {
}
// TODO(bt,2025-04-07): Heuristic to detect when logged in. Only write when logged in.
fn extract_cookies_to_file(webview: &Webview, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
let cookies = get_all_sora_cookies_as_string(webview)?;
async fn extract_cookies_to_file(
webview: &Webview,
app_data_root: &AppDataRoot,
sora_credential_manager: &SoraCredentialManager,
) -> AnyhowResult<()> {
let new_cookies = get_all_sora_cookies_as_string(webview)?.trim().to_string();
let cookie_file = app_data_root.get_sora_cookie_file_path();
if new_cookies.is_empty() {
return Ok(());
}
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(cookie_file)?;
let session_cookie_presence = has_session_cookie(&new_cookies)
.unwrap_or_else(|err| {
error!("Failed to check for session cookie: {:?}", err);
SessionCookiePresence::MaybePresent
});
file.write_all(cookies.as_bytes())?;
file.flush()?;
info!("Session cookies are: {:?}", session_cookie_presence);
let (should_write_cookies, should_upgrade_session) = match session_cookie_presence {
SessionCookiePresence::Present => (true, true),
SessionCookiePresence::MaybePresent => (true, false),
SessionCookiePresence::Absent => (false, false),
};
if !should_write_cookies {
return Ok(());
}
// TODO(bt): Race conditions ahead.
sora_credential_manager.clear_credentials()?;
let _r = fs::remove_file(app_data_root.get_sora_cookie_file_path());
let _r = fs::remove_file(app_data_root.get_sora_bearer_token_file_path());
let _r = fs::remove_file(app_data_root.get_sora_sentinel_file_path());
let mut new_credentials =
SoraCredentialSet::initialize_with_just_cookies_str(&new_cookies);
if should_upgrade_session {
let _upgraded = maybe_upgrade_or_renew_session(&mut new_credentials).await?;
}
sora_credential_manager.set_credentials(&new_credentials)?;
sora_credential_manager.persist_all_to_disk()?;
Ok(())
}
pub fn read_sora_cookies_from_disk(app_data_root: &AppDataRoot) -> Option<String> {
let cookie_file = app_data_root.get_sora_cookie_file_path();
if !cookie_file.exists() {
return None;
}
match read_to_string(cookie_file) {
Ok(contents) => Some(contents.trim().to_string()),
Err(err) => {
warn!("Failed to read cookie file: {:?}", err);
None
}
}
}
@@ -0,0 +1,142 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::data_dir::trait_data_subdir::DataSubdir;
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 errors::AnyhowResult;
use log::{error, info};
use openai_sora_client::recipes::list_sora_task_status_with_session_auto_renew::{list_sora_task_status_with_session_auto_renew, StatusRequestArgs};
use openai_sora_client::requests::image_gen::image_gen_status::{Generation, TaskId, TaskStatus};
use reqwest::Url;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use tauri::AppHandle;
use tempdir::TempDir;
use storyteller_client::media_files::upload_image_media_file_from_file::upload_image_media_file_from_file;
use storyteller_client::utils::api_host::ApiHost;
pub async fn sora_task_polling_thread(
app_data_root: AppDataRoot,
sora_creds_manager: SoraCredentialManager,
storyteller_creds_manager: StorytellerCredentialManager,
sora_task_queue: SoraTaskQueue,
) -> ! {
loop {
let res = polling_loop(
&sora_creds_manager,
&storyteller_creds_manager,
&sora_task_queue,
&app_data_root,
).await;
if let Err(err) = res {
error!("An error occurred: {:?}", err);
}
tokio::time::sleep(std::time::Duration::from_millis(30_000)).await;
}
}
async fn polling_loop(
sora_creds_manager: &SoraCredentialManager,
storyteller_creds_manager: &StorytellerCredentialManager,
sora_task_queue: &SoraTaskQueue,
app_data_root: &AppDataRoot,
) -> AnyhowResult<()> {
loop {
if sora_task_queue.is_empty()? {
// No need to poll if we don't have pending tasks.
tokio::time::sleep(std::time::Duration::from_millis(1_000)).await;
continue;
}
info!("Task queue has {} pending tasks.", sora_task_queue.len()?);
let creds = sora_creds_manager.get_credentials_required()?;
let (response, maybe_new_creds) = list_sora_task_status_with_session_auto_renew(StatusRequestArgs {
limit: None,
// TODO: How can we use the task id to poll better? Our existing code doesn't seem to illuminate this.
before: None,
credentials: creds,
}).await?;
if let Some(new_creds) = maybe_new_creds {
info!("Saving new credentials.");
sora_creds_manager.set_credentials(&new_creds)?;
}
let mut succeeded_tasks = Vec::new();
let mut failed_tasks = Vec::new();
// TODO: The cursoring logic likely needs to improve.
for task in response.task_responses {
let status = TaskStatus::from_str(&task.status);
match status {
TaskStatus::Succeeded => {
succeeded_tasks.push(task);
}
TaskStatus::Failed => {
failed_tasks.push(task);
}
TaskStatus::Queued => {}
TaskStatus::Running => {}
TaskStatus::Unknown(_) => {}
}
}
let failed_task_ids: Vec<&TaskId> = failed_tasks
.iter()
.map(|task| &task.id)
.collect();
sora_task_queue.remove_list(&failed_task_ids)?;
let creds = storyteller_creds_manager.get_credentials_required()?;
let api_host = ApiHost::Storyteller;
for task in succeeded_tasks.iter() {
if !sora_task_queue.contains_key(&task.id)? {
continue;
}
info!("Task succeeded: {:?}", task.id);
for (i, generation) in task.generations.iter().enumerate() {
info!("Downloading generated file...");
let download_path = download_generation(generation, &app_data_root).await?;
info!("Uploading to backend...");
let result = upload_image_media_file_from_file(&api_host, Some(&creds), download_path).await?;
info!("Uploaded to API backend: {:?}", result.media_file_token);
}
}
let succeeded_task_ids : Vec<&TaskId> = succeeded_tasks
.iter()
.map(|task| &task.id)
.collect();
sora_task_queue.remove_list(&succeeded_task_ids)?;
tokio::time::sleep(std::time::Duration::from_millis(2_000)).await;
}
}
async fn download_generation(generation: &Generation, app_data_root: &AppDataRoot) -> AnyhowResult<PathBuf> {
let url = Url::parse(&generation.url)?;
let response = reqwest::get(&generation.url).await?;
let image_bytes = response.bytes().await?;
let ext = url.path().split(".").last().unwrap_or("png");
let tempdir = app_data_root.temp_dir().path();
let download_filename = format!("{}.{}", generation.id, ext);
let download_path = tempdir.join(download_filename);
let mut file = File::create(&download_path)?;
file.write_all(&image_bytes)?;
Ok(download_path)
}
@@ -0,0 +1 @@
pub mod sora_webview_cookies;
@@ -1,7 +1,8 @@
pub mod clear_all_webview_cookies;
pub mod cookies;
pub mod get_url_file_extension;
pub mod image;
pub mod large_file_transfer;
pub mod object_store;
pub mod simple_http_download;
pub mod sora_webview_cookies;
pub mod sora;
pub mod sora;

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