Compare commits

..
226 changed files with 4458 additions and 7721 deletions
-13
View File
@@ -1,13 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<codeStyleSettings language="Rust">
<option name="RIGHT_MARGIN" value="180" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="ALIGN_MULTILINE_PARAMETERS_IN_CALLS" value="false" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>
-5
View File
@@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
@@ -1,12 +0,0 @@
{
"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
+14 -141
View File
@@ -840,14 +840,11 @@ 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]]
@@ -1924,7 +1921,7 @@ checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
dependencies = [
"byteorder",
"fnv",
"uuid 1.8.0",
"uuid",
]
[[package]]
@@ -2880,12 +2877,6 @@ 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"
@@ -3267,20 +3258,6 @@ 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"
@@ -5348,7 +5325,6 @@ dependencies = [
"hyper 1.6.0",
"hyper-util",
"rustls 0.23.13",
"rustls-native-certs",
"rustls-pki-types",
"tokio",
"tokio-rustls",
@@ -5571,7 +5547,7 @@ dependencies = [
name = "idempotency"
version = "0.0.1"
dependencies = [
"uuid 1.8.0",
"uuid",
"workspace-build-acceleration",
]
@@ -5677,10 +5653,8 @@ dependencies = [
name = "images"
version = "0.0.1"
dependencies = [
"anyhow",
"errors",
"image",
"mimetypes",
"testing",
"workspace-build-acceleration",
]
@@ -6794,15 +6768,6 @@ 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"
@@ -6903,9 +6868,7 @@ dependencies = [
name = "mimetypes"
version = "0.0.1"
dependencies = [
"anyhow",
"infer 0.19.0",
"testing",
"infer 0.16.0",
"workspace-build-acceleration",
]
@@ -7830,7 +7793,7 @@ dependencies = [
"toml 0.8.20",
"ureq",
"url",
"uuid 1.8.0",
"uuid",
"walkdir",
]
@@ -9657,7 +9620,6 @@ dependencies = [
"pin-project-lite",
"quinn",
"rustls 0.23.13",
"rustls-native-certs",
"rustls-pemfile 2.1.2",
"rustls-pki-types",
"serde",
@@ -9668,7 +9630,6 @@ dependencies = [
"tokio",
"tokio-native-tls",
"tokio-rustls",
"tokio-socks",
"tokio-util",
"tower 0.5.2",
"tower-service",
@@ -9739,7 +9700,7 @@ dependencies = [
"rkyv_derive",
"seahash",
"tinyvec",
"uuid 1.8.0",
"uuid",
]
[[package]]
@@ -9996,19 +9957,6 @@ 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"
@@ -10153,7 +10101,7 @@ dependencies = [
"serde",
"serde_json",
"url",
"uuid 1.8.0",
"uuid",
]
[[package]]
@@ -11243,7 +11191,6 @@ dependencies = [
"hostname 0.4.0",
"http_server_common",
"idempotency",
"images",
"infer 0.16.0",
"language-tags",
"lexical-sort",
@@ -11301,7 +11248,7 @@ dependencies = [
"user_traits_component",
"utoipa",
"utoipa-swagger-ui",
"uuid 1.8.0",
"uuid",
"videos",
"workspace-build-acceleration",
"zip 0.6.6",
@@ -11314,11 +11261,9 @@ dependencies = [
"anyhow",
"backoff",
"chrono",
"cookie 0.18.1",
"enums",
"enums_public",
"errors",
"idempotency",
"log",
"reqwest 0.12.15",
"serde",
@@ -11327,7 +11272,6 @@ dependencies = [
"tokens",
"tokio",
"url",
"uuid 1.8.0",
"workspace-build-acceleration",
]
@@ -12098,16 +12042,13 @@ 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",
@@ -12122,7 +12063,6 @@ dependencies = [
"subprocess_common",
"tauri",
"tauri-build",
"tauri-plugin-http",
"tauri-plugin-log",
"tempdir",
"tempfile",
@@ -12177,7 +12117,7 @@ dependencies = [
"thiserror 2.0.12",
"time",
"url",
"uuid 1.8.0",
"uuid",
"walkdir",
]
@@ -12197,9 +12137,9 @@ dependencies = [
[[package]]
name = "tauri-plugin"
version = "2.2.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37a5ebe6a610d1b78a94650896e6f7c9796323f408800cef436e0fa0539de601"
checksum = "a4190775d6ff73fe66d9af44c012739a2659720efd9c0e1e56a918678038699d"
dependencies = [
"anyhow",
"glob",
@@ -12212,52 +12152,6 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-fs"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88371e340ad2f07409a3b68294abe73f20bc9c1bc1b631a31dc37a3d0161f682"
dependencies = [
"anyhow",
"dunce",
"glob",
"percent-encoding",
"schemars",
"serde",
"serde_json",
"serde_repr",
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.12",
"toml 0.8.20",
"url",
"uuid 1.8.0",
]
[[package]]
name = "tauri-plugin-http"
version = "2.4.3"
dependencies = [
"bytes 1.10.0",
"cookie_store",
"data-url",
"http 1.1.0",
"regex",
"reqwest 0.12.15",
"schemars",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror 2.0.12",
"tokio",
"tracing",
"url",
"urlpattern",
]
[[package]]
name = "tauri-plugin-log"
version = "2.3.1"
@@ -12370,9 +12264,9 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "2.4.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2900399c239a471bcff7f15c4399eb1a8c4fe511ba2853e07c996d771a5e0a4"
checksum = "1f037e66c7638cc0a2213f61566932b9a06882b8346486579c90e4b019bac447"
dependencies = [
"anyhow",
"brotli 7.0.0",
@@ -12402,7 +12296,7 @@ dependencies = [
"toml 0.8.20",
"url",
"urlpattern",
"uuid 1.8.0",
"uuid",
"walkdir",
]
@@ -12855,18 +12749,6 @@ 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"
@@ -13658,15 +13540,6 @@ 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"
@@ -14828,7 +14701,7 @@ dependencies = [
"unicode-normalization",
"ureq",
"url",
"uuid 1.8.0",
"uuid",
"void",
"winapi 0.3.9",
"windows-sys 0.48.0",
-12
View File
@@ -39,7 +39,6 @@ members = [
"crates/lib/bootstrap",
"crates/lib/bucket_paths",
"crates/lib/caching/memory_caching",
"crates/lib/caching/memory_store",
"crates/lib/caching/redis_caching",
"crates/lib/concurrency",
"crates/lib/cookies",
@@ -118,7 +117,6 @@ 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",
@@ -149,10 +147,6 @@ 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
@@ -163,7 +157,6 @@ 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"
@@ -174,7 +167,6 @@ serde_derive = "1.0.219"
serde_json = "1.0.140"
serde_yaml = "0.9.1"
sha3 = "0.10.8"
tauri = { version = "2.4.1", features = ["unstable"] }
thiserror = "2.0.12"
toml = "0.8.20"
url = { version = "2.5.4", features = ["serde"] }
@@ -185,11 +177,7 @@ enums = { path = "crates/_types/enums" }
enums_public = { path = "crates/_types/enums_public" }
errors = { path = "crates/lib/errors" }
filesys = { path = "crates/lib/files/filesys" }
idempotency = { path = "crates/lib/idempotency" }
images = { path = "crates/lib/files/images" }
memory_caching = { path = "crates/lib/caching/memory_caching" }
memory_store = { path = "crates/lib/caching/memory_store" }
mimetypes = { path = "crates/lib/files/mimetypes" }
openai_sora_client = { path = "crates/lib/api_clients/openai_sora_client" }
primitives = { path = "crates/lib/data/primitives" }
shared_service_components = { path = "crates/lib/shared_service_components" }
@@ -28,12 +28,11 @@ 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 = "DO NOT USE. Originally deprecated in favor of `StorytellerStudio`.")]
#[deprecated(note = "Use StorytellerStudio instead")]
StoryEngine,
}
@@ -39,10 +39,6 @@ pub enum MediaFileOriginProductCategory {
#[serde(rename = "vst")]
VideoStyleTransfer,
/// Image Studio
#[serde(rename = "image_studio")]
ImageStudio,
/// Storyteller Studio
#[serde(rename = "studio")]
StorytellerStudio,
@@ -91,7 +87,6 @@ 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",
@@ -110,7 +105,6 @@ 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),
@@ -132,7 +126,6 @@ impl MediaFileOriginProductCategory {
Self::FaceFusion,
Self::FaceMirror,
Self::VideoStyleTransfer,
Self::ImageStudio,
Self::StorytellerStudio,
Self::TextToSpeech,
Self::VoiceConversion,
@@ -160,7 +153,6 @@ 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");
@@ -178,7 +170,6 @@ 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");
@@ -196,7 +187,6 @@ 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);
@@ -210,13 +200,12 @@ mod tests {
#[test]
fn all_variants() {
let mut variants = MediaFileOriginProductCategory::all_variants();
assert_eq!(variants.len(), 14);
assert_eq!(variants.len(), 13);
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));
+5 -12
View File
@@ -27,7 +27,6 @@ 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 }
@@ -35,8 +34,10 @@ 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 = { workspace = true }
base64 = "0.22.1"
bytes = "1.10"
chrono = { workspace = true }
const_format = "0.2.34"
@@ -44,29 +45,21 @@ 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 } # NB(bt,2025-03-10): default-features = false on Windows while I debug openssl / rustls
reqwest = { workspace = true, features = ["multipart", "stream"], default-features = false }
serde = { workspace = true }
serde_derive = { workspace = true }
serde_json = { workspace = true }
serde_yaml = { workspace = true }
tauri = { workspace = true }
tauri = { version = "2.4.1", features = ["unstable"] }
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,14 +7,6 @@
],
"permissions": [
"core:default",
"log:default",
{
"identifier": "http:default",
"allow": [
{ "url": "https://*.storyteller.ai" },
{ "url": "https://*.fakeyou.com" }
],
"deny": [{ "url": "https://private.tauri.app" }]
}
"log:default"
]
}
@@ -1,3 +1,2 @@
pub mod flip_image;
pub mod platform_info_command;
pub mod sora;
@@ -1,63 +0,0 @@
use crate::state::os_platform::OsPlatform;
use base64::prelude::BASE64_STANDARD;
use base64::Engine;
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
use image::ImageReader;
use log::info;
use serde_derive::Serialize;
use std::io::Cursor;
#[derive(Debug, Serialize)]
pub struct PlatformInfoResponse {
pub os_platform: DetectedOs,
pub webview_runtime: WebviewRuntime,
}
#[derive(Copy, Clone, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DetectedOs {
Windows,
#[serde(rename = "macos")]
MacOS,
Linux,
Unknown,
}
#[derive(Copy, Clone, Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum WebviewRuntime {
#[serde(rename = "webkit_safari")]
WebkitSafari,
#[serde(rename = "webkit_gtk")]
WebkitGtk,
#[serde(rename = "edge_webview_2")]
EdgeWebview2,
Unknown,
}
#[tauri::command]
pub fn platform_info_command() -> Result<PlatformInfoResponse, String> {
info!("platform_info_command called...");
let os_platform = match OsPlatform::maybe_get() {
Some(OsPlatform::Linux) => DetectedOs::Linux,
Some(OsPlatform::MacOs) => DetectedOs::MacOS,
Some(OsPlatform::Windows) => DetectedOs::Windows,
None => DetectedOs::Unknown,
};
// These are the webviews that Tauri uses on each OS:
// https://tauri.app/reference/webview-versions/
let webview_runtime = match os_platform {
DetectedOs::Windows => WebviewRuntime::EdgeWebview2,
DetectedOs::MacOS => WebviewRuntime::WebkitSafari,
DetectedOs::Linux => WebviewRuntime::WebkitGtk,
DetectedOs::Unknown => WebviewRuntime::Unknown,
};
Ok(PlatformInfoResponse {
os_platform,
webview_runtime,
})
}
@@ -1,4 +1,4 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::app_dir::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;
@@ -1,9 +1,7 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::data_dir::trait_data_subdir::DataSubdir;
use crate::state::app_dir::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::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;
@@ -52,13 +50,12 @@ pub async fn sora_image_remix_command(
request: SoraImageRemixCommand,
app_data_root: State<'_, AppDataRoot>,
sora_creds_manager: State<'_, SoraCredentialManager>,
sora_task_queue: State<'_, SoraTaskQueue>,
) -> Result<String, String> {
info!("image_generation_command called; processing image...");
// TODO(bt,2025-04-24): Better error messages to caller
generate_image(request, &app_data_root, &sora_creds_manager, &sora_task_queue)
generate_image(request, &app_data_root, &sora_creds_manager)
.await
.map_err(|err| {
error!("error: {:?}", err);
@@ -72,7 +69,6 @@ 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?;
@@ -89,6 +85,8 @@ pub async fn generate_image(
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)
@@ -143,9 +141,7 @@ pub async fn generate_image(
sora_creds_manager.set_credentials(&new_creds)?;
}
info!("New Sora Task ID: {:?} ", response.task_id);
sora_task_queue.insert(&response.task_id)?;
println!(">> TASK ID: {:?} ", response.task_id);
Ok(())
}
+6 -51
View File
@@ -4,30 +4,20 @@ pub mod state;
pub mod threads;
pub mod utils;
use tauri::Manager;
use crate::commands::flip_image::flip_image;
use crate::commands::platform_info_command::platform_info_command;
use crate::commands::sora::open_sora_login_command::open_sora_login_command;
use crate::commands::sora::sora_image_generation_command::sora_image_generation_command;
use crate::commands::sora::sora_image_remix_command::sora_image_remix_command;
use crate::state::app_config::AppConfig;
use crate::state::main_window_size::MainWindowSize;
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
use crate::state::sora::sora_task_queue::SoraTaskQueue;
use crate::state::storyteller::storyteller_credential_manager::StorytellerCredentialManager;
use crate::threads::discord_presence_thread::discord_presence_thread;
use crate::threads::main_window_thread::main_window_thread::main_window_thread;
use crate::threads::sora_session_login_thread::sora_session_login_thread;
use crate::threads::sora_task_polling_thread::sora_task_polling_thread;
use tauri_plugin_http;
use tauri_plugin_log::Target;
use tauri_plugin_log::TargetKind;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
// NB: Tauri wants to install the logger itself, so we can't rely on the logger crate
// NB: Tauri wants to install the logger itself, so we can't rely on the logger crate
// until the tauri runtime begins.
println!("Loading model config...");
@@ -35,24 +25,15 @@ pub fn run() {
.expect("config should load");
let app_data_root = config.app_data_root.clone();
let app_data_root_2 = config.app_data_root.clone();
let app_data_root2 = 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();
let sora_creds_manager2 = sora_creds_manager.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![
@@ -62,7 +43,7 @@ pub fn run() {
}),
])
.build())
.setup(move |app| {
.setup(|app| {
// TODO(bt): This is broken on windows
// log_environment_details();
@@ -75,42 +56,16 @@ pub fn run() {
//}
let app = app.handle().clone();
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());
tauri::async_runtime::spawn(sora_session_login_thread(app, app_data_root2, sora_creds_manager2));
Ok(())
})
.manage(app_data_root)
.manage(config)
.manage(app_data_root)
.manage(sora_creds_manager)
.manage(sora_task_queue)
.invoke_handler(tauri::generate_handler![
flip_image,
open_sora_login_command,
platform_info_command,
sora_image_generation_command,
sora_image_remix_command,
])
@@ -1,4 +1,4 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::app_dir::AppDataRoot;
use crate::state::os_platform::OsPlatform;
use crate::state::yaml_config::YamlConfig;
@@ -0,0 +1,264 @@
use crate::state::expanduser::expanduser;
use crate::state::os_platform::OsPlatform;
use anyhow::anyhow;
use directories::UserDirs;
use std::path::{Path, PathBuf};
use chrono::{DateTime, Local};
use tempdir::TempDir;
use tempfile::{Builder, NamedTempFile};
const DEFAULT_DATA_DIR : &str = "artcraft";
const ASSETS_SUBDIRECTORY : &str = "assets";
const DOWNLOADS_SUBDIRECTORY : &str = "downloads";
const WEIGHTS_SUBDIRECTORY : &str = "weights";
const TEMPORARY_SUBDIRECTORY : &str = "temp";
/// Note: Tauri appends ".log" to the end of the filename.
const LOG_FILE_NAME : &str = "application_debug";
/// The path to the application data directory, which includes "asset" and "weights" data.
#[derive(Clone)]
pub struct AppDataRoot {
path: PathBuf,
assets_dir: AppAssetsDir,
downloads_dir: AppDownloadsDir,
weights_dir: AppWeightsDir,
temp_dir: TemporaryDir,
log_file_name: PathBuf,
log_file_name_string: String,
}
#[derive(Clone)]
pub struct AppAssetsDir {
path: PathBuf,
}
#[derive(Clone)]
pub struct AppDownloadsDir {
path: PathBuf,
}
#[derive(Clone)]
pub struct AppWeightsDir {
path: PathBuf,
}
#[derive(Clone)]
pub struct TemporaryDir {
path: PathBuf,
}
impl AppDataRoot {
pub fn create_default() -> anyhow::Result<Self> {
let directory = get_default_data_dir()?;
Self::create_existing(directory)
}
pub fn create_existing<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
let mut dir = dir.as_ref().to_path_buf();
match OsPlatform::get() {
OsPlatform::Linux | OsPlatform::MacOs => {
if let Some(d) = dir.as_os_str().to_str() {
dir = expanduser(d)?;
}
},
OsPlatform::Windows => {}
}
if !dir.is_dir() {
println!("Creating directory {:?}", dir);
std::fs::create_dir_all(&dir)?;
}
match dir.canonicalize() {
Ok(d) => dir = d,
Err(err) => {
println!("Error canonicalizing {:?}: {}", dir, err);
}
}
let assets_dir = AppAssetsDir::create_existing(dir.join(ASSETS_SUBDIRECTORY))?;
let downloads_dir = AppDownloadsDir::create_existing(dir.join(DOWNLOADS_SUBDIRECTORY))?;
let weights_dir = AppWeightsDir::create_existing(dir.join(WEIGHTS_SUBDIRECTORY))?;
let temp_dir = TemporaryDir::create_existing(dir.join(TEMPORARY_SUBDIRECTORY))?;
let log_file_name = dir.join(LOG_FILE_NAME);
let log_file_name_string = log_file_name
.to_str()
.ok_or(anyhow!("couldn't convert log path to str"))?
.to_string();
Ok(Self {
path: dir,
assets_dir,
downloads_dir,
weights_dir,
temp_dir,
log_file_name,
log_file_name_string,
})
}
pub fn assets_dir(&self) -> &AppAssetsDir {
&self.assets_dir
}
pub fn downloads_dir(&self) -> &AppDownloadsDir {
&self.downloads_dir
}
pub fn weights_dir(&self) -> &AppWeightsDir {
&self.weights_dir
}
pub fn temp_dir(&self) -> &TemporaryDir {
&self.temp_dir
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn log_file_name(&self) -> &Path {
&self.log_file_name
}
pub fn log_file_name_str(&self) -> &str {
&self.log_file_name_string
}
pub fn get_sora_cookie_file_path(&self) -> PathBuf {
self.path.join("sora_cookies.txt")
}
pub fn get_sora_bearer_token_file_path(&self) -> PathBuf {
self.path.join("sora_bearer_token.txt")
}
pub fn get_sora_sentinel_file_path(&self) -> PathBuf {
self.path.join("sora_sentinel.txt")
}
}
impl AppAssetsDir {
pub fn create_existing<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
let mut dir = dir.as_ref().to_path_buf();
match dir.canonicalize() {
Ok(d) => dir = d,
Err(err) => {
println!("Error canonicalizing {:?}: {}", dir, err);
}
}
if !dir.exists() {
println!("Creating directory {:?}", dir);
std::fs::create_dir(&dir)?;
}
Ok(Self {
path: dir,
})
}
pub fn path(&self) -> &Path {
&self.path
}
/// Return the current date asset directory
/// If it doesn't exist, create it.
pub fn make_or_get_current_date_dir(&self) -> anyhow::Result<PathBuf> {
let format: DateTime<Local> = Local::now();
let date_directory_name = format.format("%Y-%d-%m").to_string();
let full_path = self.path.join(date_directory_name);
if !full_path.exists() {
std::fs::create_dir(&full_path)?;
}
Ok(full_path)
}
}
impl AppDownloadsDir {
pub fn create_existing<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
let mut dir = dir.as_ref().to_path_buf();
match dir.canonicalize() {
Ok(d) => dir = d,
Err(err) => {
println!("Error canonicalizing {:?}: {}", dir, err);
}
}
if !dir.exists() {
println!("Creating directory {:?}", dir);
std::fs::create_dir(&dir)?;
}
Ok(Self {
path: dir,
})
}
pub fn path(&self) -> &Path {
&self.path
}
}
impl AppWeightsDir {
pub fn create_existing<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
let mut dir = dir.as_ref().to_path_buf();
match dir.canonicalize() {
Ok(d) => dir = d,
Err(err) => {
println!("Error canonicalizing {:?}: {}", dir, err);
}
}
if !dir.exists() {
println!("Creating directory {:?}", dir);
std::fs::create_dir(&dir)?;
}
Ok(Self {
path: dir,
})
}
pub fn path(&self) -> &Path {
&self.path
}
}
impl TemporaryDir {
pub fn create_existing<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
let mut dir = dir.as_ref().to_path_buf();
match dir.canonicalize() {
Ok(d) => dir = d,
Err(err) => {
println!("Error canonicalizing {:?}: {}", dir, err);
}
}
if !dir.exists() {
println!("Creating directory {:?}", dir);
std::fs::create_dir(&dir)?;
}
Ok(Self {
path: dir,
})
}
pub fn with_prefix(&self, prefix: &str) -> anyhow::Result<TempDir> {
let tempdir = TempDir::new_in(&self.path, prefix)?;
Ok(tempdir)
}
pub fn new_named_temp_file(&self) -> anyhow::Result<NamedTempFile> {
let tempfile = Builder::new()
.prefix("temp_")
.suffix(".bin")
.tempfile_in(&self.path)?;
Ok(tempfile)
}
}
// eg. /home/bob/artcraft, /Users/bob/artcraft, or C:\Users\Alice\artcraft
fn get_default_data_dir() -> anyhow::Result<PathBuf> {
Ok(UserDirs::new()
.ok_or_else(|| anyhow!("could not determine user home directory"))?
.home_dir()
.join(DEFAULT_DATA_DIR))
}
@@ -1,22 +0,0 @@
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)
}
}
@@ -1,36 +0,0 @@
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)
}
}
@@ -1,43 +0,0 @@
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")
}
}
@@ -1,140 +0,0 @@
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))
}
@@ -1,21 +0,0 @@
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
}
}
@@ -1,21 +0,0 @@
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
}
}
@@ -1,7 +0,0 @@
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;
@@ -1,38 +0,0 @@
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)
}
}
@@ -1,31 +0,0 @@
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))
}
}
@@ -1,74 +0,0 @@
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,9 +1,6 @@
pub mod app_config;
pub mod app_startup_time;
pub mod data_dir;
pub mod app_dir;
pub mod expanduser;
pub mod main_window_size;
pub mod os_platform;
pub mod sora;
pub mod storyteller;
pub mod yaml_config;
@@ -7,18 +7,14 @@ pub enum OsPlatform {
}
impl OsPlatform {
pub fn maybe_get() -> Option<Self> {
pub fn get() -> OsPlatform {
// Totally scientific.
// https://doc.rust-lang.org/std/env/consts/constant.OS.html
match std::env::consts::OS {
"linux" => Some(Self::Linux),
"windows" => Some(Self::Windows),
"macos" | "apple" => Some(Self::MacOs),
_ => None,
"linux" => OsPlatform::Linux,
"windows" => OsPlatform::Windows,
"macos" | "apple" => OsPlatform::MacOs,
_ => OsPlatform::Linux,
}
}
pub fn get() -> Self {
Self::maybe_get().unwrap_or(Self::Linux)
}
}
@@ -1,5 +1,4 @@
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;
pub mod sora_credential_manager;
@@ -1,4 +1,4 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::app_dir::AppDataRoot;
use anyhow::anyhow;
use errors::AnyhowResult;
use openai_sora_client::creds::sora_cookies::SoraCookies;
@@ -1,4 +1,4 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::app_dir::AppDataRoot;
use anyhow::anyhow;
use errors::AnyhowResult;
use log::{info, warn};
@@ -1,4 +1,4 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::app_dir::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;
@@ -1,231 +0,0 @@
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(())
}
}
@@ -1,3 +0,0 @@
pub mod read_storyteller_credentials_from_disk;
pub mod storyteller_credential_holder;
pub mod storyteller_credential_manager;
@@ -1,39 +0,0 @@
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,
))
}
@@ -1,57 +0,0 @@
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()),
}
}
}
}
@@ -1,109 +0,0 @@
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(())
}
@@ -1,42 +0,0 @@
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,62 +0,0 @@
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);
}
}
@@ -1,3 +0,0 @@
pub mod main_window_thread;
pub mod persist_storyteller_cookies_task;
pub mod persist_window_resize_task;
@@ -1,114 +0,0 @@
use crate::state::app_startup_time::AppStartupTime;
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::storyteller::storyteller_credential_manager::StorytellerCredentialManager;
use crate::threads::sora_session_login_thread::LOGIN_WINDOW_NAME;
use anyhow::anyhow;
use chrono::TimeDelta;
use errors::AnyhowResult;
use log::{debug, error, info};
use once_cell::sync::Lazy;
use reqwest::Url;
use std::fs;
use std::sync::LockResult;
use storyteller_client::credentials::storyteller_avt_cookie::StorytellerAvtCookie;
use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use storyteller_client::credentials::storyteller_session_cookie::StorytellerSessionCookie;
use tauri::{AppHandle, Manager, Webview, Window};
use tauri_plugin_http::reqwest_cookie_store::CookieStore;
use tauri_plugin_http::Http;
const MAIN_WEBVIEW_NAME: &str = "main";
const STORYTELLER_ROOT_COOKIE_URL_STR: &str = "https://api.storyteller.ai/";
static STORYTELLER_ROOT_COOKIE_URL: Lazy<Url> = Lazy::new(|| {
Url::parse(STORYTELLER_ROOT_COOKIE_URL_STR).expect("URL should parse")
});
const AVT_COOKIE_NAME : &str = "visitor";
const SESSION_COOKIE_NAME : &str = "session";
/// There's some kind of race condition that we need to wait for before inspecting cookies.
const RACE_CONDITION_WAIT_TIME: TimeDelta = TimeDelta::milliseconds(5000);
pub async fn persist_storyteller_cookies_task(
app: &AppHandle,
storyteller_credential_manager: &StorytellerCredentialManager,
app_startup_time: &AppStartupTime,
) -> AnyhowResult<()> {
//if app_startup_time.time_delta_since() < RACE_CONDITION_WAIT_TIME {
// // NB: There's an issue when the "main window thread" inquires about the
// // webview cookies shortly after app startup. When it attempts to dump the
// // cookies within a few milliseconds of app launch, it deadlocks and the
// // app never launches correctly. The entire webview goes blank and the app
// // freezes. This hack seems to fix the problem.
// return Ok(())
//}
let maybe_http = app.try_state::<Http>();
let http = match maybe_http {
None => {
error!("No HTTP plugin found");
return Err(anyhow!("No HTTP plugin found"));
}
Some(http) => http,
};
match http.cookies_jar.store.lock() {
Err(err) => {
error!("Failed to lock cookie jar: {:?}", err);
return Err(anyhow!("Failed to lock cookie jar"));
}
Ok(cookie_jar) => {
sync_tauri_credentials(&cookie_jar, storyteller_credential_manager)?;
}
}
Ok(())
}
fn sync_tauri_credentials(
cookie_store: &CookieStore,
storyteller_credential_manager: &StorytellerCredentialManager,
) -> AnyhowResult<()> {
let current_http_plugin_credentials = get_credentials_from_cookie_store(cookie_store)?;
let mut replace_credentials = true;
let maybe_old_credentials = storyteller_credential_manager.get_credentials()?;
if let Some(old_credentials) = maybe_old_credentials {
if old_credentials.equals(&current_http_plugin_credentials) {
replace_credentials = false;
}
}
if replace_credentials {
info!("Syncing ArtCraft credentials ...");
storyteller_credential_manager.set_credentials(&current_http_plugin_credentials)?;
// NB: tauri-plugin-http stores the credentials on disk, so we can defer to that for now.
//storyteller_credential_manager.persist_all_to_disk()?;
}
Ok(())
}
pub fn get_credentials_from_cookie_store(cookie_jar: &CookieStore) -> AnyhowResult<StorytellerCredentialSet> {
let mut avt_cookie = None;
let mut session_cookie = None;
for cookie in cookie_jar.iter_unexpired() {
if let Some(avt) = StorytellerAvtCookie::maybe_from_cookie(&cookie) {
avt_cookie = Some(avt);
} else if let Some(session) = StorytellerSessionCookie::maybe_from_cookie(&cookie) {
session_cookie = Some(session);
}
}
Ok(StorytellerCredentialSet::initialize(
avt_cookie,
session_cookie,
))
}
@@ -1,32 +0,0 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::main_window_size::MainWindowSize;
use errors::AnyhowResult;
use log::info;
use memory_store::clone_slot::CloneSlot;
use tauri::Window;
pub async fn persist_window_resize_task(
window: &Window,
app_data_root: &AppDataRoot,
window_size_slot: &CloneSlot<MainWindowSize>,
) -> AnyhowResult<()> {
let current_window_size = MainWindowSize::from_window(window)?;
let current_physical_size = window.inner_size()?;
let mut save_size_to_disk = true;
if let Ok(Some(old_size)) = window_size_slot.get_clone() {
if old_size.matches_physical_size(&current_physical_size) {
save_size_to_disk = false;
}
}
if save_size_to_disk {
info!("Saving window size configs to disk...");
current_window_size.persist_to_filesystem(app_data_root)?;
window_size_slot.set_clone(&current_window_size)?;
}
Ok(())
}
@@ -1,4 +1 @@
pub mod discord_presence_thread;
pub mod main_window_thread;
pub mod sora_session_login_thread;
pub mod sora_task_polling_thread;
pub mod sora_session_login_thread;
@@ -1,22 +1,22 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use std::fs;
use crate::state::app_dir::AppDataRoot;
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
use crate::utils::cookies::sora_webview_cookies::get_all_sora_cookies_as_string;
use crate::utils::sora::initialize_sora_jwt_bearer_token::initialize_sora_jwt_bearer_token;
use crate::utils::sora_webview_cookies::get_all_sora_cookies_as_string;
use anyhow::anyhow;
use chrono::{DateTime, NaiveDateTime, TimeDelta};
use errors::AnyhowResult;
use log::{error, info, 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 chrono::{DateTime, NaiveDateTime, TimeDelta};
use tauri::{AppHandle, Manager, Webview};
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};
pub const LOGIN_WINDOW_NAME: &str = "login_window";
@@ -1,142 +0,0 @@
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)
}
@@ -1 +0,0 @@
pub mod sora_webview_cookies;
@@ -1,8 +1,7 @@
pub mod clear_all_webview_cookies;
pub mod cookies;
pub mod get_url_file_extension;
pub mod image;
pub mod large_file_transfer;
pub mod object_store;
pub mod simple_http_download;
pub mod sora;
pub mod sora_webview_cookies;
pub mod sora;
@@ -1,4 +1,4 @@
use crate::state::data_dir::app_data_root::AppDataRoot;
use crate::state::app_dir::AppDataRoot;
use anyhow::anyhow;
use errors::AnyhowResult;
use log::info;
@@ -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 = "npx nx run editor2d:dev"
beforeDevCommand = "nx run editor2d:dev"
devUrl = "http://localhost:5741"
@@ -1,5 +0,0 @@
# 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 = "npx nx run editor3d:dev"
beforeDevCommand = "nx run editor3d:dev"
devUrl = "http://localhost:5173"
@@ -1,6 +0,0 @@
# 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"
@@ -4,7 +4,7 @@
"version": "0.0.1",
"identifier": "ai.artcraft.app",
"build": {
"frontendDist": "../../../",
"frontendDist": "../../../frontend/apps/editor2d/dist",
"devUrl": "http://localhost:5741",
"beforeDevCommand": "nx run editor2d:dev",
"beforeBuildCommand": "nx run editor2d:build"
@@ -20,11 +20,7 @@
}
],
"security": {
"csp": null,
"headers": {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp"
}
"csp": null
}
},
"bundle": {
@@ -1,37 +0,0 @@
{
"$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"
]
}
}
@@ -21,7 +21,6 @@ pub struct ImageRemixAutoRenewRequest<'a> {
}
/// 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
@@ -23,7 +23,6 @@ pub struct ImageUploadFromBytesAutoRenewRequest<'a> {
}
/// 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> {
@@ -13,7 +13,6 @@ pub struct ImageUploadFromFileAutoRenewRequest<'a, P: AsRef<Path>> {
}
/// 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> {
@@ -1,62 +0,0 @@
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)))
}
@@ -1,7 +1,6 @@
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;
@@ -47,5 +47,5 @@ pub async fn wait_for_image_gen_status(task_id: &String, credentials: &SoraCrede
}
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)
status_response.task_responses.iter().find(|task| task.id == task_id)
}
@@ -1,8 +1,7 @@
use crate::requests::image_gen::image_gen_status::TaskId;
#[derive(Debug, Clone)]
pub struct SoraImageGenResponse {
pub task_id: TaskId,
pub task_id: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -1,7 +1,6 @@
use crate::creds::credential_migration::CredentialMigrationRef;
use serde_derive::{Deserialize, Serialize};
use thiserror::Error;
use crate::requests::image_gen::image_gen_status::TaskId;
const SORA_IMAGE_GEN_URL: &str = "https://sora.com/backend/video_gen";
@@ -95,7 +94,7 @@ pub (crate) struct RawSoraImageGenRequest {
#[serde(rename_all = "snake_case")]
pub (crate) struct RawSoraResponse {
/// eg. "task_01jqsz9dsae9tvjygf1abrv3xf"
pub id: TaskId,
pub id: String,
/// not known
pub priority: Option<String>,
}
@@ -228,17 +227,3 @@ 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(())
}
}
@@ -1,28 +1,15 @@
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, Serialize};
use std::fmt::{Debug, Display};
use std::{fs::File, io::Write, path::Path};
use log::debug;
use serde_derive::Deserialize;
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,
@@ -55,7 +42,7 @@ pub struct VideoGenStatusResponse {
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub struct TaskResponse {
pub id: TaskId,
pub id: String,
pub user: String,
pub created_at: String,
pub status: String,
@@ -92,7 +79,7 @@ pub struct TaskResponse {
#[serde(rename_all = "snake_case")]
pub struct Generation {
pub id: String,
pub task_id: TaskId,
pub task_id: String,
pub created_at: String,
pub deleted_at: Option<String>,
pub url: String,
@@ -183,7 +170,7 @@ pub struct ModerationResult {
pub results_by_frame_index: serde_json::Value,
pub code: Option<String>,
pub is_output_rejection: bool,
pub task_id: TaskId,
pub task_id: String,
}
pub struct StatusRequest {
@@ -199,11 +186,10 @@ impl StatusRequest {
}
/// Gets the status of image generation tasks from Sora API
pub async fn get_image_gen_status(status_request: &StatusRequest, credentials: &SoraCredentials) -> Result<VideoGenStatusResponse, SoraError> {
pub async fn get_image_gen_status(status_request: &StatusRequest, credentials: &SoraCredentials) -> AnyhowResult<VideoGenStatusResponse> {
let client = reqwest::Client::new();
let mut url = reqwest::Url::parse(SORA_STATUS_URL)
.map_err(|err| anyhow!("error parsing URL: {:?}", err))?;
let mut url = reqwest::Url::parse(SORA_STATUS_URL)?;
// Add query parameters
if let Some(limit) = status_request.limit {
@@ -214,11 +200,7 @@ 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()?;
@@ -227,12 +209,10 @@ pub async fn get_image_gen_status(status_request: &StatusRequest, credentials: &
let status = response.status();
let response_body = &response.text().await?;
debug!("response_body: {}", response_body);
println!("response_body: {}", response_body);
if status != reqwest::StatusCode::OK {
// 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)));
return Err(anyhow::anyhow!("The status request failed; status = {:?}, message = {:?}", status, response_body));
}
let response: VideoGenStatusResponse = serde_json::from_str(response_body)?;
@@ -305,7 +285,7 @@ mod tests {
let task_id = response.task_responses[0].id.clone();
let task_response = wait_for_image_gen_status(
&task_id.0,
&task_id,
&creds,
Some(10)
).await?;
@@ -84,7 +84,7 @@ mod tests {
println!("task_id: {}", response.task_id);
assert!(response.task_id.0.starts_with("task_"));
assert!(response.task_id.starts_with("task_"));
Ok(())
}
}
@@ -68,7 +68,7 @@ mod tests {
println!("task_id: {}", response.task_id);
assert!(response.task_id.0.starts_with("task_"));
assert!(response.task_id.starts_with("task_"));
Ok(())
}
}
@@ -5,14 +5,12 @@ use std::io;
/// The single error type this crate should surface to all callers.
#[derive(Debug)]
pub enum SoraError {
/// We haven't received a bearer token yet
/// This is our own internal application state error, not something Sora returns.
/// We haven't received a bearer token yet (this is our application error)
NoBearerTokenAvailable,
/// We're sending too many tasks to Sora.
TooManyConcurrentTasks,
// TODO: Try to make concrete error types for these.
/// Unauthorized, cookie and/or bearer token expired. We'll need to ask for a refreshed login.
///
/// Example message, e.g. from the upload endpoint:
@@ -29,7 +27,7 @@ pub enum SoraError {
/// Another error occurred.
OtherBadStatus(anyhow::Error),
/// Reqwest Error, eg. connection failures.
/// Reqwest Error
ReqwestError(reqwest::Error),
/// std::io Error that arises from our end, eg. reading from the filesystem.
@@ -37,9 +35,6 @@ pub enum SoraError {
/// anyhow::Error arises from our end
AnyhowError(anyhow::Error),
/// serde_json::Error, likely from JSON deserialization schema mismatch.
JsonError(serde_json::Error),
}
impl Error for SoraError {}
@@ -68,9 +63,6 @@ impl Display for SoraError {
Self::AnyhowError(err) => {
write!(f, "Anyhow error: {}", err)
}
Self::JsonError(err) => {
write!(f, "serde_json error: {}", err)
}
}
}
}
@@ -92,9 +84,3 @@ impl From<anyhow::Error> for SoraError {
Self::AnyhowError(err)
}
}
impl From<serde_json::Error> for SoraError {
fn from(err: serde_json::Error) -> Self {
Self::JsonError(err)
}
}
@@ -20,13 +20,11 @@ workspace-build-acceleration = { workspace = true }
enums = { workspace = true }
enums_public = { workspace = true }
errors = { workspace = true }
idempotency = { workspace = true }
tokens = { workspace = true }
# External
anyhow = { workspace = true }
chrono = { workspace = true }
cookie = { workspace = true }
log = { workspace = true }
reqwest = { workspace = true, features = ["multipart", "gzip"] }
url = { workspace = true }
@@ -37,8 +35,7 @@ serde_derive = { workspace = true }
serde_json = { workspace = true }
# HTTP
backoff = { version = "0.4.0", features = ["futures"] }
uuid = { version = "1.8.0", features = ["v4"] } # Exponential backoff retry
backoff = { version = "0.4.0", features = ["futures"] } # Exponential backoff retry
[dev-dependencies]
tokio = { version = "1.38.0", features = ["macros"] }
@@ -1,6 +1,5 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::io;
use errors::AnyhowError;
#[derive(Debug)]
@@ -34,9 +33,6 @@ pub enum ApiError {
/// Uncategorized reqwest error.
OtherReqwestError(reqwest::Error),
/// An error doing file I/O (on our side)
IoError(io::Error),
/// Another type of error.
Other(AnyhowError),
@@ -59,8 +55,6 @@ impl Display for ApiError {
// Network errors
ApiError::Timeout(msg) => write!(f, "Timeout: {}", msg),
ApiError::NetworkError(msg) => write!(f, "Network error: {}", msg),
// I/O errors
ApiError::IoError(error) => write!(f, "IO error: {}", error),
// Other
ApiError::OtherReqwestError(error) => write!(f, "Reqwest error: {}", error),
ApiError::Other(error) => write!(f, "Other error: {}", error),
@@ -85,9 +79,3 @@ impl From<serde_json::Error> for ApiError {
ApiError::DeserializationError(error)
}
}
impl From<io::Error> for ApiError {
fn from(error: io::Error) -> Self {
ApiError::IoError(error)
}
}
@@ -1,3 +0,0 @@
pub mod storyteller_avt_cookie;
pub mod storyteller_credential_set;
pub mod storyteller_session_cookie;
@@ -1,44 +0,0 @@
use cookie::Cookie;
const AVT_COOKIE_NAME : &str = "visitor";
/// The "anonymous visitor token" cookie allows users to interact with
/// the service in a logged-out state with some continuity.
#[derive(Clone)]
pub struct StorytellerAvtCookie {
cookie: String,
}
impl StorytellerAvtCookie {
pub fn new(cookie: String) -> Self {
Self { cookie }
}
pub fn maybe_from_cookie(cookie: &Cookie) -> Option<Self> {
if cookie.name() == AVT_COOKIE_NAME {
Some(Self::new(cookie.value().to_string()))
} else {
None
}
}
pub fn as_str(&self) -> &str {
&self.cookie
}
pub fn as_bytes(&self) -> &[u8] {
self.cookie.as_bytes()
}
pub fn to_string(&self) -> String {
self.cookie.clone()
}
pub fn equals(&self, other: &Self) -> bool {
self.cookie == other.cookie
}
pub fn as_cookie_header(&self) -> String {
format!("{}={}", AVT_COOKIE_NAME, self.cookie)
}
}
@@ -1,136 +0,0 @@
use crate::credentials::storyteller_avt_cookie::StorytellerAvtCookie;
use crate::credentials::storyteller_session_cookie::StorytellerSessionCookie;
#[derive(Clone)]
pub struct StorytellerCredentialSet {
pub avt: Option<StorytellerAvtCookie>,
pub session: Option<StorytellerSessionCookie>,
}
impl StorytellerCredentialSet {
pub fn empty() -> Self {
Self {
avt: None,
session: None,
}
}
pub fn initialize(
avt: Option<StorytellerAvtCookie>,
session: Option<StorytellerSessionCookie>,
) -> Self {
Self {
avt,
session,
}
}
pub fn initialize_with_just_cookie(session: StorytellerSessionCookie) -> Self {
Self {
avt: None,
session: Some(session),
}
}
pub fn initialize_with_just_avt(avt: StorytellerAvtCookie) -> Self {
Self {
avt: Some(avt),
session: None,
}
}
pub fn is_empty(&self) -> bool {
self.avt.is_none() && self.session.is_none()
}
pub fn equals(&self, other: &Self) -> bool {
match (&self.avt, &other.avt) {
(None, None) => {} // Fallthrough
(Some(avt), Some(other_avt)) => {
if !avt.equals(other_avt) {
return false;
}
}
_ => return false,
}
match (&self.session, &other.session) {
(None, None) => {} // Fallthrough
(Some(session), Some(other_session)) => {
if !session.equals(other_session) {
return false;
}
}
_ => return false,
}
true
}
pub fn maybe_as_cookie_header(&self) -> Option<String> {
let mut cookies = Vec::new();
if let Some(avt) = &self.avt {
cookies.push(avt.as_cookie_header());
}
if let Some(session) = &self.session {
cookies.push(session.as_cookie_header());
}
if cookies.is_empty() {
None
} else {
Some(cookies.join("; "))
}
}
}
#[cfg(test)]
mod tests {
use crate::credentials::storyteller_avt_cookie::StorytellerAvtCookie;
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::credentials::storyteller_session_cookie::StorytellerSessionCookie;
mod cookies_header {
use super::*;
#[test]
fn no_cookie() {
let creds = StorytellerCredentialSet::empty();
let header = creds.maybe_as_cookie_header();
assert_eq!(header, None);
}
#[test]
fn avt_cookies_header() {
let creds = StorytellerCredentialSet::initialize_with_just_avt(
StorytellerAvtCookie::new("bob".to_string()),
);
let header = creds.maybe_as_cookie_header();
assert_eq!(header, Some("visitor=bob".to_string()));
}
#[test]
fn session_cookies_header() {
let creds = StorytellerCredentialSet::initialize_with_just_cookie(
StorytellerSessionCookie::new("bob".to_string()),
);
let header = creds.maybe_as_cookie_header();
assert_eq!(header, Some("session=bob".to_string()));
}
#[test]
fn both_cookies_header() {
let creds = StorytellerCredentialSet::initialize(
Some(StorytellerAvtCookie::new("bob".to_string())),
Some(StorytellerSessionCookie::new("alice".to_string())),
);
let header = creds.maybe_as_cookie_header();
assert_eq!(header, Some("visitor=bob; session=alice".to_string()));
}
}
}
@@ -1,43 +0,0 @@
use cookie::Cookie;
const SESSION_COOKIE_NAME : &str = "session";
/// The primary FakeYou / Storyteller / Artcraft backend session identifier.
#[derive(Clone)]
pub struct StorytellerSessionCookie {
cookie: String,
}
impl StorytellerSessionCookie {
pub fn new(cookie: String) -> Self {
Self { cookie }
}
pub fn maybe_from_cookie(cookie: &Cookie) -> Option<Self> {
if cookie.name() == SESSION_COOKIE_NAME {
Some(Self::new(cookie.value().to_string()))
} else {
None
}
}
pub fn as_str(&self) -> &str {
&self.cookie
}
pub fn as_bytes(&self) -> &[u8] {
self.cookie.as_bytes()
}
pub fn to_string(&self) -> String {
self.cookie.clone()
}
pub fn equals(&self, other: &Self) -> bool {
self.cookie == other.cookie
}
pub fn as_cookie_header(&self) -> String {
format!("{}={}", SESSION_COOKIE_NAME, self.cookie)
}
}
@@ -1,7 +1,4 @@
//! An API client for Storyteller / FakeYou
pub mod api_error;
pub mod credentials;
pub mod media_files;
pub mod shared_response_types;
pub mod utils;
mod api_error;
@@ -1,12 +1,6 @@
use crate::api_error::ApiError;
use crate::shared_response_types::media_file_cover_image_details::MediaFileCoverImageDetails;
use crate::shared_response_types::media_links::MediaLinks;
use crate::shared_response_types::simple_entity_stats::SimpleEntityStats;
use crate::shared_response_types::user_details_light::UserDetailsLight;
use crate::utils::api_host::ApiHost;
use crate::utils::filter_bad_response::filter_bad_response;
use crate::utils::http_get_anonymous::http_get_anonymous;
use chrono::{DateTime, Utc};
use log::debug;
use serde_derive::{Deserialize};
use enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType;
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory;
@@ -15,12 +9,18 @@ use enums::by_table::model_weights::weights_category::WeightsCategory;
use enums::common::visibility::Visibility;
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
use enums_public::by_table::model_weights::public_weights_types::PublicWeightsType;
use log::debug;
use serde_derive::Deserialize;
use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::model_weights::ModelWeightToken;
use tokens::tokens::prompts::PromptToken;
use crate::api_error::ApiError;
use crate::shared_response_types::media_file_cover_image_details::MediaFileCoverImageDetails;
use crate::shared_response_types::media_links::MediaLinks;
use crate::shared_response_types::simple_entity_stats::SimpleEntityStats;
use crate::shared_response_types::user_details_light::UserDetailsLight;
use crate::utils::api_host::ApiHost;
use crate::utils::filter_bad_response::filter_bad_response;
use crate::utils::http_get_anonymous::http_get_anonymous;
/// Get details about a media file from our backend
pub async fn get_media_file(api_host: &ApiHost, media_file_token: &MediaFileToken) -> Result<GetMediaFileSuccessResponse, ApiError> {
@@ -205,9 +205,9 @@ pub struct GetMediaFileModelInfo {
#[cfg(test)]
mod tests {
use tokens::tokens::media_files::MediaFileToken;
use crate::media_files::get_media_file::get_media_file;
use crate::utils::api_host::ApiHost;
use tokens::tokens::media_files::MediaFileToken;
#[tokio::test]
#[ignore] // Don't run in CI. Requires valid cookie
@@ -1,2 +1 @@
pub mod get_media_file;
pub mod upload_image_media_file_from_file;
pub mod get_media_file;
@@ -1,90 +0,0 @@
use crate::api_error::ApiError;
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::shared_response_types::media_file_cover_image_details::MediaFileCoverImageDetails;
use crate::shared_response_types::media_links::MediaLinks;
use crate::shared_response_types::simple_entity_stats::SimpleEntityStats;
use crate::shared_response_types::user_details_light::UserDetailsLight;
use crate::utils::api_host::ApiHost;
use crate::utils::constants::{APPLICATION_JSON, USER_AGENT};
use crate::utils::filter_bad_response::filter_bad_response;
use crate::utils::http_get_anonymous::http_get_anonymous;
use chrono::{DateTime, Utc};
use enums::by_table::media_files::media_file_animation_type::MediaFileAnimationType;
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCategory;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::by_table::model_weights::weights_category::WeightsCategory;
use enums::common::visibility::Visibility;
use enums::no_table::style_transfer::style_transfer_name::StyleTransferName;
use enums_public::by_table::model_weights::public_weights_types::PublicWeightsType;
use idempotency::uuid::generate_random_uuid;
use log::debug;
use reqwest::multipart::Form;
use reqwest::Client;
use serde_derive::{Deserialize, Serialize};
use std::fs::File;
use std::io::Read;
use std::path::Path;
use tokens::tokens::batch_generations::BatchGenerationToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::model_weights::ModelWeightToken;
use tokens::tokens::prompts::PromptToken;
use uuid::uuid;
/// Upload an image media file from a file.
pub async fn upload_image_media_file_from_file<P: AsRef<Path>>(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
path: P,
) -> Result<UploadImageMediaFileSuccessResponse, ApiError> {
let url = get_route(api_host);
debug!("Requesting {:?}", &url);
//let mut file = File::open(path)?;
let client = Client::builder()
.gzip(true)
.build()?;
let form = Form::new()
.text("uuid_idempotency_token", generate_random_uuid())
.file("file", path)
.await?;
let mut request_builder = client.post(url)
.header("User-Agent", USER_AGENT)
.header("Accept", APPLICATION_JSON);
if let Some(creds) = maybe_creds {
if let Some(header) = &creds.maybe_as_cookie_header() {
request_builder = request_builder.header("Cookie", header);
}
}
let response = request_builder
.multipart(form)
.send()
.await?;
let response = filter_bad_response(response).await?;
let response_body = &response.text().await?;
let media_file = serde_json::from_str(&response_body)?;
Ok(media_file)
}
fn get_route(api_host: &ApiHost) -> String {
let api_hostname = api_host.to_api_hostname();
format!("https://{}/v1/media_files/upload/image", api_hostname)
}
// TODO(bt,2025-04-22): Share API definitions between client and server in common crate.
#[derive(Deserialize, Debug)]
pub struct UploadImageMediaFileSuccessResponse {
pub success: bool,
pub media_file_token: MediaFileToken,
}
@@ -1,4 +0,0 @@
pub const USER_AGENT: &str = "storyteller-client/1.0";
pub const APPLICATION_JSON : &str = "application/json";
@@ -1,5 +1,4 @@
pub mod api_host;
pub mod constants;
pub mod filter_bad_response;
pub mod http_get_anonymous;
pub mod status_codes;
@@ -1,26 +0,0 @@
[package]
name = "memory_store"
edition = "2018"
version = "0.0.1"
authors = [
"Brandon Thomas <bt@brand.io>",
"Brandon Thomas <echelon@gmail.com>",
]
publish = false
[lib]
name = "memory_store"
path = "src/lib.rs"
[package.metadata.cargo-udeps.ignore]
normal = ["workspace-build-acceleration"]
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration = { workspace = true }
# Internal
errors = { workspace = true }
# External
anyhow = { workspace = true }
@@ -1,54 +0,0 @@
use anyhow::anyhow;
use errors::AnyhowResult;
use std::sync::{Arc, RwLock};
/// Store a threadsafe value that can be passed around and cloned.
/// The object is interior mutable so it can be changed without mut/&mut references.
/// This is useful for storing a value that may or may not be set between threads.
///
/// Unlike `CloneSlot`, this always holds an object.
#[derive(Clone)]
pub struct CloneCell<T: Clone> {
pub store: Arc<RwLock<T>>,
}
impl <T: Clone> CloneCell<T> {
pub fn with_clone(object: &T) -> Self {
Self {
store: Arc::new(RwLock::new(object.clone())),
}
}
pub fn with_owned(object: T) -> Self {
Self {
store: Arc::new(RwLock::new(object)),
}
}
pub fn get_clone(&self) -> AnyhowResult<T> {
match self.store.read() {
Err(err) => Err(anyhow!("Failed to acquire read lock: {:?}", err)),
Ok(store) => Ok(store.clone()),
}
}
pub fn set_clone(&self, value: &T) -> AnyhowResult<()> {
match self.store.write() {
Err(err) => Err(anyhow!("Failed to acquire write lock: {:?}", err)),
Ok(mut store) => {
*store = value.clone();
Ok(())
}
}
}
pub fn set_owned(&self, value: T) -> AnyhowResult<()> {
match self.store.write() {
Err(err) => Err(anyhow!("Failed to acquire write lock: {:?}", err)),
Ok(mut store) => {
*store = value;
Ok(())
}
}
}
}
@@ -1,77 +0,0 @@
use anyhow::anyhow;
use errors::AnyhowResult;
use std::sync::{Arc, RwLock};
/// Store a threadsafe value that can be passed around and cloned.
/// The object is interior mutable so it can be changed without mut/&mut references.
/// This is useful for storing a value that may or may not be set between threads.
///
/// Unlike `CloneCell`, this holds an optional object.
#[derive(Clone)]
pub struct CloneSlot<T: Clone> {
pub store: Arc<RwLock<Option<T>>>,
}
impl <T: Clone> CloneSlot<T> {
pub fn empty() -> Self {
Self {
store: Arc::new(RwLock::new(None)),
}
}
pub fn with_clone(object: &T) -> Self {
Self {
store: Arc::new(RwLock::new(Some(object.clone()))),
}
}
pub fn get_clone(&self) -> AnyhowResult<Option<T>> {
match self.store.read() {
Err(err) => Err(anyhow!("Failed to acquire read lock: {:?}", err)),
Ok(store) => Ok(store.clone()),
}
}
pub fn set_clone(&self, value: &T) -> AnyhowResult<()> {
match self.store.write() {
Err(err) => Err(anyhow!("Failed to acquire write lock: {:?}", err)),
Ok(mut store) => {
*store = Some(value.clone());
Ok(())
}
}
}
pub fn clear(&self) -> AnyhowResult<()> {
match self.store.write() {
Err(err) => Err(anyhow!("Failed to acquire write lock: {:?}", err)),
Ok(mut store) => {
*store = None;
Ok(())
}
}
}
pub fn take(&self) -> AnyhowResult<Option<T>> {
match self.store.write() {
Err(err) => Err(anyhow!("Failed to acquire write lock: {:?}", err)),
Ok(mut store) => {
Ok(store.take())
}
}
}
pub fn is_some(&self) -> AnyhowResult<bool> {
match self.store.read() {
Err(err) => Err(anyhow!("Failed to acquire read lock: {:?}", err)),
Ok(store) => Ok(store.is_some()),
}
}
pub fn is_none(&self) -> AnyhowResult<bool> {
match self.store.read() {
Err(err) => Err(anyhow!("Failed to acquire read lock: {:?}", err)),
Ok(store) => Ok(store.is_none()),
}
}
}
@@ -1,2 +0,0 @@
pub mod clone_cell;
pub mod clone_slot;
+6 -7
View File
@@ -1,6 +1,6 @@
[package]
name = "images"
edition = "2024"
edition = "2018"
version = "0.0.1"
authors = [
"Brandon Thomas <bt@brand.io>",
@@ -18,16 +18,15 @@ path = "src/lib.rs"
normal = ["workspace-build-acceleration"]
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration = { workspace = true }
# Internal
errors = { workspace = true}
mimetypes = { workspace = true }
errors = { path = "../../errors" }
# Workspace hack (faster compile times)
workspace-build-acceleration = { version = "0.1", path = "../../../workspace-build-acceleration" }
# External
image = "0.25.6"
image = "0.25.1"
[dev-dependencies]
anyhow = { workspace = true }
testing = { path = "../../testing" }
@@ -1,100 +0,0 @@
use crate::image_info::image_info_error::ImageInfoError;
use image::{GenericImageView, ImageReader};
use mimetypes::mimetype_info::file_extension::FileExtension;
use mimetypes::mimetype_info::mimetype_info::MimetypeInfo;
use std::io::Cursor;
use std::path::Path;
#[derive(Clone, Debug)]
pub struct ImageInfo {
/// The mimetype we determined
mimetype_info: MimetypeInfo,
/// The width of the image in pixels
width: u32,
/// The height of the image in pixels
height: u32,
}
impl ImageInfo {
/// Note: This has to do a full file read, and we throw the parsed image on the floor.
pub fn decode_image_from_bytes(bytes: &[u8]) -> Result<Self, ImageInfoError> {
let image = ImageReader::new(Cursor::new(bytes)).with_guessed_format()?.decode()?;
let (width, height) = image.dimensions();
Ok(ImageInfo {
mimetype_info: MimetypeInfo::get_for_bytes(bytes)
.ok_or_else(|| ImageInfoError::CouldNotDetermineMimetype)?,
width,
height,
})
}
/// Note: This has to do a full file read, and we throw the parsed image on the floor.
pub fn decode_image_from_path<P: AsRef<Path>>(path: P) -> Result<Self, ImageInfoError> {
let image = ImageReader::open(&path)?.decode()?;
let (width, height) = image.dimensions();
Ok(ImageInfo {
mimetype_info: MimetypeInfo::get_for_path(path)?
.ok_or_else(|| ImageInfoError::CouldNotDetermineMimetype)?,
width,
height,
})
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn dimensions(&self) -> (u32, u32) {
(self.width, self.height)
}
pub fn mime_type(&self) -> &str {
self.mimetype_info.mime_type()
}
pub fn file_extension(&self) -> Option<FileExtension> {
self.mimetype_info.file_extension()
}
}
#[cfg(test)]
mod tests {
use crate::image_info::image_info::ImageInfo;
use mimetypes::mimetype_info::file_extension::FileExtension;
use testing::test_file_path::test_file_path;
#[test]
fn jpg_file() -> anyhow::Result<()> {
let path = test_file_path("test_data/image/mochi.jpg")?;
let info = ImageInfo::decode_image_from_path(path)?;
assert_eq!(info.width(), 753);
assert_eq!(info.height(), 1000);
assert_eq!(info.mime_type(), "image/jpeg");
let ext = info.file_extension().expect("should have extension");
assert_eq!(ext, FileExtension::Jpg);
assert_eq!(ext.extension_with_period(), ".jpg");
assert_eq!(ext.extension_without_period(), "jpg");
Ok(())
}
#[test]
fn jpg_bytes() -> anyhow::Result<()> {
let path = test_file_path("test_data/image/juno.jpg")?;
let bytes = std::fs::read(path)?;
let info = ImageInfo::decode_image_from_bytes(&bytes)?;
assert_eq!(info.width(), 1000);
assert_eq!(info.height(), 750);
assert_eq!(info.mime_type(), "image/jpeg");
let ext = info.file_extension().expect("should have extension");
assert_eq!(ext, FileExtension::Jpg);
assert_eq!(ext.extension_with_period(), ".jpg");
assert_eq!(ext.extension_without_period(), "jpg");
Ok(())
}
}
@@ -1,39 +0,0 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::io;
#[derive(Debug)]
pub enum ImageInfoError {
CouldNotDetermineMimetype,
ImageError(image::ImageError),
IoError(io::Error),
}
impl Error for ImageInfoError {}
impl Display for ImageInfoError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ImageInfoError::CouldNotDetermineMimetype => {
write!(f, "ImageInfoError::CouldNotDetermineMimetype")
}
ImageInfoError::ImageError(err) => {
write!(f, "ImageInfoError::ImageError: {}", err)
}
ImageInfoError::IoError(err) => {
write!(f, "ImageInfoError::IoError: {}", err)
}
}
}
}
impl From<image::ImageError> for ImageInfoError {
fn from(err: image::ImageError) -> Self {
ImageInfoError::ImageError(err)
}
}
impl From<io::Error> for ImageInfoError {
fn from(err: io::Error) -> Self {
ImageInfoError::IoError(err)
}
}
@@ -1,2 +0,0 @@
pub mod image_info;
pub mod image_info_error;
-1
View File
@@ -23,7 +23,6 @@
// Re-export image to everyone importing this library
pub use image;
pub mod image_info;
pub mod resize_image_file_preserving_aspect;
pub mod resize_preserving_aspect;
+5 -4
View File
@@ -16,14 +16,15 @@ path = "src/lib.rs"
normal = ["workspace-build-acceleration"]
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration = { workspace = true }
# Internal
errors = { workspace = true }
errors = { path = "../../errors" }
# Workspace hack (faster compile times)
workspace-build-acceleration = { version = "0.1", path = "../../../workspace-build-acceleration" }
# External
log = { workspace = true }
log = "0.4.14"
# Powerful audio decoder and demuxer
#symphonia = { version = "0.5.2", features = ["aac", "alac", "flac", "isomp4", "mkv", "mp3", "ogg", "vorbis", "wav"] }
+5 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "mimetypes"
edition = "2024"
edition = "2018"
version = "0.0.1"
authors = [
"Brandon Thomas <bt@brand.io>",
@@ -18,11 +18,11 @@ normal = ["workspace-build-acceleration"]
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration = { workspace = true }
workspace-build-acceleration = { version = "0.1", path = "../../../workspace-build-acceleration" }
# External
infer = "0.19.0" # Mimetype detection
infer = "0.16.0" # Mimetype detection
[dev-dependencies]
testing = { path = "../../testing" }
anyhow = { workspace = true }
# None yet
-1
View File
@@ -21,5 +21,4 @@
pub mod mimetype_for_bytes;
pub mod mimetype_for_file;
pub mod mimetype_info;
pub mod mimetype_to_extension;
@@ -1,68 +0,0 @@
/// This is a constrained set of file extensions that:
/// 1. we care about
/// 2. we can detect with magic bytes
/// 3. are not text formats / are binary formats
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub enum FileExtension {
Aac,
Flac,
Gif,
Jpg,
Mov,
Mp3,
Mp4,
Ogg,
Opus,
Png,
Wav,
Webm,
Webp,
}
impl FileExtension {
pub fn from_mimetype(mimetype: &str) -> Option<Self> {
match mimetype {
"audio/aac" => Some(FileExtension::Aac),
"audio/flac" => Some(FileExtension::Flac),
"image/gif" => Some(FileExtension::Gif),
"image/jpeg" => Some(FileExtension::Jpg),
"video/quicktime" => Some(FileExtension::Mov),
"audio/mpeg" => Some(FileExtension::Mp3),
"video/mp4" => Some(FileExtension::Mp4),
"audio/ogg" => Some(FileExtension::Ogg),
"audio/opus" => Some(FileExtension::Opus),
"image/png" => Some(FileExtension::Png),
"audio/wav" => Some(FileExtension::Wav),
"video/webm" => Some(FileExtension::Webm),
"image/webp" => Some(FileExtension::Webp),
_ => None,
}
}
pub fn extension_with_period(&self) -> &'static str {
self.with_and_without_period().0
}
pub fn extension_without_period(&self) -> &'static str {
self.with_and_without_period().1
}
fn with_and_without_period(&self) -> (&'static str, &'static str) {
match self {
FileExtension::Aac => (".aac", "aac"),
FileExtension::Flac => (".flac", "flac"),
FileExtension::Gif => (".gif", "gif"),
FileExtension::Jpg => (".jpg", "jpg"),
FileExtension::Mov => (".mov", "mov"),
FileExtension::Mp3 => (".mp3", "mp3"),
FileExtension::Mp4 => (".mp4", "mp4"),
FileExtension::Ogg => (".ogg", "ogg"),
FileExtension::Opus => (".opus", "opus"),
FileExtension::Png => (".png", "png"),
FileExtension::Wav => (".wav", "wav"),
FileExtension::Webm => (".webm", "webm"),
FileExtension::Webp => (".webp", "webp"),
}
}
}
@@ -1,80 +0,0 @@
use crate::mimetype_info::file_extension::FileExtension;
use infer::Type;
use std::io;
use std::path::Path;
#[derive(Clone, Debug)]
pub struct MimetypeInfo {
/// The mimetype we determined
mime_type: String,
/// The extension for the mimetype, if we could determine it.
file_extension: Option<FileExtension>,
}
impl MimetypeInfo {
pub fn get_for_bytes(bytes: &[u8]) -> Option<Self> {
infer::get(bytes)
.map(|t| Self::for_type(t))
}
pub fn get_for_path<P: AsRef<Path>>(path: P) -> io::Result<Option<Self>> {
infer::get_from_path(path)
.map(|maybe_type| maybe_type.map(|t| Self::for_type(t)))
}
fn for_type(r#type: Type) -> Self {
let mime_type = r#type.mime_type().to_string();
MimetypeInfo {
file_extension: FileExtension::from_mimetype(&mime_type),
mime_type,
}
}
pub fn mime_type(&self) -> &str {
&self.mime_type
}
pub fn file_extension(&self) -> Option<FileExtension> {
self.file_extension
}
}
#[cfg(test)]
mod tests {
use crate::mimetype_info::file_extension::FileExtension;
use crate::mimetype_info::mimetype_info::MimetypeInfo;
mod for_bytes {
use super::*;
#[test]
fn png() {
// NB: From "Hanashi Mask.png"
let bytes: Vec<u8> = vec![137, 80, 78, 71, 13, 10, 26, 10];
let t = MimetypeInfo::get_for_bytes(&bytes).expect("should detect bytes");
assert_eq!(t.mime_type(), "image/png");
let ext = t.file_extension.expect("should have extension");
assert_eq!(ext, FileExtension::Png);
assert_eq!(ext.extension_with_period(), ".png");
assert_eq!(ext.extension_without_period(), "png");
}
}
mod for_files {
use testing::test_file_path::test_file_path;
use super::*;
#[test]
fn mp3() -> anyhow::Result<()> {
let path = test_file_path("test_data/audio/mp3/super_mario_rpg_beware_the_forests_mushrooms.mp3")?;
let t = MimetypeInfo::get_for_path(path)?.expect("should detect type");
assert_eq!(t.mime_type(), "audio/mpeg");
let ext = t.file_extension.expect("should have extension");
assert_eq!(ext, FileExtension::Mp3);
assert_eq!(ext.extension_with_period(), ".mp3");
assert_eq!(ext.extension_without_period(), "mp3");
Ok(())
}
}
}
@@ -1,2 +0,0 @@
pub mod file_extension;
pub mod mimetype_info;
+3 -3
View File
@@ -18,14 +18,14 @@ normal = ["workspace-build-acceleration"]
[dependencies]
# Workspace hack (faster compile times)
workspace-build-acceleration = { workspace = true }
workspace-build-acceleration = { version = "0.1", path = "../../../workspace-build-acceleration" }
# Internal
errors = { workspace = true }
errors = { path = "../../errors" }
# External
ffprobe = "0.4.0"
log = { workspace = true }
log = "0.4.20"
# TODO(bt,2024-08-08): We might drop `mp4` crate because it crashes on reading
# some videos with the message "avcc not found"
mp4 = "0.14.0"
@@ -1,57 +0,0 @@
use std::time::Duration;
use anyhow::anyhow;
use sqlx::MySqlPool;
use enums::by_table::generic_inference_jobs::inference_result_type::InferenceResultType;
use errors::AnyhowResult;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use crate::queries::generic_inference::job::list_available_generic_inference_jobs::AvailableInferenceJob;
pub async fn mark_generic_inference_job_successfully_done_by_token(
pool: &MySqlPool,
token: &InferenceJobToken,
maybe_entity_type: Option<InferenceResultType>,
maybe_entity_token: Option<&str>,
total_job_duration: Option<Duration>,
inference_duration: Option<Duration>,
) -> AnyhowResult<()>
{
// NB: MySql's unsigned int (32 bits) can store integers up to 4,294,967,295.
// Given milliseconds, this is ~49.71 days, which should be plenty for us.
let truncated_total_job_execution_millis = total_job_duration
.map(|duration| duration.as_millis() as u32)
.unwrap_or(0);
let truncated_inference_execution_millis = inference_duration
.map(|duration| duration.as_millis() as u32)
.unwrap_or(0);
let query_result = sqlx::query!(
r#"
UPDATE generic_inference_jobs
SET
status = "complete_success",
on_success_result_entity_type = ?,
on_success_result_entity_token = ?,
failure_reason = NULL,
internal_debugging_failure_reason = NULL,
success_execution_millis = ?,
success_inference_execution_millis = ?,
retry_at = NULL,
successfully_completed_at = NOW()
WHERE token = ?
"#,
maybe_entity_type,
maybe_entity_token,
truncated_total_job_execution_millis,
truncated_inference_execution_millis,
token.as_str()
)
.execute(pool)
.await;
match query_result {
Err(err) => Err(anyhow!("error with query: {:?}", err)),
Ok(_r) => Ok(()),
}
}
@@ -7,5 +7,4 @@ pub mod job_status;
pub mod kill_generic_inference_jobs;
pub mod kill_jobs_in_development;
pub mod list_session_jobs;
pub mod mark_generic_inference_job_cancelled_by_user;
pub mod mark_generic_inference_job_successfully_done_by_token;
pub mod mark_generic_inference_job_cancelled_by_user;
@@ -1,269 +0,0 @@
use crate::queries::media_files::create::generic_insert::insert_media_file_generic::{insert_media_file_generic, InsertArgs};
use anyhow::anyhow;
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
use enums::by_table::generic_synthetic_ids::id_category::IdCategory;
use enums::by_table::media_files::media_file_class::MediaFileClass;
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
use enums::by_table::media_files::media_file_type::MediaFileType;
use enums::common::visibility::Visibility;
use errors::AnyhowResult;
use sqlx::MySqlPool;
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::users::UserToken;
use crate::queries::media_files::create::insert_builder::media_file_insert_builder_error::MediaFileInsertBuilderError;
/// Builder for inserting a media files into the database.
/// Resembles an ORM, but can still use statically type checked queries (which is probably
/// really important since we don't have tests on database functionality yet).
#[derive(Clone)]
pub struct MediaFileInsertBuilder {
// TODO: Allow specifying the token externally
// token: MediaFileToken,
// Creator info
maybe_creator_user_token: Option<UserToken>,
maybe_creator_anonymous_visitor_token: Option<AnonymousVisitorTrackingToken>,
creator_ip_address: Option<String>, // NB: Non-nullable field
creator_set_visibility: Visibility, // NB: Non-nullable field
// Important database indices
media_file_class: MediaFileClass, // NB: Non-nullable field
media_file_type: Option<MediaFileType>, // NB: Non-nullable field
is_user_upload: bool,
is_intermediate_system_file: bool,
// Product and other origination information
origin_category: Option<MediaFileOriginCategory>, // NB: Non-nullable field
origin_product_category: MediaFileOriginProductCategory, // NB: Non-nullable field
// maybe_origin_model_type: Option<MediaFileOriginModelType>,
// maybe_origin_model_token: Option<&'a ModelWeightToken>,
// maybe_origin_filename: Option<String>,
// Media info
maybe_mime_type: Option<String>,
file_size_bytes: u64, // NB: Non-nullable field
// maybe_duration_millis: Option<u64>,
// maybe_audio_encoding: Option<&'a str>,
// maybe_video_encoding: Option<&'a str>,
maybe_frame_width: Option<u32>,
maybe_frame_height: Option<u32>,
checksum_sha2: Option<String>, // NB: Non-nullable field
// // User text information
// maybe_title: Option<&'a str>,
// maybe_text_transcript: Option<&'a str>,
// // If generated from a scene, this is the scene media file token.
// maybe_scene_source_media_file_token: Option<&'a MediaFileToken>,
// // If additional prompt details are stored, this is the prompt token.
// maybe_prompt_token: Option<&'a PromptToken>,
// // If batch generated, this is the batch token.
// maybe_batch_token: Option<&'a BatchGenerationToken>,
// Storage details
public_bucket_directory_hash: Option<MediaFileBucketPath>, // NB: Non-nullable field(s)
// Remaining fields...
}
impl MediaFileInsertBuilder {
//pub fn generate_token() -> Self {
// Self::with_token(&MediaFileToken::generate())
//}
pub fn new() -> Self {
MediaFileInsertBuilder {
//token: token.clone(),
maybe_creator_user_token: None,
maybe_creator_anonymous_visitor_token: None,
creator_ip_address: None,
creator_set_visibility: Visibility::Public,
media_file_class: MediaFileClass::Unknown,
media_file_type: None,
is_user_upload: false,
is_intermediate_system_file: false,
origin_category: None,
origin_product_category: MediaFileOriginProductCategory::Unknown,
maybe_mime_type: None,
file_size_bytes: 0,
maybe_frame_width: None,
maybe_frame_height: None,
checksum_sha2: None,
public_bucket_directory_hash: None,
}
}
pub fn creator_user(mut self, user_token: &UserToken) -> Self {
self.maybe_creator_user_token = Some(user_token.clone());
self
}
pub fn maybe_creator_user(mut self, maybe_user: Option<&UserToken>) -> Self {
self.maybe_creator_user_token = maybe_user.map(|token| token.clone());
self
}
pub fn creator_anonymous_visitor(mut self, avt: &AnonymousVisitorTrackingToken) -> Self {
self.maybe_creator_anonymous_visitor_token = Some(avt.clone());
self
}
pub fn maybe_creator_anonymous_visitor(mut self, maybe_avt: Option<&AnonymousVisitorTrackingToken>) -> Self {
self.maybe_creator_anonymous_visitor_token = maybe_avt.map(|token| token.clone());
self
}
pub fn creator_ip_address(mut self, ip_address: &str) -> Self {
self.creator_ip_address = Some(ip_address.to_string());
self
}
pub fn creator_set_visibility(mut self, visibility: Visibility) -> Self {
self.creator_set_visibility = visibility;
self
}
pub fn media_file_class(mut self, media_file_class: MediaFileClass) -> Self {
self.media_file_class = media_file_class;
self
}
pub fn media_file_type(mut self, media_file_type: MediaFileType) -> Self {
self.media_file_type = Some(media_file_type);
self
}
#[allow(clippy::wrong_self_convention)]
pub fn is_user_upload(mut self, is_user_upload: bool) -> Self {
self.is_user_upload = is_user_upload;
self
}
#[allow(clippy::wrong_self_convention)]
pub fn is_intermediate_system_file(mut self, is_intermediate_system_file: bool) -> Self {
self.is_intermediate_system_file = is_intermediate_system_file;
self
}
pub fn media_file_origin_category(mut self, origin_category: MediaFileOriginCategory) -> Self {
self.origin_category = Some(origin_category);
self
}
pub fn media_file_origin_product_category(mut self, origin_category: MediaFileOriginProductCategory) -> Self {
self.origin_product_category = origin_category;
self
}
// TODO: Method for maybe_origin_model_type
// TODO: Method for maybe_origin_model_token
// TODO: Method for maybe_origin_filename
pub fn mime_type(mut self, mime_type: &str) -> Self {
self.maybe_mime_type = Some(mime_type.to_string());
self
}
pub fn file_size_bytes(mut self, file_size_bytes: u64) -> Self {
self.file_size_bytes = file_size_bytes;
self
}
// TODO: maybe_duration_millis
// TODO: maybe_audio_encoding
// TODO: maybe_video_encoding
pub fn frame_width(mut self, frame_width: u32) -> Self {
self.maybe_frame_width = Some(frame_width);
self
}
pub fn frame_height(mut self, frame_height: u32) -> Self {
self.maybe_frame_height = Some(frame_height);
self
}
pub fn checksum_sha2(mut self, checksum_sha2: &str) -> Self {
self.checksum_sha2 = Some(checksum_sha2.to_string());
self
}
// TODO: maybe_title
// TODO: maybe_text_transcript
// TODO: maybe_scene_source_media_file_token
// TODO: maybe_prompt_token
// TODO: maybe_batch_token
pub fn public_bucket_directory_hash(mut self, public_bucket_directory_hash: &MediaFileBucketPath) -> Self {
self.public_bucket_directory_hash = Some(public_bucket_directory_hash.clone());
self
}
// TODO(bt,2025-04-26): Other connector options.
pub async fn insert_pool(self, mysql_pool: &MySqlPool) -> Result<MediaFileToken, MediaFileInsertBuilderError> {
let media_file_type = self.media_file_type
.ok_or_else(|| MediaFileInsertBuilderError::MissingRequiredField(
"Media file type is required".to_string()))?;
let origin_category = self.origin_category
.ok_or_else(|| MediaFileInsertBuilderError::MissingRequiredField(
"Origin category is required".to_string()))?;
let checksum_sha2 = self.checksum_sha2
.ok_or_else(|| MediaFileInsertBuilderError::MissingRequiredField(
"Checksum SHA2 is required".to_string()))?;
let bucket_path = self.public_bucket_directory_hash
.ok_or_else(|| MediaFileInsertBuilderError::MissingRequiredField(
"Public bucket directory hash is required".to_string()))?;
let result = insert_media_file_generic(InsertArgs {
pool: mysql_pool,
maybe_creator_user_token: self.maybe_creator_user_token.as_ref(),
maybe_creator_anonymous_visitor_token: self.maybe_creator_anonymous_visitor_token.as_ref(),
creator_ip_address: self.creator_ip_address.as_deref().unwrap_or("127.0.0.1"),
creator_set_visibility: self.creator_set_visibility,
media_class: self.media_file_class,
media_type: media_file_type,
is_user_upload: self.is_user_upload,
is_intermediate_system_file: self.is_intermediate_system_file,
origin_category,
origin_product_category: self.origin_product_category,
maybe_origin_model_type: None, // TODO
maybe_origin_model_token: None, // TODO
maybe_origin_filename: None, // TODO
maybe_mime_type: self.maybe_mime_type.as_deref(),
file_size_bytes: self.file_size_bytes,
maybe_duration_millis: None, // TODO
maybe_audio_encoding: None, // TODO
maybe_video_encoding: None, // TODO
maybe_frame_width: self.maybe_frame_width,
maybe_frame_height: self.maybe_frame_height,
checksum_sha2: &checksum_sha2,
maybe_title: None, // TODO
maybe_text_transcript: None, // TODO
maybe_scene_source_media_file_token: None, // TODO
maybe_prompt_token: None, // TODO
maybe_batch_token: None, // TODO
public_bucket_directory_hash: bucket_path.get_object_hash(),
maybe_public_bucket_prefix: bucket_path.get_optional_prefix(),
maybe_public_bucket_extension: bucket_path.get_optional_extension(),
maybe_creator_file_synthetic_id_category: IdCategory::MediaFile, // TODO: Remove this
maybe_creator_category_synthetic_id_category: IdCategory::FileUpload, // TODO: Remove this
maybe_extra_media_info: None, // TODO
is_generated_on_prem: false, // TODO
generated_by_worker: None, // TODO
generated_by_cluster: None, // TODO
maybe_mod_user_token: None, // TODO
}).await;
match result {
Ok(result) => Ok(result.0),
Err(err) => Err(MediaFileInsertBuilderError::ProbablyDatabaseError(err)),
}
}
}
@@ -1,27 +0,0 @@
use std::error::Error;
use std::fmt::{Debug, Display, Formatter};
use errors::AnyhowError;
#[derive(Debug)]
pub enum MediaFileInsertBuilderError {
/// A user-provided field is missing (400 error).
MissingRequiredField(String),
// TODO: We need to refactor to ensure these are actually DB errors (500s).
ProbablyDatabaseError(AnyhowError),
}
impl Error for MediaFileInsertBuilderError {}
impl Display for MediaFileInsertBuilderError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
MediaFileInsertBuilderError::MissingRequiredField(reason) => {
write!(f, "Missing required field: {}", reason)
}
MediaFileInsertBuilderError::ProbablyDatabaseError(reason) => {
write!(f, "Probably database error: {:?}", reason)
}
}
}
}
@@ -1,2 +0,0 @@
pub mod media_file_insert_builder;
mod media_file_insert_builder_error;

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