mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-20 17:02:53 -04:00
Compare commits
129
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48f80f201b | ||
|
|
86c77e9625 | ||
|
|
776312ef6e | ||
|
|
7da40d5177 | ||
|
|
f06b5098e2 | ||
|
|
eac503af46 | ||
|
|
16932374f5 | ||
|
|
35940a4a63 | ||
|
|
55543b3735 | ||
|
|
3d44e6e29e | ||
|
|
3244b802fa | ||
|
|
5e7a5d2cff | ||
|
|
ecb73e4f80 | ||
|
|
39092fa866 | ||
|
|
ca80275d28 | ||
|
|
8c192b17f1 | ||
|
|
40da6f6500 | ||
|
|
2a3982aa5a | ||
|
|
a1c2b4f5c8 | ||
|
|
213d3f7bd2 | ||
|
|
0788e98b54 | ||
|
|
65c8ddfa91 | ||
|
|
a60e20c0b4 | ||
|
|
54d417f4c5 | ||
|
|
98a74e3a96 | ||
|
|
b537223ffa | ||
|
|
8001f24195 | ||
|
|
778a4e72c8 | ||
|
|
083680232c | ||
|
|
b1b44ceb60 | ||
|
|
d988acbf6e | ||
|
|
6e05b7a1b5 | ||
|
|
8a364605cf | ||
|
|
30a8e72efe | ||
|
|
ff1a83898b | ||
|
|
f1b9299e46 | ||
|
|
2eefaaf7a1 | ||
|
|
a21b4a4275 | ||
|
|
e4c561eb41 | ||
|
|
420a23d359 | ||
|
|
9cd3b493c6 | ||
|
|
bbd5953000 | ||
|
|
b346750968 | ||
|
|
e5749f0e77 | ||
|
|
826cf00cbb | ||
|
|
6b7d034068 | ||
|
|
7bd3f6f8c6 | ||
|
|
0c550fcb1a | ||
|
|
3b2b7ede18 | ||
|
|
782b68e9a2 | ||
|
|
5c23568646 | ||
|
|
43e781571f | ||
|
|
84361eb93e | ||
|
|
38ab84d52e | ||
|
|
82e87fa883 | ||
|
|
de5daa3f92 | ||
|
|
fc0fba7573 | ||
|
|
b199738ee0 | ||
|
|
aa76113e8c | ||
|
|
bfe02a7880 | ||
|
|
ac81de6a48 | ||
|
|
2c2a63e80b | ||
|
|
3521f73142 | ||
|
|
88fd732eec | ||
|
|
490915a3e7 | ||
|
|
1802b0939e | ||
|
|
36b6aa78f8 | ||
|
|
499c07548c | ||
|
|
65580ec502 | ||
|
|
5d5b782760 | ||
|
|
09d0ae263f | ||
|
|
c3509d770f | ||
|
|
ee1043f92c | ||
|
|
e88b6ce39b | ||
|
|
0e535e52ad | ||
|
|
aa594b773e | ||
|
|
ac209f8f4f | ||
|
|
66abfd18ba | ||
|
|
79cf730270 | ||
|
|
698351f2a3 | ||
|
|
fc4eaff7f6 | ||
|
|
dadf14dbf4 | ||
|
|
ff695d7af6 | ||
|
|
a5e9bf2262 | ||
|
|
b49da80762 | ||
|
|
a0db62f0fd | ||
|
|
ebc0f1ae42 | ||
|
|
dc5f5c5906 | ||
|
|
98b7042192 | ||
|
|
76db001f59 | ||
|
|
60637394d6 | ||
|
|
f3b5a77475 | ||
|
|
d3e1d7fab9 | ||
|
|
e34f9cad68 | ||
|
|
37f29e2e48 | ||
|
|
f2a2aa6e28 | ||
|
|
76a639a045 | ||
|
|
4fd69c2439 | ||
|
|
64948e82a6 | ||
|
|
878763c987 | ||
|
|
959bdcd918 | ||
|
|
a9967f0e76 | ||
|
|
cd680ac319 | ||
|
|
3eebb7fec9 | ||
|
|
63753c878a | ||
|
|
769ee07d69 | ||
|
|
2fee3e12d5 | ||
|
|
5eb1e256e7 | ||
|
|
46b687aca2 | ||
|
|
df07e1f1e9 | ||
|
|
bb78fb389f | ||
|
|
5015cd30a1 | ||
|
|
33699a16ef | ||
|
|
9e54e9b864 | ||
|
|
d11c30c914 | ||
|
|
fe9dfbb63a | ||
|
|
aca784042e | ||
|
|
a2e14aaea3 | ||
|
|
ab97e73279 | ||
|
|
4313ccc980 | ||
|
|
397172e036 | ||
|
|
d7566dd7ab | ||
|
|
acf5852acc | ||
|
|
d5e2a222b5 | ||
|
|
06ca3be4f5 | ||
|
|
a4e7a785d7 | ||
|
|
dd8b03692a | ||
|
|
638f453cb8 | ||
|
|
5e5da8c95b |
@@ -0,0 +1,63 @@
|
||||
name: 'publish'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- tauri-prod-release
|
||||
|
||||
# This workflow will trigger on each push to the `release` branch to create or update a GitHub release, build your app, and upload the artifacts to the release.
|
||||
|
||||
jobs:
|
||||
publish-tauri:
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: 'macos-latest' # for Arm based macs (M1 and above).
|
||||
args: '--target aarch64-apple-darwin'
|
||||
# - platform: 'macos-latest' # for Intel based macs.
|
||||
# args: '--target x86_64-apple-darwin'
|
||||
# - platform: 'ubuntu-22.04' # for Tauri v1 you could replace this with ubuntu-20.04.
|
||||
# args: ''
|
||||
# - platform: 'windows-latest'
|
||||
# args: ''
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
|
||||
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above.
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
|
||||
# You can remove the one that doesn't apply to your app to speed up the workflow a bit.
|
||||
|
||||
- name: install frontend dependencies
|
||||
run: cd ./crates/desktop/tauri-artcraft && yarn install # change this to npm, pnpm or bun depending on which one you use.
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
projectPath: ./crates/desktop/tauri-artcraft
|
||||
tagName: app-v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version.
|
||||
releaseName: 'App v__VERSION__'
|
||||
releaseBody: 'See the assets to download this version and install.'
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
args: ${{ matrix.args }}
|
||||
Generated
+13
@@ -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>
|
||||
Generated
+5
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
+12
@@ -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
+254
-81
@@ -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]]
|
||||
@@ -1079,6 +1082,19 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backoff"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"getrandom 0.2.14",
|
||||
"instant",
|
||||
"pin-project-lite",
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.69"
|
||||
@@ -1179,7 +1195,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"toml 0.8.14",
|
||||
"toml 0.8.20",
|
||||
"url_config",
|
||||
"user_traits_component",
|
||||
"utoipa",
|
||||
@@ -1834,7 +1850,7 @@ dependencies = [
|
||||
"semver 1.0.23",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1844,7 +1860,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02260d489095346e5cafd04dea8e8cb54d1d74fcd759022a9b72986ebe9a1257"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"toml 0.8.14",
|
||||
"toml 0.8.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1908,7 +1924,7 @@ checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"fnv",
|
||||
"uuid",
|
||||
"uuid 1.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2864,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"
|
||||
@@ -3245,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"
|
||||
@@ -3666,7 +3702,7 @@ dependencies = [
|
||||
"cc",
|
||||
"memchr",
|
||||
"rustc_version 0.4.0",
|
||||
"toml 0.8.14",
|
||||
"toml 0.8.20",
|
||||
"vswhom",
|
||||
"winreg 0.52.0",
|
||||
]
|
||||
@@ -3727,7 +3763,7 @@ dependencies = [
|
||||
"sqlx-core",
|
||||
"sqlx-mysql",
|
||||
"strum",
|
||||
"toml 0.8.14",
|
||||
"toml 0.8.20",
|
||||
"utoipa",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
@@ -3741,7 +3777,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"toml 0.8.14",
|
||||
"toml 0.8.20",
|
||||
"utoipa",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
@@ -4881,7 +4917,7 @@ dependencies = [
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"http 0.2.11",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.9.0",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -4900,7 +4936,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http 1.1.0",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.9.0",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -4940,6 +4976,12 @@ dependencies = [
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||
|
||||
[[package]]
|
||||
name = "hashing"
|
||||
version = "0.0.1"
|
||||
@@ -5034,7 +5076,7 @@ dependencies = [
|
||||
"reqwest 0.12.15",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"ureq",
|
||||
]
|
||||
@@ -5306,6 +5348,7 @@ dependencies = [
|
||||
"hyper 1.6.0",
|
||||
"hyper-util",
|
||||
"rustls 0.23.13",
|
||||
"rustls-native-certs",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
@@ -5528,7 +5571,7 @@ dependencies = [
|
||||
name = "idempotency"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"uuid",
|
||||
"uuid 1.8.0",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
@@ -5634,8 +5677,10 @@ dependencies = [
|
||||
name = "images"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"errors",
|
||||
"image",
|
||||
"mimetypes",
|
||||
"testing",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
@@ -5665,12 +5710,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.2.6"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
||||
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.14.5",
|
||||
"hashbrown 0.15.2",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -6749,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"
|
||||
@@ -6798,7 +6852,7 @@ dependencies = [
|
||||
"sqlx",
|
||||
"tokens",
|
||||
"tokio",
|
||||
"toml 0.8.14",
|
||||
"toml 0.8.20",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
@@ -6849,7 +6903,9 @@ dependencies = [
|
||||
name = "mimetypes"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"infer 0.16.0",
|
||||
"anyhow",
|
||||
"infer 0.19.0",
|
||||
"testing",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
@@ -7086,7 +7142,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"png",
|
||||
"serde",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
@@ -7470,7 +7526,7 @@ dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"toml 0.8.14",
|
||||
"toml 0.8.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7771,10 +7827,10 @@ dependencies = [
|
||||
"sha2 0.10.8",
|
||||
"tar",
|
||||
"thiserror 1.0.63",
|
||||
"toml 0.8.14",
|
||||
"toml 0.8.20",
|
||||
"ureq",
|
||||
"url",
|
||||
"uuid",
|
||||
"uuid 1.8.0",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -7825,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",
|
||||
@@ -7839,9 +7897,9 @@ dependencies = [
|
||||
"sha3",
|
||||
"subprocess_common",
|
||||
"testing",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"toml 0.7.8",
|
||||
"toml 0.8.20",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
@@ -8283,7 +8341,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8486,7 +8544,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.9.0",
|
||||
"quick-xml 0.32.0",
|
||||
"serde",
|
||||
"time",
|
||||
@@ -9467,7 +9525,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
|
||||
dependencies = [
|
||||
"getrandom 0.2.14",
|
||||
"libredox",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9571,6 +9629,7 @@ version = "0.12.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"base64 0.22.1",
|
||||
"bytes 1.10.0",
|
||||
"cookie 0.18.1",
|
||||
@@ -9598,6 +9657,7 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls 0.23.13",
|
||||
"rustls-native-certs",
|
||||
"rustls-pemfile 2.1.2",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
@@ -9608,6 +9668,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tokio-socks",
|
||||
"tokio-util",
|
||||
"tower 0.5.2",
|
||||
"tower-service",
|
||||
@@ -9628,7 +9689,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"sqlx",
|
||||
"toml 0.8.14",
|
||||
"toml 0.8.20",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
@@ -9678,7 +9739,7 @@ dependencies = [
|
||||
"rkyv_derive",
|
||||
"seahash",
|
||||
"tinyvec",
|
||||
"uuid",
|
||||
"uuid 1.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9935,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"
|
||||
@@ -10079,7 +10153,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
"uuid",
|
||||
"uuid 1.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10305,7 +10379,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "224e6a14f315852940f3ec103125aa6482f0e224732ed91ed3330ed633077c34"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.9.0",
|
||||
"itoa 1.0.11",
|
||||
"ryu",
|
||||
"serde",
|
||||
@@ -10317,7 +10391,7 @@ version = "1.0.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.9.0",
|
||||
"itoa 1.0.11",
|
||||
"memchr",
|
||||
"ryu",
|
||||
@@ -10378,9 +10452,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.6"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
|
||||
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -10417,7 +10491,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.9.0",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
@@ -10455,7 +10529,7 @@ version = "0.9.34+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.9.0",
|
||||
"itoa 1.0.11",
|
||||
"ryu",
|
||||
"serde",
|
||||
@@ -10613,6 +10687,7 @@ dependencies = [
|
||||
"log",
|
||||
"openai_sora_client",
|
||||
"r2d2_redis",
|
||||
"reqwest 0.12.15",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
@@ -10911,7 +10986,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"hashlink",
|
||||
"hex",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.9.0",
|
||||
"log",
|
||||
"memchr",
|
||||
"native-tls",
|
||||
@@ -11168,6 +11243,7 @@ dependencies = [
|
||||
"hostname 0.4.0",
|
||||
"http_server_common",
|
||||
"idempotency",
|
||||
"images",
|
||||
"infer 0.16.0",
|
||||
"language-tags",
|
||||
"lexical-sort",
|
||||
@@ -11217,7 +11293,7 @@ dependencies = [
|
||||
"time",
|
||||
"tokens",
|
||||
"tokio",
|
||||
"toml 0.8.14",
|
||||
"toml 0.8.20",
|
||||
"tts_common",
|
||||
"url",
|
||||
"url_config",
|
||||
@@ -11225,12 +11301,36 @@ dependencies = [
|
||||
"user_traits_component",
|
||||
"utoipa",
|
||||
"utoipa-swagger-ui",
|
||||
"uuid",
|
||||
"uuid 1.8.0",
|
||||
"videos",
|
||||
"workspace-build-acceleration",
|
||||
"zip 0.6.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "storyteller_client"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"backoff",
|
||||
"chrono",
|
||||
"cookie 0.18.1",
|
||||
"enums",
|
||||
"enums_public",
|
||||
"errors",
|
||||
"idempotency",
|
||||
"log",
|
||||
"reqwest 0.12.15",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tokens",
|
||||
"tokio",
|
||||
"url",
|
||||
"uuid 1.8.0",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "storyteller_root"
|
||||
version = "0.0.1"
|
||||
@@ -11858,7 +11958,7 @@ dependencies = [
|
||||
"cfg-expr",
|
||||
"heck 0.5.0",
|
||||
"pkg-config",
|
||||
"toml 0.8.14",
|
||||
"toml 0.8.20",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
@@ -11974,7 +12074,7 @@ dependencies = [
|
||||
"tauri-runtime",
|
||||
"tauri-runtime-wry",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tray-icon",
|
||||
"url",
|
||||
@@ -11998,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",
|
||||
@@ -12015,12 +12118,15 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"serial_test",
|
||||
"storyteller_client",
|
||||
"subprocess_common",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-http",
|
||||
"tauri-plugin-log",
|
||||
"tempdir",
|
||||
"tempfile",
|
||||
"tokens",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"workspace-build-acceleration",
|
||||
@@ -12044,7 +12150,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"tauri-winres",
|
||||
"toml 0.8.14",
|
||||
"toml 0.8.20",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -12068,10 +12174,10 @@ dependencies = [
|
||||
"sha2 0.10.8",
|
||||
"syn 2.0.87",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"url",
|
||||
"uuid",
|
||||
"uuid 1.8.0",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -12091,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",
|
||||
@@ -12102,10 +12208,56 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"toml 0.8.14",
|
||||
"toml 0.8.20",
|
||||
"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"
|
||||
@@ -12124,7 +12276,7 @@ dependencies = [
|
||||
"swift-rs",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
]
|
||||
|
||||
@@ -12184,7 +12336,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
"url",
|
||||
"windows 0.60.0",
|
||||
]
|
||||
@@ -12218,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",
|
||||
@@ -12246,11 +12398,11 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_with 3.12.0",
|
||||
"swift-rs",
|
||||
"thiserror 2.0.11",
|
||||
"toml 0.8.14",
|
||||
"thiserror 2.0.12",
|
||||
"toml 0.8.20",
|
||||
"url",
|
||||
"urlpattern",
|
||||
"uuid",
|
||||
"uuid 1.8.0",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -12261,7 +12413,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56eaa45f707bedf34d19312c26d350bc0f3c59a47e58e8adbeecdc850d2c13a0"
|
||||
dependencies = [
|
||||
"embed-resource",
|
||||
"toml 0.8.14",
|
||||
"toml 0.8.20",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -12350,11 +12502,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.11"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
|
||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.11",
|
||||
"thiserror-impl 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -12370,9 +12522,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.11"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
|
||||
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -12587,7 +12739,7 @@ dependencies = [
|
||||
"sqlx-core",
|
||||
"sqlx-mysql",
|
||||
"strum",
|
||||
"toml 0.8.14",
|
||||
"toml 0.8.20",
|
||||
"utoipa",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
@@ -12703,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"
|
||||
@@ -12783,21 +12947,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.14"
|
||||
version = "0.8.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335"
|
||||
checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit 0.22.14",
|
||||
"toml_edit 0.22.24",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.6"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
|
||||
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -12808,7 +12972,7 @@ version = "0.19.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.9.0",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
@@ -12821,7 +12985,7 @@ version = "0.20.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.9.0",
|
||||
"toml_datetime",
|
||||
"winnow 0.5.40",
|
||||
]
|
||||
@@ -12832,22 +12996,22 @@ version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.9.0",
|
||||
"toml_datetime",
|
||||
"winnow 0.5.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.14"
|
||||
version = "0.22.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
|
||||
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.9.0",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow 0.6.7",
|
||||
"winnow 0.7.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13030,7 +13194,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"png",
|
||||
"serde",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
@@ -13457,7 +13621,7 @@ version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ff05e3bac2c9428f57ade702667753ca3f5cf085e2011fe697de5bfd49aa72d"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.9.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"utoipa-gen",
|
||||
@@ -13494,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"
|
||||
@@ -13933,7 +14106,7 @@ version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfb27fccd3c27f68e9a6af1bcf48c2d82534b8675b83608a4d81446d095a17ac"
|
||||
dependencies = [
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
"windows 0.60.0",
|
||||
"windows-core 0.60.1",
|
||||
]
|
||||
@@ -14489,9 +14662,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.7"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14b9415ee827af173ebb3f15f9083df5a122eb93572ec28741fb153356ea2578"
|
||||
checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -14577,7 +14750,7 @@ dependencies = [
|
||||
"hyper-util",
|
||||
"image",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.9.0",
|
||||
"itertools 0.12.1",
|
||||
"itertools 0.13.0",
|
||||
"lazy_static",
|
||||
@@ -14655,7 +14828,7 @@ dependencies = [
|
||||
"unicode-normalization",
|
||||
"ureq",
|
||||
"url",
|
||||
"uuid",
|
||||
"uuid 1.8.0",
|
||||
"void",
|
||||
"winapi 0.3.9",
|
||||
"windows-sys 0.48.0",
|
||||
@@ -14714,7 +14887,7 @@ dependencies = [
|
||||
"sha2 0.10.8",
|
||||
"soup3",
|
||||
"tao-macros",
|
||||
"thiserror 2.0.11",
|
||||
"thiserror 2.0.12",
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webkit2gtk-sys",
|
||||
@@ -14934,7 +15107,7 @@ dependencies = [
|
||||
"crossbeam-utils 0.8.20",
|
||||
"displaydoc",
|
||||
"flate2",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.9.0",
|
||||
"num_enum",
|
||||
"thiserror 1.0.63",
|
||||
]
|
||||
@@ -14955,7 +15128,7 @@ dependencies = [
|
||||
"displaydoc",
|
||||
"flate2",
|
||||
"hmac 0.12.1",
|
||||
"indexmap 2.2.6",
|
||||
"indexmap 2.9.0",
|
||||
"lzma-rs",
|
||||
"memchr",
|
||||
"pbkdf2 0.12.2",
|
||||
|
||||
+23
-4
@@ -34,10 +34,12 @@ members = [
|
||||
"crates/lib/api_clients/google_drive_common",
|
||||
"crates/lib/api_clients/google_sign_in",
|
||||
"crates/lib/api_clients/openai_sora_client",
|
||||
"crates/lib/api_clients/storyteller_client",
|
||||
"crates/lib/api_clients/thumbnail_generator",
|
||||
"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",
|
||||
@@ -116,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",
|
||||
|
||||
@@ -146,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
|
||||
@@ -154,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"
|
||||
@@ -165,15 +174,25 @@ serde_derive = "1.0.219"
|
||||
serde_json = "1.0.140"
|
||||
serde_yaml = "0.9.1"
|
||||
sha3 = "0.10.8"
|
||||
thiserror = "2"
|
||||
toml = "0.7.4"
|
||||
url = "2.5.4"
|
||||
tauri = { version = "2.4.1", features = ["unstable"] }
|
||||
thiserror = "2.0.12"
|
||||
toml = "0.8.20"
|
||||
url = { version = "2.5.4", features = ["serde"] }
|
||||
|
||||
# Internal libs
|
||||
crockford = { path = "crates/lib/data/crockford" }
|
||||
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" }
|
||||
shared_service_components = { path = "crates/lib/shared_service_components" }
|
||||
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" }
|
||||
storyteller_client = { path = "crates/lib/api_clients/storyteller_client" }
|
||||
subprocess_common = { path = "crates/lib/subprocess_common" }
|
||||
tokens = { path = "crates/_types/tokens" }
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -27,15 +27,16 @@ 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 }
|
||||
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"
|
||||
@@ -43,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" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
use std::fs::read_to_string;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use errors::AnyhowResult;
|
||||
use base64::Engine;
|
||||
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
|
||||
use image::{DynamicImage, ImageReader};
|
||||
use std::io::Cursor;
|
||||
use log::{error, info};
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use openai_sora_client::credentials::SoraCredentials;
|
||||
use openai_sora_client::image_gen::common::{ImageSize, NumImages};
|
||||
use openai_sora_client::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
|
||||
use openai_sora_client::upload::upload_media_from_bytes::sora_media_upload_from_bytes;
|
||||
use openai_sora_client::upload::upload_media_from_file::SoraMediaUploadRequest;
|
||||
use crate::state::app_dir::AppDataRoot;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn image_generation_command(
|
||||
image: &str,
|
||||
prompt: &str,
|
||||
app: AppHandle,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
) -> Result<String, String> {
|
||||
info!("image_generation_command called; processing image...");
|
||||
|
||||
let bytes = BASE64_STANDARD.decode(image)
|
||||
.map_err(|err| format!("Base64 decode error: {}", err))?;
|
||||
|
||||
generate_image(bytes, prompt, &app_data_root)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("error: {:?}", err);
|
||||
"there was an error".to_string()
|
||||
})?;
|
||||
|
||||
Ok("success".to_string())
|
||||
}
|
||||
|
||||
pub async fn generate_image(file_bytes: Vec<u8>, prompt: &str, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
|
||||
let sora_credentials = get_credentials(app_data_root)?;
|
||||
|
||||
let filename = "image.png".to_string();
|
||||
|
||||
let response= sora_media_upload_from_bytes(file_bytes, filename, &sora_credentials)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to upload image to Sora: {:?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
let sora_media_tokens = vec![response.id];
|
||||
|
||||
let response = sora_image_gen_remix(SoraImageGenRemixRequest {
|
||||
prompt: prompt.to_string(),
|
||||
num_images: NumImages::One,
|
||||
image_size: ImageSize::Square,
|
||||
sora_media_tokens: sora_media_tokens.clone(),
|
||||
credentials: &sora_credentials,
|
||||
}).await
|
||||
.map_err(|err| {
|
||||
error!("Failed to call Sora image generation: {:?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
println!(">> TASK ID: {:?} ", response.task_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_credentials(app_data_root: &AppDataRoot) -> AnyhowResult<SoraCredentials> {
|
||||
let cookie_file = app_data_root.get_sora_cookie_file_path();
|
||||
|
||||
let bearer = read_to_string("/Users/bt/dev/storyteller/storyteller-rust/test_data/temp/bearer.txt")?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let cookie= read_to_string(cookie_file)?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let sentinel = read_to_string("/Users/bt/dev/storyteller/storyteller-rust/test_data/temp/sentinel.txt")?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
Ok(SoraCredentials {
|
||||
bearer_token: bearer,
|
||||
cookie: cookie,
|
||||
sentinel: Some(sentinel),
|
||||
})
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
pub mod flip_image;
|
||||
pub mod image_generation_command;
|
||||
pub mod open_login_command;
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod open_sora_login_command;
|
||||
pub mod sora_image_generation_command;
|
||||
pub mod sora_image_remix_command;
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
use std::time::Duration;
|
||||
use crate::threads::sora_session_login_thread::{LOGIN_WINDOW_NAME, SORA_LOGIN_URL};
|
||||
use crate::utils::clear_all_webview_cookies::clear_all_webview_cookies;
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::time::Duration;
|
||||
use tauri::{AppHandle, Manager, Url, WebviewUrl, WebviewWindowBuilder, WindowBuilder};
|
||||
use tokio::time::sleep_until;
|
||||
use errors::AnyhowResult;
|
||||
use crate::threads::login_thread::{LOGIN_WINDOW_NAME, SORA_LOGIN_URL};
|
||||
use crate::utils::clear_all_webview_cookies::clear_all_webview_cookies;
|
||||
|
||||
pub const START_URL_STR: &str = "https://storyteller.ai/";
|
||||
|
||||
@@ -20,7 +20,7 @@ pub static SORA_HOMEPAGE_URL : Lazy<Url> = Lazy::new(|| {
|
||||
});
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_login_command(
|
||||
pub async fn open_sora_login_command(
|
||||
app: AppHandle,
|
||||
) -> Result<String, String> {
|
||||
info!("open_login_command called");
|
||||
@@ -0,0 +1,84 @@
|
||||
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;
|
||||
use base64::Engine;
|
||||
use errors::AnyhowResult;
|
||||
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 std::fs::read_to_string;
|
||||
use std::io::Cursor;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn sora_image_generation_command(
|
||||
prompt: &str,
|
||||
image: Option<&str>,
|
||||
_app: AppHandle,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
sora_creds_holder: State<'_, SoraCredentialHolder>,
|
||||
) -> Result<String, String> {
|
||||
info!("image_generation_command called; processing image...");
|
||||
|
||||
generate_image(image, prompt, &app_data_root, &sora_creds_holder)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("error: {:?}", err);
|
||||
"there was an error".to_string()
|
||||
})?;
|
||||
|
||||
Ok("success".to_string())
|
||||
}
|
||||
|
||||
pub async fn generate_image(
|
||||
maybe_image: Option<&str>,
|
||||
prompt: &str,
|
||||
app_data_root: &AppDataRoot,
|
||||
sora_creds_holder: &SoraCredentialHolder,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
let creds = read_sora_credentials_from_disk(app_data_root)
|
||||
.map_err(|err| {
|
||||
error!("Failed to read Sora credentials from disk: {:?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
let mut sora_media_tokens = vec![];
|
||||
|
||||
if let Some(image) = maybe_image {
|
||||
let image_bytes = BASE64_STANDARD.decode(image)?;
|
||||
|
||||
let filename = "image.png".to_string();
|
||||
|
||||
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);
|
||||
err
|
||||
})?;
|
||||
|
||||
sora_media_tokens.push(response.id);
|
||||
}
|
||||
|
||||
let response = sora_image_gen_remix(SoraImageGenRemixRequest {
|
||||
prompt: prompt.to_string(),
|
||||
num_images: NumImages::One,
|
||||
image_size: ImageSize::Square,
|
||||
sora_media_tokens: sora_media_tokens.clone(),
|
||||
credentials: CredentialMigrationRef::New(&creds),
|
||||
}).await
|
||||
.map_err(|err| {
|
||||
error!("Failed to call Sora image generation: {:?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
println!(">> TASK ID: {:?} ", response.task_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
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;
|
||||
use base64::Engine;
|
||||
use errors::AnyhowResult;
|
||||
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::upload::upload_media_from_bytes::sora_media_upload_from_bytes;
|
||||
use openai_sora_client::requests::upload::upload_media_from_file::sora_media_upload_from_file;
|
||||
use serde_derive::Deserialize;
|
||||
use std::fs::read_to_string;
|
||||
use std::io::Cursor;
|
||||
use storyteller_client::media_files::get_media_file::get_media_file;
|
||||
use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SoraImageRemixCommand {
|
||||
/// Image media file; the engine or canvas snapshot (screenshot).
|
||||
pub snapshot_media_token: MediaFileToken,
|
||||
|
||||
/// The user's image generation prompt.
|
||||
pub prompt: String,
|
||||
|
||||
/// Turn off the system prompt.
|
||||
pub disable_system_prompt: Option<bool>,
|
||||
|
||||
/// Additional images to include (optional). Up to nine images.
|
||||
pub maybe_additional_images: Option<Vec<MediaFileToken>>,
|
||||
|
||||
pub maybe_number_of_samples: Option<u32>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn sora_image_remix_command(
|
||||
_app: AppHandle,
|
||||
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...");
|
||||
|
||||
// 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);
|
||||
"there was an error".to_string()
|
||||
})?;
|
||||
|
||||
Ok("success".to_string())
|
||||
}
|
||||
|
||||
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?;
|
||||
|
||||
let media_file_url = &response.media_file.media_links.cdn_url;
|
||||
let extension_with_dot = get_url_file_extension(media_file_url)
|
||||
.map(|ext| format!(".{}", ext))
|
||||
.unwrap_or_else(|| ".png".to_string());
|
||||
|
||||
let filename = format!("{}{}", response.media_file.token.as_str(), extension_with_dot);
|
||||
let filename = app_data_root.downloads_dir().path().join(&filename);
|
||||
|
||||
simple_http_download(&media_file_url, &filename).await?;
|
||||
|
||||
let files_to_upload = vec![filename];
|
||||
|
||||
let mut creds = sora_creds_manager.get_credentials_required()?;
|
||||
|
||||
let credential_updated = maybe_upgrade_or_renew_session(&mut creds)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to upgrade or renew session: {:?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
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 (i, file_path) in files_to_upload.iter().enumerate() {
|
||||
info!("Uploading image {} of {}...", (i+1), files_to_upload.len());
|
||||
|
||||
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 (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 Some(new_creds) = maybe_new_creds {
|
||||
info!("Storing updated credentials.");
|
||||
sora_creds_manager.set_credentials(&new_creds)?;
|
||||
}
|
||||
|
||||
info!("New Sora Task ID: {:?} ", response.task_id);
|
||||
|
||||
sora_task_queue.insert(&response.task_id)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,21 +2,32 @@ pub mod commands;
|
||||
pub mod events;
|
||||
pub mod state;
|
||||
pub mod threads;
|
||||
pub mod transfer;
|
||||
pub mod utils;
|
||||
|
||||
use crate::commands::flip_image::flip_image;
|
||||
use crate::commands::image_generation_command::image_generation_command;
|
||||
use crate::commands::open_login_command::open_login_command;
|
||||
use crate::state::app_config::AppConfig;
|
||||
use crate::threads::login_thread::login_thread;
|
||||
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...");
|
||||
|
||||
@@ -24,19 +35,34 @@ 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_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![
|
||||
Target::new(TargetKind::Stdout),
|
||||
Target::new(TargetKind::LogDir { file_name: Some(app_data_root.log_file_name_str().to_string()) }),
|
||||
Target::new(TargetKind::LogDir {
|
||||
file_name: Some(app_data_root.log_file_name_str().to_string())
|
||||
}),
|
||||
])
|
||||
.build())
|
||||
.setup(|app| {
|
||||
.setup(move |app| {
|
||||
// TODO(bt): This is broken on windows
|
||||
// log_environment_details();
|
||||
|
||||
@@ -49,16 +75,44 @@ pub fn run() {
|
||||
//}
|
||||
let app = app.handle().clone();
|
||||
|
||||
tauri::async_runtime::spawn(login_thread(app, app_data_root2));
|
||||
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,
|
||||
image_generation_command,
|
||||
open_login_command,
|
||||
open_sora_login_command,
|
||||
platform_info_command,
|
||||
sora_image_generation_command,
|
||||
sora_image_remix_command,
|
||||
])
|
||||
.run(tauri::generate_context!("tauri.conf.json"))
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -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,219 +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 WEIGHTS_SUBDIRECTORY : &str = "weights";
|
||||
|
||||
const TEMPORARY_SUBDIRECTORY : &str = "temp";
|
||||
|
||||
/// Note: Tauri appends ".log" to the end of the filename.
|
||||
const LOG_FILE_NAME : &str = "application_debug";
|
||||
|
||||
/// The path to the application data directory, which includes "asset" and "weights" data.
|
||||
#[derive(Clone)]
|
||||
pub struct AppDataRoot {
|
||||
path: PathBuf,
|
||||
assets_dir: AppAssetsDir,
|
||||
weights_dir: AppWeightsDir,
|
||||
temp_dir: TemporaryDir,
|
||||
log_file_name: PathBuf,
|
||||
log_file_name_string: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppAssetsDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppWeightsDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TemporaryDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl AppDataRoot {
|
||||
pub fn create_default() -> anyhow::Result<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 weights_dir = AppWeightsDir::create_existing(dir.join(WEIGHTS_SUBDIRECTORY))?;
|
||||
let temp_dir = TemporaryDir::create_existing(dir.join(TEMPORARY_SUBDIRECTORY))?;
|
||||
let log_file_name = dir.join(LOG_FILE_NAME);
|
||||
let log_file_name_string = log_file_name
|
||||
.to_str()
|
||||
.ok_or(anyhow!("couldn't convert log path to str"))?
|
||||
.to_string();
|
||||
|
||||
Ok(Self {
|
||||
path: dir,
|
||||
assets_dir,
|
||||
weights_dir,
|
||||
temp_dir,
|
||||
log_file_name,
|
||||
log_file_name_string,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn assets_dir(&self) -> &AppAssetsDir {
|
||||
&self.assets_dir
|
||||
}
|
||||
|
||||
pub fn weights_dir(&self) -> &AppWeightsDir {
|
||||
&self.weights_dir
|
||||
}
|
||||
|
||||
pub fn temp_dir(&self) -> &TemporaryDir {
|
||||
&self.temp_dir
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn log_file_name(&self) -> &Path {
|
||||
&self.log_file_name
|
||||
}
|
||||
|
||||
pub fn log_file_name_str(&self) -> &str {
|
||||
&self.log_file_name_string
|
||||
}
|
||||
|
||||
pub fn get_sora_cookie_file_path(&self) -> PathBuf {
|
||||
self.path.join("sora_cookies.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 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,5 +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 yaml_config;
|
||||
pub mod expanduser;
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +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_task_queue;
|
||||
@@ -0,0 +1,48 @@
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
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<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));
|
||||
}
|
||||
|
||||
Ok(SoraCredentialSet::initialize(
|
||||
cookie,
|
||||
bearer,
|
||||
sentinel,
|
||||
))
|
||||
}
|
||||
+92
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
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<SoraCredentialSet>>>,
|
||||
}
|
||||
|
||||
impl SoraCredentialHolder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
credentials: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
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) => {
|
||||
*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<SoraCredentialSet>> {
|
||||
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<SoraCredentialSet> {
|
||||
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,157 @@
|
||||
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::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 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 {
|
||||
holder: SoraCredentialHolder,
|
||||
app_data_root: AppDataRoot,
|
||||
}
|
||||
|
||||
impl SoraCredentialManager {
|
||||
|
||||
pub fn initialize_from_disk_infallible(app_data_root: &AppDataRoot) -> Self {
|
||||
let holder = SoraCredentialHolder::new();
|
||||
|
||||
match read_sora_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: &SoraCredentialSet) -> AnyhowResult<()> {
|
||||
self.holder.set_credentials(creds)
|
||||
}
|
||||
|
||||
pub fn clear_credentials(&self) -> AnyhowResult<()> {
|
||||
self.holder.clear_credentials()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_credentials(&self) -> AnyhowResult<Option<SoraCredentialSet>> {
|
||||
self.holder.get_credentials()
|
||||
}
|
||||
|
||||
pub fn get_credentials_required(&self) -> AnyhowResult<SoraCredentialSet> {
|
||||
self.holder.get_credentials_required()
|
||||
}
|
||||
|
||||
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<SoraCredentialSet> {
|
||||
// NB(bt,2025-04-21): Technically we don't need credentials to get a sentinel.
|
||||
let mut creds = self.holder.get_credentials_required()?;
|
||||
|
||||
info!("Generating token...");
|
||||
|
||||
let sentinel = generate_token()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to refresh: {:?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
info!("Token obtained.");
|
||||
|
||||
creds.sora_sentinel = Some(SoraSentinel::new(sentinel.clone()));
|
||||
|
||||
self.holder.set_credentials(&creds)?;
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(self.app_data_root.get_sora_sentinel_file_path())?;
|
||||
|
||||
file.write_all(sentinel.as_bytes())?;
|
||||
file.flush()?;
|
||||
|
||||
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;
|
||||
+39
@@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+109
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use anyhow::anyhow;
|
||||
use log::{error, info};
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::Url;
|
||||
use tauri::{AppHandle, Manager, Webview};
|
||||
use errors::AnyhowResult;
|
||||
use crate::state::app_dir::AppDataRoot;
|
||||
use crate::utils::sora_webview_cookies::get_all_sora_cookies_as_string;
|
||||
|
||||
pub const LOGIN_WINDOW_NAME: &str = "login_window";
|
||||
|
||||
pub const SORA_LOGIN_URL_STR: &str = "https://sora.com/auth/login?callback_path=/";
|
||||
|
||||
pub static SORA_LOGIN_URL: Lazy<Url> = Lazy::new(|| {
|
||||
Url::parse(SORA_LOGIN_URL_STR).expect("URL should parse")
|
||||
});
|
||||
|
||||
pub const SORA_ROOT_URL_STR: &str = "https://sora.com/";
|
||||
|
||||
pub static SORA_ROOT_URL: Lazy<Url> = Lazy::new(|| {
|
||||
Url::parse(SORA_ROOT_URL_STR).expect("URL should parse")
|
||||
});
|
||||
|
||||
pub async fn login_thread(app: AppHandle, app_data_root: AppDataRoot) -> ! {
|
||||
loop {
|
||||
for (window_name, webview) in app.webviews() {
|
||||
if window_name == LOGIN_WINDOW_NAME {
|
||||
let result = check_login_window(&webview, &app_data_root).await;
|
||||
if let Err(err) = result {
|
||||
error!("Error checking login window: {:?}", err);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(2_000)).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_login_window(webview: &Webview, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
|
||||
clear_browsing_data_on_test_domain(webview)?;
|
||||
//keep_on_task(webview)?;
|
||||
extract_cookies_to_file(webview, app_data_root)?;
|
||||
// TODO: Other maintenance tasks ...
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn keep_on_task(webview: &Webview) -> AnyhowResult<()> {
|
||||
let url = webview.url()?;
|
||||
let hostname= url.host()
|
||||
.ok_or(anyhow!("no host in url"))?
|
||||
.to_string();
|
||||
match hostname.as_str() {
|
||||
"auth.openai.com" => {},
|
||||
"openai.com" => {},
|
||||
"sora.com" => {},
|
||||
// Third party SSO
|
||||
"accounts.google.com" => {},
|
||||
"accounts.youtube.com" => {},
|
||||
"login.live.com" => {},
|
||||
"appleid.apple.com" => {},
|
||||
_ => {
|
||||
info!("Non login hostname: {}", hostname);
|
||||
webview.navigate(SORA_LOGIN_URL.clone())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This is just so we have a way to clear browsing data.
|
||||
fn clear_browsing_data_on_test_domain(webview: &Webview) -> AnyhowResult<()> {
|
||||
let url = webview.url()?;
|
||||
let hostname= url.host()
|
||||
.ok_or(anyhow!("no host in url"))?
|
||||
.to_string();
|
||||
match hostname.as_str() {
|
||||
"storyteller.ai" => {
|
||||
info!("Clearing all browsing data...");
|
||||
webview.clear_all_browsing_data()?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 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)?;
|
||||
|
||||
let cookie_file = app_data_root.get_sora_cookie_file_path();
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(cookie_file)?;
|
||||
|
||||
file.write_all(cookies.as_bytes())?;
|
||||
file.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -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;
|
||||
+114
@@ -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(¤t_http_plugin_credentials) {
|
||||
replace_credentials = false;
|
||||
}
|
||||
}
|
||||
|
||||
if replace_credentials {
|
||||
info!("Syncing ArtCraft credentials ...");
|
||||
storyteller_credential_manager.set_credentials(¤t_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,
|
||||
))
|
||||
}
|
||||
+32
@@ -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(¤t_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(¤t_window_size)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1 +1,4 @@
|
||||
pub mod login_thread;
|
||||
pub mod discord_presence_thread;
|
||||
pub mod main_window_thread;
|
||||
pub mod sora_session_login_thread;
|
||||
pub mod sora_task_polling_thread;
|
||||
@@ -0,0 +1,172 @@
|
||||
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 anyhow::anyhow;
|
||||
use chrono::{DateTime, NaiveDateTime, TimeDelta};
|
||||
use errors::AnyhowResult;
|
||||
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;
|
||||
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";
|
||||
|
||||
pub const SORA_LOGIN_URL_STR: &str = "https://sora.com/auth/login?callback_path=/";
|
||||
|
||||
pub static SORA_LOGIN_URL: Lazy<Url> = Lazy::new(|| {
|
||||
Url::parse(SORA_LOGIN_URL_STR).expect("URL should parse")
|
||||
});
|
||||
|
||||
pub const SORA_ROOT_URL_STR: &str = "https://sora.com/";
|
||||
|
||||
pub static SORA_ROOT_URL: Lazy<Url> = Lazy::new(|| {
|
||||
Url::parse(SORA_ROOT_URL_STR).expect("URL should parse")
|
||||
});
|
||||
|
||||
pub async fn sora_session_login_thread(
|
||||
app: AppHandle,
|
||||
app_data_root: AppDataRoot,
|
||||
sora_creds_manager: SoraCredentialManager
|
||||
) -> ! {
|
||||
loop {
|
||||
for (window_name, webview) in app.webviews() {
|
||||
if window_name == LOGIN_WINDOW_NAME {
|
||||
let result = check_login_window(
|
||||
&webview,
|
||||
&app_data_root,
|
||||
&sora_creds_manager,
|
||||
).await;
|
||||
if let Err(err) = result {
|
||||
error!("Error checking login window: {:?}", err);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(2_000)).await;
|
||||
}
|
||||
}
|
||||
|
||||
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, sora_credential_manager).await?;
|
||||
//initialize_sora_jwt_bearer_token(app_data_root).await?; // TODO: This only runs once. We need better management.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn keep_on_task(webview: &Webview) -> AnyhowResult<()> {
|
||||
let url = webview.url()?;
|
||||
let hostname= url.host()
|
||||
.ok_or(anyhow!("no host in url"))?
|
||||
.to_string();
|
||||
match hostname.as_str() {
|
||||
"auth.openai.com" => {},
|
||||
"openai.com" => {},
|
||||
"sora.com" => {},
|
||||
// Third party SSO
|
||||
"accounts.google.com" => {},
|
||||
"accounts.youtube.com" => {},
|
||||
"login.live.com" => {},
|
||||
"appleid.apple.com" => {},
|
||||
_ => {
|
||||
info!("Non login hostname: {}", hostname);
|
||||
webview.navigate(SORA_LOGIN_URL.clone())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This is just so we have a way to clear browsing data.
|
||||
fn clear_browsing_data_on_test_domain(webview: &Webview) -> AnyhowResult<()> {
|
||||
let url = webview.url()?;
|
||||
let hostname= url.host()
|
||||
.ok_or(anyhow!("no host in url"))?
|
||||
.to_string();
|
||||
match hostname.as_str() {
|
||||
"storyteller.ai" => {
|
||||
info!("Clearing all browsing data...");
|
||||
webview.clear_all_browsing_data()?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO(bt,2025-04-07): Heuristic to detect when logged in. Only write when logged in.
|
||||
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();
|
||||
|
||||
if new_cookies.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let session_cookie_presence = has_session_cookie(&new_cookies)
|
||||
.unwrap_or_else(|err| {
|
||||
error!("Failed to check for session cookie: {:?}", err);
|
||||
SessionCookiePresence::MaybePresent
|
||||
});
|
||||
|
||||
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;
|
||||
@@ -0,0 +1,35 @@
|
||||
use std::path::Path;
|
||||
use reqwest::Url;
|
||||
|
||||
pub fn get_url_file_extension(url: &Url) -> Option<&str> {
|
||||
Path::new(url.path())
|
||||
.extension()
|
||||
.filter(|ext| !ext.is_empty())
|
||||
.and_then(|ext| ext.to_str())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use reqwest::Url;
|
||||
|
||||
#[test]
|
||||
fn extension_exists() {
|
||||
let url = Url::parse("https://example.com/image.png").unwrap();
|
||||
let extension = super::get_url_file_extension(&url);
|
||||
assert_eq!(extension, Some("png"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_does_not_exist() {
|
||||
let url = Url::parse("https://example.com/foo").unwrap();
|
||||
let extension = super::get_url_file_extension(&url);
|
||||
assert_eq!(extension, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn period_not_in_path() {
|
||||
let url = Url::parse("https://example.com/invalid.period/foo").unwrap();
|
||||
let extension = super::get_url_file_extension(&url);
|
||||
assert_eq!(extension, None);
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -1,9 +1,10 @@
|
||||
//! Implemented from https://github.com/huggingface/hf_transfer/blob/main/src/lib.rs
|
||||
|
||||
use crate::transfer::error::Error;
|
||||
use crate::transfer::util::{exponential_backoff, BASE_WAIT_TIME, MAX_WAIT_TIME};
|
||||
use crate::utils::large_file_transfer::error::Error;
|
||||
use crate::utils::large_file_transfer::util::{exponential_backoff, BASE_WAIT_TIME, MAX_WAIT_TIME};
|
||||
use futures::stream::FuturesUnordered;
|
||||
use futures::StreamExt;
|
||||
use log::info;
|
||||
use reqwest::header::{
|
||||
HeaderMap, HeaderName, HeaderValue, AUTHORIZATION, CONTENT_RANGE,
|
||||
RANGE,
|
||||
@@ -14,15 +15,14 @@ use std::fmt::Display;
|
||||
use std::fs::remove_file;
|
||||
use std::io::SeekFrom;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use log::info;
|
||||
use tempfile::NamedTempFile;
|
||||
use tokio::fs::OpenOptions;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::io::AsyncSeekExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::time::sleep;
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
//! Implemented from https://github.com/huggingface/hf_transfer/blob/main/src/lib.rs
|
||||
|
||||
use crate::transfer::error::Error;
|
||||
use crate::transfer::util::{exponential_backoff, BASE_WAIT_TIME, MAX_WAIT_TIME};
|
||||
use crate::utils::large_file_transfer::error::Error;
|
||||
use crate::utils::large_file_transfer::util::{exponential_backoff, BASE_WAIT_TIME, MAX_WAIT_TIME};
|
||||
use futures::stream::FuturesUnordered;
|
||||
use futures::StreamExt;
|
||||
use reqwest::header::CONTENT_LENGTH;
|
||||
@@ -1,3 +1,8 @@
|
||||
pub mod clear_all_webview_cookies;
|
||||
pub mod cookies;
|
||||
pub mod get_url_file_extension;
|
||||
pub mod image;
|
||||
pub mod sora_webview_cookies;
|
||||
pub mod large_file_transfer;
|
||||
pub mod object_store;
|
||||
pub mod simple_http_download;
|
||||
pub mod sora;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use reqwest::Url;
|
||||
use reqwest::Client;
|
||||
use errors::AnyhowResult;
|
||||
|
||||
const USER_AGENT: &str = "storyteller-client/1.0";
|
||||
|
||||
/// Downloads a (binary) file to a filesystem path. Good for images, etc. Not great for large files.
|
||||
pub async fn simple_http_download<P: AsRef<Path>>(url: &Url, download_path: P) -> AnyhowResult<()> {
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()?;
|
||||
|
||||
let response = client.get(url.clone()) // NB: No IntoUrl for &Url.
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Accept", "*/*")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
//let response : Response = reqwest::get(url).await?;
|
||||
let bytes = response.bytes().await?;
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true) // To create a new file
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(download_path)?;
|
||||
|
||||
file.write_all(&bytes)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use log::info;
|
||||
use openai_sora_client::requests::bearer::generate::generate_bearer_with_cookie;
|
||||
|
||||
/// Loading the JWT bearer token for the first time.
|
||||
pub async fn initialize_sora_jwt_bearer_token(app_data_root: &AppDataRoot) -> AnyhowResult<()> {
|
||||
if !app_data_root.get_sora_cookie_file_path().exists() {
|
||||
return Err(anyhow!("Cookie file does not exist"));
|
||||
}
|
||||
|
||||
if app_data_root.get_sora_bearer_token_file_path().exists() {
|
||||
return Ok(()); // Bearer token already exists.
|
||||
}
|
||||
|
||||
let cookie = std::fs::read_to_string(app_data_root.get_sora_cookie_file_path())?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
info!("Requesting initial JWT bearer token...");
|
||||
|
||||
let bearer = generate_bearer_with_cookie(&cookie).await?;
|
||||
|
||||
std::fs::write(app_data_root.get_sora_bearer_token_file_path(), bearer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod initialize_sora_jwt_bearer_token;
|
||||
@@ -1,5 +1,5 @@
|
||||
# For some reason, we can't pass json files to the tauri dev command
|
||||
[build]
|
||||
frontendDist = "../../../frontend/apps/editor2d/dist"
|
||||
beforeDevCommand = "nx run editor2d:dev"
|
||||
beforeDevCommand = "npx nx run editor2d:dev"
|
||||
devUrl = "http://localhost:5741"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# For some reason, we can't pass json files to the tauri dev command
|
||||
[build]
|
||||
frontendDist = "../../../frontend/apps/editor2d/dist"
|
||||
beforeDevCommand = "echo 'no dev'"
|
||||
devUrl = "http://localhost:5741"
|
||||
@@ -1,5 +1,5 @@
|
||||
# For some reason, we can't pass json files to the tauri dev command
|
||||
[build]
|
||||
frontendDist = "../../../frontend/apps/editor3d/dist"
|
||||
beforeDevCommand = "nx run editor3d:dev"
|
||||
beforeDevCommand = "npx nx run editor3d:dev"
|
||||
devUrl = "http://localhost:5173"
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# For some reason, we can't pass json files to the tauri dev command
|
||||
[build]
|
||||
frontendDist = "../../../frontend/apps/editor3d/dist"
|
||||
#beforeDevCommand = "nx run editor3d:dev"
|
||||
beforeDevCommand = "echo 'no dev'"
|
||||
devUrl = "http://localhost:5173"
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
|
||||
"productName": "ArtCraft",
|
||||
"version": "0.1.0",
|
||||
"version": "0.0.1",
|
||||
"identifier": "ai.artcraft.app",
|
||||
"build": {
|
||||
"frontendDist": "../../../frontend/apps/editor2d/dist",
|
||||
"frontendDist": "../../../",
|
||||
"devUrl": "http://localhost:5741",
|
||||
"beforeDevCommand": "nx run editor2d:dev",
|
||||
"beforeBuildCommand": "nx run editor2d:build"
|
||||
@@ -20,7 +20,11 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
"csp": null,
|
||||
"headers": {
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
"Cross-Origin-Embedder-Policy": "require-corp"
|
||||
}
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
|
||||
"productName": "ArtCraft",
|
||||
"version": "0.0.1",
|
||||
"identifier": "ai.artcraft.app",
|
||||
"build": {
|
||||
"frontendDist": "../../../frontend/apps/editor3d/dist",
|
||||
"devUrl": "http://localhost:5173",
|
||||
"beforeDevCommand": "echo",
|
||||
"beforeBuildCommand": "nx run editor2d:build"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "ArtCraft",
|
||||
"width": 2400,
|
||||
"height": 1300,
|
||||
"resizable": true,
|
||||
"fullscreen": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -10,4 +10,8 @@ pub fn add_artcraft_2d(cors: Cors, _is_production: bool) -> Cors {
|
||||
.allowed_origin_fn(|origin, _req_head| {
|
||||
netlify_branch_domain_matches(origin, "storyteller-2d.netlify.app")
|
||||
})
|
||||
// Tauri localhost (2D engine, first three ports)
|
||||
.allowed_origin("http://localhost:5741")
|
||||
.allowed_origin("http://localhost:5742") // If already started
|
||||
.allowed_origin("http://localhost:5743") // If already started
|
||||
}
|
||||
|
||||
@@ -10,4 +10,8 @@ pub fn add_artcraft_3d(cors: Cors, _is_production: bool) -> Cors {
|
||||
.allowed_origin_fn(|origin, _req_head| {
|
||||
netlify_branch_domain_matches(origin, "storyteller-3d.netlify.app")
|
||||
})
|
||||
// Tauri localhost (3D engine, first three ports)
|
||||
.allowed_origin("http://localhost:5173")
|
||||
.allowed_origin("http://localhost:5174") // If already started
|
||||
.allowed_origin("http://localhost:5175") // If already started
|
||||
}
|
||||
|
||||
@@ -19,25 +19,27 @@ workspace-build-acceleration = { workspace = true }
|
||||
# Internal
|
||||
crockford = { path = "../../data/crockford" }
|
||||
errors = { path = "../../errors" }
|
||||
subprocess_common = { path = "../../subprocess_common" }
|
||||
idempotency = { path = "../../idempotency" }
|
||||
subprocess_common = { path = "../../subprocess_common" }
|
||||
|
||||
# External
|
||||
anyhow = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
chrono = { workspace = true}
|
||||
cookie = { workspace = true }
|
||||
hex-literal = { workspace = true }
|
||||
log = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["multipart"] }
|
||||
once_cell= { workspace = true }
|
||||
rand = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["multipart", "gzip"] }
|
||||
serde = { workspace = true }
|
||||
serde_derive = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sha3 = { workspace = true }
|
||||
hex-literal = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { version = "1.38.0", features = ["macros"] }
|
||||
toml = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
chrono = { workspace = true}
|
||||
rand = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
testing = { path = "../../testing" }
|
||||
filesys = { path = "../../files/filesys" }
|
||||
filesys = { path = "../../files/filesys" }
|
||||
@@ -7,13 +7,18 @@ use errors::AnyhowResult;
|
||||
pub const USER_AGENT : &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36";
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[deprecated(note="Use `SoraCredentialSet` instead as it has auto-healing behavior and doesn't require a bearer token initially.")]
|
||||
pub struct SoraCredentials {
|
||||
#[deprecated(note="Use `SoraCredentialSet` instead as it has auto-healing behavior and doesn't require a bearer token initially.")]
|
||||
pub bearer_token: String,
|
||||
#[deprecated(note="Use `SoraCredentialSet` instead as it has auto-healing behavior and doesn't require a bearer token initially.")]
|
||||
pub cookie: String,
|
||||
#[deprecated(note="Use `SoraCredentialSet` instead as it has auto-healing behavior and doesn't require a bearer token initially.")]
|
||||
pub sentinel: Option<String>,
|
||||
}
|
||||
|
||||
impl SoraCredentials {
|
||||
#[deprecated(note="Use `SoraCredentialSet` instead as it has auto-healing behavior and doesn't require a bearer token initially.")]
|
||||
pub fn authorization_header_value(&self) -> String {
|
||||
match self.bearer_token.get(0..6) {
|
||||
Some("bearer") | Some("Bearer") => self.bearer_token.clone(),
|
||||
@@ -21,6 +26,7 @@ impl SoraCredentials {
|
||||
}
|
||||
}
|
||||
|
||||
#[deprecated(note="Use `SoraCredentialSet` instead as it has auto-healing behavior and doesn't require a bearer token initially.")]
|
||||
pub fn from_env() -> AnyhowResult<Self> {
|
||||
let bearer = std::env::var("SORA_BEARER_TOKEN")?;
|
||||
let cookie = std::env::var("SORA_COOKIE")?;
|
||||
@@ -29,12 +35,14 @@ impl SoraCredentials {
|
||||
Ok(Self { bearer_token: bearer, cookie, sentinel })
|
||||
}
|
||||
|
||||
#[deprecated(note="Use `SoraCredentialSet` instead as it has auto-healing behavior and doesn't require a bearer token initially.")]
|
||||
pub fn from_toml_file<P: AsRef<Path>>(filename: P) -> AnyhowResult<Self> {
|
||||
let contents = std::fs::read_to_string(filename)?;
|
||||
let credentials = toml::from_str(&contents)?;
|
||||
Ok(credentials)
|
||||
}
|
||||
|
||||
#[deprecated(note="Use `SoraCredentialSet` instead as it has auto-healing behavior and doesn't require a bearer token initially.")]
|
||||
pub fn add_credential_headers_to_request(&self, request: RequestBuilder) -> RequestBuilder {
|
||||
let bearer_header = self.authorization_header_value();
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
use crate::credentials::SoraCredentials;
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
|
||||
pub enum CredentialMigration {
|
||||
Legacy(SoraCredentials),
|
||||
New(SoraCredentialSet),
|
||||
}
|
||||
|
||||
pub enum CredentialMigrationRef<'a> {
|
||||
Legacy(&'a SoraCredentials),
|
||||
New(&'a SoraCredentialSet),
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod credential_migration;
|
||||
pub mod sora_cookies;
|
||||
pub mod sora_credential_set;
|
||||
pub mod sora_jwt_bearer_token;
|
||||
pub mod sora_sentinel;
|
||||
@@ -0,0 +1,25 @@
|
||||
/// Cookies are the credential that are always required.
|
||||
/// You can mint a JWT bearer token with just the cookies.
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SoraCookies {
|
||||
cookies: String,
|
||||
}
|
||||
|
||||
impl SoraCookies {
|
||||
pub fn new(cookies: String) -> Self {
|
||||
SoraCookies { cookies }
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.cookies
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.cookies.as_bytes()
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> String {
|
||||
self.cookies.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use crate::credentials::SoraCredentials;
|
||||
use crate::creds::sora_cookies::SoraCookies;
|
||||
use crate::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
|
||||
use crate::creds::sora_sentinel::SoraSentinel;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SoraCredentialSet {
|
||||
pub cookies: SoraCookies,
|
||||
pub jwt_bearer_token: Option<SoraJwtBearerToken>,
|
||||
pub sora_sentinel: Option<SoraSentinel>,
|
||||
}
|
||||
|
||||
impl SoraCredentialSet {
|
||||
pub fn initialize(
|
||||
cookies: SoraCookies,
|
||||
bearer: Option<SoraJwtBearerToken>,
|
||||
sentinel: Option<SoraSentinel>
|
||||
) -> Self {
|
||||
Self {
|
||||
cookies,
|
||||
jwt_bearer_token: bearer,
|
||||
sora_sentinel: sentinel,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize_with_just_cookies(cookies: SoraCookies) -> Self {
|
||||
SoraCredentialSet {
|
||||
cookies,
|
||||
jwt_bearer_token: None,
|
||||
sora_sentinel: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize_with_just_cookies_str(cookies: &str) -> Self {
|
||||
Self::initialize_with_just_cookies(
|
||||
SoraCookies::new(cookies.to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
// TODO(bt,2025-04-23): Just here for migration
|
||||
pub fn from_legacy_credentials(credentials: &SoraCredentials) -> AnyhowResult<Self> {
|
||||
let cookies = SoraCookies::new(credentials.cookie.clone());
|
||||
let jwt_bearer_token = SoraJwtBearerToken::new(credentials.bearer_token.clone())?;
|
||||
let mut sora_sentinel = None;
|
||||
|
||||
if let Some(sentinel) = &credentials.sentinel {
|
||||
sora_sentinel = Some(SoraSentinel::new(sentinel.clone()));
|
||||
}
|
||||
|
||||
Ok(SoraCredentialSet {
|
||||
cookies,
|
||||
jwt_bearer_token: Some(jwt_bearer_token),
|
||||
sora_sentinel,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_legacy_credentials(&self) -> AnyhowResult<SoraCredentials> {
|
||||
let bearer_token= self.jwt_bearer_token
|
||||
.as_ref()
|
||||
.map(|token| token.token_str().to_string())
|
||||
.ok_or_else(|| anyhow!("There is no bearer token to convert to a required legacy bearer token"))?;
|
||||
|
||||
let sentinel = self.sora_sentinel
|
||||
.as_ref()
|
||||
.map(|sentinel| sentinel.get_sentinel().to_string());
|
||||
|
||||
Ok(SoraCredentials {
|
||||
cookie: self.cookies.as_str().to_string(),
|
||||
bearer_token,
|
||||
sentinel,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
use crate::utils::lightweight_sora_jwt_parse::{lightweight_sora_jwt_parse, SoraJwtClaims};
|
||||
use errors::AnyhowResult;
|
||||
|
||||
/// Sora bearer tokens are JWT tokens that can be minted with a valid cookie/session.
|
||||
#[derive(Clone)]
|
||||
pub struct SoraJwtBearerToken {
|
||||
token: String,
|
||||
jwt_claims: SoraJwtClaims,
|
||||
}
|
||||
|
||||
impl SoraJwtBearerToken {
|
||||
pub fn new(token: String) -> AnyhowResult<Self> {
|
||||
let jwt_claims = lightweight_sora_jwt_parse(&token)?;
|
||||
Ok(SoraJwtBearerToken {
|
||||
token,
|
||||
jwt_claims,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn token_str(&self) -> &str {
|
||||
&self.token
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.token.as_bytes()
|
||||
}
|
||||
|
||||
pub fn jwt_claims(&self) -> &SoraJwtClaims {
|
||||
&self.jwt_claims
|
||||
}
|
||||
|
||||
pub fn to_authorization_header_value(&self) -> String {
|
||||
match self.token.get(0..6) {
|
||||
Some("bearer") | Some("Bearer") => self.token.clone(),
|
||||
_ => "Bearer ".to_owned() + &self.token,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
|
||||
/// Sora Sentinel tokens can be generated without cookies or JWTs, and are needed for some API calls.
|
||||
#[derive(Clone)]
|
||||
pub struct SoraSentinel {
|
||||
sentinel: String,
|
||||
|
||||
// TODO(bt,2025-04-23): We may want to store the user agent alongside the sentinel
|
||||
// as that appears to be important for the validity of the sentinel token.
|
||||
}
|
||||
|
||||
impl SoraSentinel {
|
||||
pub fn new(sentinel: String) -> Self {
|
||||
SoraSentinel { sentinel }
|
||||
}
|
||||
|
||||
pub fn get_sentinel(&self) -> &str {
|
||||
&self.sentinel
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.sentinel.as_bytes()
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
pub mod credentials;
|
||||
pub mod image_gen;
|
||||
pub mod job;
|
||||
pub mod upload;
|
||||
pub mod sentinel_refresh;
|
||||
mod sora_error;
|
||||
pub mod creds;
|
||||
pub mod recipes;
|
||||
pub mod requests;
|
||||
pub mod sora_error;
|
||||
pub mod utils;
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
use anyhow::anyhow;
|
||||
use log::{info, warn};
|
||||
use errors::AnyhowResult;
|
||||
use crate::creds::credential_migration::CredentialMigrationRef;
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
use crate::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
|
||||
use crate::creds::sora_sentinel::SoraSentinel;
|
||||
use crate::requests::bearer::generate::generate_bearer_with_cookie;
|
||||
use crate::requests::image_gen::common::{ImageSize, NumImages, SoraImageGenResponse};
|
||||
use crate::requests::image_gen::image_gen_http_request;
|
||||
use crate::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
|
||||
use crate::requests::sentinel_refresh::generate::token::generate_token;
|
||||
use crate::sora_error::SoraError;
|
||||
|
||||
pub struct ImageRemixAutoRenewRequest<'a> {
|
||||
pub prompt: String,
|
||||
pub num_images: NumImages,
|
||||
pub image_size: ImageSize,
|
||||
pub sora_media_tokens: Vec<String>,
|
||||
pub credentials: &'a SoraCredentialSet,
|
||||
}
|
||||
|
||||
/// Image request with retry and session auto-renewal.
|
||||
/// If a new sora credential is returned, replace the old one with the new one.
|
||||
pub async fn image_remix_with_session_auto_renew(request: ImageRemixAutoRenewRequest<'_>) -> Result<(SoraImageGenResponse, Option<SoraCredentialSet>), SoraError> {
|
||||
let result = sora_image_gen_remix(SoraImageGenRemixRequest {
|
||||
prompt: request.prompt.clone(), // NB: Clone because used again
|
||||
num_images: request.num_images,
|
||||
image_size: request.image_size,
|
||||
sora_media_tokens: request.sora_media_tokens.clone(), // NB: Clone because used again
|
||||
credentials: CredentialMigrationRef::New(request.credentials),
|
||||
}).await;
|
||||
|
||||
let err = match result {
|
||||
Ok(response) => return Ok((response, None)),
|
||||
Err(err) => err,
|
||||
};
|
||||
|
||||
warn!("Image generation failed: {:?}", err);
|
||||
|
||||
let mut refresh_jwt = false;
|
||||
let mut refresh_sentinel = false;
|
||||
|
||||
match err {
|
||||
// We'll fail these requests...
|
||||
image_gen_http_request::SoraError::TooManyConcurrentTasks(_) => {
|
||||
return Err(SoraError::TooManyConcurrentTasks);
|
||||
}
|
||||
image_gen_http_request::SoraError::GenericError(err) => {
|
||||
return Err(SoraError::OtherBadStatus(anyhow!("image remix failed with GenericError: {:?}", err)))
|
||||
}
|
||||
image_gen_http_request::SoraError::NetworkError(err) => {
|
||||
// TODO: The underlying type should be a reqwest::Error.
|
||||
return Err(SoraError::OtherBadStatus(anyhow!("network error: {:?}", err)))
|
||||
}
|
||||
|
||||
// We'll retry these requests...
|
||||
|
||||
image_gen_http_request::SoraError::SentinelBlock(err) => {
|
||||
warn!("Image generation failed due to sentinel block error: {:?}", err);
|
||||
refresh_sentinel = true;
|
||||
}
|
||||
image_gen_http_request::SoraError::TokenExpired(err) => {
|
||||
warn!("Image generation failed due to token expired error: {:?}", err);
|
||||
refresh_sentinel = true; // TODO: Not sure what this error is, actually.
|
||||
refresh_jwt = true;
|
||||
}
|
||||
image_gen_http_request::SoraError::InvalidJwt(err) => {
|
||||
warn!("Image generation failed due to invalid jwt error: {:?}", err);
|
||||
refresh_jwt = true;
|
||||
}
|
||||
}
|
||||
|
||||
let mut new_bearer = None;
|
||||
|
||||
if refresh_jwt {
|
||||
info!("Generating new JWT bearer token...");
|
||||
let cookies = request.credentials.cookies.as_str();
|
||||
let response = generate_bearer_with_cookie(cookies).await;
|
||||
match response {
|
||||
Err(err) => {
|
||||
return Err(SoraError::OtherBadStatus(anyhow!("failed to generate new JWT bearer token: {:?}", err)))
|
||||
}
|
||||
Ok(bearer) => {
|
||||
match SoraJwtBearerToken::new(bearer) {
|
||||
Err(err) => {
|
||||
return Err(SoraError::OtherBadStatus(anyhow!("Failed to parse new JWT bearer token: {:?}", err)));
|
||||
}
|
||||
Ok(bearer) => new_bearer = Some(bearer),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let mut new_sentinel = None;
|
||||
|
||||
if refresh_sentinel {
|
||||
info!("Generating new sentinel...");
|
||||
let response = generate_token().await;
|
||||
match response {
|
||||
Err(err) => {
|
||||
return Err(SoraError::OtherBadStatus(anyhow!("failed to generate new sentinel: {:?}", err)))
|
||||
}
|
||||
Ok(sentinel) => new_sentinel = Some(SoraSentinel::new(sentinel)),
|
||||
}
|
||||
}
|
||||
|
||||
let mut new_creds = request.credentials.clone();
|
||||
|
||||
if let Some(bearer) = new_bearer {
|
||||
new_creds.jwt_bearer_token = Some(bearer);
|
||||
}
|
||||
|
||||
if let Some(sentinel) = new_sentinel {
|
||||
new_creds.sora_sentinel = Some(sentinel);
|
||||
}
|
||||
|
||||
// Now try again...
|
||||
|
||||
let result = sora_image_gen_remix(SoraImageGenRemixRequest {
|
||||
prompt: request.prompt,
|
||||
num_images: request.num_images,
|
||||
image_size: request.image_size,
|
||||
sora_media_tokens: request.sora_media_tokens,
|
||||
credentials: CredentialMigrationRef::New(&new_creds),
|
||||
}).await;
|
||||
|
||||
match result {
|
||||
Ok(response) => Ok((response, Some(new_creds))),
|
||||
Err(err) => match err {
|
||||
image_gen_http_request::SoraError::TooManyConcurrentTasks(err) => {
|
||||
Err(SoraError::TooManyConcurrentTasks)
|
||||
}
|
||||
_ => {
|
||||
warn!("Image remix failed again: {:?}", err);
|
||||
Err(SoraError::OtherBadStatus(anyhow!("image remix failed again: {:?}", err)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
use crate::creds::credential_migration::CredentialMigrationRef;
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
use crate::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
|
||||
use crate::creds::sora_sentinel::SoraSentinel;
|
||||
use crate::recipes::maybe_refresh_credentials_on_sora_error::maybe_refresh_credentials_on_sora_error;
|
||||
use crate::requests::bearer::generate::generate_bearer_with_cookie;
|
||||
use crate::requests::image_gen::common::{ImageSize, NumImages, SoraImageGenResponse};
|
||||
use crate::requests::image_gen::image_gen_http_request;
|
||||
use crate::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
|
||||
use crate::requests::sentinel_refresh::generate::token::generate_token;
|
||||
use crate::requests::upload::upload_media_from_bytes::sora_media_upload_from_bytes;
|
||||
use crate::requests::upload::upload_media_http_request::{upload_media_http_request, SoraMediaUploadResponse};
|
||||
use crate::sora_error::SoraError;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info, warn};
|
||||
|
||||
pub struct ImageUploadFromBytesAutoRenewRequest<'a> {
|
||||
pub file_bytes: Vec<u8>,
|
||||
pub filename: String,
|
||||
pub mime_type: &'a str,
|
||||
pub credentials: &'a SoraCredentialSet,
|
||||
}
|
||||
|
||||
/// Image upload with retry and session auto-renewal.
|
||||
/// If a new sora credential is returned, replace the old one with the new one.
|
||||
pub async fn image_upload_from_bytes_with_session_auto_renew(
|
||||
request: ImageUploadFromBytesAutoRenewRequest<'_>
|
||||
) -> Result<(SoraMediaUploadResponse, Option<SoraCredentialSet>), SoraError> {
|
||||
|
||||
let result = sora_media_upload_from_bytes(
|
||||
request.file_bytes.clone(), // FIXME(bt): This is horrible, but the client needs to take ownership. :(
|
||||
request.filename.clone(), // FIXME: Same
|
||||
CredentialMigrationRef::New(request.credentials),
|
||||
).await;
|
||||
|
||||
let err = match result {
|
||||
Ok(response) => return Ok((response, None)),
|
||||
Err(err) => err,
|
||||
};
|
||||
|
||||
warn!("Image upload failed: {:?}", err);
|
||||
|
||||
let new_creds = maybe_refresh_credentials_on_sora_error(request.credentials, err).await?;
|
||||
|
||||
// Now try again...
|
||||
|
||||
info!("Retrying image upload with new credentials...");
|
||||
|
||||
let result = sora_media_upload_from_bytes(
|
||||
request.file_bytes,
|
||||
request.filename,
|
||||
CredentialMigrationRef::New(&new_creds),
|
||||
).await?;
|
||||
|
||||
Ok((result, Some(new_creds)))
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
use crate::creds::credential_migration::CredentialMigrationRef;
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
use crate::recipes::maybe_refresh_credentials_on_sora_error::maybe_refresh_credentials_on_sora_error;
|
||||
use crate::requests::upload::upload_media_from_file::sora_media_upload_from_file;
|
||||
use crate::requests::upload::upload_media_http_request::{upload_media_http_request, SoraMediaUploadResponse};
|
||||
use crate::sora_error::SoraError;
|
||||
use log::{info, warn};
|
||||
use std::path::Path;
|
||||
|
||||
pub struct ImageUploadFromFileAutoRenewRequest<'a, P: AsRef<Path>> {
|
||||
pub file_path: P,
|
||||
pub credentials: &'a SoraCredentialSet,
|
||||
}
|
||||
|
||||
/// Image upload with retry and session auto-renewal.
|
||||
/// If a new sora credential is returned, replace the old one with the new one.
|
||||
pub async fn image_upload_from_file_with_session_auto_renew<P: AsRef<Path>>(
|
||||
request: ImageUploadFromFileAutoRenewRequest<'_, P>
|
||||
) -> Result<(SoraMediaUploadResponse, Option<SoraCredentialSet>), SoraError> {
|
||||
|
||||
let result = sora_media_upload_from_file(
|
||||
request.file_path.as_ref().clone(), // FIXME(bt): This is horrible, but the client needs to take ownership. :(
|
||||
CredentialMigrationRef::New(request.credentials),
|
||||
).await;
|
||||
|
||||
let err = match result {
|
||||
Ok(response) => return Ok((response, None)),
|
||||
Err(err) => err,
|
||||
};
|
||||
|
||||
warn!("Image upload failed: {:?}", err);
|
||||
|
||||
let new_creds = maybe_refresh_credentials_on_sora_error(request.credentials, err).await?;
|
||||
|
||||
info!("Retrying image upload with new credentials...");
|
||||
|
||||
let result = sora_media_upload_from_file(
|
||||
request.file_path,
|
||||
CredentialMigrationRef::New(&new_creds),
|
||||
).await?;
|
||||
|
||||
Ok((result, Some(new_creds)))
|
||||
}
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
use crate::creds::credential_migration::CredentialMigrationRef;
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
use crate::recipes::image_upload_from_file_with_session_auto_renew::ImageUploadFromFileAutoRenewRequest;
|
||||
use crate::recipes::maybe_refresh_credentials_on_sora_error::maybe_refresh_credentials_on_sora_error;
|
||||
use crate::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use crate::requests::image_gen::image_gen_status::{get_image_gen_status, StatusRequest, TaskId, TaskResponse, VideoGenStatusResponse};
|
||||
use crate::requests::upload::upload_media_from_file::sora_media_upload_from_file;
|
||||
use crate::requests::upload::upload_media_http_request::SoraMediaUploadResponse;
|
||||
use crate::sora_error::SoraError;
|
||||
use anyhow::Error;
|
||||
use log::{info, warn};
|
||||
use std::path::Path;
|
||||
|
||||
pub struct StatusRequestArgs {
|
||||
pub limit: Option<u32>,
|
||||
pub before: Option<TaskId>,
|
||||
pub credentials: SoraCredentialSet,
|
||||
}
|
||||
|
||||
/// Check Sora task statuses with session auto-renewal.
|
||||
/// If a new sora credential is returned, replace the old one with the new one.
|
||||
pub async fn list_sora_task_status_with_session_auto_renew(
|
||||
args: StatusRequestArgs,
|
||||
) -> Result<(VideoGenStatusResponse, Option<SoraCredentialSet>), SoraError> {
|
||||
|
||||
let request = StatusRequest {
|
||||
limit: args.limit,
|
||||
before: args.before.map(|t| t.0),
|
||||
};
|
||||
|
||||
let mut new_creds = None;
|
||||
|
||||
if args.credentials.jwt_bearer_token.is_none() {
|
||||
info!("JWT not set. Upgrading credentials...");
|
||||
let mut updated_creds = args.credentials.clone();
|
||||
if let Err(err) = maybe_upgrade_or_renew_session(&mut updated_creds).await {
|
||||
warn!("Failed to upgrade or renew session: {:?}", err);
|
||||
}
|
||||
new_creds = Some(updated_creds);
|
||||
}
|
||||
|
||||
let legacy_creds = new_creds.as_ref()
|
||||
.unwrap_or_else(|| &args.credentials)
|
||||
.to_legacy_credentials()?;
|
||||
|
||||
let result = get_image_gen_status(&request, &legacy_creds).await;
|
||||
|
||||
let err = match result {
|
||||
Ok(response) => return Ok((response, new_creds)),
|
||||
Err(err) => err,
|
||||
};
|
||||
|
||||
warn!("Task status polling failed: {:?}", err);
|
||||
|
||||
let new_creds = maybe_refresh_credentials_on_sora_error(&args.credentials, err).await?;
|
||||
|
||||
info!("Retrying image upload with new credentials...");
|
||||
|
||||
let result = get_image_gen_status(&request, &legacy_creds).await?;
|
||||
|
||||
Ok((result, Some(new_creds)))
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
use crate::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
|
||||
use crate::requests::bearer::generate::generate_bearer_with_cookie;
|
||||
use crate::sora_error::SoraError;
|
||||
use anyhow::anyhow;
|
||||
use log::{error, info, warn};
|
||||
|
||||
// TODO: Make other APIs work with this.
|
||||
|
||||
pub async fn maybe_refresh_credentials_on_sora_error(creds: &SoraCredentialSet, error: SoraError) -> Result<SoraCredentialSet, SoraError> {
|
||||
|
||||
match error {
|
||||
SoraError::NoBearerTokenAvailable => {
|
||||
warn!("Previous request failed due to missing bearer token.");
|
||||
}
|
||||
SoraError::UnauthorizedCookieOrBearerExpired => {
|
||||
warn!("Previous request failed due to invalid bearer token.");
|
||||
}
|
||||
_ => {
|
||||
error!("Previous request failed with error: {:?}", error);
|
||||
return Err(SoraError::OtherBadStatus(anyhow!("previous request failed with error: {:?}", error)))
|
||||
}
|
||||
}
|
||||
|
||||
info!("Generating new JWT bearer token...");
|
||||
|
||||
let cookies = creds.cookies.as_str();
|
||||
let response = generate_bearer_with_cookie(cookies).await;
|
||||
|
||||
let new_bearer = match response {
|
||||
Err(err) => {
|
||||
return Err(SoraError::OtherBadStatus(anyhow!("failed to generate new JWT bearer token: {:?}", err)))
|
||||
}
|
||||
Ok(bearer) => {
|
||||
match SoraJwtBearerToken::new(bearer) {
|
||||
Err(err) => {
|
||||
return Err(SoraError::OtherBadStatus(anyhow!("failed to parse new JWT bearer token: {:?}", err)));
|
||||
}
|
||||
Ok(bearer) => bearer,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut new_creds = creds.clone();
|
||||
|
||||
new_creds.jwt_bearer_token = Some(new_bearer);
|
||||
|
||||
Ok(new_creds)
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
use crate::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
|
||||
use crate::creds::sora_sentinel::SoraSentinel;
|
||||
use crate::requests::bearer::generate::generate_bearer_with_cookie;
|
||||
use crate::requests::sentinel_refresh::generate::token::generate_token;
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
use errors::AnyhowResult;
|
||||
use log::info;
|
||||
use std::ops::Sub;
|
||||
|
||||
const EXPIRATION_DEADLINE : TimeDelta = TimeDelta::hours(12);
|
||||
|
||||
/// Call this at session startup to generate the tokens for the first time.
|
||||
/// Call this periodically to refresh the JWT as it nears its expiration date.
|
||||
/// This will not refresh the sentinel token once it's been generated - we can use other flows to accomplish that.
|
||||
pub async fn maybe_upgrade_or_renew_session(sora_credentials: &mut SoraCredentialSet) -> AnyhowResult<bool> {
|
||||
let mut refresh_jwt = !sora_credentials.jwt_bearer_token.is_some();
|
||||
let refresh_sentinel = !sora_credentials.sora_sentinel.is_some();
|
||||
|
||||
if let Some(jwt) = &sora_credentials.jwt_bearer_token {
|
||||
let now = Utc::now();
|
||||
let refresh_deadline = now.sub(EXPIRATION_DEADLINE);
|
||||
if jwt.jwt_claims().expiration.lt(&now) {
|
||||
info!("JWT expiration time has elapsed.");
|
||||
refresh_jwt = true;
|
||||
} else if jwt.jwt_claims().expiration.lt(&refresh_deadline) {
|
||||
info!("JWT expiration time is under the expiration deadline.");
|
||||
refresh_jwt = true;
|
||||
}
|
||||
}
|
||||
|
||||
let mut credential_updated = false;
|
||||
|
||||
if refresh_jwt {
|
||||
let cookies = sora_credentials.cookies.as_str();
|
||||
|
||||
info!("Generating new JWT bearer token...");
|
||||
let token = generate_bearer_with_cookie(cookies).await?;
|
||||
|
||||
info!("Parsing JWT bearer token.");
|
||||
let token = SoraJwtBearerToken::new(token)?;
|
||||
sora_credentials.jwt_bearer_token = Some(token);
|
||||
credential_updated = true;
|
||||
}
|
||||
|
||||
if refresh_sentinel {
|
||||
info!("Generating new sentinel...");
|
||||
let token = generate_token().await?;
|
||||
let token = SoraSentinel::new(token);
|
||||
sora_credentials.sora_sentinel = Some(token);
|
||||
credential_updated = true;
|
||||
}
|
||||
|
||||
Ok(credential_updated)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod image_remix_with_session_auto_renew;
|
||||
pub mod image_upload_from_bytes_with_session_auto_renew;
|
||||
pub mod image_upload_from_file_with_session_auto_renew;
|
||||
pub mod list_sora_task_status_with_session_auto_renew;
|
||||
pub mod maybe_refresh_credentials_on_sora_error;
|
||||
pub mod maybe_upgrade_or_renew_session;
|
||||
pub mod wait_for_image_gen_status;
|
||||
@@ -0,0 +1,51 @@
|
||||
use crate::credentials::SoraCredentials;
|
||||
use crate::requests::image_gen::image_gen_status::TaskStatus;
|
||||
use crate::requests::image_gen::image_gen_status::{get_image_gen_status, StatusRequest, TaskResponse, VideoGenStatusResponse};
|
||||
use errors::AnyhowResult;
|
||||
|
||||
pub async fn wait_for_image_gen_status(task_id: &String, credentials: &SoraCredentials, retry_limit: Option<u32>) -> AnyhowResult<TaskResponse> {
|
||||
let status_request = StatusRequest {
|
||||
limit: None,
|
||||
before: None,
|
||||
// before: Some(task_id.clone()),
|
||||
};
|
||||
let retry_limit = retry_limit.unwrap_or(10);
|
||||
|
||||
for _ in 0..retry_limit {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
|
||||
|
||||
let status_response = get_image_gen_status(&status_request, credentials).await?;
|
||||
|
||||
match find_task_response_by_id(&status_response, task_id.clone()) {
|
||||
Some(task_response) => match TaskStatus::from_str(&task_response.status) {
|
||||
TaskStatus::Succeeded => {
|
||||
return Ok(task_response.clone());
|
||||
},
|
||||
TaskStatus::Failed => {
|
||||
return Err(anyhow::anyhow!("Task failed"));
|
||||
},
|
||||
TaskStatus::Unknown(status) => {
|
||||
println!("Unknown task status: {}", status);
|
||||
continue;
|
||||
},
|
||||
TaskStatus::Queued => {
|
||||
println!("Task is queued");
|
||||
continue;
|
||||
},
|
||||
TaskStatus::Running => {
|
||||
println!("Task is running");
|
||||
continue;
|
||||
},
|
||||
},
|
||||
None => {
|
||||
return Err(anyhow::anyhow!("Task not found"));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!("Task not found"))
|
||||
}
|
||||
|
||||
fn find_task_response_by_id(status_response: &VideoGenStatusResponse, task_id: String) -> Option<&TaskResponse> {
|
||||
status_response.task_responses.iter().find(|task| task.id.0 == task_id)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
use errors::AnyhowResult;
|
||||
use log::error;
|
||||
use reqwest::Client;
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
const SORA_BEARER_GENERATE_URL: &str = "https://sora.com/api/auth/session";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SoraAuthResponse {
|
||||
pub user: SoraUser,
|
||||
pub expires: String,
|
||||
#[serde(rename = "accessToken")]
|
||||
pub access_token: String,
|
||||
#[serde(rename = "internalApiBase")]
|
||||
pub internal_api_base: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SoraUser {
|
||||
pub id: String,
|
||||
pub name: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub image: Option<String>,
|
||||
pub picture: Option<String>,
|
||||
pub provider: Option<String>,
|
||||
#[serde(rename = "lastAuthorizationCheck")]
|
||||
pub last_authorization_check: Option<i64>,
|
||||
}
|
||||
|
||||
pub async fn generate_bearer_with_cookie(cookie: &str) -> AnyhowResult<String> {
|
||||
let client = Client::builder()
|
||||
.gzip(true)
|
||||
.build()?;
|
||||
|
||||
let response = client.get(SORA_BEARER_GENERATE_URL)
|
||||
.header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:137.0) Gecko/20100101 Firefox/137.0")
|
||||
.header("Accept", "*/*")
|
||||
.header("Accept-Encoding", "gzip, deflate, br")
|
||||
.header("Accept-Language", "en-US,en;q=0.5")
|
||||
.header("Cookie", cookie)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
error!("Failed to generate bearer token: {}", response.status());
|
||||
return Err(anyhow::anyhow!("Failed to generate bearer token: {}", response.status()));
|
||||
}
|
||||
let response_body = &response.text().await?;
|
||||
println!("response_body: {}", response_body);
|
||||
|
||||
let auth_response: SoraAuthResponse = serde_json::from_str(&response_body)?;
|
||||
Ok(auth_response.access_token)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Don't run in CI. Requires valid cookie
|
||||
async fn test_generate_bearer_with_cookie() {
|
||||
let cookie = "cf_clearance=nelyAZB.T1J.3ToyOuc_NozgPVqEmOoZXZgVqICRVLQ-1745296855-1.2.1.1-R..9Ca0ZCZjSoqDaWLD62phe1hUu.43nPpJV2zqxDibkFPCLJ2R2Am6ygUqJTB1_b86ctFGktBZ4YIPrHHow3jqPk5Mfu_VubL2A5r0R6R_1ILrdvY168CLSpAFRsJKIupMUPBFDePmQ738P7afr8W0YK6dO_m0qjYDYquZyCu5dC3u9CRUGaM8SLDZTTZkxoLzuxDzB1Skuns5rqdjTft4DoNUI_7EJvsYxKoklWylvL2FqcETULoyVq5FyElbZenz8VblXU4HLgspRTz3q0_SNh4_LAIxHygfXaCOBmmii1KY6mFHE83z4JYWgx7MM9jqxKKz1pHgVd8JR72aYgTo7WwE6ZpKmvuxhTtgobsA; oai-did=8ae351f6-bc99-45c9-9fff-bb8dd6eb128b; __Secure-authjs.session-token=eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwia2lkIjoiYWNBYVdTWHBfaDZnNkxtUEs1S0RvTjR3NHFxTEsxVlpSTHA4VWdiaFdWNjctMG5pbDdVVjBCN2dSUU9Bem84ZXBjMVFjb1ozNGxwa0c1OEUyU2Q4cUEifQ..n6nMP5MA40XKktVazIps6w.tsgjkdkBaHt2jpO7GjIBzgammeanUH5W-3SQMPUDIG_cMrGcgmW_uaMwQY-LJ7qfWGfSqwZyGND6LQD_e-8vYdWbhE_gl1O8AQVJaLfCxJbpauCaXQTsSD0PD_XzGYsW0GZjWIbb-J8YFc-XKi8Y2AhjPhf2El5LJrvkQ4MbtlIJAhDOBNAp7UiBR4SWvWgtD60WqqnW3BhUjqXSigqhopUIkcsXAx_NHkfQpwFyJfkI8WK7JJoW4SbD6hdOX0BCKPjXKn7bTpYG---QkfjJ5f5wsRl0QYRVIQ4lzz4iak0A7oyV5jyX_aXYzK4iroVtEjUBAt77wTDOZ8ID3idkPOWQHPWnaugDzX9ZxZ8sWOxsttqbIMWWpEIm9G9ODc3-F29XHPLzciiLuRlRp2tn2ky12vXBdpiFINL7i5-OEryQ1xs1yEsSnAkzrGcXboXDgPSh-wPpmOhWLwqsydmflSahxQH-k0fUQAnjCq2EocdXnfIL84kqwnOxD36YGsKcX0DOir_3gL2PnAcE25Dql1OTwo9SBSLEf2c8dejDVfFT63UHmCkEqvHgdtTnZRApeqJj57A_2aFvy4d8mhflSaSD5qYmYUIdRo5I-uJumWOuHeDOwXOrlnCzxpgvcl1c5ng560I_oYgNUAhIGUnL5ezMQ3MD6jKwVqk-0kKZUPr2ebE1_w7jh4Rau6esLYiMcJI-Jfm_0TfSxlAXMKC3CKIII50PcXXOrvxy0yiBU_DwU79L6fYPAq7NFJ26P6821iOQGP-0mkaHPZHxEJGaPVKKjg2Y9ZQJ9u1gnm0GWwssY4uhFet6CxURnpqzIXKf78kevXOwkEsgFa-2hhMnr_OWBzKSH4UfVIymulxqoAoOh7Em51NUF-HJ0I0rr_3m7wBlhNvVVnmaxSYkuArZoboixX9ka-JVDopTPXUvoEykm45LRRvPdGiPB5hvCrglzNrp_vep4n-YbcLp4Uzn9ljcMjhSFZfXAzqquaiO7H5ys50_-mBquTq0Hoag2pHbVr5uJL8I1V-b8F1XWOnRtRIlj5pHoLX6KyxlT7pNTTSRgMo9PwAWxmLXpRba1h4ceKQTDputfrD1-itFJfZ3FZgkAXVQGrYyXEQIj79ERqq9xw18qGoBi9-GQkqBmCg1TxeS35SBnc62L7gh-UsZ_LSchizC4l4KODSeKdyNGHRONzK0cNHAGWqAJpLu5vBh_VxiljeaQxc4-p0SRiMXPYQf06uWDaRPdglCcavnlPKOcbL1GLku8CzMMCTAeqN3TJBV8BOHXsW81jw73oaBv18peTpgaJS6-Og9jMQVDb8pwlCi7SZnf1uafeGnBb6jJN4qCBN5EjZcoQikHxv0SOYmjs2XN7xE7EyczkovGcU2K3v03cnbbe01U_1mBAXgiJrW8gb4u3x8kngtphuWjEbStqHivLCtP5dyD22LTEMUE4ixvDmKwuLoB2NukM4QcB7PwrMnjI30SfCx0l5OVToz-TmLkLa-Ous-2cckw-D5pXD4RsgLv5NsxGQ9rtaL3qWeA8KnOByvYH1thLFtBUw6rjHtwLnQUGPomJkXIV8OuEuIWWnbsD1jW7hIpbYynhizTyrLChTnxuD8Witu01NtRa0dqSEhy3H5Y0qDXqFhaziydmW_kIdDxQBCWvPBNVBMAlXldqXgazM05eK3DFP86Div8b8k0pjXNoEtmEcwGLR06taiwQ3Fjbyc3y4HSLnQ-_yYz2jKLbDzpVJdaS6tlWT9rOrgniCkfvxJ5LE6KMF_2i6L_yFprOQ1DCYniM39CFehsD-7XVpS2Ui9t-Kx1g7eebK229kdQzB6Dm_HBEHTOxyXMiR4C0n24WYxEHukfCSiAvChiXHhfHOldmzusKY0r-MB_knKgM69elcXN_VAiukuUo1EdrsaqBprHqwmbkEWYfg-TmYefXJnf-58Xtx0lC2T7Uq8JJDdQL4f4PTnuyU_JX5NOwg9CwDUZoPE4gvYTicshEKxw4TABawLB6LXzr5vxjr1PK9lP1H7iwSyutBUGHEyK_R7fM0OzFQSfMEENwKtaGmGT5GnMTkvF4iGTyEtmZS1RhWqYyIagSFkf66ZRIwt5uohQJhZ8kpU_EOfihCkPC6c8he18W2TPtvGWxe0ljNfykMl-w4564IuHIYO3sESs9RzOdHkul9YqpjZC1C831zzEyM3vgM3hYWKo2VmovC5uMYZO0roqQyuy9RNbGPbXnbzMCE5tTPNeHMd1FVYNGy76t-BLj69MfJN0C-U9RgaLRe2Nz34EZBBfK3v9NNCLYhqm04WhM7XwX3CMrG9_j8mlMKpb_MxYqvQUodtttrBoe3bS4WId7OSz8k3KiswcZD86yopGq5X_a910-C3VzVfxo9jCLlRVP4moY2iohjc9asuQrKjqkUgEXp0bLy-gBB67UxV69rXMBEtz2pLYqfNKEUFFL7cVF1d407QUuSRpANitXUFbmtWo5M-0mJQM7LA0ktMDZUl3_-hPxNHFt9Sba4xDEiSyjPdMZDd--iNpCkJAGvManh-cNYI7HIBQM3kLE4GwD3ihStlW3IT5MfITEXfaaz0vWSvYdMXXmcVjVzgVn5JchynFilRsx7QK4JT7jPb_LQ1vAENBCfIuEERty8dgrKOvS4k21Br6IXckd9hl6l_5GoZUpbQzEY4BWGA8Ly8xKGbwwF4L-Mncc4TQt-2y09qYqU0TMTTXKBGgFfjVn4r0yIvryieCRAHSWuethGyYVX_Qhk_nZB3jqFkp42vMc1mkTjBCR-jlUCJWMbMcmL0tW2_SXDOk5JrACsq-T1djWeUCPgGYXCqYIygtSEE0f025feOpQJAynV78UM2w6fbgGBvsODpVr8Q2hjiI9Ydwh6MIJW1owo1QDDnuEOvTmdwdsQT9wX1H-SyN0cusSITAzgNMEp-NCj8NDW60kqTCbi7kMLF4OtPOK8THCBBkZ5c_SasAvSIxXo36fnMEKGPOLZs9GzLKMtKupym5PZ4H6H2zl_Y6iOIBniyQ3HBs5edLBBGQ3qqeeI_j7Xpwsr06cTCuQGYFAsNhu9dSs3LQTKlVfVeTiqBjRkxAiaQJgPUvFpOCvhIe2t-5n-MNj4.0dhenwDdC-iJ2W4uNnbx9YQcfEgnYSJzo1vXNawiifA; __Host-authjs.csrf-token=61aa1209933a1c967b7af6a787481f4e249a2d6b1d535924540addd4bd4ba196%7C510cca933fb90ce231290471f3885cdee2fdaf4f8f6994d96cfef9c63227e5f7; __Secure-authjs.callback-url=https%3A%2F%2Fsora.com; _cfuvid=XjJAS2zDerYymewq8Gl3e572OMiJ3cyyMqsmFiuGMn4-1745275079858-0.0.1.1-604800000; __cf_bm=hVGZ2X.67OhOZrqzUiW9Xg4IYGdCnss73d7GDU75YHk-1745296490-1.0.1.1-ppPUYZX7zAWYWXknUVzYPLjI7HDdBfmQz9DDMT5pMUNvqKBMhbUrJCp30PzxmBjMYwQL3injMe2IammgQNE5cj7JO7o6Alljt.MN1Pp8s8k; __cflb=0H28vtyuPgbsDEUozcoHSWoAnvpSqAh9friCA2BEchB; _dd_s=rum=2&id=b52d0910-2332-43c2-9fa0-da1c33d06227&created=1745296855240&expire=1745297781201";
|
||||
let result = generate_bearer_with_cookie(cookie).await;
|
||||
assert!(result.is_ok());
|
||||
println!("Bearer token: {}", result.unwrap());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod generate;
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
use crate::requests::image_gen::image_gen_status::TaskId;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SoraImageGenResponse {
|
||||
pub task_id: String,
|
||||
pub task_id: TaskId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
+59
-16
@@ -1,6 +1,7 @@
|
||||
use crate::creds::credential_migration::CredentialMigrationRef;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use crate::credentials::SoraCredentials;
|
||||
use thiserror::Error;
|
||||
use crate::requests::image_gen::image_gen_status::TaskId;
|
||||
|
||||
const SORA_IMAGE_GEN_URL: &str = "https://sora.com/backend/video_gen";
|
||||
|
||||
@@ -18,6 +19,9 @@ pub enum SoraError {
|
||||
#[error("Sora too many concurrent tasks: {0}")]
|
||||
TooManyConcurrentTasks(String),
|
||||
|
||||
#[error("Sora invalid JWT: {0}")]
|
||||
InvalidJwt(String),
|
||||
|
||||
#[error("Sora API error: {0}")]
|
||||
GenericError(String),
|
||||
|
||||
@@ -25,13 +29,13 @@ pub enum SoraError {
|
||||
NetworkError(String),
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub (crate) enum VideoGenType {
|
||||
ImageGen,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub (crate) enum OperationType {
|
||||
/// Simple prompt without reference images
|
||||
@@ -40,13 +44,13 @@ pub (crate) enum OperationType {
|
||||
Remix,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub (crate) enum InpaintItemType {
|
||||
Image,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub (crate) struct InpaintItem {
|
||||
pub r#type: InpaintItemType,
|
||||
@@ -66,7 +70,7 @@ pub (crate) struct InpaintItem {
|
||||
pub crop_bounds: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub (crate) struct RawSoraImageGenRequest {
|
||||
/// eg. "image_gen"
|
||||
@@ -87,11 +91,11 @@ pub (crate) struct RawSoraImageGenRequest {
|
||||
pub inpaint_items: Vec<InpaintItem>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub (crate) struct RawSoraResponse {
|
||||
/// eg. "task_01jqsz9dsae9tvjygf1abrv3xf"
|
||||
pub id: String,
|
||||
pub id: TaskId,
|
||||
/// not known
|
||||
pub priority: Option<String>,
|
||||
}
|
||||
@@ -120,6 +124,7 @@ pub enum RawSoraError {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SoraErrorCode {
|
||||
SentinelBlock,
|
||||
InvalidJwt,
|
||||
TokenExpired,
|
||||
TooManyConcurrentTasks,
|
||||
Unknown(String),
|
||||
@@ -142,20 +147,41 @@ impl std::fmt::Display for RawSoraErrorInner {
|
||||
}
|
||||
|
||||
/// Don't expose the internal request implementation as there are only a few "correct" ways to call the API.
|
||||
pub (crate) async fn image_gen_http_request(sora_request: RawSoraImageGenRequest, credentials: &SoraCredentials) -> Result<RawSoraResponse, SoraError> {
|
||||
pub (crate) async fn image_gen_http_request(sora_request: RawSoraImageGenRequest, credentials: CredentialMigrationRef<'_>) -> Result<RawSoraResponse, SoraError> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let sentinel = match credentials.sentinel.as_deref() {
|
||||
None => return Err(SoraError::GenericError("Calls to image gen require a sentinel in the credentials payload.".to_string())),
|
||||
Some(sentinel) => sentinel,
|
||||
};
|
||||
let mut cookie;
|
||||
let mut authorization_header;
|
||||
let mut sentinel;
|
||||
|
||||
match credentials {
|
||||
CredentialMigrationRef::Legacy(creds) => {
|
||||
cookie = creds.cookie.clone();
|
||||
authorization_header = creds.authorization_header_value();
|
||||
// TODO(bt,2025-04-23): We're using a Sora payload error in place of application state error. Surface this differently.
|
||||
sentinel = creds.sentinel.as_ref()
|
||||
.map(|sentinel| sentinel.to_string())
|
||||
.ok_or(SoraError::SentinelBlock("Sentinel is required for image generation.".to_string()))?;
|
||||
}
|
||||
CredentialMigrationRef::New(creds) => {
|
||||
cookie = creds.cookies.to_string();
|
||||
// TODO(bt,2025-04-23): We're using a Sora payload error in place of application state error. Surface this differently.
|
||||
authorization_header = creds.jwt_bearer_token.as_ref()
|
||||
.ok_or(SoraError::InvalidJwt("JWT bearer is required for image generation".to_string()))?
|
||||
.to_authorization_header_value();
|
||||
// TODO(bt,2025-04-23): We're using a Sora payload error in place of application state error. Surface this differently.
|
||||
sentinel = creds.sora_sentinel.as_ref()
|
||||
.map(|sentinel| sentinel.get_sentinel().to_string())
|
||||
.ok_or(SoraError::SentinelBlock("Sentinel is required for image generation.".to_string()))?;
|
||||
}
|
||||
}
|
||||
|
||||
let http_request = client.post(SORA_IMAGE_GEN_URL)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Cookie", &credentials.cookie)
|
||||
.header("Authorization", credentials.authorization_header_value())
|
||||
.header("Cookie", &cookie)
|
||||
.header("Authorization", &authorization_header)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("OpenAI-Sentinel-Token", sentinel);
|
||||
.header("OpenAI-Sentinel-Token", &sentinel);
|
||||
|
||||
let http_request = http_request.json(&sora_request).build()
|
||||
.map_err(|e| SoraError::NetworkError(e.to_string()))?;
|
||||
@@ -188,6 +214,9 @@ pub (crate) async fn image_gen_http_request(sora_request: RawSoraImageGenRequest
|
||||
SoraErrorCode::Unknown(_) => {
|
||||
return Err(SoraError::GenericError(error_response.error.message));
|
||||
}
|
||||
SoraErrorCode::InvalidJwt => {
|
||||
return Err(SoraError::InvalidJwt(error_response.error.message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,3 +228,17 @@ pub (crate) async fn image_gen_http_request(sora_request: RawSoraImageGenRequest
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use errors::AnyhowResult;
|
||||
use crate::requests::image_gen::image_gen_http_request::RawSoraResponse;
|
||||
|
||||
#[test]
|
||||
fn deserialize_task_id() -> AnyhowResult<()> {
|
||||
let json = "{\"id\": \"task_foobarbaz\"}";
|
||||
let response : RawSoraResponse = serde_json::from_str(json)?;
|
||||
assert_eq!(response.id.0, "task_foobarbaz");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+34
-58
@@ -1,15 +1,28 @@
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
|
||||
use crate::credentials::SoraCredentials;
|
||||
use crate::sora_error::SoraError;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use reqwest::Url;
|
||||
use serde_derive::Deserialize;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
use log::debug;
|
||||
|
||||
const SORA_STATUS_URL: &str = "https://sora.com/backend/video_gen";
|
||||
|
||||
/// This user agent is tied to the sentinel generation. If we need to change it, we may need to change sentinel generation too.
|
||||
const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36";
|
||||
|
||||
/// A strongly typed task ID for Sora tasks.
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
|
||||
pub struct TaskId(pub String);
|
||||
|
||||
impl Display for TaskId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
pub enum TaskStatus {
|
||||
Queued,
|
||||
@@ -42,7 +55,7 @@ pub struct VideoGenStatusResponse {
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct TaskResponse {
|
||||
pub id: String,
|
||||
pub id: TaskId,
|
||||
pub user: String,
|
||||
pub created_at: String,
|
||||
pub status: String,
|
||||
@@ -79,7 +92,7 @@ pub struct TaskResponse {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct Generation {
|
||||
pub id: String,
|
||||
pub task_id: String,
|
||||
pub task_id: TaskId,
|
||||
pub created_at: String,
|
||||
pub deleted_at: Option<String>,
|
||||
pub url: String,
|
||||
@@ -170,7 +183,7 @@ pub struct ModerationResult {
|
||||
pub results_by_frame_index: serde_json::Value,
|
||||
pub code: Option<String>,
|
||||
pub is_output_rejection: bool,
|
||||
pub task_id: String,
|
||||
pub task_id: TaskId,
|
||||
}
|
||||
|
||||
pub struct StatusRequest {
|
||||
@@ -186,10 +199,11 @@ impl StatusRequest {
|
||||
}
|
||||
|
||||
/// Gets the status of image generation tasks from Sora API
|
||||
pub async fn get_image_gen_status(status_request: &StatusRequest, credentials: &SoraCredentials) -> AnyhowResult<VideoGenStatusResponse> {
|
||||
pub async fn get_image_gen_status(status_request: &StatusRequest, credentials: &SoraCredentials) -> Result<VideoGenStatusResponse, SoraError> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let mut url = reqwest::Url::parse(SORA_STATUS_URL)?;
|
||||
let mut url = reqwest::Url::parse(SORA_STATUS_URL)
|
||||
.map_err(|err| anyhow!("error parsing URL: {:?}", err))?;
|
||||
|
||||
// Add query parameters
|
||||
if let Some(limit) = status_request.limit {
|
||||
@@ -200,7 +214,11 @@ pub async fn get_image_gen_status(status_request: &StatusRequest, credentials: &
|
||||
url.query_pairs_mut().append_pair("before", &before);
|
||||
}
|
||||
|
||||
let http_request = client.get(url).header("User-Agent", USER_AGENT).header("Cookie", &credentials.cookie).header("Authorization", credentials.authorization_header_value()).header("Content-Type", "application/json");
|
||||
let http_request = client.get(url)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Cookie", &credentials.cookie)
|
||||
.header("Authorization", credentials.authorization_header_value())
|
||||
.header("Content-Type", "application/json");
|
||||
|
||||
let http_request = http_request.build()?;
|
||||
|
||||
@@ -209,10 +227,12 @@ pub async fn get_image_gen_status(status_request: &StatusRequest, credentials: &
|
||||
let status = response.status();
|
||||
|
||||
let response_body = &response.text().await?;
|
||||
println!("response_body: {}", response_body);
|
||||
|
||||
debug!("response_body: {}", response_body);
|
||||
|
||||
if status != reqwest::StatusCode::OK {
|
||||
return Err(anyhow::anyhow!("The status request failed; status = {:?}, message = {:?}", status, response_body));
|
||||
// TODO: Handle non-200 status codes, esp. JWT errors and credential errors
|
||||
return Err(SoraError::AnyhowError(anyhow::anyhow!("The status request failed; status = {:?}, message = {:?}", status, response_body)));
|
||||
}
|
||||
|
||||
let response: VideoGenStatusResponse = serde_json::from_str(response_body)?;
|
||||
@@ -220,47 +240,6 @@ pub async fn get_image_gen_status(status_request: &StatusRequest, credentials: &
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub async fn wait_for_image_gen_status(task_id: &String, credentials: &SoraCredentials, retry_limit: Option<u32>) -> AnyhowResult<TaskResponse> {
|
||||
let status_request = StatusRequest {
|
||||
limit: None,
|
||||
before: None,
|
||||
// before: Some(task_id.clone()),
|
||||
};
|
||||
let retry_limit = retry_limit.unwrap_or(10);
|
||||
|
||||
for _ in 0..retry_limit {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
|
||||
let status_response = get_image_gen_status(&status_request, credentials).await?;
|
||||
match find_task_response_by_id(&status_response, task_id.clone()) {
|
||||
Some(task_response) => match TaskStatus::from_str(&task_response.status) {
|
||||
TaskStatus::Succeeded => {
|
||||
return Ok(task_response.clone());
|
||||
},
|
||||
TaskStatus::Failed => {
|
||||
return Err(anyhow::anyhow!("Task failed"));
|
||||
},
|
||||
TaskStatus::Unknown(status) => {
|
||||
println!("Unknown task status: {}", status);
|
||||
continue;
|
||||
},
|
||||
TaskStatus::Queued => {
|
||||
println!("Task is queued");
|
||||
continue;
|
||||
},
|
||||
TaskStatus::Running => {
|
||||
println!("Task is running");
|
||||
continue;
|
||||
},
|
||||
},
|
||||
None => {
|
||||
return Err(anyhow::anyhow!("Task not found"));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!("Task not found"))
|
||||
}
|
||||
|
||||
pub async fn save_generations_to_dir(generations: &[Generation], dir: &str) -> AnyhowResult<()> {
|
||||
let dir = Path::new(dir);
|
||||
if !dir.exists() {
|
||||
@@ -288,14 +267,11 @@ pub async fn save_generations_to_dir(generations: &[Generation], dir: &str) -> A
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find_task_response_by_id(status_response: &VideoGenStatusResponse, task_id: String) -> Option<&TaskResponse> {
|
||||
status_response.task_responses.iter().find(|task| task.id == task_id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::credentials::SoraCredentials;
|
||||
use crate::image_gen::image_gen_status::{get_image_gen_status, save_generations_to_dir, wait_for_image_gen_status, StatusRequest, VideoGenStatusResponse};
|
||||
use crate::recipes::wait_for_image_gen_status::wait_for_image_gen_status;
|
||||
use crate::requests::image_gen::image_gen_status::{get_image_gen_status, save_generations_to_dir, StatusRequest, VideoGenStatusResponse};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use testing::test_file_path::test_file_path;
|
||||
@@ -329,7 +305,7 @@ mod tests {
|
||||
|
||||
let task_id = response.task_responses[0].id.clone();
|
||||
let task_response = wait_for_image_gen_status(
|
||||
&task_id,
|
||||
&task_id.0,
|
||||
&creds,
|
||||
Some(10)
|
||||
).await?;
|
||||
-1
@@ -4,4 +4,3 @@ pub mod common;
|
||||
pub mod sora_image_gen_remix;
|
||||
pub mod sora_image_gen_simple;
|
||||
pub mod image_gen_status;
|
||||
pub use image_gen_status::{get_image_gen_status, save_generations_to_dir, wait_for_image_gen_status, Generation, StatusRequest, TaskResponse, VideoGenStatusResponse};
|
||||
+17
-13
@@ -1,14 +1,14 @@
|
||||
use errors::AnyhowResult;
|
||||
use crate::credentials::SoraCredentials;
|
||||
use crate::image_gen::common::{ImageSize, NumImages, SoraImageGenResponse};
|
||||
use crate::image_gen::image_gen_http_request::{image_gen_http_request, InpaintItem, InpaintItemType, OperationType, RawSoraImageGenRequest, SoraError, VideoGenType};
|
||||
use log::info;
|
||||
use crate::creds::credential_migration::CredentialMigrationRef;
|
||||
use crate::requests::image_gen::common::{ImageSize, NumImages, SoraImageGenResponse};
|
||||
use crate::requests::image_gen::image_gen_http_request::{image_gen_http_request, InpaintItem, InpaintItemType, OperationType, RawSoraImageGenRequest, SoraError, VideoGenType};
|
||||
|
||||
pub struct SoraImageGenRemixRequest<'a> {
|
||||
pub prompt: String,
|
||||
pub num_images: NumImages,
|
||||
pub image_size: ImageSize,
|
||||
pub sora_media_tokens: Vec<String>,
|
||||
pub credentials: &'a SoraCredentials,
|
||||
pub credentials: CredentialMigrationRef<'a>,
|
||||
}
|
||||
|
||||
/// The "remix" commands let you supply additional images as context.
|
||||
@@ -36,7 +36,10 @@ pub async fn sora_image_gen_remix(request: SoraImageGenRemixRequest<'_>) -> Resu
|
||||
}).collect(),
|
||||
};
|
||||
|
||||
let result = image_gen_http_request(args, &request.credentials).await?;
|
||||
// TODO: Replace this with debug level logging in the future.
|
||||
info!("Sending remix request: {:?}", args);
|
||||
|
||||
let result = image_gen_http_request(args, request.credentials).await?;
|
||||
|
||||
Ok(SoraImageGenResponse {
|
||||
task_id: result.id,
|
||||
@@ -45,12 +48,13 @@ pub async fn sora_image_gen_remix(request: SoraImageGenRemixRequest<'_>) -> Resu
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs::read_to_string;
|
||||
use errors::AnyhowResult;
|
||||
use testing::test_file_path::test_file_path;
|
||||
use crate::credentials::SoraCredentials;
|
||||
use crate::image_gen::common::{ImageSize, NumImages};
|
||||
use crate::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
|
||||
use crate::creds::credential_migration::CredentialMigrationRef;
|
||||
use crate::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use crate::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use testing::test_file_path::test_file_path;
|
||||
|
||||
#[ignore] // You can manually run "ignore" tests in the IDE, but they won't run in CI.
|
||||
#[tokio::test]
|
||||
@@ -75,12 +79,12 @@ mod tests {
|
||||
num_images: NumImages::One,
|
||||
image_size: ImageSize::Square,
|
||||
sora_media_tokens: vec!["media_01jqyhrz4detyvtzwp2p4j63ad".to_string()],
|
||||
credentials: &creds,
|
||||
credentials: CredentialMigrationRef::Legacy(&creds),
|
||||
}).await?;
|
||||
|
||||
println!("task_id: {}", response.task_id);
|
||||
|
||||
assert!(response.task_id.starts_with("task_"));
|
||||
assert!(response.task_id.0.starts_with("task_"));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+13
-11
@@ -1,13 +1,14 @@
|
||||
use errors::AnyhowResult;
|
||||
use crate::credentials::SoraCredentials;
|
||||
use crate::image_gen::common::{ImageSize, NumImages, SoraImageGenResponse};
|
||||
use crate::image_gen::image_gen_http_request::{image_gen_http_request, OperationType, RawSoraImageGenRequest, VideoGenType};
|
||||
use crate::requests::image_gen::common::{ImageSize, NumImages, SoraImageGenResponse};
|
||||
use crate::requests::image_gen::image_gen_http_request::{image_gen_http_request, OperationType, RawSoraImageGenRequest, VideoGenType};
|
||||
use errors::AnyhowResult;
|
||||
use crate::creds::credential_migration::CredentialMigrationRef;
|
||||
|
||||
pub struct SoraImageGenSimpleRequest<'a> {
|
||||
pub prompt: String,
|
||||
pub num_images: NumImages,
|
||||
pub image_size: ImageSize,
|
||||
pub credentials: &'a SoraCredentials,
|
||||
pub credentials: CredentialMigrationRef<'a>,
|
||||
}
|
||||
|
||||
pub async fn sora_image_gen_simple(args: SoraImageGenSimpleRequest<'_>) -> AnyhowResult<SoraImageGenResponse> {
|
||||
@@ -32,12 +33,13 @@ pub async fn sora_image_gen_simple(args: SoraImageGenSimpleRequest<'_>) -> Anyho
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs::read_to_string;
|
||||
use errors::AnyhowResult;
|
||||
use testing::test_file_path::test_file_path;
|
||||
use crate::credentials::SoraCredentials;
|
||||
use crate::image_gen::common::{ImageSize, NumImages};
|
||||
use crate::image_gen::sora_image_gen_simple::{sora_image_gen_simple, SoraImageGenSimpleRequest};
|
||||
use crate::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use crate::requests::image_gen::sora_image_gen_simple::{sora_image_gen_simple, SoraImageGenSimpleRequest};
|
||||
use errors::AnyhowResult;
|
||||
use std::fs::read_to_string;
|
||||
use testing::test_file_path::test_file_path;
|
||||
use crate::creds::credential_migration::CredentialMigrationRef;
|
||||
|
||||
#[ignore] // You can manually run "ignore" tests in the IDE, but they won't run in CI.
|
||||
#[tokio::test]
|
||||
@@ -61,12 +63,12 @@ mod tests {
|
||||
prompt: "A pirate and a ninja fight in a battle inside a UFO. Fully photo realistic, lifelike, lens flare".to_string(),
|
||||
num_images: NumImages::One,
|
||||
image_size: ImageSize::Square,
|
||||
credentials: &creds,
|
||||
credentials: CredentialMigrationRef::Legacy(&creds),
|
||||
}).await?;
|
||||
|
||||
println!("task_id: {}", response.task_id);
|
||||
|
||||
assert!(response.task_id.starts_with("task_"));
|
||||
assert!(response.task_id.0.starts_with("task_"));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user