Compare commits

...
48 changed files with 606 additions and 272 deletions
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE tasks\n SET task_status = \"cancelled_by_user\"\n WHERE task_status NOT IN (\n \"complete_success\",\n \"complete_failure\",\n \"dead\",\n \"cancelled_by_user\",\n \"cancelled_by_provider\",\n \"cancelled_by_us\"\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
},
"hash": "30272128d83543d9fbfca930b7cdd4af30adfb4d90f2de044463a111c6340495"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE tasks\n SET is_dismissed_by_user = 1\n WHERE is_dismissed_by_user != 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
},
"hash": "f83018ec5493982659a56576296914f937bb4e75db3a522eb4bda486d5caa402"
}
@@ -1,6 +1,6 @@
use crate::api::api_types::run_object_id::RunObjectId;
use crate::api::api_types::upload_object_id::UploadObjectId;
use crate::api::api_types::upload_mime_type::UploadMimeType;
use crate::api::api_types::upload_object_id::UploadObjectId;
use crate::api::common::common_header_values::{ORIGIN_VALUE, REFERER_VALUE};
use crate::credentials::world_labs_bearer_token::WorldLabsBearerToken;
use crate::credentials::world_labs_cookies::WorldLabsCookies;
@@ -25,7 +25,10 @@ use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CACH
use wreq::Client;
use wreq_util::Emulation;
const BASE_URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects";
const BASE_URL : &str = "https://api.worldlabs.ai/api/v1/objects";
// Note: WorldLabs is phasing out the old URL scheme:
// const BASE_URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects";
fn get_url(upload_id: &UploadObjectId) -> String {
format!("{}/{}:upload", BASE_URL, upload_id.0)
@@ -24,7 +24,10 @@ use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CACH
use wreq::Client;
use wreq_util::Emulation;
const URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects";
const URL : &str = "https://api.worldlabs.ai/api/v1/objects";
// Note: WorldLabs is phasing out the old URL scheme:
// const URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects";
pub struct CreateRunObjectArgs<'a> {
pub cookies: &'a WorldLabsCookies,
@@ -1,6 +1,6 @@
use crate::api::api_types::run_object_id::RunObjectId;
use crate::api::api_types::upload_object_id::UploadObjectId;
use crate::api::api_types::upload_mime_type::UploadMimeType;
use crate::api::api_types::upload_object_id::UploadObjectId;
use crate::api::common::common_header_values::{ORIGIN_VALUE, REFERER_VALUE};
use crate::credentials::world_labs_bearer_token::WorldLabsBearerToken;
use crate::credentials::world_labs_cookies::WorldLabsCookies;
@@ -25,7 +25,10 @@ use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CACH
use wreq::Client;
use wreq_util::Emulation;
const URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects";
const URL : &str = "https://api.worldlabs.ai/api/v1/objects";
// Note: WorldLabs is phasing out the old URL scheme:
// const URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects";
pub struct CreateUploadObjectArgs<'a> {
pub cookies: &'a WorldLabsCookies,
@@ -1,6 +1,6 @@
use crate::api::api_types::run_object_id::RunObjectId;
use crate::api::api_types::upload_object_id::UploadObjectId;
use crate::api::api_types::upload_mime_type::UploadMimeType;
use crate::api::api_types::upload_object_id::UploadObjectId;
use crate::api::common::common_header_values::{ORIGIN_VALUE, REFERER_VALUE};
use crate::credentials::world_labs_bearer_token::WorldLabsBearerToken;
use crate::credentials::world_labs_cookies::WorldLabsCookies;
@@ -25,7 +25,10 @@ use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CACH
use wreq::Client;
use wreq_util::Emulation;
const BASE_URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects";
const BASE_URL : &str = "https://api.worldlabs.ai/api/v1/objects";
// Note: WorldLabs is phasing out the old URL scheme:
// const BASE_URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects";
fn get_url(upload_id: &UploadObjectId) -> String {
format!("{}/{}:complete", BASE_URL, upload_id.0)
@@ -131,12 +134,12 @@ struct RawResponse {
#[cfg(test)]
mod tests {
use log::LevelFilter;
use crate::api::api_types::upload_object_id::UploadObjectId;
use crate::api::requests::objects::finalize_object_image_upload::{finalize_object_image_upload, get_url, FinalizeObjectImageUploadArgs};
use crate::test_utils::get_test_bearer_token::get_test_bearer_token;
use crate::test_utils::get_test_cookies::get_typed_test_cookies;
use crate::test_utils::setup_test_logging::setup_test_logging;
use log::LevelFilter;
#[test]
fn test_get_url() {
@@ -1,9 +1,9 @@
use crate::api::api_types::image_input_object_id::ImageInputObjectId;
use crate::api::api_types::meta_world_object_id::MetaWorldObjectId;
use crate::api::api_types::pano_object_id::PanoObjectId;
use crate::api::api_types::run_object_id::RunObjectId;
use crate::api::api_types::upload_mime_type::UploadMimeType;
use crate::api::api_types::upload_object_id::UploadObjectId;
use crate::api::api_types::meta_world_object_id::MetaWorldObjectId;
use crate::api::common::common_header_values::{ORIGIN_VALUE, REFERER_VALUE};
use crate::api::utils::upload_id_to_image_url::upload_id_to_image_url;
use crate::credentials::world_labs_bearer_token::WorldLabsBearerToken;
@@ -31,7 +31,10 @@ use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CACH
use wreq::Client;
use wreq_util::Emulation;
const BASE_URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects";
const BASE_URL : &str = "https://api.worldlabs.ai/api/v1/objects";
// Note: WorldLabs is phasing out the old URL scheme:
// const BASE_URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects";
fn get_url(run_id: &RunObjectId) -> String {
format!("{}/{}", BASE_URL, run_id.0)
@@ -398,9 +401,9 @@ struct RawResponse {
#[cfg(test)]
mod tests {
use crate::api::api_types::image_input_object_id::ImageInputObjectId;
use crate::api::api_types::meta_world_object_id::MetaWorldObjectId;
use crate::api::api_types::pano_object_id::PanoObjectId;
use crate::api::api_types::run_object_id::RunObjectId;
use crate::api::api_types::meta_world_object_id::MetaWorldObjectId;
use crate::api::requests::objects::update_run_object_with_upload::{RawRequest, UpdateRunObjectWithUploadPayloadArgs};
#[test]
@@ -31,8 +31,10 @@ use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CACH
use wreq::Client;
use wreq_util::Emulation;
use crate::api::api_types::world_id::WorldObjectId;
const BASE_URL : &str = "https://api.worldlabs.ai/api/v1/objects";
const BASE_URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects";
// Note: WorldLabs is phasing out the old URL scheme:
// const BASE_URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects";
fn get_url(run_id: &RunObjectId) -> String {
format!("{}/{}", BASE_URL, run_id.0)
@@ -1,6 +1,6 @@
use crate::api::api_types::run_object_id::RunObjectId;
use crate::api::api_types::upload_object_id::UploadObjectId;
use crate::api::api_types::upload_mime_type::UploadMimeType;
use crate::api::api_types::upload_object_id::UploadObjectId;
use crate::api::common::common_header_values::{ORIGIN_VALUE, REFERER_VALUE};
use crate::api::utils::upload_id_to_image_url::upload_id_to_image_url;
use crate::credentials::world_labs_bearer_token::WorldLabsBearerToken;
@@ -24,7 +24,10 @@ use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CACH
use wreq::Client;
use wreq_util::Emulation;
const URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/recaption2";
const URL : &str = "https://api.worldlabs.ai/api/v1/recaption2";
// Note: WorldLabs is phasing out the old URL scheme:
// const URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/recaption2";
pub struct RecaptionImageArgs<'a> {
pub cookies: &'a WorldLabsCookies,
@@ -27,7 +27,10 @@ use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CACH
use wreq::Client;
use wreq_util::Emulation;
const URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/worlds";
const URL : &str = "https://api.worldlabs.ai/api/v1/worlds";
// Note: WorldLabs is phasing out the old URL scheme:
//const URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/worlds";
pub struct CreateWorldArgs<'a> {
pub cookies: &'a WorldLabsCookies,
@@ -18,7 +18,7 @@ use http_headers::values::pragma::PRAGMA_NO_CACHE;
use http_headers::values::priority::PRIORITY_4;
use http_headers::values::sec::{SEC_FETCH_DEST_EMPTY, SEC_FETCH_MODE_CORS, SEC_FETCH_SITE_CROSS_SITE};
use http_headers::values::te::TE_TRAILERS;
use log::{debug, error};
use log::{debug, error, info};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use uuid::Uuid;
@@ -26,7 +26,10 @@ use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CACH
use wreq::Client;
use wreq_util::Emulation;
const BASE_URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/worlds";
const BASE_URL : &str = "https://api.worldlabs.ai/api/v1/worlds";
// Note: WorldLabs is phasing out the old URL scheme:
// const BASE_URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/worlds";
fn get_url(world_id: &WorldObjectId) -> String {
format!("{}/{}", BASE_URL, world_id.0)
@@ -55,7 +58,7 @@ pub async fn poll_world_status(args: PollWorldStatusArgs<'_>) -> Result<PollWorl
let url = get_url(args.world_id);
debug!("Requesting URL: {}", url);
info!("Polling World: {}", url);
let mut request_builder = client.get(url)
.header(ACCEPT, ACCEPT_ALL)
@@ -169,7 +172,30 @@ mod tests {
let cookies = get_typed_test_cookies().unwrap();
let bearer_token = get_test_bearer_token().unwrap();
let world_id = WorldObjectId("d60cfa21-5506-43de-9a9c-0707fc17a5ec".to_string());
let world_id = WorldObjectId("403c379b-b7d2-4449-8f1e-ebdb56142604".to_string());
let response = poll_world_status(PollWorldStatusArgs {
cookies: &cookies,
bearer_token: &bearer_token,
world_id: &world_id,
request_timeout: None,
}).await.unwrap();
println!("Is complete: {}", response.is_complete);
println!("Spz Url: {}", response.spz_splat_url.unwrap_or_else(|| "incomplete".to_string()));
assert_eq!(1, 2);
}
#[tokio::test]
#[ignore] // Client side tests only
async fn test_request_2() {
setup_test_logging(LevelFilter::Debug);
let cookies = get_typed_test_cookies().unwrap();
let bearer_token = get_test_bearer_token().unwrap();
let world_id = WorldObjectId("e739b11e-6039-49c7-b73d-b79e1b28e295".to_string());
let response = poll_world_status(PollWorldStatusArgs {
cookies: &cookies,
@@ -277,12 +277,10 @@ pub async fn upload_image_and_create_world_with_retry(args: UploadImageAndCreate
#[cfg(test)]
mod tests {
use crate::credentials::world_labs_bearer_token::WorldLabsBearerToken;
use crate::credentials::world_labs_cookies::WorldLabsCookies;
use crate::credentials::worldlabs_refresh_token::WorldLabsRefreshToken;
use crate::recipes::upload_image_and_create_world_with_retry::{upload_image_and_create_world_with_retry, FileBytesOrPath, UploadImageAndCreateWorldWithRetryArgs};
use crate::test_utils::get_test_bearer_token::get_test_bearer_token;
use crate::test_utils::get_test_cookies::get_typed_test_cookies;
use crate::test_utils::get_test_refresh_token::get_typed_test_refresh_token;
use crate::test_utils::setup_test_logging::setup_test_logging;
use filesys::file_read_bytes::file_read_bytes;
use log::LevelFilter;
@@ -294,10 +292,10 @@ mod tests {
let cookies = get_typed_test_cookies().unwrap();
let bearer_token = get_test_bearer_token().unwrap();
let refresh_token = WorldLabsRefreshToken::new("AMf-vBy91tuAOoeCwbXV3zNcFAM79ayEKb_cRmTBhVLAIRB1kDzswDgenVnxHfgbmUP95YTlJkpSwJUK3eTZv0iV1An-rZUVII8tOoyLNpV-O1dhytsMXFjR5Mc_hRxUhwuRphUZT6Zub9T1Twd8OL88twFnN-qlwq12W_eRZnEC_IExX0-j6vhfRbVXY0Bf9F1jddeaN7FMgagVzlIt7c7hbboR3XRlU61oBhuEw90ynHWPhDrnTVBtT9xI_TrNFr-v6mMh3M1DGFN9iaHVl74fEwbV6VanUGua55uxdVgia-SyOkYboKRGanWof2g69vZNvREdlMiNFYfWDtsHgcd_DaS6oemS5CVoSbK8sRREUAXHRCmwO5OyZehacnKstyy0TnNwZuvtn_KMzpk3ZmLM-Q-yERlJvpd9rHU-d-uV2R-yZIWgT3rshE9WyK9i4p731RgXfEC6".to_string());
let refresh_token = get_typed_test_refresh_token().unwrap();
//let file_path = "/home/bt/Pictures/locations/island.jpg";
let file_path = "/Users/bt/Pictures/Midjourney/dusk_house.jpeg";
let file_path = "/Users/bt/Pictures/Midjourney/jeep.jpeg";
let file_bytes = file_read_bytes(file_path).unwrap();
println!("File bytes len: {}", file_bytes.len());
@@ -310,8 +308,12 @@ mod tests {
file: FileBytesOrPath::Bytes(file_bytes),
}).await.unwrap();
println!("Upload URL: {}", results.image_upload_url);
println!("Object ID: {}", results.run_id.0);
println!("World ID: {}", results.world_id.0);
println!("Upload URL: {}", results.image_upload_url);
println!("Has new access tokens: {}", results.maybe_new_access_tokens.is_some());
assert_eq!(1, 2);
}
@@ -320,16 +322,9 @@ mod tests {
async fn test_requests_2() {
setup_test_logging(LevelFilter::Debug);
// bt@brand.io
let cookies = "";
let bearer_token = "eyJhbGciOiJSUzI1NiIsImtpZCI6Ijk4OGQ1YTM3OWI3OGJkZjFlNTBhNDA5MTEzZjJiMGM3NWU0NTJlNDciLCJ0eXAiOiJKV1QifQ.eyJuYW1lIjoiQnJhbmRvbiBUaG9tYXMiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EvQUNnOG9jS0lucl8yWGJ2Z1VERUxNOEpCTGdfa2tKbVpxWl9WcTlzZkI2bVR4VmQzc1NJRk4tST1zOTYtYyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS93bHQtdHJhaW5pbmctZ3NjIiwiYXVkIjoid2x0LXRyYWluaW5nLWdzYyIsImF1dGhfdGltZSI6MTc2MDE3Nzk3OCwidXNlcl9pZCI6IjA1c0dhaHRySWdVMjVMaXR5NjhoZjRpMmV4SjIiLCJzdWIiOiIwNXNHYWh0cklnVTI1TGl0eTY4aGY0aTJleEoyIiwiaWF0IjoxNzY2MTI2ODg4LCJleHAiOjE3NjYxMzA0ODgsImVtYWlsIjoiYnRAYnJhbmQuaW8iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6eyJnb29nbGUuY29tIjpbIjEwNjEwODI2NzgzNTYyOTEwMTc1OSJdLCJlbWFpbCI6WyJidEBicmFuZC5pbyJdfSwic2lnbl9pbl9wcm92aWRlciI6Imdvb2dsZS5jb20ifX0.YLXZHg0rzMzyhhyJsO3DSMhU8GjnxtQtg1GiDwaxZjuybmabqOcR_b4C2lE2XwESg_dDYnIkmdas9wsHshy2bGPCAAtrinQpxTHWhIDxy0bsF8yW0BCOszwVseiTkXd6vveaGYH06EvH-nM4Q5AB_oKbsgRyMEbXhepvX7SlrmeaXxqldCLRalDj6gcEIY_TSwMmjnZ6-sFMPe92YjLY1ITPRx7S3YyWNwBG6mVpgoHcHudNAJ4TFxborZlhC1zpF0NDgtIhN5ITg0I1P0A-JTaBTJHukQwCPKhifFRA1kfcxzbAoZyPiPw2DdgXpDDnNzXUYgnaOOVyGs96Tx8hOQ";
let refresh_token = "AMf-vByUgA0J9S93vULECe-sD50cbR85AFVYF60gKAmLFF1MBYwXlKRJCsv7z3oSpLtP0ApyOU5fAl52qHWM2U0yKgEu5gQ5UCUDt5SEG4UBRHWvHEmLLcu-Zl04Fq4ljocSEO3qJzuX1wyshmptZxlDPRkjdLYgyn-Kp03woI2yRPKLuQF5hQtABHabsNUh9Zhs129sgxuUlOuw4zsFI4L8UdE3bEuF3k6Mic7KlE3440YwyQ8Qtmk9zHtXIx2ob56N-1WTGSqwMUuQ1mX4OOvLve9bw2zDW3UzchomTWkh732886RPrP-DNRkPbO3FejUEMxuYQFWYX3EEeIEdfNGXFNGPL1dNniSXlSaKadpX699Ishjviv-x9o-pPQa3zoAj3NzWLyd8mn5rDBh9qAO_iDETaeo01Sjhb4mGYfAWZ-g4S6nW778";
//let cookies = get_typed_test_cookies().unwrap();
let bearer_token = WorldLabsBearerToken::new(bearer_token.to_string());
let refresh_token = WorldLabsRefreshToken::new(refresh_token.to_string());
let cookies = WorldLabsCookies::new(cookies.to_string());
let cookies = get_typed_test_cookies().unwrap();
let bearer_token = get_test_bearer_token().unwrap();
let refresh_token = get_typed_test_refresh_token().unwrap();
//let file_path = "/home/bt/Pictures/locations/island.jpg";
let file_path = "/Users/bt/Pictures/Midjourney/train_ghibli.jpeg";
@@ -4,7 +4,7 @@ use std::fs::read_to_string;
#[cfg(test)]
pub (crate) fn get_test_bearer_token_raw() -> AnyhowResult<String> {
let cookies = read_to_string("/Users/bt/Artcraft/credentials/world_labs_bearer.txt")?;
let cookies = read_to_string("/Users/bt/Artcraft/credentials/worldlabs_bearer.txt")?;
//let cookies = read_to_string("/home/bt/Artcraft/credentials/world_labs_bearer.txt")?;
let cookies = cookies.trim().to_string();
Ok(cookies)
@@ -4,8 +4,8 @@ use std::fs::read_to_string;
#[cfg(test)]
pub (crate) fn get_test_cookies() -> AnyhowResult<String> {
//let cookies = read_to_string("/home/bt/Artcraft/credentials/world_labs_cookies.txt")?;
let cookies = read_to_string("/Users/bt/Artcraft/credentials/world_labs_cookies.txt")?;
//let cookies = read_to_string("/home/bt/Artcraft/credentials/worldlabs_cookies.txt")?;
let cookies = read_to_string("/Users/bt/Artcraft/credentials/worldlabs_cookies.txt")?;
let cookies = cookies.trim().to_string();
Ok(cookies)
}
@@ -0,0 +1,17 @@
use crate::credentials::worldlabs_refresh_token::WorldLabsRefreshToken;
use errors::AnyhowResult;
use std::fs::read_to_string;
#[cfg(test)]
pub (crate) fn get_test_refresh_token() -> AnyhowResult<String> {
//let cookies = read_to_string("/home/bt/Artcraft/credentials/worldlabs_cookies.txt")?;
let token = read_to_string("/Users/bt/Artcraft/credentials/worldlabs_refresh.txt")?;
let token = token.trim().to_string();
Ok(token)
}
#[cfg(test)]
pub (crate) fn get_typed_test_refresh_token() -> AnyhowResult<WorldLabsRefreshToken> {
let token = get_test_refresh_token()?;
Ok(WorldLabsRefreshToken::new(token))
}
@@ -1,3 +1,4 @@
pub mod get_test_cookies;
pub mod get_test_bearer_token;
pub mod setup_test_logging;
pub mod setup_test_logging;
pub mod get_test_refresh_token;
@@ -1,2 +1,3 @@
pub mod get_task_queue_command;
pub mod mark_task_as_dismissed_command;
pub mod mark_task_as_dismissed_command;
pub mod tasks_nuke_all_command;
@@ -0,0 +1,61 @@
use crate::core::commands::response::shorthand::ResponseOrErrorMessage;
use crate::core::commands::response::success_response_wrapper::SerializeMarker;
use crate::core::events::generation_events::generation_enqueue_success_event::GenerationEnqueueSuccessEvent;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::provider_priority::{Provider, ProviderPriorityStore};
use crate::core::state::task_database::TaskDatabase;
use crate::services::sora::state::sora_task_queue::SoraTaskQueue;
use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager;
use chrono::{DateTime, Utc};
use enums::common::generation_provider::GenerationProvider;
use enums::tauri::tasks::task_model_type::TaskModelType;
use enums::tauri::tasks::task_status::TaskStatus;
use enums::tauri::tasks::task_type::TaskType;
use errors::AnyhowResult;
use log::{error, info, warn};
use serde_derive::{Deserialize, Serialize};
use sqlite_tasks::queries::dismiss_all_tasks::dismiss_all_tasks;
use sqlite_tasks::queries::list_tasks_for_frontend::list_tasks_for_frontend;
use sqlite_tasks::queries::mark_task_as_dismissed::mark_task_as_dismissed;
use sqlite_tasks::queries::nuke_all_tasks::nuke_all_tasks;
use storyteller_client::endpoints::media_files::delete_media_file::delete_media_file;
use tauri::{AppHandle, State};
use tokens::tokens::media_files::MediaFileToken;
use tokens::tokens::sqlite::tasks::TaskId;
#[derive(Serialize)]
pub struct TasksNukeAllResponse {
success: bool,
}
impl SerializeMarker for TasksNukeAllResponse {}
#[tauri::command]
pub async fn tasks_nuke_all_command(
task_database: State<'_, TaskDatabase>,
) -> ResponseOrErrorMessage<TasksNukeAllResponse> {
info!("tasks_nuke_all_command called");
let result = handle_request(
&task_database,
).await;
if let Err(err) = &result {
error!("tasks_nuke_all_command failed: {:?}", err);
return Err("tasks_nuke_all_command failed".into())
}
Ok(TasksNukeAllResponse{
success: true,
}.into())
}
pub async fn handle_request(
task_database: &TaskDatabase,
) -> AnyhowResult<()> {
let _result = nuke_all_tasks(task_database.get_connection()).await?;
let _result = dismiss_all_tasks(task_database.get_connection()).await?;
Ok(())
}
@@ -69,4 +69,12 @@ impl AppCredentialsDir {
pub fn get_worldlabs_cookies_path(&self) -> PathBuf {
self.path.join("worldlabs_cookies.txt")
}
pub fn get_worldlabs_bearer_path(&self) -> PathBuf {
self.path.join("worldlabs_bearer.txt")
}
pub fn get_worldlabs_refresh_path(&self) -> PathBuf {
self.path.join("worldlabs_refresh.txt")
}
}
+2
View File
@@ -21,6 +21,7 @@ use crate::core::commands::providers::get_provider_order_command::get_provider_o
use crate::core::commands::providers::set_provider_order_command::set_provider_order_command;
use crate::core::commands::task_queue::get_task_queue_command::get_task_queue_command;
use crate::core::commands::task_queue::mark_task_as_dismissed_command::mark_task_as_dismissed_command;
use crate::core::commands::task_queue::tasks_nuke_all_command::tasks_nuke_all_command;
use crate::core::lifecycle::startup::handle_tauri_startup::handle_tauri_startup;
use crate::core::lifecycle::startup::setup_main_window::setup_main_window;
use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs;
@@ -221,6 +222,7 @@ pub fn run() {
storyteller_open_customer_portal_cancel_plan_command,
storyteller_open_customer_portal_manage_plan_command,
storyteller_open_customer_portal_switch_plan_command,
tasks_nuke_all_command,
storyteller_open_customer_portal_update_payment_method_command,
storyteller_open_subscription_purchase_command,
storyteller_purge_credentials_command,
@@ -192,8 +192,12 @@ impl WorldlabsCredentialManager {
.map(|cookies| cookies.to_serializable()),
user_id: None, // TODO
user_email: None, // TODO
bearer_token: creds.world_labs_bearer_token.map(|token| token.to_raw_string()),
refresh_token: creds.world_labs_refresh_token.map(|token| token.to_raw_string()),
bearer_token: creds.world_labs_bearer_token
.as_ref()
.map(|token| token.to_raw_string()),
refresh_token: creds.world_labs_refresh_token
.as_ref()
.map(|token| token.to_raw_string()),
};
let path = self.app_data_root.credentials_dir().get_worldlabs_state_path();
@@ -202,10 +206,25 @@ impl WorldlabsCredentialManager {
std::fs::write(path, serialized)?;
// TODO: This is just for building the client and testing.
if let Some(cookies) = creds.browser_cookies.as_ref() {
let cookies_header = cookies.to_cookie_string();
let path = self.app_data_root.credentials_dir().get_worldlabs_cookies_path();
std::fs::write(path, cookies_header)?;
{
if let Some(cookies) = creds.browser_cookies.as_ref() {
let cookies_header = cookies.to_cookie_string();
let path = self.app_data_root.credentials_dir().get_worldlabs_cookies_path();
std::fs::write(path, cookies_header)?;
}
// TODO: This is just for building the client and testing.
if let Some(bearer) = creds.world_labs_bearer_token.as_ref() {
let value = bearer.to_raw_string();
let path = self.app_data_root.credentials_dir().get_worldlabs_bearer_path();
std::fs::write(path, value)?;
}
if let Some(refresh) = creds.world_labs_refresh_token.as_ref() {
let value = refresh.to_raw_string();
let path = self.app_data_root.credentials_dir().get_worldlabs_refresh_path();
std::fs::write(path, value)?;
}
}
Ok(())
@@ -0,0 +1,13 @@
{
"build": {
"frontendDist": "../../../frontend/apps/artcraft/dist",
"beforeDevCommand": "echo 'this is static development; no live reload'",
"beforeBuildCommand": "echo 'this is static development; no live reload'",
"devUrl": null
},
"app": {
"withGlobalTauri": true,
"windows": [
]
}
}
@@ -0,0 +1,19 @@
use crate::connection::TaskDbConnection;
use crate::error::SqliteTasksError;
/// Mark everything as dismissed.
pub async fn dismiss_all_tasks(
db: &TaskDbConnection,
) -> Result<bool, SqliteTasksError> {
let query = sqlx::query!(r#"
UPDATE tasks
SET is_dismissed_by_user = 1
WHERE is_dismissed_by_user != 1
"#);
let res = query.execute(db.get_pool()).await?;
let rows_updated = res.rows_affected() > 0;
Ok(rows_updated)
}
@@ -1,9 +1,11 @@
pub mod create_task;
pub mod dismiss_all_tasks;
pub mod get_task_by_provider_and_provider_job_id;
pub mod list_tasks_by_provider_and_status;
pub mod list_tasks_by_provider_and_tokens;
pub mod list_tasks_for_frontend;
pub mod mark_task_as_dismissed;
pub mod nuke_all_tasks;
pub mod task;
pub mod update_successful_task_status_with_metadata;
pub mod update_successful_task_status_with_metadata_by_provider;
@@ -0,0 +1,28 @@
use crate::connection::TaskDbConnection;
use crate::error::SqliteTasksError;
/// Abandon all in-progress tasks.
/// Marked as user cancellation.
pub async fn nuke_all_tasks(
db: &TaskDbConnection,
) -> Result<bool, SqliteTasksError> {
// TODO(bt,2025-07-15): We can't set a LIMIT without a certain compiler flag for SQLite ?
let query = sqlx::query!(r#"
UPDATE tasks
SET task_status = "cancelled_by_user"
WHERE task_status NOT IN (
"complete_success",
"complete_failure",
"dead",
"cancelled_by_user",
"cancelled_by_provider",
"cancelled_by_us"
)
"#);
let res = query.execute(db.get_pool()).await?;
let rows_updated = res.rows_affected() > 0;
Ok(rows_updated)
}
@@ -18,7 +18,7 @@ import {
} from "@storyteller/ui-gallery-modal";
import type { GalleryItem } from "@storyteller/ui-gallery-modal";
import { useEffect, useMemo, useRef, useState } from "react";
import { GetTaskQueue, MarkTaskAsDismissed } from "@storyteller/tauri-api";
import { GetTaskQueue, MarkTaskAsDismissed, TasksNukeAll } from "@storyteller/tauri-api";
import type { TaskQueueItem } from "@storyteller/tauri-api";
import { listen, UnlistenFn } from "@tauri-apps/api/event";
import {
@@ -528,12 +528,8 @@ export const TaskQueue = () => {
};
const dismissAll = async () => {
const allIds = [
...completed.map((t) => t.id),
...inProgress.map((t) => t.id),
];
try {
await Promise.all(allIds.map((id) => MarkTaskAsDismissed(id)));
await TasksNukeAll();
} catch (_) {
// ignore
} finally {
@@ -18,9 +18,9 @@ const billingTabs = [
const pricingConfig = {
header: {
title: "Purchase a subscription",
title: "Subscribe to ArtCraft",
subtitle:
"Upgrade to gain access to Pro features and generate more, faster.",
"Support ArtCraft development and get generation credits without having to add 3rd party services.",
},
yearlyDiscount: 20,
};
@@ -41,6 +41,7 @@ import { ImageModel } from "@storyteller/model-list";
import { ImagePromptRow } from "./ImagePromptRow";
import { twMerge } from "tailwind-merge";
import { GenerationProvider } from "@storyteller/api-enums";
import { GenerationCountPicker } from "./common/GenerationCountPicker";
export type AspectRatio = "wide" | "tall" | "square";
@@ -133,40 +134,13 @@ export const PromptBox2D = ({
},
]);
const [generationCountList, setGenerationCountList] = useState<PopoverItem[]>(
[],
);
// Update generation count options from selected model capabilities
useEffect(() => {
const caps = getCapabilitiesForModel(selectedImageModel);
const items: PopoverItem[] = Array.from(
{ length: caps.maxGenerationCount },
(_, i) => i + 1,
).map((count) => ({
label: String(count),
selected: count === generationCount,
}));
setGenerationCountList(items);
// Clamp selection to allowed range
if (generationCount < 1 || generationCount > caps.maxGenerationCount) {
setGenerationCount(
Math.min(Math.max(1, generationCount), caps.maxGenerationCount),
);
if (selectedImageModel?.isValidGenerationCount(generationCount)) {
return;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedImageModel]);
// Keep UI selection in sync when store value changes
useEffect(() => {
setGenerationCountList((prev) =>
prev.map((item) => ({
...item,
selected: item.label === String(generationCount),
})),
);
}, [generationCount]);
const defaultGenerations = selectedImageModel?.defaultGenerationCount || 4;
setGenerationCount(defaultGenerations);
}, [selectedImageModel, generationCount]);
useEffect(() => {
setResolutionList((prev) =>
@@ -208,17 +182,6 @@ export const PromptBox2D = ({
// Image prompt row replaces legacy upload/gallery UI
const handleGenerationCountSelect = (selectedItem: PopoverItem) => {
const count = parseInt(selectedItem.label, 10);
setGenerationCount(count);
setGenerationCountList((prev) =>
prev.map((item) => ({
...item,
selected: item.label === selectedItem.label,
})),
);
};
const handlePaste = (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
e.preventDefault();
e.stopPropagation();
@@ -612,23 +575,13 @@ export const PromptBox2D = ({
</Button>
</Tooltip>
)}
<Tooltip
content="Number of generations"
position="top"
className="z-50"
closeOnClick={true}
>
<PopoverMenu
items={generationCountList}
onSelect={handleGenerationCountSelect}
mode="toggle"
panelTitle="No. of images"
triggerIcon={
<FontAwesomeIcon icon={faCopy} className="h-4 w-4" />
}
buttonClassName="h-9"
/>
</Tooltip>
<GenerationCountPicker
currentModel={selectedImageModel}
currentCount={generationCount}
handleCountChange={(count) => {
setGenerationCount(count);
}}
/>
<Button
className="flex items-center border-none bg-primary px-3 text-sm text-white disabled:cursor-not-allowed disabled:opacity-50"
icon={!isEnqueueing ? faSparkles : undefined}
@@ -31,6 +31,7 @@ import { ImagePromptRow, type UploadImageFn } from "./ImagePromptRow";
import { RefImage, usePromptEditStore } from "./promptStore";
import { GenerationProvider } from "@storyteller/api-enums";
import { AspectRatioPicker } from "./common/AspectRatioPicker";
import { GenerationCountPicker } from "./common/GenerationCountPicker";
export interface PromptBoxEditProps {
onModeChange?: (mode: string) => void;
@@ -82,9 +83,6 @@ export const PromptBoxEdit = ({
const [prompt, setPrompt] = useState("");
const [useSystemPrompt, setUseSystemPrompt] = useState(true);
const [isFocused, setIsFocused] = useState(false);
const [generationCount, setGenerationCount] = useState<number>(
typeof generationCountProp === "number" ? generationCountProp : 1,
);
const [internalEnqueueing, setInternalEnqueueing] = useState(false);
const referenceImages = usePromptEditStore((s) => s.referenceImages);
const setReferenceImages = usePromptEditStore((s) => s.setReferenceImages);
@@ -95,10 +93,6 @@ export const PromptBoxEdit = ({
const [showImagePrompts, setShowImagePrompts] = useState(false);
const [generationCountList, setGenerationCountList] = useState<PopoverItem[]>(
[],
);
const [aspectRatioList, setAspectRatioList] = useState<PopoverItem[]>([
{
label: "Auto",
@@ -142,6 +136,8 @@ export const PromptBoxEdit = ({
},
]);
const generationCount = generationCountProp || 1;
useEffect(() => {
setAspectRatioList((prev) =>
prev.map((item) => ({
@@ -193,42 +189,12 @@ export const PromptBoxEdit = ({
});
useEffect(() => {
if (typeof generationCountProp === "number") {
setGenerationCount(generationCountProp);
if (selectedImageModel?.isValidGenerationCount(generationCount)) {
return;
}
}, [generationCountProp]);
useEffect(() => {
const caps = getCapabilitiesForModel(selectedImageModel);
const items: PopoverItem[] = Array.from(
{ length: caps.maxGenerationCount },
(_, i) => i + 1,
).map((count) => ({
label: String(count),
selected: count === generationCount,
icon: <FontAwesomeIcon icon={faCopy} className="h-4 w-4" />,
}));
setGenerationCountList(items);
if (generationCount < 1 || generationCount > caps.maxGenerationCount) {
const clamped = Math.min(
Math.max(1, generationCount),
caps.maxGenerationCount,
);
setGenerationCount(clamped);
onGenerationCountChange?.(clamped);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedImageModel]);
useEffect(() => {
setGenerationCountList((prev) =>
prev.map((item) => ({
...item,
selected: item.label === String(generationCount),
})),
);
}, [generationCount]);
const defaultGenerations = selectedImageModel?.defaultGenerationCount || 4;
onGenerationCountChange?.(defaultGenerations);
}, [selectedImageModel, generationCount]);
const handlePaste = (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
e.preventDefault();
@@ -532,24 +498,13 @@ export const PromptBoxEdit = ({
</Button>
</Tooltip>
)}
<Tooltip
content="Number of generations"
position="top"
className="z-50"
closeOnClick={true}
>
<PopoverMenu
items={generationCountList}
onSelect={handleGenerationCountSelect}
mode="toggle"
panelTitle="No. of images"
triggerIcon={
<FontAwesomeIcon icon={faCopy} className="h-4 w-4" />
}
panelClassName="min-w-28"
buttonClassName="h-9"
/>
</Tooltip>
<GenerationCountPicker
currentModel={selectedImageModel}
currentCount={generationCount}
handleCountChange={(count) => {
onGenerationCountChange?.(count);
}}
/>
<Button
className="flex items-center border-none bg-primary px-3 text-sm text-white disabled:cursor-not-allowed disabled:opacity-50"
icon={
@@ -34,6 +34,7 @@ import { twMerge } from "tailwind-merge";
import { ImagePromptRow } from "./ImagePromptRow";
import { GenerationProvider } from "@storyteller/api-enums";
import { AspectRatioPicker } from "./common/AspectRatioPicker";
import { GenerationCountPicker } from "./common/GenerationCountPicker";
interface PromptBoxImageProps {
useJobContext: () => JobContextType;
@@ -150,9 +151,6 @@ export const PromptBoxImage = ({
icon: <FontAwesomeIcon icon={faExpand} className="h-4 w-4" />,
},
]);
const [generationCountList, setGenerationCountList] = useState<PopoverItem[]>(
[],
);
const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -176,33 +174,12 @@ export const PromptBoxImage = ({
}, [imageMediaId, url]);
useEffect(() => {
const caps = getCapabilitiesForModel(selectedModel);
const defaultCount = Math.min(
Math.max(1, caps.defaultGenerationCount ?? 1),
caps.maxGenerationCount,
);
setGenerationCount(defaultCount);
const items: PopoverItem[] = Array.from(
{ length: caps.maxGenerationCount },
(_, i) => i + 1,
).map((count) => ({
label: String(count),
selected: count === defaultCount,
icon: <FontAwesomeIcon icon={faCopy} className="h-4 w-4" />,
}));
setGenerationCountList(items);
}, [selectedModel]);
useEffect(() => {
setGenerationCountList((prev) =>
prev.map((item) => ({
...item,
selected: item.label === String(generationCount),
})),
);
}, [generationCount]);
if (selectedModel?.isValidGenerationCount(generationCount)) {
return;
}
const defaultGenerations = selectedModel?.defaultGenerationCount || 4;
setGenerationCount(defaultGenerations);
}, [selectedModel, generationCount]);
useEffect(() => {
setAspectRatioList((prev) =>
@@ -236,17 +213,6 @@ export const PromptBoxImage = ({
setResolution(selectedItem.label.toLowerCase() as any);
};
const handleGenerationCountSelect = (selectedItem: PopoverItem) => {
const count = parseInt(selectedItem.label, 10);
setGenerationCount(count);
setGenerationCountList((prev) =>
prev.map((item) => ({
...item,
selected: item.label === selectedItem.label,
})),
);
};
const handlePaste = (e: React.ClipboardEvent<HTMLTextAreaElement>) => {
e.preventDefault();
const pastedText = e.clipboardData.getData("text");
@@ -544,24 +510,13 @@ export const PromptBoxImage = ({
</Tooltip>
</div>
<div className="flex items-center gap-2">
<Tooltip
content="Number of generations"
position="top"
className="z-50"
closeOnClick={true}
>
<PopoverMenu
items={generationCountList}
onSelect={handleGenerationCountSelect}
mode="toggle"
triggerIcon={
<FontAwesomeIcon icon={faCopy} className="h-4 w-4" />
}
panelClassName="min-w-28"
panelTitle="No. of images"
buttonClassName="h-9"
/>
</Tooltip>
<GenerationCountPicker
currentModel={selectedModel}
currentCount={generationCount}
handleCountChange={(count) => {
setGenerationCount(count);
}}
/>
<Button
className="flex items-center border-none bg-primary px-3 text-sm text-white disabled:cursor-not-allowed disabled:opacity-50"
icon={!isEnqueueing ? faSparkles : undefined}
@@ -1,9 +1,9 @@
import { faMagicWandSparkles, faRectangle, faRectangleVertical, faRectangleWide, faSquare, IconDefinition } from "@fortawesome/pro-regular-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { PopoverItem, PopoverMenu } from "libs/components/popover/src/lib/popover"
import Tooltip from "libs/components/tooltip/src/lib/tooltip"
import { ImageModel } from "libs/model-list/src/lib/classes/ImageModel"
import { CommonAspectRatio } from "libs/model-list/src/lib/classes/properties/CommonAspectRatio"
import { PopoverItem, PopoverMenu } from "@storyteller/ui-popover"
import { Tooltip } from "@storyteller/ui-tooltip"
import { ImageModel } from "@storyteller/model-list"
import { CommonAspectRatio } from "@storyteller/model-list"
interface AspectRatioPickerProps {
@@ -0,0 +1,89 @@
import { faCopy } from "@fortawesome/pro-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { PopoverMenu, PopoverItem } from "@storyteller/ui-popover";
import { Tooltip } from "@storyteller/ui-tooltip";
import { ImageModel } from "@storyteller/model-list";
const DEFAULT_GENERATION_COUNT : number = 4;
interface GenerationCountPickerProps {
currentModel?: ImageModel,
currentCount: number,
handleCountChange: (count: number) => void,
}
export const GenerationCountPicker = ({
currentModel,
currentCount,
handleCountChange
}: GenerationCountPickerProps) => {
const maxGenerationCount = currentModel?.maxGenerationCount || DEFAULT_GENERATION_COUNT;
const hasPredefinedOptions = !!currentModel?.predefinedGenerationCounts;
let generationCountOptions: PopoverItem[];
// Count pickers either have a "[1,2, ... max]" set of options, or a pre-defined list of options.
if (hasPredefinedOptions) {
generationCountOptions = buildPredefinedCountOptions(currentModel?.predefinedGenerationCounts || [], currentCount);
} else {
generationCountOptions = buildSequentialCountOptions(maxGenerationCount, currentCount);
}
const onSelect = (item: PopoverItem) => {
let count = parseInt(item.label, 10);
if (isNaN(count)) {
return;
}
if (count < 1 || count > maxGenerationCount) {
// Clamp to valid range
count = Math.min(Math.max(1, count), maxGenerationCount);
}
handleCountChange(count);
};
return (
<>
<Tooltip
content="Number of generations"
position="top"
className="z-50"
closeOnClick={true}
>
<PopoverMenu
items={generationCountOptions}
onSelect={onSelect}
mode="toggle"
panelTitle="No. of images"
triggerIcon={
<FontAwesomeIcon icon={faCopy} className="h-4 w-4" />
}
buttonClassName="h-9"
/>
</Tooltip>
</>
)
}
const buildSequentialCountOptions = (maxCount: number, currentCount: number): PopoverItem[] => {
const options = [];
for (let i = 0; i < maxCount; i++) {
const count = i + 1;
options.push({
label: String(count),
selected: count === currentCount,
});
}
return options;
}
const buildPredefinedCountOptions = (options: number[], currentCount: number): PopoverItem[] => {
const result: PopoverItem[] = [];
options.forEach(option => {
result.push({
label: String(option),
selected: option === currentCount,
});
});
return result;
};
@@ -8,6 +8,9 @@
{
"path": "../camera-settings-modal"
},
{
"path": "../../api-enums"
},
{
"path": "../../model-list"
},
@@ -17,32 +20,32 @@
{
"path": "../../tauri-api"
},
{
"path": "../gallery-modal"
},
{
"path": "../../tauri-utils"
},
{
"path": "../modal"
},
{
"path": "../button"
},
{
"path": "../tooltip"
},
{
"path": "../popover"
},
{
"path": "../../api"
"path": "../toaster"
},
{
"path": "../../common"
},
{
"path": "../toaster"
"path": "../../api"
},
{
"path": "../gallery-modal"
},
{
"path": "../tooltip"
},
{
"path": "../button"
},
{
"path": "./tsconfig.lib.json"
@@ -49,6 +49,9 @@
{
"path": "../camera-settings-modal/tsconfig.lib.json"
},
{
"path": "../../api-enums/tsconfig.lib.json"
},
{
"path": "../../model-list/tsconfig.lib.json"
},
@@ -58,32 +61,32 @@
{
"path": "../../tauri-api/tsconfig.lib.json"
},
{
"path": "../gallery-modal/tsconfig.lib.json"
},
{
"path": "../../tauri-utils/tsconfig.lib.json"
},
{
"path": "../modal/tsconfig.lib.json"
},
{
"path": "../button/tsconfig.lib.json"
},
{
"path": "../tooltip/tsconfig.lib.json"
},
{
"path": "../popover/tsconfig.lib.json"
},
{
"path": "../../api/tsconfig.lib.json"
"path": "../toaster/tsconfig.lib.json"
},
{
"path": "../../common/tsconfig.lib.json"
},
{
"path": "../toaster/tsconfig.lib.json"
"path": "../../api/tsconfig.lib.json"
},
{
"path": "../gallery-modal/tsconfig.lib.json"
},
{
"path": "../tooltip/tsconfig.lib.json"
},
{
"path": "../button/tsconfig.lib.json"
}
]
}
@@ -10,7 +10,13 @@ export function ArtcraftBillingBlock({
<div className="w-full">
<PricingContent
title="Video Generation is Resource Intensive"
subtitle="Creating high-quality videos requires significant computing power. To generate more, you'll need to subscribe for monthly credits. Your support also helps us keep building and improving ArtCraft!"
subtitle="
Creating high-quality videos requires significant computing power.
To generate more, you can subscribe to ArtCraft or buy ArtCraft credits.
You can also add 3rd party providers and use your existing services.
Your support helps us keep building and improving ArtCraft!
We're building the image and video tool that you can own forever.
"
/>
</div>
);
@@ -24,8 +24,12 @@ function getServiceProviderName(provider: GenerationProvider) : string {
return "Sora";
case GenerationProvider.Fal:
return "Fal";
case GenerationProvider.Grok:
return "Grok";
case GenerationProvider.Midjourney:
return "Midjourney";
case GenerationProvider.WorldLabs:
return "World Labs";
case GenerationProvider.Artcraft:
default:
return "Artcraft";
@@ -23,9 +23,11 @@ export function ProviderBillingModal({
let block;
switch (provider) {
case GenerationProvider.Fal:
case GenerationProvider.Grok:
case GenerationProvider.Midjourney:
case GenerationProvider.Sora:
case GenerationProvider.Fal:
case GenerationProvider.WorldLabs:
// NB: We're just going to ask users to set up billing on the provider's website.
block = <GenericProviderBillingBlock provider={provider} />;
break;
@@ -11,6 +11,9 @@
{
"path": "../../common"
},
{
"path": "../pricing-modal"
},
{
"path": "./tsconfig.lib.json"
},
@@ -51,6 +51,9 @@
},
{
"path": "../../common/tsconfig.lib.json"
},
{
"path": "../pricing-modal/tsconfig.lib.json"
}
]
}
@@ -51,9 +51,19 @@ export const BillingSettingsPane = (args: BillingSettingsPaneProps) => {
return (
<>
<div className="space-y-4 text-base-fg">
<Label>Support ArtCraft</Label>
<p className="text-sm opacity-70">
You do not have to purchase anything from us to use ArtCraft, but
you can support ArtCraft development by subscribing or buying credits.
</p>
</div>
<br />
<div className="space-y-4 text-base-fg">
<div className="space-y-2">
<Label>Current Plan</Label>
<Label>Current ArtCraft Plan</Label>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-xl font-semibold ">
<FontAwesomeIcon
@@ -102,7 +112,7 @@ export const BillingSettingsPane = (args: BillingSettingsPaneProps) => {
<div className="flex flex-col">
<Label htmlFor="credits" className="flex items-center gap-2">
Your credit balance
Your ArtCraft credit balance
</Label>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
@@ -17,6 +17,9 @@ export class ImageModel extends Model {
// Default number of images that can be generated at once
readonly defaultGenerationCount: number;
// If set, use this instead of `maxGenerationCount`.
readonly predefinedGenerationCounts?: number[];
// Signals image editing models that focus on editing a single image.
readonly canEditImages: boolean;
@@ -63,6 +66,7 @@ export class ImageModel extends Model {
selectorBadges: string[];
maxGenerationCount: number;
defaultGenerationCount: number;
predefinedGenerationCounts?: number[];
canEditImages?: boolean;
usesInpaintingMask?: boolean;
editingIsInpainting?: boolean;
@@ -91,6 +95,7 @@ export class ImageModel extends Model {
super(args);
this.maxGenerationCount = args.maxGenerationCount;
this.defaultGenerationCount = args.defaultGenerationCount;
this.predefinedGenerationCounts = args.predefinedGenerationCounts;
this.canEditImages = args.canEditImages ?? false;
this.usesInpaintingMask = args.usesInpaintingMask ?? false;
this.editingIsInpainting = args.editingIsInpainting ?? false;
@@ -125,4 +130,13 @@ export class ImageModel extends Model {
&& this.aspectRatios
&& this.aspectRatios.length > 0;
}
// Return whether the count of generations is valid for this model
isValidGenerationCount(count: number): boolean {
if (this.predefinedGenerationCounts) {
return this.predefinedGenerationCounts.includes(count);
} else {
return count >= 1 && count <= this.maxGenerationCount;
}
}
}
@@ -19,8 +19,10 @@ export const IMAGE_MODELS: ImageModel[] = [
selectorName: "Grok",
selectorDescription: "Fast af",
selectorBadges: ["10 sec."],
defaultGenerationCount: 4,
// NB: Grok only supports 6 images at a time
defaultGenerationCount: 6,
maxGenerationCount: 6,
predefinedGenerationCounts: [6],
canUseImagePrompt: false,
maxImagePromptCount: 6,
progressBarTime: 10000,
@@ -44,8 +46,10 @@ export const IMAGE_MODELS: ImageModel[] = [
selectorName: "Midjourney",
selectorDescription: "Stunning style and quality",
selectorBadges: ["45 sec."],
// NB: Midjourney only supports 4 images at a time
maxGenerationCount: 4,
defaultGenerationCount: 4,
predefinedGenerationCounts: [4],
canUseImagePrompt: false,
maxImagePromptCount: 6,
progressBarTime: 45000,
@@ -1,8 +1,6 @@
import { SoundEffect } from "./SoundEffect";
export class SoundRegistry {
static #instance: SoundRegistry | undefined;
#sounds: Map<string, SoundEffect>;
private constructor() {
@@ -10,10 +8,30 @@ export class SoundRegistry {
}
public static getInstance() {
if (SoundRegistry.#instance === undefined) {
SoundRegistry.#instance = new SoundRegistry();
// NB(bt,2026-01-03): Our `nx` build setup is deeply flawed and in need
// of repair: Production builds will compile libraries to multiple
// independent symbolic spaces, making singleton pattern unreliable.
// Effectively, the singletons cannot be used across bundle boundaries.
// We're going to be gross and bind everything to the global namespace.
//
// To be clear, this does not work:
//
// class SoundRegistry {
// static #instance: SoundRegistry | undefined;
//
// [...]
//
// static getInstance() {
// if (SoundRegistry.#instance === undefined) {
// SoundRegistry.#instance = new SoundRegistry();
// }
// return SoundRegistry.#instance;
// }
if ((window as any).artcraft_sound_registry === undefined) {
(window as any).artcraft_sound_registry = new SoundRegistry();
}
return SoundRegistry.#instance;
return (window as any).artcraft_sound_registry;
}
public hasSound(key: string) : boolean {
+3 -2
View File
@@ -3,13 +3,14 @@ export * from "./lib/artcraft/ArtcraftGetSubscription.js";
export * from "./lib/artcraft/media_files/MediaFileDelete.js";
export * from "./lib/artcraft/task_queue/GetTaskQueue.js";
export * from "./lib/artcraft/task_queue/MarkTaskAsDismissed.js";
export * from "./lib/artcraft/task_queue/TasksNukeAll.js";
export * from "./lib/common/CommandStatus.js";
export * from "./lib/enqueue/EnqueueEditImage.js";
export * from "./lib/enqueue/EnqueueImageBgRemovalCommand.js";
export * from "./lib/enqueue/EnqueueImageInpaint.js";
export * from "./lib/enqueue/EnqueueImageTo3dObject.js";
export * from "./lib/enqueue/EnqueueImageToGaussian.js";
export * from "./lib/enqueue/EnqueueImageTo3dWorld.js";
export * from "./lib/enqueue/EnqueueImageToGaussian.js";
export * from "./lib/enqueue/EnqueueImageToVideo.js";
export * from "./lib/enqueue/EnqueueTextToImage.js";
export * from "./lib/events/BasicEventWrapper.js";
@@ -36,4 +37,4 @@ export * from "./lib/sora/useSoraLoginListener.js";
export * from "./lib/sora/waitForSoraLogin.js";
export * from "./lib/system/GetAppInfo.js";
export * from "./lib/util/LoadWithoutCors.js";
export * from "./lib/worldlabs/WorldLabsGetCredentialInfo.js";
export * from "./lib/worldlabs/WorldLabsGetCredentialInfo.js";
@@ -0,0 +1,18 @@
import { invoke } from "@tauri-apps/api/core";
import { CommandResult } from "../../common/CommandStatus";
interface TasksNukeAllSuccess extends CommandResult {
payload: void;
}
export const TasksNukeAll = async () : Promise<TasksNukeAllSuccess> => {
try {
let result = await invoke("tasks_nuke_all_command") as TasksNukeAllSuccess;
return result;
} catch (error) {
throw error;
}
}
// Temporary just for testing -
(window as any).test_tasks_nuke_all = TasksNukeAll;
+1 -1
View File
@@ -19,7 +19,7 @@ sqlite_db_file="/tmp/tasks.sqlite"
# 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.json"
config_path="${rust_crate_path}/tauri-dev-hot-reload.conf.json"
export TAURI_FRONTEND_PATH="${frontend_path}"
export TAURI_APP_PATH="${rust_crate_path}"
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# This works on Linux and MacOS to launch the Rust dev server
set -euxo pipefail
echo "Running Artcraft Rust in Dev Mode..."
echo ""
echo "You'll need to launch the frontend dev server as a second script!"
echo ""
root_dir=$(pwd)
frontend_path="${root_dir}/frontend"
rust_crate_path="${root_dir}/crates/desktop/artcraft"
sqlite_db_file="/tmp/tasks.sqlite"
# 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-dev-static.conf.json"
export TAURI_FRONTEND_PATH="${frontend_path}"
export TAURI_APP_PATH="${rust_crate_path}"
echo "Create database file..."
touch "${sqlite_db_file}"
sqlx_migrate() {
echo "SQLx migrate database file..."
sqlx migrate run \
--database-url "sqlite:${sqlite_db_file}" \
--source "${root_dir}/_sql/artcraft_migrations"
export DATABASE_URL="sqlite:${sqlite_db_file}"
}
sqlx_offline() {
echo "SQLx offline..."
export SQLX_OFFLINE=true
}
# NB: Switch this for actively developing queries.
#sqlx_migrate
sqlx_offline
# Issue with Linux performance
# https://github.com/tauri-apps/tauri/issues/13498
export WEBKIT_DISABLE_DMABUF_RENDERER=1
export WEBKIT_DISABLE_COMPOSITING_MODE=1
echo "Build and run..."
RUSTFLAGS="-Awarnings" cargo tauri dev \
--no-dev-server \
--no-dev-server-wait \
--no-watch \
--config "${config_path}"