mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-31 15:55:54 -04:00
Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e44f083ab1 | ||
|
|
0059a9d63e | ||
|
|
b346750968 | ||
|
|
e5749f0e77 | ||
|
|
826cf00cbb | ||
|
|
6b7d034068 | ||
|
|
7bd3f6f8c6 | ||
|
|
0c550fcb1a | ||
|
|
3b2b7ede18 | ||
|
|
782b68e9a2 | ||
|
|
5c23568646 | ||
|
|
43e781571f | ||
|
|
84361eb93e | ||
|
|
38ab84d52e | ||
|
|
82e87fa883 | ||
|
|
de5daa3f92 | ||
|
|
fc0fba7573 | ||
|
|
b199738ee0 | ||
|
|
aa76113e8c | ||
|
|
bfe02a7880 | ||
|
|
ac81de6a48 | ||
|
|
2c2a63e80b | ||
|
|
3521f73142 | ||
|
|
88fd732eec | ||
|
|
490915a3e7 | ||
|
|
1802b0939e | ||
|
|
36b6aa78f8 | ||
|
|
499c07548c | ||
|
|
65580ec502 | ||
|
|
5d5b782760 | ||
|
|
09d0ae263f | ||
|
|
c3509d770f |
Generated
+13
@@ -0,0 +1,13 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<codeStyleSettings language="Rust">
|
||||
<option name="RIGHT_MARGIN" value="180" />
|
||||
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||
<option name="ALIGN_MULTILINE_PARAMETERS_IN_CALLS" value="false" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
Generated
+5
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE generic_inference_jobs\nSET\n status = \"complete_success\",\n on_success_result_entity_type = ?,\n on_success_result_entity_token = ?,\n failure_reason = NULL,\n internal_debugging_failure_reason = NULL,\n success_execution_millis = ?,\n success_inference_execution_millis = ?,\n retry_at = NULL,\n successfully_completed_at = NOW()\nWHERE token = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2bd7a5b886c9624ae48533ddac409cfca0668f30ab1f7016c412876b5904168b"
|
||||
}
|
||||
Generated
+34
-9
@@ -1921,7 +1921,7 @@ checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"fnv",
|
||||
"uuid",
|
||||
"uuid 1.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3258,6 +3258,20 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "discord-rich-presence"
|
||||
version = "0.2.5"
|
||||
source = "git+https://github.com/vionya/discord-rich-presence#9c6cabe04a70cfcc822affd1bf8be51321f8a821"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"thiserror 2.0.12",
|
||||
"uuid 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dispatch"
|
||||
version = "0.2.0"
|
||||
@@ -5547,7 +5561,7 @@ dependencies = [
|
||||
name = "idempotency"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"uuid",
|
||||
"uuid 1.8.0",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
@@ -7797,7 +7811,7 @@ dependencies = [
|
||||
"toml 0.8.20",
|
||||
"ureq",
|
||||
"url",
|
||||
"uuid",
|
||||
"uuid 1.8.0",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -9704,7 +9718,7 @@ dependencies = [
|
||||
"rkyv_derive",
|
||||
"seahash",
|
||||
"tinyvec",
|
||||
"uuid",
|
||||
"uuid 1.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10105,7 +10119,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
"uuid",
|
||||
"uuid 1.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -11253,7 +11267,7 @@ dependencies = [
|
||||
"user_traits_component",
|
||||
"utoipa",
|
||||
"utoipa-swagger-ui",
|
||||
"uuid",
|
||||
"uuid 1.8.0",
|
||||
"videos",
|
||||
"workspace-build-acceleration",
|
||||
"zip 0.6.6",
|
||||
@@ -12047,10 +12061,12 @@ 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",
|
||||
@@ -12122,7 +12138,7 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"url",
|
||||
"uuid",
|
||||
"uuid 1.8.0",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -12301,7 +12317,7 @@ dependencies = [
|
||||
"toml 0.8.20",
|
||||
"url",
|
||||
"urlpattern",
|
||||
"uuid",
|
||||
"uuid 1.8.0",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -13545,6 +13561,15 @@ dependencies = [
|
||||
"zip 1.1.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
|
||||
dependencies = [
|
||||
"getrandom 0.2.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.8.0"
|
||||
@@ -14706,7 +14731,7 @@ dependencies = [
|
||||
"unicode-normalization",
|
||||
"ureq",
|
||||
"url",
|
||||
"uuid",
|
||||
"uuid 1.8.0",
|
||||
"void",
|
||||
"winapi 0.3.9",
|
||||
"windows-sys 0.48.0",
|
||||
|
||||
@@ -147,6 +147,10 @@ publish = false
|
||||
# - tauri-runtime-wry
|
||||
# - tauri-utils
|
||||
#
|
||||
# Broken on Windows:
|
||||
# - core-foundation
|
||||
# - core-foundation-sys
|
||||
#
|
||||
workspace-build-acceleration = { path = "crates/workspace-build-acceleration" }
|
||||
|
||||
# External libs
|
||||
@@ -157,6 +161,7 @@ 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"
|
||||
|
||||
@@ -34,10 +34,8 @@ subprocess_common = { workspace = true }
|
||||
tokens = { workspace = true }
|
||||
|
||||
# External
|
||||
#openssl = { version = "0.10", features = ["vendored"] }
|
||||
# NB(bt,2025-03-10): default-features = false on Windows while I debug openssl / rustls
|
||||
anyhow = { workspace = true }
|
||||
base64 = "0.22.1"
|
||||
base64 = { workspace = true }
|
||||
bytes = "1.10"
|
||||
chrono = { workspace = true }
|
||||
const_format = "0.2.34"
|
||||
@@ -45,10 +43,11 @@ directories = "6.0.0"
|
||||
dirs = "6.0.0"
|
||||
futures = "0.3"
|
||||
image = "0.25.5"
|
||||
indexmap = { workspace = true }
|
||||
log = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["multipart", "stream"], default-features = false }
|
||||
reqwest = { workspace = true, features = ["multipart", "stream"], default-features = false } # NB(bt,2025-03-10): default-features = false on Windows while I debug openssl / rustls
|
||||
serde = { workspace = true }
|
||||
serde_derive = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
@@ -60,6 +59,10 @@ tempfile = "3.18.0"
|
||||
tokio = { version = "1.42", features = ["rt", "rt-multi-thread", "fs"] }
|
||||
tokio-util = { version = "0.7", features = ["codec"] }
|
||||
|
||||
# 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"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::state::app_dir::AppDataRoot;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::sora::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
|
||||
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::state::app_dir::AppDataRoot;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::state::sora::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
|
||||
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
|
||||
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::state::sora::sora_task_queue::SoraTaskQueue;
|
||||
use crate::utils::get_url_file_extension::get_url_file_extension;
|
||||
use crate::utils::simple_http_download::simple_http_download;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
@@ -50,12 +52,13 @@ 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)
|
||||
generate_image(request, &app_data_root, &sora_creds_manager, &sora_task_queue)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("error: {:?}", err);
|
||||
@@ -69,6 +72,7 @@ pub async fn generate_image(
|
||||
request: SoraImageRemixCommand,
|
||||
app_data_root: &AppDataRoot,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
let response = get_media_file(&ApiHost::Storyteller, &request.snapshot_media_token).await?;
|
||||
@@ -85,8 +89,6 @@ 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)
|
||||
@@ -141,7 +143,9 @@ pub async fn generate_image(
|
||||
sora_creds_manager.set_credentials(&new_creds)?;
|
||||
}
|
||||
|
||||
println!(">> TASK ID: {:?} ", response.task_id);
|
||||
info!("New Sora Task ID: {:?} ", response.task_id);
|
||||
|
||||
sora_task_queue.insert(&response.task_id)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,20 +4,27 @@ pub mod state;
|
||||
pub mod threads;
|
||||
pub mod utils;
|
||||
|
||||
use tauri::{Manager};
|
||||
|
||||
use crate::commands::flip_image::flip_image;
|
||||
use crate::commands::sora::open_sora_login_command::open_sora_login_command;
|
||||
use crate::state::main_window_size::MainWindowSize;
|
||||
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::sora::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::state::sora::sora_task_queue::SoraTaskQueue;
|
||||
use crate::threads::discord_presence_thread::discord_presence_thread;
|
||||
use crate::threads::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_log::Target;
|
||||
use tauri_plugin_log::TargetKind;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
// NB: Tauri wants to install the logger itself, so we can't rely on the logger crate
|
||||
// NB: Tauri wants to install the logger itself, so we can't rely on the logger crate
|
||||
// until the tauri runtime begins.
|
||||
println!("Loading model config...");
|
||||
|
||||
@@ -25,11 +32,15 @@ pub fn run() {
|
||||
.expect("config should load");
|
||||
|
||||
let app_data_root = config.app_data_root.clone();
|
||||
let app_data_root2 = config.app_data_root.clone();
|
||||
let app_data_root_2 = config.app_data_root.clone();
|
||||
|
||||
println!("Attempting to read existing credentials...");
|
||||
let sora_creds_manager = SoraCredentialManager::initialize_from_disk_infallible(&app_data_root);
|
||||
let sora_creds_manager2 = sora_creds_manager.clone();
|
||||
let sora_creds_manager_2 = sora_creds_manager.clone();
|
||||
|
||||
// Other state
|
||||
let sora_task_queue = SoraTaskQueue::new();
|
||||
let sora_task_queue_2 = sora_task_queue.clone();
|
||||
|
||||
println!("Initializing backend runtime...");
|
||||
|
||||
@@ -56,13 +67,37 @@ pub fn run() {
|
||||
//}
|
||||
let app = app.handle().clone();
|
||||
|
||||
tauri::async_runtime::spawn(sora_session_login_thread(app, app_data_root2, sora_creds_manager2));
|
||||
let app_2 = app.clone();
|
||||
let app_3 = app.clone();
|
||||
let app_data_root_3 = app_data_root_2.clone();
|
||||
let app_data_root_4 = app_data_root_2.clone();
|
||||
let sora_creds_manager_3 = sora_creds_manager_2.clone();
|
||||
|
||||
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));
|
||||
tauri::async_runtime::spawn(sora_task_polling_thread(app_data_root_4, sora_creds_manager_3, sora_task_queue_2));
|
||||
tauri::async_runtime::spawn(discord_presence_thread());
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.manage(config)
|
||||
.manage(app_data_root)
|
||||
.manage(config)
|
||||
.manage(sora_creds_manager)
|
||||
.manage(sora_task_queue)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
flip_image,
|
||||
open_sora_login_command,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::state::app_dir::AppDataRoot;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::os_platform::OsPlatform;
|
||||
use crate::state::yaml_config::YamlConfig;
|
||||
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
use crate::state::expanduser::expanduser;
|
||||
use crate::state::os_platform::OsPlatform;
|
||||
use anyhow::anyhow;
|
||||
use directories::UserDirs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use chrono::{DateTime, Local};
|
||||
use tempdir::TempDir;
|
||||
use tempfile::{Builder, NamedTempFile};
|
||||
|
||||
const DEFAULT_DATA_DIR : &str = "artcraft";
|
||||
const ASSETS_SUBDIRECTORY : &str = "assets";
|
||||
const DOWNLOADS_SUBDIRECTORY : &str = "downloads";
|
||||
const WEIGHTS_SUBDIRECTORY : &str = "weights";
|
||||
|
||||
const TEMPORARY_SUBDIRECTORY : &str = "temp";
|
||||
|
||||
/// Note: Tauri appends ".log" to the end of the filename.
|
||||
const LOG_FILE_NAME : &str = "application_debug";
|
||||
|
||||
/// The path to the application data directory, which includes "asset" and "weights" data.
|
||||
#[derive(Clone)]
|
||||
pub struct AppDataRoot {
|
||||
path: PathBuf,
|
||||
assets_dir: AppAssetsDir,
|
||||
downloads_dir: AppDownloadsDir,
|
||||
weights_dir: AppWeightsDir,
|
||||
temp_dir: TemporaryDir,
|
||||
log_file_name: PathBuf,
|
||||
log_file_name_string: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppAssetsDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppDownloadsDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppWeightsDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TemporaryDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl AppDataRoot {
|
||||
pub fn create_default() -> anyhow::Result<Self> {
|
||||
let directory = get_default_data_dir()?;
|
||||
Self::create_existing(directory)
|
||||
}
|
||||
|
||||
pub fn create_existing<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
|
||||
let mut dir = dir.as_ref().to_path_buf();
|
||||
|
||||
match OsPlatform::get() {
|
||||
OsPlatform::Linux | OsPlatform::MacOs => {
|
||||
if let Some(d) = dir.as_os_str().to_str() {
|
||||
dir = expanduser(d)?;
|
||||
}
|
||||
},
|
||||
OsPlatform::Windows => {}
|
||||
}
|
||||
|
||||
if !dir.is_dir() {
|
||||
println!("Creating directory {:?}", dir);
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
}
|
||||
|
||||
match dir.canonicalize() {
|
||||
Ok(d) => dir = d,
|
||||
Err(err) => {
|
||||
println!("Error canonicalizing {:?}: {}", dir, err);
|
||||
}
|
||||
}
|
||||
|
||||
let assets_dir = AppAssetsDir::create_existing(dir.join(ASSETS_SUBDIRECTORY))?;
|
||||
let downloads_dir = AppDownloadsDir::create_existing(dir.join(DOWNLOADS_SUBDIRECTORY))?;
|
||||
let weights_dir = AppWeightsDir::create_existing(dir.join(WEIGHTS_SUBDIRECTORY))?;
|
||||
let temp_dir = TemporaryDir::create_existing(dir.join(TEMPORARY_SUBDIRECTORY))?;
|
||||
let log_file_name = dir.join(LOG_FILE_NAME);
|
||||
let log_file_name_string = log_file_name
|
||||
.to_str()
|
||||
.ok_or(anyhow!("couldn't convert log path to str"))?
|
||||
.to_string();
|
||||
|
||||
Ok(Self {
|
||||
path: dir,
|
||||
assets_dir,
|
||||
downloads_dir,
|
||||
weights_dir,
|
||||
temp_dir,
|
||||
log_file_name,
|
||||
log_file_name_string,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn assets_dir(&self) -> &AppAssetsDir {
|
||||
&self.assets_dir
|
||||
}
|
||||
|
||||
pub fn downloads_dir(&self) -> &AppDownloadsDir {
|
||||
&self.downloads_dir
|
||||
}
|
||||
|
||||
pub fn weights_dir(&self) -> &AppWeightsDir {
|
||||
&self.weights_dir
|
||||
}
|
||||
|
||||
pub fn temp_dir(&self) -> &TemporaryDir {
|
||||
&self.temp_dir
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn log_file_name(&self) -> &Path {
|
||||
&self.log_file_name
|
||||
}
|
||||
|
||||
pub fn log_file_name_str(&self) -> &str {
|
||||
&self.log_file_name_string
|
||||
}
|
||||
|
||||
pub fn get_sora_cookie_file_path(&self) -> PathBuf {
|
||||
self.path.join("sora_cookies.txt")
|
||||
}
|
||||
|
||||
pub fn get_sora_bearer_token_file_path(&self) -> PathBuf {
|
||||
self.path.join("sora_bearer_token.txt")
|
||||
}
|
||||
|
||||
pub fn get_sora_sentinel_file_path(&self) -> PathBuf {
|
||||
self.path.join("sora_sentinel.txt")
|
||||
}
|
||||
}
|
||||
|
||||
impl AppAssetsDir {
|
||||
pub fn create_existing<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
|
||||
let mut dir = dir.as_ref().to_path_buf();
|
||||
match dir.canonicalize() {
|
||||
Ok(d) => dir = d,
|
||||
Err(err) => {
|
||||
println!("Error canonicalizing {:?}: {}", dir, err);
|
||||
}
|
||||
}
|
||||
if !dir.exists() {
|
||||
println!("Creating directory {:?}", dir);
|
||||
std::fs::create_dir(&dir)?;
|
||||
}
|
||||
Ok(Self {
|
||||
path: dir,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
/// Return the current date asset directory
|
||||
/// If it doesn't exist, create it.
|
||||
pub fn make_or_get_current_date_dir(&self) -> anyhow::Result<PathBuf> {
|
||||
let format: DateTime<Local> = Local::now();
|
||||
let date_directory_name = format.format("%Y-%d-%m").to_string();
|
||||
let full_path = self.path.join(date_directory_name);
|
||||
if !full_path.exists() {
|
||||
std::fs::create_dir(&full_path)?;
|
||||
}
|
||||
Ok(full_path)
|
||||
}
|
||||
}
|
||||
|
||||
impl AppDownloadsDir {
|
||||
pub fn create_existing<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
|
||||
let mut dir = dir.as_ref().to_path_buf();
|
||||
match dir.canonicalize() {
|
||||
Ok(d) => dir = d,
|
||||
Err(err) => {
|
||||
println!("Error canonicalizing {:?}: {}", dir, err);
|
||||
}
|
||||
}
|
||||
if !dir.exists() {
|
||||
println!("Creating directory {:?}", dir);
|
||||
std::fs::create_dir(&dir)?;
|
||||
}
|
||||
Ok(Self {
|
||||
path: dir,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl AppWeightsDir {
|
||||
pub fn create_existing<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
|
||||
let mut dir = dir.as_ref().to_path_buf();
|
||||
match dir.canonicalize() {
|
||||
Ok(d) => dir = d,
|
||||
Err(err) => {
|
||||
println!("Error canonicalizing {:?}: {}", dir, err);
|
||||
}
|
||||
}
|
||||
if !dir.exists() {
|
||||
println!("Creating directory {:?}", dir);
|
||||
std::fs::create_dir(&dir)?;
|
||||
}
|
||||
Ok(Self {
|
||||
path: dir,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl TemporaryDir {
|
||||
pub fn create_existing<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
|
||||
let mut dir = dir.as_ref().to_path_buf();
|
||||
match dir.canonicalize() {
|
||||
Ok(d) => dir = d,
|
||||
Err(err) => {
|
||||
println!("Error canonicalizing {:?}: {}", dir, err);
|
||||
}
|
||||
}
|
||||
if !dir.exists() {
|
||||
println!("Creating directory {:?}", dir);
|
||||
std::fs::create_dir(&dir)?;
|
||||
}
|
||||
Ok(Self {
|
||||
path: dir,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn with_prefix(&self, prefix: &str) -> anyhow::Result<TempDir> {
|
||||
let tempdir = TempDir::new_in(&self.path, prefix)?;
|
||||
Ok(tempdir)
|
||||
}
|
||||
|
||||
pub fn new_named_temp_file(&self) -> anyhow::Result<NamedTempFile> {
|
||||
let tempfile = Builder::new()
|
||||
.prefix("temp_")
|
||||
.suffix(".bin")
|
||||
.tempfile_in(&self.path)?;
|
||||
Ok(tempfile)
|
||||
}
|
||||
}
|
||||
|
||||
// eg. /home/bob/artcraft, /Users/bob/artcraft, or C:\Users\Alice\artcraft
|
||||
fn get_default_data_dir() -> anyhow::Result<PathBuf> {
|
||||
Ok(UserDirs::new()
|
||||
.ok_or_else(|| anyhow!("could not determine user home directory"))?
|
||||
.home_dir()
|
||||
.join(DEFAULT_DATA_DIR))
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use crate::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use chrono::{DateTime, Local};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppAssetsDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl DataSubdir for AppAssetsDir {
|
||||
const DIRECTORY_NAME: &'static str = "assets";
|
||||
|
||||
fn new_from<P: AsRef<Path>>(dir: P) -> Self {
|
||||
Self {
|
||||
path: dir.as_ref().to_path_buf(),
|
||||
}
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl AppAssetsDir {
|
||||
/// Return the current date asset directory
|
||||
/// If it doesn't exist, create it.
|
||||
pub fn make_or_get_current_date_dir(&self) -> anyhow::Result<PathBuf> {
|
||||
let format: DateTime<Local> = Local::now();
|
||||
let date_directory_name = format.format("%Y-%d-%m").to_string();
|
||||
let full_path = self.path.join(date_directory_name);
|
||||
if !full_path.exists() {
|
||||
std::fs::create_dir(&full_path)?;
|
||||
}
|
||||
Ok(full_path)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
use crate::state::data_dir::app_assets_dir::AppAssetsDir;
|
||||
use crate::state::data_dir::app_credentials_dir::AppCredentialsDir;
|
||||
use crate::state::data_dir::app_downloads_dir::AppDownloadsDir;
|
||||
use crate::state::data_dir::app_weights_dir::AppWeightsDir;
|
||||
use crate::state::data_dir::temporary_dir::TemporaryDir;
|
||||
use crate::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::state::expanduser::expanduser;
|
||||
use crate::state::os_platform::OsPlatform;
|
||||
use anyhow::anyhow;
|
||||
use directories::UserDirs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const DEFAULT_DATA_DIR : &str = "Artcraft";
|
||||
|
||||
|
||||
/// Note: Tauri appends ".log" to the end of the filename.
|
||||
const LOG_FILE_NAME : &str = "application_debug";
|
||||
|
||||
/// The path to the application data directory, which includes "asset" and "weights" data.
|
||||
#[derive(Clone)]
|
||||
pub struct AppDataRoot {
|
||||
path: PathBuf,
|
||||
assets_dir: AppAssetsDir,
|
||||
credentials_dir: AppCredentialsDir,
|
||||
downloads_dir: AppDownloadsDir,
|
||||
weights_dir: AppWeightsDir,
|
||||
temp_dir: TemporaryDir,
|
||||
log_file_name: PathBuf,
|
||||
log_file_name_string: String,
|
||||
}
|
||||
|
||||
impl AppDataRoot {
|
||||
pub fn create_default() -> anyhow::Result<Self> {
|
||||
let directory = get_default_data_dir()?;
|
||||
Self::create_existing(directory)
|
||||
}
|
||||
|
||||
pub fn create_existing<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
|
||||
let mut dir = dir.as_ref().to_path_buf();
|
||||
|
||||
match OsPlatform::get() {
|
||||
OsPlatform::Linux | OsPlatform::MacOs => {
|
||||
if let Some(d) = dir.as_os_str().to_str() {
|
||||
dir = expanduser(d)?;
|
||||
}
|
||||
},
|
||||
OsPlatform::Windows => {}
|
||||
}
|
||||
|
||||
if !dir.is_dir() {
|
||||
println!("Creating directory {:?}", dir);
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
}
|
||||
|
||||
match dir.canonicalize() {
|
||||
Ok(d) => dir = d,
|
||||
Err(err) => {
|
||||
println!("Error canonicalizing {:?}: {}", dir, err);
|
||||
}
|
||||
}
|
||||
|
||||
let assets_dir = AppAssetsDir::get_or_create_in_root_dir(&dir)?;
|
||||
let downloads_dir = AppDownloadsDir::get_or_create_in_root_dir(&dir)?;
|
||||
let credentials_dir = AppCredentialsDir::get_or_create_in_root_dir(&dir)?;
|
||||
let weights_dir = AppWeightsDir::get_or_create_in_root_dir(&dir)?;
|
||||
let temp_dir = TemporaryDir::get_or_create_in_root_dir(&dir)?;
|
||||
let log_file_name = dir.join(LOG_FILE_NAME);
|
||||
let log_file_name_string = log_file_name
|
||||
.to_str()
|
||||
.ok_or(anyhow!("couldn't convert log path to str"))?
|
||||
.to_string();
|
||||
|
||||
Ok(Self {
|
||||
path: dir,
|
||||
assets_dir,
|
||||
credentials_dir,
|
||||
downloads_dir,
|
||||
weights_dir,
|
||||
temp_dir,
|
||||
log_file_name,
|
||||
log_file_name_string,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn assets_dir(&self) -> &AppAssetsDir {
|
||||
&self.assets_dir
|
||||
}
|
||||
|
||||
pub fn credentials_dir(&self) -> &AppCredentialsDir {
|
||||
&self.credentials_dir
|
||||
}
|
||||
|
||||
pub fn downloads_dir(&self) -> &AppDownloadsDir {
|
||||
&self.downloads_dir
|
||||
}
|
||||
|
||||
pub fn weights_dir(&self) -> &AppWeightsDir {
|
||||
&self.weights_dir
|
||||
}
|
||||
|
||||
pub fn temp_dir(&self) -> &TemporaryDir {
|
||||
&self.temp_dir
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn log_file_name(&self) -> &Path {
|
||||
&self.log_file_name
|
||||
}
|
||||
|
||||
pub fn log_file_name_str(&self) -> &str {
|
||||
&self.log_file_name_string
|
||||
}
|
||||
|
||||
pub fn get_sora_cookie_file_path(&self) -> PathBuf {
|
||||
self.credentials_dir().get_sora_cookie_file_path()
|
||||
}
|
||||
|
||||
pub fn get_sora_bearer_token_file_path(&self) -> PathBuf {
|
||||
self.credentials_dir().get_sora_bearer_token_file_path()
|
||||
}
|
||||
|
||||
pub fn get_sora_sentinel_file_path(&self) -> PathBuf {
|
||||
self.credentials_dir().get_sora_sentinel_file_path()
|
||||
}
|
||||
|
||||
pub fn get_window_size_config_file(&self) -> PathBuf {
|
||||
self.path.join("window_size.json")
|
||||
}
|
||||
}
|
||||
|
||||
// eg. /home/bob/artcraft, /Users/bob/artcraft, or C:\Users\Alice\artcraft
|
||||
fn get_default_data_dir() -> anyhow::Result<PathBuf> {
|
||||
Ok(UserDirs::new()
|
||||
.ok_or_else(|| anyhow!("could not determine user home directory"))?
|
||||
.home_dir()
|
||||
.join(DEFAULT_DATA_DIR))
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use crate::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppDownloadsDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl DataSubdir for AppDownloadsDir {
|
||||
const DIRECTORY_NAME: &'static str = "downloads";
|
||||
|
||||
fn new_from<P: AsRef<Path>>(dir: P) -> Self {
|
||||
Self {
|
||||
path: dir.as_ref().to_path_buf(),
|
||||
}
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use crate::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppWeightsDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl DataSubdir for AppWeightsDir {
|
||||
const DIRECTORY_NAME: &'static str = "weights";
|
||||
|
||||
fn new_from<P: AsRef<Path>>(dir: P) -> Self {
|
||||
Self {
|
||||
path: dir.as_ref().to_path_buf(),
|
||||
}
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod app_assets_dir;
|
||||
pub mod app_credentials_dir;
|
||||
pub mod app_data_root;
|
||||
pub mod app_downloads_dir;
|
||||
pub mod app_weights_dir;
|
||||
pub mod temporary_dir;
|
||||
pub mod trait_data_subdir;
|
||||
@@ -0,0 +1,38 @@
|
||||
use crate::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tempdir::TempDir;
|
||||
use tempfile::{Builder, NamedTempFile};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TemporaryDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl DataSubdir for TemporaryDir {
|
||||
const DIRECTORY_NAME: &'static str = "temp";
|
||||
|
||||
fn new_from<P: AsRef<Path>>(dir: P) -> Self {
|
||||
Self {
|
||||
path: dir.as_ref().to_path_buf(),
|
||||
}
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl TemporaryDir {
|
||||
pub fn with_prefix(&self, prefix: &str) -> anyhow::Result<TempDir> {
|
||||
let tempdir = TempDir::new_in(&self.path, prefix)?;
|
||||
Ok(tempdir)
|
||||
}
|
||||
|
||||
pub fn new_named_temp_file(&self) -> anyhow::Result<NamedTempFile> {
|
||||
let tempfile = Builder::new()
|
||||
.prefix("temp_")
|
||||
.suffix(".bin")
|
||||
.tempfile_in(&self.path)?;
|
||||
Ok(tempfile)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use std::path::Path;
|
||||
|
||||
pub trait DataSubdir : Sized {
|
||||
const DIRECTORY_NAME: &'static str;
|
||||
|
||||
fn new_from<P: AsRef<Path>>(dir: P) -> Self;
|
||||
|
||||
fn path(&self) -> &Path;
|
||||
|
||||
fn get_or_create_in_root_dir<P: AsRef<Path>>(root_dir: P) -> anyhow::Result<Self> {
|
||||
Self::get_or_create_dir(root_dir.as_ref().join(Self::DIRECTORY_NAME))
|
||||
}
|
||||
|
||||
fn get_or_create_dir<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
|
||||
let mut dir = dir.as_ref().to_path_buf();
|
||||
|
||||
match dir.canonicalize() {
|
||||
Ok(d) => dir = d,
|
||||
Err(err) => {
|
||||
println!("Error canonicalizing {:?}: {}", dir, err);
|
||||
}
|
||||
}
|
||||
|
||||
if !dir.exists() {
|
||||
println!("Creating directory {:?}", dir);
|
||||
std::fs::create_dir(&dir)?;
|
||||
}
|
||||
|
||||
Ok(Self::new_from(dir))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use errors::AnyhowResult;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use tauri::{AppHandle, Manager, PhysicalSize, Window};
|
||||
|
||||
const MAIN_WINDOW_NAME: &str = "main";
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
|
||||
pub struct MainWindowSize {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
impl MainWindowSize {
|
||||
pub fn new(width: u32, height: u32) -> Self {
|
||||
Self { width, height }
|
||||
}
|
||||
|
||||
pub fn from_main_window(app: &AppHandle) -> AnyhowResult<Self> {
|
||||
let windows = app.windows();
|
||||
windows.get(MAIN_WINDOW_NAME)
|
||||
.map(|window| Self::from_window(window))
|
||||
.unwrap_or_else(|| Err(anyhow::anyhow!("Main window not found")))
|
||||
}
|
||||
|
||||
pub fn from_window(window: &Window) -> AnyhowResult<Self> {
|
||||
let size = window.inner_size()?;
|
||||
Ok(Self {
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_filesystem_configs(app_data_root: &AppDataRoot) -> AnyhowResult<Option<Self>> {
|
||||
let filename = app_data_root.get_window_size_config_file();
|
||||
if !filename.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
let contents = std::fs::read_to_string(filename)?;
|
||||
let size: MainWindowSize = serde_json::from_str(&contents)?;
|
||||
Ok(Some(size))
|
||||
}
|
||||
|
||||
pub fn apply_to_main_window(&self, app: &AppHandle) -> AnyhowResult<()> {
|
||||
let windows = app.windows();
|
||||
let window = windows.get(MAIN_WINDOW_NAME)
|
||||
.ok_or_else(|| anyhow::anyhow!("Main window not found"))?;
|
||||
window.set_size(PhysicalSize::new(self.width, self.height))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn persist_to_filesystem(&self, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
|
||||
let filename = app_data_root.get_window_size_config_file();
|
||||
let json = serde_json::to_string(self)?;
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(filename)?;
|
||||
file.write_all(json.as_bytes())?;
|
||||
file.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_physical_size(&self) -> PhysicalSize<u32> {
|
||||
PhysicalSize::new(self.width, self.height)
|
||||
}
|
||||
|
||||
pub fn matches_physical_size(&self, size: &PhysicalSize<u32>) -> bool {
|
||||
self.width == size.width && self.height == size.height
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
pub mod app_config;
|
||||
pub mod app_dir;
|
||||
pub mod data_dir;
|
||||
pub mod expanduser;
|
||||
pub mod main_window_size;
|
||||
pub mod os_platform;
|
||||
pub mod sora;
|
||||
pub mod storyteller;
|
||||
pub mod yaml_config;
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod read_sora_credentials_from_disk;
|
||||
pub mod read_sora_credentials_from_disk_with_upgrade;
|
||||
pub mod sora_credential_holder;
|
||||
pub mod sora_credential_manager;
|
||||
pub mod sora_credential_manager;
|
||||
pub mod sora_task_queue;
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::state::app_dir::AppDataRoot;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use openai_sora_client::creds::sora_cookies::SoraCookies;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
use crate::state::app_dir::AppDataRoot;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use log::{info, warn};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::state::app_dir::AppDataRoot;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::sora::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
|
||||
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
|
||||
use anyhow::anyhow;
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
use std::collections::HashSet;
|
||||
use chrono::{DateTime, Utc};
|
||||
use errors::AnyhowResult;
|
||||
use indexmap::IndexMap;
|
||||
use openai_sora_client::requests::image_gen::image_gen_status::TaskId;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TaskDetails {
|
||||
pub enqueue_time: DateTime<Utc>
|
||||
}
|
||||
|
||||
impl TaskDetails {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
enqueue_time: Utc::now()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TaskIdAndDetails {
|
||||
pub task_id: TaskId,
|
||||
pub details: TaskDetails,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SoraTaskQueue {
|
||||
// Insertion-order map of task IDs to task details
|
||||
queue: Arc<RwLock<IndexMap<TaskId, TaskDetails>>>
|
||||
}
|
||||
|
||||
impl SoraTaskQueue {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
queue: Arc::new(RwLock::new(IndexMap::new()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine if queue is empty
|
||||
/// This is an O(1) operation.
|
||||
pub fn is_empty(&self) -> AnyhowResult<bool> {
|
||||
match self.queue.read() {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err))
|
||||
}
|
||||
Ok(lock) => Ok(lock.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
/// Get length of set
|
||||
/// This is an O(1) operation.
|
||||
pub fn len(&self) -> AnyhowResult<usize> {
|
||||
match self.queue.read() {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err))
|
||||
}
|
||||
Ok(lock) => Ok(lock.len())
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine if the key is in the queue
|
||||
/// This is an O(1) operation.
|
||||
pub fn contains_key(&self, task_id: &TaskId) -> AnyhowResult<bool> {
|
||||
match self.queue.read() {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err))
|
||||
}
|
||||
Ok(lock) => Ok(lock.contains_key(task_id))
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the first enqueued task
|
||||
/// This is an O(1) operation.
|
||||
pub fn first(&self) -> AnyhowResult<Option<TaskIdAndDetails>> {
|
||||
match self.queue.read() {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err))
|
||||
}
|
||||
Ok(lock) => match lock.first() {
|
||||
None => Ok(None),
|
||||
Some((task_id, task_details)) => Ok(Some(TaskIdAndDetails {
|
||||
task_id: task_id.clone(),
|
||||
details: task_details.clone(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the last enqueued task
|
||||
/// This is an O(1) operation.
|
||||
pub fn last(&self) -> AnyhowResult<Option<TaskIdAndDetails>> {
|
||||
match self.queue.read() {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err))
|
||||
}
|
||||
Ok(lock) => match lock.last() {
|
||||
None => Ok(None),
|
||||
Some((task_id, task_details)) => Ok(Some(TaskIdAndDetails {
|
||||
task_id: task_id.clone(),
|
||||
details: task_details.clone(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a new task. If a task already exists, we update its value.
|
||||
/// This is an O(1) operation.
|
||||
pub fn insert(&self, task_id: &TaskId) -> AnyhowResult<Option<TaskDetails>> {
|
||||
match self.queue.write() {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err))
|
||||
}
|
||||
Ok(mut lock) => {
|
||||
// NB: Probably fine to compute TaskDetails here for now. We may eventually expose this if we include additional info.
|
||||
let task_details = TaskDetails::new();
|
||||
Ok(lock.insert(task_id.clone(), task_details))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a task by ID.
|
||||
/// This is an O(n) operation.
|
||||
pub fn remove(&self, task_id: &TaskId) -> AnyhowResult<Option<TaskDetails>> {
|
||||
match self.queue.write() {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err))
|
||||
}
|
||||
Ok(mut lock) => {
|
||||
Ok(lock.shift_remove(task_id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a list of tasks by ID.
|
||||
/// This is an O(n) operation.
|
||||
pub fn remove_list(&self, task_ids: &[&TaskId]) -> AnyhowResult<()> {
|
||||
match self.queue.write() {
|
||||
Err(err) => {
|
||||
Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err))
|
||||
}
|
||||
Ok(mut lock) => {
|
||||
// TODO: Cleaner way to do this?
|
||||
let mut set : HashSet<&TaskId, _> = HashSet::with_capacity(task_ids.len());
|
||||
set.extend(task_ids);
|
||||
lock.retain(|k, v| !set.contains(k));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use errors::AnyhowResult;
|
||||
use openai_sora_client::requests::image_gen::image_gen_status::TaskId;
|
||||
use crate::state::sora::sora_task_queue::SoraTaskQueue;
|
||||
|
||||
#[test]
|
||||
fn remove_list() -> AnyhowResult<()> {
|
||||
let task_queue = SoraTaskQueue::new();
|
||||
|
||||
assert!(task_queue.is_empty()?);
|
||||
assert_eq!(task_queue.len()?, 0);
|
||||
|
||||
task_queue.insert(&TaskId("a".to_string()))?;
|
||||
task_queue.insert(&TaskId("b".to_string()))?;
|
||||
task_queue.insert(&TaskId("c".to_string()))?;
|
||||
task_queue.insert(&TaskId("d".to_string()))?;
|
||||
task_queue.insert(&TaskId("e".to_string()))?;
|
||||
task_queue.insert(&TaskId("f".to_string()))?;
|
||||
|
||||
assert!(!task_queue.is_empty()?);
|
||||
assert_eq!(task_queue.len()?, 6);
|
||||
|
||||
task_queue.insert(&TaskId("a".to_string()))?;
|
||||
task_queue.insert(&TaskId("b".to_string()))?;
|
||||
task_queue.insert(&TaskId("c".to_string()))?;
|
||||
task_queue.insert(&TaskId("d".to_string()))?;
|
||||
task_queue.insert(&TaskId("e".to_string()))?;
|
||||
task_queue.insert(&TaskId("f".to_string()))?;
|
||||
|
||||
assert!(!task_queue.is_empty()?);
|
||||
assert_eq!(task_queue.len()?, 6);
|
||||
|
||||
task_queue.remove_list(&[
|
||||
&TaskId("a".to_string()),
|
||||
&TaskId("c".to_string()),
|
||||
&TaskId("e".to_string()),
|
||||
])?;
|
||||
|
||||
assert!(!task_queue.is_empty()?);
|
||||
assert_eq!(task_queue.len()?, 3);
|
||||
|
||||
assert!(task_queue.contains_key(&TaskId("b".to_string()))?);
|
||||
assert!(task_queue.contains_key(&TaskId("d".to_string()))?);
|
||||
assert!(task_queue.contains_key(&TaskId("f".to_string()))?);
|
||||
|
||||
assert!(!task_queue.contains_key(&TaskId("a".to_string()))?);
|
||||
assert!(!task_queue.contains_key(&TaskId("c".to_string()))?);
|
||||
assert!(!task_queue.contains_key(&TaskId("e".to_string()))?);
|
||||
|
||||
task_queue.remove_list(&[
|
||||
&TaskId("foo".to_string()),
|
||||
&TaskId("bar".to_string()),
|
||||
&TaskId("baz".to_string()),
|
||||
])?;
|
||||
|
||||
assert!(!task_queue.is_empty()?);
|
||||
assert_eq!(task_queue.len()?, 3);
|
||||
|
||||
assert!(task_queue.contains_key(&TaskId("b".to_string()))?);
|
||||
assert!(task_queue.contains_key(&TaskId("d".to_string()))?);
|
||||
assert!(task_queue.contains_key(&TaskId("f".to_string()))?);
|
||||
|
||||
task_queue.remove_list(&[
|
||||
&TaskId("b".to_string()),
|
||||
&TaskId("d".to_string()),
|
||||
&TaskId("f".to_string()),
|
||||
])?;
|
||||
|
||||
assert!(task_queue.is_empty()?);
|
||||
assert_eq!(task_queue.len()?, 0);
|
||||
|
||||
assert!(!task_queue.contains_key(&TaskId("b".to_string()))?);
|
||||
assert!(!task_queue.contains_key(&TaskId("d".to_string()))?);
|
||||
assert!(!task_queue.contains_key(&TaskId("f".to_string()))?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use anyhow::anyhow;
|
||||
use discord_rich_presence::{DiscordIpc, DiscordIpcClient};
|
||||
use errors::AnyhowResult;
|
||||
use log::info;
|
||||
|
||||
/// Our Discord App ID. Not sure if this is a secret.
|
||||
const DISCORD_APP_ID : &str = "1366596912593113138";
|
||||
|
||||
pub async fn discord_presence_thread() -> ! {
|
||||
loop {
|
||||
let client = DiscordIpcClient::new(DISCORD_APP_ID);
|
||||
let _err = discord_main_loop(client).await;
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10_000)).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn discord_main_loop(mut client: DiscordIpcClient) -> AnyhowResult<()> {
|
||||
info!("Connecting Discord IPC client");
|
||||
|
||||
client.connect()
|
||||
.map_err(|err| anyhow!("Error connecting to Discord IPC: {:?}", err))?;
|
||||
|
||||
loop {
|
||||
info!("Notifying discord...");
|
||||
|
||||
let assets = discord_rich_presence::activity::Assets::new()
|
||||
.large_image("https://storyteller.ai/android-chrome-512x512.png")
|
||||
.large_text("https://GetArtCraft.com");
|
||||
//.small_image("https://storyteller.ai/android-chrome-192x192.png")
|
||||
//.small_text("small text");
|
||||
|
||||
client.set_activity(discord_rich_presence::activity::Activity::new()
|
||||
//.state("Anyone can make arts!")
|
||||
.state("GetArtCraft.com")
|
||||
.details("Making Arts with ArtCraft")
|
||||
.activity_type(discord_rich_presence::activity::ActivityType::Playing)
|
||||
.assets(assets))
|
||||
.map_err(|err| anyhow!("Error setting Discord activity: {:?}", err))?;
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(30_000)).await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::main_window_size::MainWindowSize;
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use tauri::{AppHandle, Manager, Webview, Window};
|
||||
|
||||
const MAIN_WINDOW_NAME : &str = "main";
|
||||
|
||||
pub async fn main_window_thread(
|
||||
app: AppHandle,
|
||||
app_data_root: AppDataRoot,
|
||||
) -> ! {
|
||||
loop {
|
||||
for (window_name, window) in app.windows() {
|
||||
if window_name == MAIN_WINDOW_NAME {
|
||||
let result = handle_main_window(
|
||||
&window,
|
||||
&app_data_root,
|
||||
).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(
|
||||
window: &Window,
|
||||
app_data_root: &AppDataRoot,
|
||||
) -> AnyhowResult<()> {
|
||||
let mut window_size = MainWindowSize::from_window(window)?;
|
||||
|
||||
loop {
|
||||
let old_size = window_size.to_physical_size();
|
||||
let new_size = window.inner_size()?;
|
||||
|
||||
if !window_size.matches_physical_size(&new_size) {
|
||||
info!("Window size changed from {:?} to {:?}", old_size, new_size);
|
||||
window_size = MainWindowSize::from_window(&window)?;
|
||||
info!("Saving window size configs to disk...");
|
||||
window_size.persist_to_filesystem(app_data_root)?;
|
||||
}
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10_000)).await;
|
||||
}
|
||||
}
|
||||
@@ -1 +1,4 @@
|
||||
pub mod sora_session_login_thread;
|
||||
pub mod discord_presence_thread;
|
||||
pub mod main_window_thread;
|
||||
pub mod sora_session_login_thread;
|
||||
pub mod sora_task_polling_thread;
|
||||
@@ -1,22 +1,22 @@
|
||||
use std::fs;
|
||||
use crate::state::app_dir::AppDataRoot;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
|
||||
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::utils::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 reqwest::Url;
|
||||
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};
|
||||
use reqwest::Url;
|
||||
use std::fs;
|
||||
use std::fs::{read_to_string, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::ops::Sub;
|
||||
use tauri::{AppHandle, Manager, Webview};
|
||||
|
||||
pub const LOGIN_WINDOW_NAME: &str = "login_window";
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::state::sora::sora_task_queue::SoraTaskQueue;
|
||||
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::{TaskId, TaskStatus};
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub async fn sora_task_polling_thread(
|
||||
app_data_root: AppDataRoot,
|
||||
sora_creds_manager: SoraCredentialManager,
|
||||
sora_task_queue: SoraTaskQueue,
|
||||
) -> ! {
|
||||
loop {
|
||||
let res = polling_loop(&sora_creds_manager, &sora_task_queue).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,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> 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)?;
|
||||
|
||||
// TODO: Handle succeeded tasks.
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -3,5 +3,5 @@ pub mod get_url_file_extension;
|
||||
pub mod image;
|
||||
pub mod large_file_transfer;
|
||||
pub mod simple_http_download;
|
||||
pub mod sora_webview_cookies;
|
||||
pub mod sora;
|
||||
pub mod sora;
|
||||
pub mod sora_webview_cookies;
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::state::app_dir::AppDataRoot;
|
||||
use crate::state::data_dir::app_data_root::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 = "nx run editor2d:dev"
|
||||
beforeDevCommand = "npx nx run editor2d:dev"
|
||||
devUrl = "http://localhost:5741"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# For some reason, we can't pass json files to the tauri dev command
|
||||
[build]
|
||||
frontendDist = "../../../frontend/apps/editor3d/dist"
|
||||
beforeDevCommand = "nx run editor3d:dev"
|
||||
beforeDevCommand = "npx nx run editor3d:dev"
|
||||
devUrl = "http://localhost:5173"
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# For some reason, we can't pass json files to the tauri dev command
|
||||
[build]
|
||||
frontendDist = "../../../frontend/apps/editor3d/dist"
|
||||
#beforeDevCommand = "nx run editor3d:dev"
|
||||
beforeDevCommand = "echo 'no dev'"
|
||||
devUrl = "http://localhost:5173"
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "0.0.1",
|
||||
"identifier": "ai.artcraft.app",
|
||||
"build": {
|
||||
"frontendDist": "../../../frontend/apps/editor2d/dist",
|
||||
"frontendDist": "../../../",
|
||||
"devUrl": "http://localhost:5741",
|
||||
"beforeDevCommand": "nx run editor2d:dev",
|
||||
"beforeBuildCommand": "nx run editor2d:build"
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
|
||||
"productName": "ArtCraft",
|
||||
"version": "0.0.1",
|
||||
"identifier": "ai.artcraft.app",
|
||||
"build": {
|
||||
"frontendDist": "../../../frontend/apps/editor3d/dist",
|
||||
"devUrl": "http://localhost:5173",
|
||||
"beforeDevCommand": "echo",
|
||||
"beforeBuildCommand": "nx run editor2d:build"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "ArtCraft",
|
||||
"width": 2400,
|
||||
"height": 1300,
|
||||
"resizable": true,
|
||||
"fullscreen": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
+1
@@ -21,6 +21,7 @@ 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
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ 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> {
|
||||
|
||||
+1
@@ -13,6 +13,7 @@ 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> {
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
use crate::creds::credential_migration::CredentialMigrationRef;
|
||||
use crate::creds::sora_credential_set::SoraCredentialSet;
|
||||
use crate::recipes::image_upload_from_file_with_session_auto_renew::ImageUploadFromFileAutoRenewRequest;
|
||||
use crate::recipes::maybe_refresh_credentials_on_sora_error::maybe_refresh_credentials_on_sora_error;
|
||||
use crate::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use crate::requests::image_gen::image_gen_status::{get_image_gen_status, StatusRequest, TaskId, TaskResponse, VideoGenStatusResponse};
|
||||
use crate::requests::upload::upload_media_from_file::sora_media_upload_from_file;
|
||||
use crate::requests::upload::upload_media_http_request::SoraMediaUploadResponse;
|
||||
use crate::sora_error::SoraError;
|
||||
use anyhow::Error;
|
||||
use log::{info, warn};
|
||||
use std::path::Path;
|
||||
|
||||
pub struct StatusRequestArgs {
|
||||
pub limit: Option<u32>,
|
||||
pub before: Option<TaskId>,
|
||||
pub credentials: SoraCredentialSet,
|
||||
}
|
||||
|
||||
/// Check Sora task statuses with session auto-renewal.
|
||||
/// If a new sora credential is returned, replace the old one with the new one.
|
||||
pub async fn list_sora_task_status_with_session_auto_renew(
|
||||
args: StatusRequestArgs,
|
||||
) -> Result<(VideoGenStatusResponse, Option<SoraCredentialSet>), SoraError> {
|
||||
|
||||
let request = StatusRequest {
|
||||
limit: args.limit,
|
||||
before: args.before.map(|t| t.0),
|
||||
};
|
||||
|
||||
let mut new_creds = None;
|
||||
|
||||
if args.credentials.jwt_bearer_token.is_none() {
|
||||
info!("JWT not set. Upgrading credentials...");
|
||||
let mut updated_creds = args.credentials.clone();
|
||||
if let Err(err) = maybe_upgrade_or_renew_session(&mut updated_creds).await {
|
||||
warn!("Failed to upgrade or renew session: {:?}", err);
|
||||
}
|
||||
new_creds = Some(updated_creds);
|
||||
}
|
||||
|
||||
let legacy_creds = new_creds.as_ref()
|
||||
.unwrap_or_else(|| &args.credentials)
|
||||
.to_legacy_credentials()?;
|
||||
|
||||
let result = get_image_gen_status(&request, &legacy_creds).await;
|
||||
|
||||
let err = match result {
|
||||
Ok(response) => return Ok((response, new_creds)),
|
||||
Err(err) => err,
|
||||
};
|
||||
|
||||
warn!("Task status polling failed: {:?}", err);
|
||||
|
||||
let new_creds = maybe_refresh_credentials_on_sora_error(&args.credentials, err).await?;
|
||||
|
||||
info!("Retrying image upload with new credentials...");
|
||||
|
||||
let result = get_image_gen_status(&request, &legacy_creds).await?;
|
||||
|
||||
Ok((result, Some(new_creds)))
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod image_remix_with_session_auto_renew;
|
||||
pub mod image_upload_from_bytes_with_session_auto_renew;
|
||||
pub mod image_upload_from_file_with_session_auto_renew;
|
||||
pub mod list_sora_task_status_with_session_auto_renew;
|
||||
pub mod maybe_refresh_credentials_on_sora_error;
|
||||
pub mod maybe_upgrade_or_renew_session;
|
||||
pub mod wait_for_image_gen_status;
|
||||
@@ -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 == task_id)
|
||||
status_response.task_responses.iter().find(|task| task.id.0 == task_id)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::requests::image_gen::image_gen_status::TaskId;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SoraImageGenResponse {
|
||||
pub task_id: String,
|
||||
pub task_id: TaskId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
||||
+16
-1
@@ -1,6 +1,7 @@
|
||||
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";
|
||||
|
||||
@@ -94,7 +95,7 @@ pub (crate) struct RawSoraImageGenRequest {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub (crate) struct RawSoraResponse {
|
||||
/// eg. "task_01jqsz9dsae9tvjygf1abrv3xf"
|
||||
pub id: String,
|
||||
pub id: TaskId,
|
||||
/// not known
|
||||
pub priority: Option<String>,
|
||||
}
|
||||
@@ -227,3 +228,17 @@ pub (crate) async fn image_gen_http_request(sora_request: RawSoraImageGenRequest
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use errors::AnyhowResult;
|
||||
use crate::requests::image_gen::image_gen_http_request::RawSoraResponse;
|
||||
|
||||
#[test]
|
||||
fn deserialize_task_id() -> AnyhowResult<()> {
|
||||
let json = "{\"id\": \"task_foobarbaz\"}";
|
||||
let response : RawSoraResponse = serde_json::from_str(json)?;
|
||||
assert_eq!(response.id.0, "task_foobarbaz");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+32
-12
@@ -1,15 +1,28 @@
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
|
||||
use crate::credentials::SoraCredentials;
|
||||
use crate::sora_error::SoraError;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use reqwest::Url;
|
||||
use serde_derive::Deserialize;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
use log::debug;
|
||||
|
||||
const SORA_STATUS_URL: &str = "https://sora.com/backend/video_gen";
|
||||
|
||||
/// This user agent is tied to the sentinel generation. If we need to change it, we may need to change sentinel generation too.
|
||||
const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36";
|
||||
|
||||
/// A strongly typed task ID for Sora tasks.
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
|
||||
pub struct TaskId(pub String);
|
||||
|
||||
impl Display for TaskId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
pub enum TaskStatus {
|
||||
Queued,
|
||||
@@ -42,7 +55,7 @@ pub struct VideoGenStatusResponse {
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct TaskResponse {
|
||||
pub id: String,
|
||||
pub id: TaskId,
|
||||
pub user: String,
|
||||
pub created_at: String,
|
||||
pub status: String,
|
||||
@@ -79,7 +92,7 @@ pub struct TaskResponse {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct Generation {
|
||||
pub id: String,
|
||||
pub task_id: String,
|
||||
pub task_id: TaskId,
|
||||
pub created_at: String,
|
||||
pub deleted_at: Option<String>,
|
||||
pub url: String,
|
||||
@@ -170,7 +183,7 @@ pub struct ModerationResult {
|
||||
pub results_by_frame_index: serde_json::Value,
|
||||
pub code: Option<String>,
|
||||
pub is_output_rejection: bool,
|
||||
pub task_id: String,
|
||||
pub task_id: TaskId,
|
||||
}
|
||||
|
||||
pub struct StatusRequest {
|
||||
@@ -186,10 +199,11 @@ impl StatusRequest {
|
||||
}
|
||||
|
||||
/// Gets the status of image generation tasks from Sora API
|
||||
pub async fn get_image_gen_status(status_request: &StatusRequest, credentials: &SoraCredentials) -> AnyhowResult<VideoGenStatusResponse> {
|
||||
pub async fn get_image_gen_status(status_request: &StatusRequest, credentials: &SoraCredentials) -> Result<VideoGenStatusResponse, SoraError> {
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let mut url = reqwest::Url::parse(SORA_STATUS_URL)?;
|
||||
let mut url = reqwest::Url::parse(SORA_STATUS_URL)
|
||||
.map_err(|err| anyhow!("error parsing URL: {:?}", err))?;
|
||||
|
||||
// Add query parameters
|
||||
if let Some(limit) = status_request.limit {
|
||||
@@ -200,7 +214,11 @@ pub async fn get_image_gen_status(status_request: &StatusRequest, credentials: &
|
||||
url.query_pairs_mut().append_pair("before", &before);
|
||||
}
|
||||
|
||||
let http_request = client.get(url).header("User-Agent", USER_AGENT).header("Cookie", &credentials.cookie).header("Authorization", credentials.authorization_header_value()).header("Content-Type", "application/json");
|
||||
let http_request = client.get(url)
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Cookie", &credentials.cookie)
|
||||
.header("Authorization", credentials.authorization_header_value())
|
||||
.header("Content-Type", "application/json");
|
||||
|
||||
let http_request = http_request.build()?;
|
||||
|
||||
@@ -209,10 +227,12 @@ pub async fn get_image_gen_status(status_request: &StatusRequest, credentials: &
|
||||
let status = response.status();
|
||||
|
||||
let response_body = &response.text().await?;
|
||||
println!("response_body: {}", response_body);
|
||||
|
||||
debug!("response_body: {}", response_body);
|
||||
|
||||
if status != reqwest::StatusCode::OK {
|
||||
return Err(anyhow::anyhow!("The status request failed; status = {:?}, message = {:?}", status, response_body));
|
||||
// TODO: Handle non-200 status codes, esp. JWT errors and credential errors
|
||||
return Err(SoraError::AnyhowError(anyhow::anyhow!("The status request failed; status = {:?}, message = {:?}", status, response_body)));
|
||||
}
|
||||
|
||||
let response: VideoGenStatusResponse = serde_json::from_str(response_body)?;
|
||||
@@ -285,7 +305,7 @@ mod tests {
|
||||
|
||||
let task_id = response.task_responses[0].id.clone();
|
||||
let task_response = wait_for_image_gen_status(
|
||||
&task_id,
|
||||
&task_id.0,
|
||||
&creds,
|
||||
Some(10)
|
||||
).await?;
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@ mod tests {
|
||||
|
||||
println!("task_id: {}", response.task_id);
|
||||
|
||||
assert!(response.task_id.starts_with("task_"));
|
||||
assert!(response.task_id.0.starts_with("task_"));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ mod tests {
|
||||
|
||||
println!("task_id: {}", response.task_id);
|
||||
|
||||
assert!(response.task_id.starts_with("task_"));
|
||||
assert!(response.task_id.0.starts_with("task_"));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,14 @@ 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 application error)
|
||||
/// We haven't received a bearer token yet
|
||||
/// This is our own internal application state error, not something Sora returns.
|
||||
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:
|
||||
@@ -27,7 +29,7 @@ pub enum SoraError {
|
||||
/// Another error occurred.
|
||||
OtherBadStatus(anyhow::Error),
|
||||
|
||||
/// Reqwest Error
|
||||
/// Reqwest Error, eg. connection failures.
|
||||
ReqwestError(reqwest::Error),
|
||||
|
||||
/// std::io Error that arises from our end, eg. reading from the filesystem.
|
||||
@@ -35,6 +37,9 @@ 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 {}
|
||||
@@ -63,6 +68,9 @@ impl Display for SoraError {
|
||||
Self::AnyhowError(err) => {
|
||||
write!(f, "Anyhow error: {}", err)
|
||||
}
|
||||
Self::JsonError(err) => {
|
||||
write!(f, "serde_json error: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,3 +92,9 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod storyteller_avt_cookie;
|
||||
pub mod storyteller_credential_set;
|
||||
pub mod storyteller_session_cookie;
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
/// 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 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()
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
use crate::credentials::storyteller_avt_cookie::StorytellerAvtCookie;
|
||||
use crate::credentials::storyteller_session_cookie::StorytellerSessionCookie;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct StorytellerCredentialSet {
|
||||
pub cookie: Option<StorytellerSessionCookie>,
|
||||
pub avt: Option<StorytellerAvtCookie>,
|
||||
}
|
||||
|
||||
impl StorytellerCredentialSet {
|
||||
pub fn initialize(
|
||||
cookie: Option<StorytellerSessionCookie>,
|
||||
avt: Option<StorytellerAvtCookie>,
|
||||
) -> Self {
|
||||
Self { cookie, avt }
|
||||
}
|
||||
|
||||
pub fn initialize_with_just_cookie(cookie: StorytellerSessionCookie) -> Self {
|
||||
Self {
|
||||
cookie: Some(cookie),
|
||||
avt: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize_with_just_avt(avt: StorytellerAvtCookie) -> Self {
|
||||
Self {
|
||||
cookie: None,
|
||||
avt: Some(avt),
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
|
||||
/// 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 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()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
//! 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;
|
||||
+8
-10
@@ -142,18 +142,16 @@ impl Display for EnqueueImageGenRequestError {
|
||||
post,
|
||||
tag = "Image Studio",
|
||||
path = "/v1/image_studio/prompt",
|
||||
params(
|
||||
("request" = EnqueueStudioImageGenRequest, description = "Payload for Image Generation Request"),
|
||||
),
|
||||
responses(
|
||||
(
|
||||
status = 200,
|
||||
description = "Enqueue TTS generically",
|
||||
body = EnqueueImageGenRequestSuccessResponse,
|
||||
),
|
||||
(status = 400, description = "Bad input", body = EnqueueImageGenRequestError),
|
||||
(status = 401, description = "Not authorized", body = EnqueueImageGenRequestError),
|
||||
(status = 429, description = "Rate limited", body = EnqueueImageGenRequestError),
|
||||
(status = 500, description = "Server error", body = EnqueueImageGenRequestError)
|
||||
(status = 200, description = "Success response", body = EnqueueImageGenRequestSuccessResponse),
|
||||
(status = 400, description = "Bad input", body = EnqueueImageGenRequestError),
|
||||
(status = 401, description = "Not authorized", body = EnqueueImageGenRequestError),
|
||||
(status = 429, description = "Rate limited", body = EnqueueImageGenRequestError),
|
||||
(status = 500, description = "Server error", body = EnqueueImageGenRequestError),
|
||||
),
|
||||
params(("request" = EnqueueStudioImageGenRequest, description = "Payload for Image Generation Request"))
|
||||
)]
|
||||
pub async fn enqueue_studio_image_generation_handler(http_request: HttpRequest, request: Json<EnqueueStudioImageGenRequest>, server_state: Data<Arc<ServerState>>) -> Result<HttpResponse, EnqueueImageGenRequestError> {
|
||||
validate_request(&request)?;
|
||||
|
||||
+18
-3
@@ -148,6 +148,7 @@ pub async fn update_gpt_image_job_status_handler(
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
match inference_job.status {
|
||||
JobStatusPlus::CompleteSuccess => {
|
||||
// Job has already been completed. Don't replay the request.
|
||||
@@ -162,6 +163,14 @@ pub async fn update_gpt_image_job_status_handler(
|
||||
_ => {} // Intentional fall through.
|
||||
}
|
||||
|
||||
let target_status = match request.job_status {
|
||||
UpdatedJobStatus::Started => JobStatusPlus::Started,
|
||||
UpdatedJobStatus::CompleteSuccess => JobStatusPlus::CompleteSuccess,
|
||||
UpdatedJobStatus::CompleteFailure => JobStatusPlus::CompleteFailure,
|
||||
UpdatedJobStatus::AttemptFailed => JobStatusPlus::AttemptFailed,
|
||||
UpdatedJobStatus::Dead => JobStatusPlus::Dead,
|
||||
};
|
||||
|
||||
if let Some(images) = &request.images {
|
||||
for image in images.iter() {
|
||||
let result = upload_and_save_image(
|
||||
@@ -180,13 +189,17 @@ pub async fn update_gpt_image_job_status_handler(
|
||||
}
|
||||
}
|
||||
|
||||
update_job_record(&request, &server_state, inference_job)
|
||||
info!("Updating job record for job: {:?}", request.job_token);
|
||||
|
||||
update_job_record(&request, &server_state, inference_job, target_status)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error updating job: {:?}", err);
|
||||
UpdateGptImageJobStatusError::ServerError
|
||||
})?;
|
||||
|
||||
info!("Job record updated for job: {:?}", request.job_token);
|
||||
|
||||
Ok(Json(UpdateGptImageJobStatusSuccessResponse {
|
||||
success: true,
|
||||
}))
|
||||
@@ -254,13 +267,15 @@ async fn upload_and_save_image(
|
||||
async fn update_job_record(
|
||||
request: &Json<UpdateGptImageJobStatusRequest>,
|
||||
server_state: &Data<Arc<ServerState>>,
|
||||
inference_job: GenericInferenceJobStatus
|
||||
inference_job: GenericInferenceJobStatus,
|
||||
target_status: JobStatusPlus,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
match inference_job.status {
|
||||
match target_status {
|
||||
JobStatusPlus::Pending => {}
|
||||
JobStatusPlus::Started => {}
|
||||
JobStatusPlus::CompleteSuccess => {
|
||||
info!("Matched job status complete_success");
|
||||
mark_generic_inference_job_successfully_done_by_token(
|
||||
&server_state.mysql_pool,
|
||||
&request.job_token,
|
||||
|
||||
@@ -33,8 +33,8 @@ bytes = { version = "1.10.0", features = ["serde"] }
|
||||
chrono = { version = "0.4.40", features = ["serde"] }
|
||||
clap = { version = "4.5.13", features = ["derive"] }
|
||||
cookie = { version = "0.18.1", default-features = false, features = ["percent-encode"] }
|
||||
core-foundation = { version = "0.9.4" }
|
||||
core-foundation-sys = { version = "0.8.7" }
|
||||
#core-foundation = { version = "0.9.4" }
|
||||
#core-foundation-sys = { version = "0.8.7" }
|
||||
crossbeam-utils = { version = "0.8.20" }
|
||||
crypto-common = { version = "0.1.6", default-features = false, features = ["std"] }
|
||||
#cudarc = { version = "0.13.7", features = ["cuda-version-from-build-system", "cudnn", "dynamic-linking", "f16", "nccl"] }
|
||||
|
||||
@@ -55,4 +55,13 @@ npm install
|
||||
2.import it first in code
|
||||
3.nx sync
|
||||
4.nx build
|
||||
|
||||
or
|
||||
|
||||
# frontend build
|
||||
in frontend folder
|
||||
|
||||
% nx build -project type
|
||||
% nx build editor3d
|
||||
% nx dev editor3d
|
||||
```
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
[build]
|
||||
command = "./apps/artcraft-website/script/netlify_build.sh"
|
||||
publish = "apps/artcraft-website/dist/"
|
||||
|
||||
[[headers]]
|
||||
for = "/build/*"
|
||||
|
||||
[headers.values]
|
||||
"Cache-Control" = "public, max-age=31560000, immutable"
|
||||
|
||||
# [[redirects]]
|
||||
# from = "/ingest/static/*"
|
||||
# to = "https://us-assets.i.posthog.com/static/:splat"
|
||||
# host = "us-assets.i.posthog.com"
|
||||
# status = 200
|
||||
# force = true
|
||||
#
|
||||
# [[redirects]]
|
||||
# from = "/ingest/*"
|
||||
# to = "https://us.i.posthog.com/:splat"
|
||||
# host = "us.i.posthog.com"
|
||||
# status = 200
|
||||
# force = true
|
||||
Binary file not shown.
Binary file not shown.
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
echo "Run build script (TODO: Make strict)"
|
||||
nx build artcraft-website
|
||||
|
||||
echo "Change to project dir"
|
||||
pushd apps/artcraft-website/
|
||||
|
||||
#echo "Copy netlify configs into dist"
|
||||
#cp _headers dist/
|
||||
#cp _redirects dist/
|
||||
|
||||
#echo "Copy netlify 404.html page into dist"
|
||||
#cp "404.html" dist/
|
||||
|
||||
echo "List final files in build"
|
||||
find dist/
|
||||
|
||||
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
import { useParams } from "react-router-dom";
|
||||
import { withProtectionRoute } from "~/components/hoc";
|
||||
import { useRenderCounter } from "~/hooks/useRenderCounter";
|
||||
import { Resource, invoke } from "@tauri-apps/api/core";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
//Components of the Konva App are all in the KonvaComponent
|
||||
import { KonvaRootComponent } from "~/KonvaRootComponent";
|
||||
import { ToolbarTopLeft } from "~/components/features/ToolbarTopLeft";
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Tooltip } from "~/components/reusable/Tooltip";
|
||||
import { Button } from "~/components/reusable/Button";
|
||||
import { ToggleButton } from "~/components/reusable/ToggleButton";
|
||||
import { Toaster } from "~/components/ui/Toast";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
import {
|
||||
faMessageXmark,
|
||||
faMessageCheck,
|
||||
@@ -49,13 +50,14 @@ export const PromptBox = () => {
|
||||
useSignals();
|
||||
const isDesktopApp = window.navigator.userAgent.includes("Tauri");
|
||||
console.log("Is this a desktop app?", isDesktopApp);
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [content, setContent] = useState("");
|
||||
const { jobTokens, addJobToken, removeJobToken, clearJobTokens } =
|
||||
useJobContext();
|
||||
|
||||
//const { lastRenderedBitmap } = useCanvasSignal();
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [isEnqueueing, setisEnqueueing] = useState(false);
|
||||
const [isEnqueueing, setIsEnqueueing] = useState(false);
|
||||
const [useSystemPrompt, setUseSystemPrompt] = useState(true);
|
||||
const [isGalleryModalOpen, setIsGalleryModalOpen] = useState(false);
|
||||
const [selectedGalleryImages, setSelectedGalleryImages] = useState<string[]>(
|
||||
@@ -317,10 +319,9 @@ export const PromptBox = () => {
|
||||
}
|
||||
};
|
||||
const handleEnqueue = async () => {
|
||||
if (isEnqueueing) return;
|
||||
setIsEnqueueing(true);
|
||||
if (!prompt.trim()) return;
|
||||
|
||||
setisEnqueueing(true);
|
||||
|
||||
try {
|
||||
console.log(
|
||||
"Enqueuing with prompt:",
|
||||
@@ -333,9 +334,9 @@ export const PromptBox = () => {
|
||||
console.log("Is this a desktop app?", isDesktop);
|
||||
|
||||
if (isDesktop) {
|
||||
handleTauriEnqueue();
|
||||
await handleTauriEnqueue();
|
||||
} else {
|
||||
handleWebEnqueue();
|
||||
await handleWebEnqueue();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during image generation:", error);
|
||||
@@ -343,7 +344,8 @@ export const PromptBox = () => {
|
||||
"An error occurred while generating the image. Please try again.",
|
||||
);
|
||||
} finally {
|
||||
setisEnqueueing(false);
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
setIsEnqueueing(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -363,6 +365,15 @@ export const PromptBox = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => {
|
||||
setIsModalOpen(false);
|
||||
setContent("");
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</Modal>
|
||||
<div className="absolute bottom-4 left-1/2 flex -translate-x-1/2 flex-col gap-3">
|
||||
{(referenceImages.length > 0 || uploadingImages.length > 0) && (
|
||||
<div className="flex w-full gap-2">
|
||||
|
||||
Binary file not shown.
@@ -116,12 +116,12 @@ export class EngineApi extends ApiManager {
|
||||
snapshot_media_token: string;
|
||||
additional_images?: string[];
|
||||
},
|
||||
{ success?: boolean; job_token?: string; BadInput?: string }
|
||||
{ success?: boolean; job_token?: string; error_reason?: string, failure_reason?: string}
|
||||
>({
|
||||
endpoint,
|
||||
body,
|
||||
});
|
||||
|
||||
console.log("postResponse", postResponse);
|
||||
// Check if the response is successful
|
||||
const isSuccess = postResponse.success ?? false;
|
||||
|
||||
@@ -129,7 +129,7 @@ export class EngineApi extends ApiManager {
|
||||
const result = {
|
||||
success: isSuccess,
|
||||
data: isSuccess ? postResponse.job_token : undefined,
|
||||
errorMessage: isSuccess ? undefined : postResponse.BadInput,
|
||||
errorMessage: isSuccess ? undefined : (postResponse.error_reason || postResponse.failure_reason),
|
||||
};
|
||||
|
||||
return result;
|
||||
|
||||
@@ -247,7 +247,7 @@ class Editor {
|
||||
// Recording params.
|
||||
this.render_timer = 0;
|
||||
this.fps_number = 60;
|
||||
this.cap_fps = 60;
|
||||
this.cap_fps = 120;
|
||||
// Timeline settings.
|
||||
this.can_playback = false;
|
||||
this.playback_location = 0;
|
||||
@@ -753,8 +753,11 @@ class Editor {
|
||||
}
|
||||
|
||||
// Store original renderer and camera state
|
||||
const originalWidth = this.renderer.domElement.width;
|
||||
const originalHeight = this.renderer.domElement.height;
|
||||
const sizeVector = new THREE.Vector2();
|
||||
this.renderer.getSize(sizeVector);
|
||||
|
||||
const originalWidth = sizeVector.x;
|
||||
const originalHeight = sizeVector.y;
|
||||
const originalPixelRatio = this.renderer.getPixelRatio();
|
||||
const originalCameraAspect = this.camera.aspect;
|
||||
const originalRenderCameraAspect =
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { EventDispatcher, Quaternion, Vector3, Vector2 } from "three";
|
||||
import {
|
||||
EventDispatcher,
|
||||
Quaternion,
|
||||
Vector3,
|
||||
Vector2,
|
||||
MathUtils,
|
||||
} from "three";
|
||||
import * as THREE from "three";
|
||||
import { cameras, selectedCameraId } from "~/pages/PageEnigma/signals/camera";
|
||||
import { isPromptBoxFocused } from "~/pages/PageEnigma/signals/promptBox";
|
||||
@@ -15,6 +21,11 @@ class FreeCam extends EventDispatcher {
|
||||
xOffset = 0;
|
||||
yOffset = 0;
|
||||
|
||||
// Smoothing properties
|
||||
smoothingFactor = 0.2;
|
||||
currentVelocity = new Vector3();
|
||||
lastPosition = new Vector3();
|
||||
|
||||
lastMousePosition = new Vector2();
|
||||
mouseVelocity = new Vector2();
|
||||
tmpQuaternion = new Quaternion();
|
||||
@@ -46,6 +57,7 @@ class FreeCam extends EventDispatcher {
|
||||
|
||||
this.object = object;
|
||||
this.domElement = domElement;
|
||||
this.lastPosition.copy(object.position);
|
||||
|
||||
this.keydown = this.keydown.bind(this);
|
||||
this.keyup = this.keyup.bind(this);
|
||||
@@ -168,6 +180,7 @@ class FreeCam extends EventDispatcher {
|
||||
this.dragging = true;
|
||||
this.xOffset = event.clientX;
|
||||
this.yOffset = event.clientY;
|
||||
this.resetVelocity();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,6 +191,7 @@ class FreeCam extends EventDispatcher {
|
||||
if (event.button === 2) {
|
||||
this.xOffset = event.clientX;
|
||||
this.yOffset = event.clientY;
|
||||
this.resetVelocity();
|
||||
}
|
||||
this.dragging = false;
|
||||
}
|
||||
@@ -191,9 +205,28 @@ class FreeCam extends EventDispatcher {
|
||||
const mouseY = event.clientY - this.yOffset;
|
||||
this.xOffset = event.clientX;
|
||||
this.yOffset = event.clientY;
|
||||
|
||||
if (Math.abs(mouseX + mouseY) > 0) {
|
||||
this.object.translateX(-mouseX * (this.movementSpeed * 0.01));
|
||||
this.object.translateY(mouseY * (this.movementSpeed * 0.01));
|
||||
// Store last position before movement
|
||||
this.lastPosition.copy(this.object.position);
|
||||
|
||||
// Apply movement with smoothing
|
||||
const translationX = -mouseX * (this.movementSpeed * 0.01);
|
||||
const translationY = mouseY * (this.movementSpeed * 0.01);
|
||||
|
||||
this.currentVelocity.x = MathUtils.lerp(
|
||||
this.currentVelocity.x,
|
||||
translationX,
|
||||
this.smoothingFactor,
|
||||
);
|
||||
this.currentVelocity.y = MathUtils.lerp(
|
||||
this.currentVelocity.y,
|
||||
translationY,
|
||||
this.smoothingFactor,
|
||||
);
|
||||
|
||||
this.object.translateX(this.currentVelocity.x);
|
||||
this.object.translateY(this.currentVelocity.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,7 +236,21 @@ class FreeCam extends EventDispatcher {
|
||||
return;
|
||||
}
|
||||
|
||||
this.object.translateZ(event.deltaY / 120); // 120 is the lowest mouse wheel rot.
|
||||
// Store last position before zoom
|
||||
this.lastPosition.copy(this.object.position);
|
||||
|
||||
const zoomDelta = event.deltaY / 120;
|
||||
this.currentVelocity.z = MathUtils.lerp(
|
||||
this.currentVelocity.z,
|
||||
zoomDelta,
|
||||
this.smoothingFactor,
|
||||
);
|
||||
this.object.translateZ(this.currentVelocity.z);
|
||||
}
|
||||
|
||||
resetVelocity() {
|
||||
this.currentVelocity.set(0, 0, 0);
|
||||
this.lastPosition.copy(this.object.position);
|
||||
}
|
||||
|
||||
reset() {
|
||||
@@ -217,25 +264,50 @@ class FreeCam extends EventDispatcher {
|
||||
this.moveState.yawRight = 0;
|
||||
this.moveState.down = 0;
|
||||
this.moveState.up = 0;
|
||||
|
||||
this.resetVelocity();
|
||||
|
||||
this.updateMovementVector();
|
||||
}
|
||||
|
||||
isStationary() {
|
||||
return this.moveVector.distanceTo(new Vector3(0, 0, 0)) === 0;
|
||||
return (
|
||||
this.moveVector.distanceTo(new Vector3(0, 0, 0)) === 0 &&
|
||||
this.currentVelocity.distanceTo(new Vector3(0, 0, 0)) < 0.0001
|
||||
);
|
||||
}
|
||||
|
||||
update(delta: number) {
|
||||
if (!this.enabled || this.isStationary()) {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip update if there's no movement and velocity
|
||||
if (this.isStationary()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const moveMulti = delta * this.movementSpeed;
|
||||
|
||||
this.object.translateX(this.moveVector.x * moveMulti);
|
||||
this.object.translateY(this.moveVector.y * moveMulti);
|
||||
this.object.translateZ(this.moveVector.z * moveMulti);
|
||||
// Apply WASD movement with smoothing
|
||||
const targetMovement = new Vector3(
|
||||
this.moveVector.x * moveMulti,
|
||||
this.moveVector.y * moveMulti,
|
||||
this.moveVector.z * moveMulti,
|
||||
);
|
||||
|
||||
// Update camera position in signals
|
||||
// Smooth out the movement
|
||||
this.currentVelocity.lerp(targetMovement, this.smoothingFactor);
|
||||
|
||||
// Apply the smoothed movement
|
||||
this.object.translateX(this.currentVelocity.x);
|
||||
this.object.translateY(this.currentVelocity.y);
|
||||
this.object.translateZ(this.currentVelocity.z);
|
||||
|
||||
// Store last position
|
||||
this.lastPosition.copy(this.object.position);
|
||||
|
||||
// Update camera signals
|
||||
if (selectedCameraId.value) {
|
||||
const pos = this.object.position;
|
||||
const rot = this.object.rotation;
|
||||
|
||||
@@ -292,6 +292,12 @@ export class MouseControls {
|
||||
if (this.selected) {
|
||||
this.selected.forEach((selected) => {
|
||||
this.deleteObject(selected.uuid);
|
||||
this.selected = [];
|
||||
if (this.kinMode === KinMode.FK) {
|
||||
this.toggleFKMode();
|
||||
}
|
||||
this.removeTransformControls();
|
||||
showPoseControls.value = false;
|
||||
});
|
||||
}
|
||||
return;
|
||||
@@ -352,11 +358,11 @@ export class MouseControls {
|
||||
}
|
||||
|
||||
if (event.shiftKey) {
|
||||
this.cameraViewControls.movementSpeed = 4;
|
||||
this.cameraViewControls.movementSpeed = 3;
|
||||
} else if (event.altKey) {
|
||||
this.cameraViewControls.movementSpeed = 0.1;
|
||||
} else {
|
||||
this.cameraViewControls.movementSpeed = 1.15;
|
||||
this.cameraViewControls.movementSpeed = 0.75;
|
||||
}
|
||||
|
||||
if (event.key === "Escape") {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { LoadingDots, TopBar } from "~/components";
|
||||
import { Controls3D } from "./comps/Controls3D";
|
||||
@@ -22,6 +23,7 @@ import { Outliner } from "./comps/Outliner";
|
||||
import { CameraAspectRatio } from "./enums";
|
||||
import { PromptBox } from "./comps/PromptBox";
|
||||
import { OnboardingHelper } from "./comps/OnboardingHelper";
|
||||
import { IsDesktopApp } from "@storyteller/tauri-utils";
|
||||
import { FocalLengthDisplay } from "./comps/FocalLengthDisplay/FocalLengthDisplay";
|
||||
import { DemoModal } from "@storyteller/ui-demo-modal";
|
||||
|
||||
@@ -157,7 +159,13 @@ export const PageEditor = () => {
|
||||
description="Set up your scene by adding objects and start bringing your ideas to life!"
|
||||
videoSrc="/resources/videos/artcraft-3d-demo.mp4"
|
||||
buttonText="Sign in to OpenAI to get started"
|
||||
buttonOnClick={() => {}}
|
||||
buttonOnClick={async () => {
|
||||
if (IsDesktopApp()) {
|
||||
await invoke("open_sora_login_command");
|
||||
} else {
|
||||
console.error("This is not the Desktop app.");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useState, useRef, useEffect, useContext } from "react";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { EngineContext } from "~/pages/PageEnigma/contexts/EngineContext";
|
||||
import { IsDesktopApp } from "@storyteller/tauri-utils";
|
||||
import { uploadImage } from "~/components/reusable/UploadModalMedia/uploadImage";
|
||||
import {
|
||||
faPlus,
|
||||
@@ -24,9 +26,11 @@ import {
|
||||
} from "@fortawesome/pro-regular-svg-icons";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { PopoverItem, PopoverMenu } from "~/components/reusable/Popover";
|
||||
import { Button, ToggleButton, Tooltip } from "~/components";
|
||||
import { CameraAspectRatio } from "~/pages/PageEnigma/enums";
|
||||
|
||||
import {
|
||||
cameraAspectRatio,
|
||||
disableHotkeyInput,
|
||||
@@ -35,9 +39,11 @@ import {
|
||||
gridVisibility,
|
||||
setGridVisibility,
|
||||
} from "~/pages/PageEnigma/signals";
|
||||
|
||||
import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames";
|
||||
import Queue from "~/pages/PageEnigma/Queue/Queue";
|
||||
import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions";
|
||||
|
||||
import {
|
||||
cameras,
|
||||
selectedCameraId,
|
||||
@@ -55,17 +61,22 @@ interface ReferenceImage {
|
||||
file: File;
|
||||
mediaToken: string;
|
||||
}
|
||||
|
||||
import { EngineApi } from "~/Classes/ApiManager/EngineApi";
|
||||
import { UploaderStates } from "~/enums/UploaderStates";
|
||||
import { CameraSettingsModal } from "../CameraSettingsModal";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { GalleryModal, GalleryItem } from "@storyteller/ui-gallery-modal";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
|
||||
export const PromptBox = () => {
|
||||
useSignals();
|
||||
const editorEngine = useContext(EngineContext);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const [content, setContent] = useState<React.ReactNode>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [isEnqueueing, setisEnqueueing] = useState(false);
|
||||
const [useSystemPrompt, setUseSystemPrompt] = useState(true);
|
||||
@@ -417,7 +428,23 @@ export const PromptBox = () => {
|
||||
setPrompt(e.target.value);
|
||||
};
|
||||
|
||||
const handleError = (errorMessage: string) => {
|
||||
setContent(errorMessage);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleEnqueue = async () => {
|
||||
const isDesktop = IsDesktopApp();
|
||||
console.log("Is this a desktop app?", isDesktop);
|
||||
|
||||
if (isDesktop) {
|
||||
await handleTauriEnqueue();
|
||||
} else {
|
||||
await handleWebEnqueue();
|
||||
}
|
||||
};
|
||||
|
||||
const handleWebEnqueue = async () => {
|
||||
if (!prompt.trim()) return;
|
||||
|
||||
setisEnqueueing(true);
|
||||
@@ -427,6 +454,7 @@ export const PromptBox = () => {
|
||||
|
||||
const engineApi = new EngineApi();
|
||||
const snapshot = editorEngine.snapShotOfCurrentFrame(false);
|
||||
|
||||
if (snapshot) {
|
||||
const snapshotResult = await engineApi.uploadSceneSnapshot({
|
||||
screenshot: snapshot.file,
|
||||
@@ -434,12 +462,83 @@ export const PromptBox = () => {
|
||||
});
|
||||
|
||||
console.log("useSystemPrompt", useSystemPrompt);
|
||||
await engineApi.enqueueImageGeneration({
|
||||
|
||||
const response = await engineApi.enqueueImageGeneration({
|
||||
disableSystemPrompt: !useSystemPrompt,
|
||||
prompt: prompt,
|
||||
snapshotMediaToken: snapshotResult.data || "",
|
||||
additionalImages: referenceImages.map((image) => image.mediaToken),
|
||||
});
|
||||
|
||||
console.log("response", response);
|
||||
|
||||
if (response.errorMessage) {
|
||||
handleError(response.errorMessage);
|
||||
setisEnqueueing(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Here we would pass both the prompt and reference images to the generation
|
||||
console.log(
|
||||
"Enqueuing with prompt:",
|
||||
prompt,
|
||||
"and reference images:",
|
||||
referenceImages,
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
} finally {
|
||||
setisEnqueueing(false);
|
||||
toast.success("Image added to queue");
|
||||
}
|
||||
}
|
||||
setisEnqueueing(false);
|
||||
};
|
||||
|
||||
const handleTauriEnqueue = async () => {
|
||||
if (!prompt.trim()) return;
|
||||
|
||||
setisEnqueueing(true);
|
||||
|
||||
if (editorEngine) {
|
||||
editorEngine.positive_prompt = prompt;
|
||||
|
||||
const engineApi = new EngineApi();
|
||||
const snapshot = editorEngine.snapShotOfCurrentFrame(false);
|
||||
|
||||
if (snapshot) {
|
||||
const snapshotResult = await engineApi.uploadSceneSnapshot({
|
||||
screenshot: snapshot.file,
|
||||
//sceneMediaToken: "",
|
||||
});
|
||||
|
||||
//const response = await engineApi.enqueueImageGeneration({
|
||||
// disableSystemPrompt: !useSystemPrompt,
|
||||
// prompt: prompt,
|
||||
// snapshotMediaToken: "",
|
||||
// additionalImages: referenceImages.map((image) => image.mediaToken),
|
||||
//});
|
||||
|
||||
const generateResponse = await invoke("sora_image_remix_command", {
|
||||
request: {
|
||||
snapshot_media_token: snapshotResult.data!,
|
||||
disable_system_prompt: !useSystemPrompt,
|
||||
prompt: prompt,
|
||||
maybe_additional_images: referenceImages.map(
|
||||
(image) => image.mediaToken,
|
||||
),
|
||||
maybe_number_of_samples: 1,
|
||||
},
|
||||
});
|
||||
|
||||
//console.log("response", generateResponse);
|
||||
|
||||
//if (generateResponse.errorMessage) {
|
||||
// handleError(response.errorMessage);
|
||||
// setisEnqueueing(false);
|
||||
// return;
|
||||
//}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -462,7 +561,7 @@ export const PromptBox = () => {
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleEnqueue();
|
||||
handleWebEnqueue();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -503,6 +602,15 @@ export const PromptBox = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => {
|
||||
setIsModalOpen(false);
|
||||
setContent("");
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</Modal>
|
||||
<div className="absolute bottom-4 left-1/2 flex -translate-x-1/2 flex-col gap-3">
|
||||
{(referenceImages.length > 0 || uploadingImages.length > 0) && (
|
||||
<div className="flex w-full gap-2">
|
||||
|
||||
+11
-11
@@ -1,9 +1,9 @@
|
||||
import { useCallback, useEffect, useState, useRef } from "react";
|
||||
import { useCallback, useState, useRef } from "react";
|
||||
import { FetchStatus } from "~/pages/PageEnigma/enums";
|
||||
import { FetchMediaItemStates, fetchUserMediaItems } from "../utilities";
|
||||
import { FilterEngineCategories, FilterMediaType } from "~/enums";
|
||||
|
||||
import { MAX_FAILED_FETCHES } from "~/constants";
|
||||
// import { MAX_FAILED_FETCHES } from "~/constants";
|
||||
|
||||
interface useUserObjectsProps {
|
||||
defaultErrorMessage: string;
|
||||
@@ -74,15 +74,15 @@ export const useUserObjects = (props: useUserObjectsProps) => {
|
||||
[props],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
(firstFetch.current === FetchStatus.READY ||
|
||||
firstFetch.current === FetchStatus.ERROR) &&
|
||||
failedFetches.current <= MAX_FAILED_FETCHES
|
||||
) {
|
||||
fetchUserObjects();
|
||||
}
|
||||
}, [fetchUserObjects]);
|
||||
// useEffect(() => {
|
||||
// if (
|
||||
// (firstFetch.current === FetchStatus.READY ||
|
||||
// firstFetch.current === FetchStatus.ERROR) &&
|
||||
// failedFetches.current <= MAX_FAILED_FETCHES
|
||||
// ) {
|
||||
// fetchUserObjects();
|
||||
// }
|
||||
// }, [fetchUserObjects]);
|
||||
|
||||
return {
|
||||
userObjects,
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": ["env.d.ts", "**/*.ts", "**/*.tsx", "netlify-edge-plugin.ts"],
|
||||
"include": [
|
||||
"env.d.ts",
|
||||
"**/*.js",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"netlify-edge-plugin.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"lib": [
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
"ESNext"
|
||||
],
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react-jsx",
|
||||
@@ -15,7 +25,9 @@
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"paths": {
|
||||
"~/*": ["apps/editor3d/app/src/*"]
|
||||
"~/*": [
|
||||
"apps/editor3d/app/src/*"
|
||||
]
|
||||
},
|
||||
// Vite takes care of building everything, not tsc.
|
||||
"noEmit": true
|
||||
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
root_dir=$(pwd)
|
||||
frontend_path="${root_dir}/frontend"
|
||||
rust_crate_path="${root_dir}/crates/desktop/tauri-artcraft"
|
||||
|
||||
# The tauri dev server integration is super annoying: It eats ctrl-c interrupts,
|
||||
# it decoheres the and corrupts terminal output, and it's slow. This configuration
|
||||
# allows us to start without it. Simply launch `nx` and the dev server as a separate
|
||||
# process and leave tauri out of the loop
|
||||
config_path="${rust_crate_path}/tauri.artcraft_3d.no_dev.conf.toml"
|
||||
|
||||
export TAURI_FRONTEND_PATH="${frontend_path}"
|
||||
export TAURI_APP_PATH="${rust_crate_path}"
|
||||
|
||||
cargo tauri dev --no-dev-server \
|
||||
--no-dev-server-wait \
|
||||
--no-watch \
|
||||
--config "${config_path}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user