diff --git a/Cargo.lock b/Cargo.lock index 0e9594bcd6..fdce43a041 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -916,31 +916,7 @@ dependencies = [ "hmac 0.12.1", "http-types", "hyper 0.14.28", - "hyper-tls 0.5.0", - "serde", - "serde_json", - "serde_path_to_error", - "serde_qs 0.10.1", - "sha2 0.10.8", - "smart-default 0.6.0", - "smol_str 0.1.24", - "thiserror 1.0.63", - "tokio", -] - -[[package]] -name = "async-stripe" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecbddf002ad7a13d2041eadf1b234cb3f57653ffdd901a01bc3f1c65aa77440" -dependencies = [ - "chrono", - "futures-util", - "hex", - "hmac 0.12.1", - "http-types", - "hyper 0.14.28", - "hyper-tls 0.5.0", + "hyper-rustls 0.24.2", "serde", "serde_json", "serde_path_to_error", @@ -963,7 +939,7 @@ dependencies = [ "bytes 1.10.1", "http-body-util", "hyper 1.6.0", - "hyper-tls 0.6.0", + "hyper-rustls 0.27.3", "hyper-util", "miniserde", "thiserror 2.0.12", @@ -1148,10 +1124,11 @@ checksum = "9a13149d0cf3f7f9b9261fad4ec63b2efbf9a80665f52def86282d26255e6331" dependencies = [ "http 1.3.1", "log", - "native-tls", + "rustls 0.22.4", "serde", "serde_json", "url", + "webpki-roots 0.26.2", ] [[package]] @@ -5262,6 +5239,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.11", + "hyper 0.14.28", + "log", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "tokio", + "tokio-rustls 0.24.1", +] + [[package]] name = "hyper-rustls" version = "0.27.3" @@ -5272,11 +5265,12 @@ dependencies = [ "http 1.3.1", "hyper 1.6.0", "hyper-util", + "log", "rustls 0.23.13", - "rustls-native-certs", + "rustls-native-certs 0.8.0", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.0", "tower-service", "webpki-roots 0.26.2", ] @@ -5293,19 +5287,6 @@ dependencies = [ "tokio-io-timeout", ] -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes 1.10.1", - "hyper 0.14.28", - "native-tls", - "tokio", - "tokio-native-tls", -] - [[package]] name = "hyper-tls" version = "0.6.0" @@ -6133,13 +6114,15 @@ dependencies = [ "httpdate", "idna 0.5.0", "mime", - "native-tls", "nom", "once_cell", "quoted_printable", + "rustls 0.21.12", + "rustls-pemfile 1.0.4", "socket2 0.5.10", "tokio", "url", + "webpki-roots 0.25.3", ] [[package]] @@ -9103,15 +9086,15 @@ dependencies = [ "http 0.2.11", "http-body 0.4.6", "hyper 0.14.28", - "hyper-tls 0.5.0", + "hyper-rustls 0.24.2", "ipnet", "js-sys", "log", "mime", - "native-tls", "once_cell", "percent-encoding", "pin-project-lite", + "rustls 0.21.12", "rustls-pemfile 1.0.4", "serde", "serde_json", @@ -9119,13 +9102,14 @@ dependencies = [ "sync_wrapper 0.1.2", "system-configuration 0.5.1", "tokio", - "tokio-native-tls", + "tokio-rustls 0.24.1", "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots 0.25.3", "winreg 0.50.0", ] @@ -9149,8 +9133,8 @@ dependencies = [ "http-body 1.0.1", "http-body-util", "hyper 1.6.0", - "hyper-rustls", - "hyper-tls 0.6.0", + "hyper-rustls 0.27.3", + "hyper-tls", "hyper-util", "js-sys", "log", @@ -9161,7 +9145,7 @@ dependencies = [ "pin-project-lite", "quinn", "rustls 0.23.13", - "rustls-native-certs", + "rustls-native-certs 0.8.0", "rustls-pki-types", "serde", "serde_json", @@ -9169,7 +9153,7 @@ dependencies = [ "sync_wrapper 1.0.2", "tokio", "tokio-native-tls", - "tokio-rustls", + "tokio-rustls 0.26.0", "tokio-util", "tower 0.5.2", "tower-http", @@ -9190,6 +9174,7 @@ dependencies = [ "serde", "serde_derive", "sqlx", + "sqlx-core", "toml 0.8.22", "workspace-build-acceleration", ] @@ -9497,17 +9482,33 @@ version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ + "log", "ring", "rustls-webpki 0.101.7", "sct", ] +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + [[package]] name = "rustls" version = "0.23.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", @@ -9516,6 +9517,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework", +] + [[package]] name = "rustls-native-certs" version = "0.8.0" @@ -10608,7 +10621,6 @@ dependencies = [ "indexmap 2.9.0", "log", "memchr", - "native-tls", "once_cell", "paste", "percent-encoding", @@ -10825,7 +10837,6 @@ dependencies = [ "anyhow", "artcraft_api_defs", "async-stripe 0.26.0", - "async-stripe 0.41.0", "async-trait", "base64 0.22.1", "billing_artcraft_component", @@ -12092,6 +12103,16 @@ dependencies = [ "tokio-sync", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.0" @@ -14074,11 +14095,6 @@ dependencies = [ "hashbrown 0.14.5", "heck 0.4.1", "hmac 0.12.1", - "hyper 0.14.28", - "hyper 1.6.0", - "hyper-rustls", - "hyper-tls 0.6.0", - "hyper-util", "idna 1.0.3", "indexmap 1.9.3", "indexmap 2.9.0", @@ -14088,15 +14104,12 @@ dependencies = [ "md-5 0.10.6", "memchr", "mime_guess", - "native-tls", "num-bigint", "num-integer", "num-iter", "num-rational", "num-traits", "once_cell", - "openssl", - "openssl-sys", "parking_lot 0.12.2", "percent-encoding", "proc-macro2", @@ -14109,7 +14122,6 @@ dependencies = [ "regex", "regex-automata", "regex-syntax", - "reqwest 0.12.23", "rsa", "rustls-pki-types", "schemars", @@ -14123,10 +14135,6 @@ dependencies = [ "smallvec 1.15.1", "socket2 0.5.10", "spin 0.9.8", - "sqlx", - "sqlx-core", - "sqlx-macros", - "sqlx-macros-core", "sqlx-mysql", "sqlx-sqlite", "stable_deref_trait", diff --git a/Cargo.toml b/Cargo.toml index 4922216a02..61c73df1b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -246,7 +246,7 @@ log = "0.4.27" once_cell = "1.21.3" rand = "0.9.1" regex = "1.12.2" -reqwest = { version = "0.12.23", features = ["json", "cookies"] } +reqwest = { version = "0.12.23", default-features = false, features = [ "charset", "http2", "system-proxy", "json", "cookies", "rustls-tls" ] } reqwest-websocket = { version = "0.5.0" } serde = { version = "1.0.219", features = ["derive"] } serde_derive = "1.0.219" diff --git a/crates/api_clients/cloud_storage/Cargo.toml b/crates/api_clients/cloud_storage/Cargo.toml index 20f74c5f0f..e84eb71608 100644 --- a/crates/api_clients/cloud_storage/Cargo.toml +++ b/crates/api_clients/cloud_storage/Cargo.toml @@ -36,7 +36,7 @@ env_logger = "0.11.3" async-trait = "0.1.75" # The "never-encode-slash" feature seems to have been removed: # https://github.com/durch/rust-s3/pull/254 -rust-s3 = { version = "0.36.0-beta.2" } +rust-s3 = { version = "0.36.0-beta.2", default-features = false, features = ["fail-on-err", "tags", "tokio-rustls-tls"] } # Runtime tokio.workspace = true diff --git a/crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_bytedance_seedream_v4_edit_image_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_bytedance_seedream_v4_edit_image_webhook.rs new file mode 100644 index 0000000000..3c56f13cba --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_bytedance_seedream_v4_edit_image_webhook.rs @@ -0,0 +1,143 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::bytedance::seedream::bytedance_seedream_v4_edit_image::{bytedance_seedream_v4_edit_image, BytedanceSeedreamV4EditImageInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueBytedanceSeedreamV4EditImageArgs<'a, R: IntoUrl> { + // Request required + pub prompt: &'a str, + pub image_urls: Vec, + + // Optional args + pub num_images: Option, + pub max_images: Option, + pub image_size: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueBytedanceSeedreamV4EditImageNumImages { + One, + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueBytedanceSeedreamV4EditImageMaxImages { + One, + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueBytedanceSeedreamV4EditImageSize { + // Square + Square, + SquareHd, + // Tall + PortraitFourThree, + PortraitSixteenNine, + // Wide + LandscapeFourThree, + LandscapeSixteenNine, + // Auto + Auto, + Auto2k, + Auto4k, +} + +pub async fn enqueue_bytedance_seedream_v4_edit_image_webhook( + args: EnqueueBytedanceSeedreamV4EditImageArgs<'_, R> +) -> Result { + + let num_images = args.num_images + .map(|num_images| match num_images { + EnqueueBytedanceSeedreamV4EditImageNumImages::One => 1, + EnqueueBytedanceSeedreamV4EditImageNumImages::Two => 2, + EnqueueBytedanceSeedreamV4EditImageNumImages::Three => 3, + EnqueueBytedanceSeedreamV4EditImageNumImages::Four => 4, + }); + + let max_images = args.max_images + .map(|num_images| match num_images { + EnqueueBytedanceSeedreamV4EditImageMaxImages::One => 1, + EnqueueBytedanceSeedreamV4EditImageMaxImages::Two => 2, + EnqueueBytedanceSeedreamV4EditImageMaxImages::Three => 3, + EnqueueBytedanceSeedreamV4EditImageMaxImages::Four => 4, + }); + + let image_size = args.image_size + .map(|image_size| match image_size { + EnqueueBytedanceSeedreamV4EditImageSize::Square => "square", + EnqueueBytedanceSeedreamV4EditImageSize::SquareHd => "square_hd", + EnqueueBytedanceSeedreamV4EditImageSize::PortraitFourThree => "portrait_4_3", + EnqueueBytedanceSeedreamV4EditImageSize::PortraitSixteenNine => "portrait_16_9", + EnqueueBytedanceSeedreamV4EditImageSize::LandscapeFourThree => "landscape_4_3", + EnqueueBytedanceSeedreamV4EditImageSize::LandscapeSixteenNine => "landscape_16_9", + EnqueueBytedanceSeedreamV4EditImageSize::Auto => "auto", + EnqueueBytedanceSeedreamV4EditImageSize::Auto2k => "auto_2K", + EnqueueBytedanceSeedreamV4EditImageSize::Auto4k => "auto_4K", + }) + .map(|resolution| resolution.to_string()); + + let request = BytedanceSeedreamV4EditImageInput { + prompt: args.prompt.to_string(), + image_urls: args.image_urls, + // Optionals + num_images, + max_images, + image_size, + // Constants + enhance_prompt_mode: Some("standard".to_string()), + enable_safety_checker: Some(false), + }; + + let result = bytedance_seedream_v4_edit_image(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::image::edit::enqueue_bytedance_seedream_v4_edit_image_webhook::{enqueue_bytedance_seedream_v4_edit_image_webhook, EnqueueBytedanceSeedreamV4EditImageArgs, EnqueueBytedanceSeedreamV4EditImageMaxImages, EnqueueBytedanceSeedreamV4EditImageNumImages, EnqueueBytedanceSeedreamV4EditImageSize}; + use errors::AnyhowResult; + use std::fs::read_to_string; + use test_data::web::image_urls::{GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL}; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueBytedanceSeedreamV4EditImageArgs { + image_urls: vec![ + GHOST_IMAGE_URL.to_string(), + TREX_SKELETON_IMAGE_URL.to_string(), + ], + prompt: "add the ghost to the image of the t-rex skeleton, make it look spooky but friendly", + num_images: Some(EnqueueBytedanceSeedreamV4EditImageNumImages::Two), + max_images: Some(EnqueueBytedanceSeedreamV4EditImageMaxImages::Two), + image_size: Some(EnqueueBytedanceSeedreamV4EditImageSize::Auto2k), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_bytedance_seedream_v4_edit_image_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_bytedance_seedream_v4p5_edit_image_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_bytedance_seedream_v4p5_edit_image_webhook.rs new file mode 100644 index 0000000000..558971422f --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_bytedance_seedream_v4p5_edit_image_webhook.rs @@ -0,0 +1,142 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::bytedance::seedream::bytedance_seedream_v4p5_edit_image::{bytedance_seedream_v4p5_edit_image, BytedanceSeedreamV4p5EditImageInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueBytedanceSeedreamV4p5EditImageArgs<'a, R: IntoUrl> { + // Request required + pub prompt: &'a str, + pub image_urls: Vec, + + // Optional args + pub num_images: Option, + pub max_images: Option, + pub image_size: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueBytedanceSeedreamV4p5EditImageNumImages { + One, + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueBytedanceSeedreamV4p5EditImageMaxImages { + One, + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueBytedanceSeedreamV4p5EditImageSize { + // Square + Square, + SquareHd, + // Tall + PortraitFourThree, + PortraitSixteenNine, + // Wide + LandscapeFourThree, + LandscapeSixteenNine, + // Auto + Auto2k, + Auto4k, +} + +pub async fn enqueue_bytedance_seedream_v4p5_edit_image_webhook( + args: EnqueueBytedanceSeedreamV4p5EditImageArgs<'_, R> +) -> Result { + + let num_images = args.num_images + .map(|num_images| match num_images { + EnqueueBytedanceSeedreamV4p5EditImageNumImages::One => 1, + EnqueueBytedanceSeedreamV4p5EditImageNumImages::Two => 2, + EnqueueBytedanceSeedreamV4p5EditImageNumImages::Three => 3, + EnqueueBytedanceSeedreamV4p5EditImageNumImages::Four => 4, + }); + + let max_images = args.max_images + .map(|num_images| match num_images { + EnqueueBytedanceSeedreamV4p5EditImageMaxImages::One => 1, + EnqueueBytedanceSeedreamV4p5EditImageMaxImages::Two => 2, + EnqueueBytedanceSeedreamV4p5EditImageMaxImages::Three => 3, + EnqueueBytedanceSeedreamV4p5EditImageMaxImages::Four => 4, + }); + + let image_size = args.image_size + .map(|image_size| match image_size { + EnqueueBytedanceSeedreamV4p5EditImageSize::Square => "square", + EnqueueBytedanceSeedreamV4p5EditImageSize::SquareHd => "square_hd", + EnqueueBytedanceSeedreamV4p5EditImageSize::PortraitFourThree => "portrait_4_3", + EnqueueBytedanceSeedreamV4p5EditImageSize::PortraitSixteenNine => "portrait_16_9", + EnqueueBytedanceSeedreamV4p5EditImageSize::LandscapeFourThree => "landscape_4_3", + EnqueueBytedanceSeedreamV4p5EditImageSize::LandscapeSixteenNine => "landscape_16_9", + EnqueueBytedanceSeedreamV4p5EditImageSize::Auto2k => "auto_2K", + EnqueueBytedanceSeedreamV4p5EditImageSize::Auto4k => "auto_4K", + }) + .map(|resolution| resolution.to_string()); + + let request = BytedanceSeedreamV4p5EditImageInput { + prompt: args.prompt.to_string(), + image_urls: args.image_urls, + // Optionals + num_images, + max_images, + image_size, + // Constants + enable_safety_checker: Some(false), + seed: None, + }; + + let result = bytedance_seedream_v4p5_edit_image(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::image::edit::enqueue_bytedance_seedream_v4p5_edit_image_webhook::{enqueue_bytedance_seedream_v4p5_edit_image_webhook, EnqueueBytedanceSeedreamV4p5EditImageArgs, EnqueueBytedanceSeedreamV4p5EditImageMaxImages, EnqueueBytedanceSeedreamV4p5EditImageNumImages, EnqueueBytedanceSeedreamV4p5EditImageSize}; + use errors::AnyhowResult; + use std::fs::read_to_string; + use test_data::web::image_urls::{GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL}; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + //let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueBytedanceSeedreamV4p5EditImageArgs { + image_urls: vec![ + GHOST_IMAGE_URL.to_string(), + TREX_SKELETON_IMAGE_URL.to_string(), + ], + prompt: "add the ghost to the image of the t-rex skeleton, make it look spooky but friendly", + num_images: Some(EnqueueBytedanceSeedreamV4p5EditImageNumImages::Two), + max_images: Some(EnqueueBytedanceSeedreamV4p5EditImageMaxImages::Two), + image_size: Some(EnqueueBytedanceSeedreamV4p5EditImageSize::Auto2k), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_bytedance_seedream_v4p5_edit_image_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_nano_banana_pro_edit_image_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_nano_banana_pro_edit_image_webhook.rs new file mode 100644 index 0000000000..2ee07fadc3 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_nano_banana_pro_edit_image_webhook.rs @@ -0,0 +1,147 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::nano_banana_pro::nano_banana_pro_image_edit::{nano_banana_pro_image_edit, NanoBananaProImageEditInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueNanoBananaProEditImageArgs<'a, R: IntoUrl> { + // Request required + pub prompt: &'a str, + pub image_urls: Vec, + pub num_images: EnqueueNanoBananaProEditImageNumImages, + + // Optional args + pub resolution: Option, + pub aspect_ratio: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueNanoBananaProEditImageNumImages { + One, + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueNanoBananaProEditImageResolution { + OneK, + TwoK, + FourK, +} + +/// 21:9, 16:9, 3:2, 4:3, 5:4, 1:1, 4:5, 3:4, 2:3, 9:16 +/// Default is "1:1" +#[derive(Copy, Clone, Debug)] +pub enum EnqueueNanoBananaProEditImageAspectRatio { + // Square + OneByOne, + // Wide + FiveByFour, + FourByThree, + ThreeByTwo, + SixteenByNine, + TwentyOneByNine, + // Tall + FourByFive, + ThreeByFour, + TwoByThree, + NineBySixteen, // NB: No NineByTwentyOne ? +} + +pub async fn enqueue_nano_banana_pro_image_edit_webhook( + args: EnqueueNanoBananaProEditImageArgs<'_, R> +) -> Result { + + let num_images = match args.num_images { + EnqueueNanoBananaProEditImageNumImages::One => 1, + EnqueueNanoBananaProEditImageNumImages::Two => 2, + EnqueueNanoBananaProEditImageNumImages::Three => 3, + EnqueueNanoBananaProEditImageNumImages::Four => 4, + }; + + let resolution = args.resolution + .map(|resolution| match resolution { + EnqueueNanoBananaProEditImageResolution::OneK => "1K", + EnqueueNanoBananaProEditImageResolution::TwoK => "2K", + EnqueueNanoBananaProEditImageResolution::FourK => "4K", + }) + .map(|resolution| resolution.to_string()); + + let aspect_ratio = args.aspect_ratio + .map(|aspect_ratio| match aspect_ratio { + // Square + EnqueueNanoBananaProEditImageAspectRatio::OneByOne => "1:1", + // Wide + EnqueueNanoBananaProEditImageAspectRatio::FiveByFour => "5:4", + EnqueueNanoBananaProEditImageAspectRatio::FourByThree => "4:3", + EnqueueNanoBananaProEditImageAspectRatio::ThreeByTwo => "3:2", + EnqueueNanoBananaProEditImageAspectRatio::SixteenByNine => "16:9", + EnqueueNanoBananaProEditImageAspectRatio::TwentyOneByNine => "21:9", + // Tall + EnqueueNanoBananaProEditImageAspectRatio::FourByFive => "4:5", + EnqueueNanoBananaProEditImageAspectRatio::ThreeByFour => "3:4", + EnqueueNanoBananaProEditImageAspectRatio::TwoByThree => "2:3", + EnqueueNanoBananaProEditImageAspectRatio::NineBySixteen => "9:16", + }) + .map(|aspect_ratio| aspect_ratio.to_string()); + + let request = NanoBananaProImageEditInput { + prompt: args.prompt.to_string(), + image_urls: args.image_urls, + num_images: Some(num_images), + // Optionals + aspect_ratio, + resolution, + // Constants + output_format: Some("png".to_string()), + }; + + let result = nano_banana_pro_image_edit(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::image::edit::enqueue_nano_banana_pro_edit_image_webhook::{enqueue_nano_banana_pro_image_edit_webhook, EnqueueNanoBananaProEditImageArgs, EnqueueNanoBananaProEditImageAspectRatio, EnqueueNanoBananaProEditImageNumImages, EnqueueNanoBananaProEditImageResolution}; + use errors::AnyhowResult; + use std::fs::read_to_string; + use test_data::web::image_urls::{ERNEST_SCARED_STUPID_IMAGE_URL, GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL}; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueNanoBananaProEditImageArgs { + image_urls: vec![ + GHOST_IMAGE_URL.to_string(), + TREX_SKELETON_IMAGE_URL.to_string(), + ERNEST_SCARED_STUPID_IMAGE_URL.to_string(), + ], + prompt: "add the ghost and scared man to the image of the t-rex skeleton, make it look spooky but friendly", + num_images: EnqueueNanoBananaProEditImageNumImages::Two, + aspect_ratio: Some(EnqueueNanoBananaProEditImageAspectRatio::SixteenByNine), + resolution: Some(EnqueueNanoBananaProEditImageResolution::TwoK), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_nano_banana_pro_image_edit_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/image/edit/mod.rs b/crates/api_clients/fal_client/src/requests/webhook/image/edit/mod.rs index aaf27b4faf..bd0f2a6971 100644 --- a/crates/api_clients/fal_client/src/requests/webhook/image/edit/mod.rs +++ b/crates/api_clients/fal_client/src/requests/webhook/image/edit/mod.rs @@ -1,5 +1,8 @@ +pub mod enqueue_bytedance_seedream_v4_edit_image_webhook; +pub mod enqueue_bytedance_seedream_v4p5_edit_image_webhook; pub mod enqueue_flux_pro_kontext_max_edit_webhook; pub mod enqueue_gemini_25_flash_edit_webhook; pub mod enqueue_gpt_image_1_edit_image_webhook; +pub mod enqueue_nano_banana_pro_edit_image_webhook; pub mod enqueue_qwen_image_edit_webhook; pub mod enqueue_seededit_v3_edit_webhook; \ No newline at end of file diff --git a/crates/api_clients/fal_client/src/requests/webhook/image/mod.rs b/crates/api_clients/fal_client/src/requests/webhook/image/mod.rs index 3c32e1f5e7..d8126730bb 100644 --- a/crates/api_clients/fal_client/src/requests/webhook/image/mod.rs +++ b/crates/api_clients/fal_client/src/requests/webhook/image/mod.rs @@ -6,3 +6,4 @@ pub mod enqueue_flux_pro_11_ultra_text_to_image_webhook; pub mod enqueue_gpt_image_1_text_to_image_webhook; pub mod infill; pub mod remove_background_rembg_webhook; +pub mod text; diff --git a/crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_bytedance_seedream_v4_text_to_image_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_bytedance_seedream_v4_text_to_image_webhook.rs new file mode 100644 index 0000000000..92a2c427fe --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_bytedance_seedream_v4_text_to_image_webhook.rs @@ -0,0 +1,137 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::bytedance::seedream::bytedance_seedream_v4_text_to_image::BytedanceSeedreamV4TextToImageInput; +use fal::prelude::fal_ai::bytedance::seedream::bytedance_seedream_v4_text_to_image::bytedance_seedream_v4_text_to_image; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueBytedanceSeedreamV4TextToImageArgs<'a, R: IntoUrl> { + // Request required + pub prompt: &'a str, + + // Optional args + pub num_images: Option, + pub max_images: Option, + pub image_size: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueBytedanceSeedreamV4TextToImageNumImages { + One, + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueBytedanceSeedreamV4TextToImageMaxImages { + One, + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueBytedanceSeedreamV4TextToImageSize { + // Square + Square, + SquareHd, + // Tall + PortraitFourThree, + PortraitSixteenNine, + // Wide + LandscapeFourThree, + LandscapeSixteenNine, + // Auto + Auto, + Auto2k, + Auto4k, +} + +pub async fn enqueue_bytedance_seedream_v4_text_to_image_webhook( + args: EnqueueBytedanceSeedreamV4TextToImageArgs<'_, R> +) -> Result { + + let num_images = args.num_images + .map(|num_images| match num_images { + EnqueueBytedanceSeedreamV4TextToImageNumImages::One => 1, + EnqueueBytedanceSeedreamV4TextToImageNumImages::Two => 2, + EnqueueBytedanceSeedreamV4TextToImageNumImages::Three => 3, + EnqueueBytedanceSeedreamV4TextToImageNumImages::Four => 4, + }); + + let max_images = args.max_images + .map(|num_images| match num_images { + EnqueueBytedanceSeedreamV4TextToImageMaxImages::One => 1, + EnqueueBytedanceSeedreamV4TextToImageMaxImages::Two => 2, + EnqueueBytedanceSeedreamV4TextToImageMaxImages::Three => 3, + EnqueueBytedanceSeedreamV4TextToImageMaxImages::Four => 4, + }); + + let image_size = args.image_size + .map(|image_size| match image_size { + EnqueueBytedanceSeedreamV4TextToImageSize::Square => "square", + EnqueueBytedanceSeedreamV4TextToImageSize::SquareHd => "square_hd", + EnqueueBytedanceSeedreamV4TextToImageSize::PortraitFourThree => "portrait_4_3", + EnqueueBytedanceSeedreamV4TextToImageSize::PortraitSixteenNine => "portrait_16_9", + EnqueueBytedanceSeedreamV4TextToImageSize::LandscapeFourThree => "landscape_4_3", + EnqueueBytedanceSeedreamV4TextToImageSize::LandscapeSixteenNine => "landscape_16_9", + EnqueueBytedanceSeedreamV4TextToImageSize::Auto => "auto", + EnqueueBytedanceSeedreamV4TextToImageSize::Auto2k => "auto_2K", + EnqueueBytedanceSeedreamV4TextToImageSize::Auto4k => "auto_4K", + }) + .map(|resolution| resolution.to_string()); + + let request = BytedanceSeedreamV4TextToImageInput { + prompt: args.prompt.to_string(), + // Optionals + num_images, + max_images, + image_size, + // Constants + enhance_prompt_mode: Some("standard".to_string()), + enable_safety_checker: Some(false), + }; + + let result = bytedance_seedream_v4_text_to_image(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::image::text::enqueue_bytedance_seedream_v4_text_to_image_webhook::{enqueue_bytedance_seedream_v4_text_to_image_webhook, EnqueueBytedanceSeedreamV4TextToImageArgs, EnqueueBytedanceSeedreamV4TextToImageMaxImages, EnqueueBytedanceSeedreamV4TextToImageNumImages, EnqueueBytedanceSeedreamV4TextToImageSize}; + use errors::AnyhowResult; + use std::fs::read_to_string; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueBytedanceSeedreamV4TextToImageArgs { + prompt: "an anime girl is riding a t-rex in the forest", + num_images: Some(EnqueueBytedanceSeedreamV4TextToImageNumImages::Two), + max_images: Some(EnqueueBytedanceSeedreamV4TextToImageMaxImages::Two), + image_size: Some(EnqueueBytedanceSeedreamV4TextToImageSize::LandscapeSixteenNine), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_bytedance_seedream_v4_text_to_image_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_bytedance_seedream_v4p5_text_to_image_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_bytedance_seedream_v4p5_text_to_image_webhook.rs new file mode 100644 index 0000000000..b47b2133bc --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_bytedance_seedream_v4p5_text_to_image_webhook.rs @@ -0,0 +1,134 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::bytedance::seedream::bytedance_seedream_v4p5_text_to_image::{bytedance_seedream_v4p5_text_to_image, BytedanceSeedreamV4p5TextToImageInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueBytedanceSeedreamV4p5TextToImageArgs<'a, R: IntoUrl> { + // Request required + pub prompt: &'a str, + + // Optional args + pub num_images: Option, + pub max_images: Option, + pub image_size: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueBytedanceSeedreamV4p5TextToImageNumImages { + One, + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueBytedanceSeedreamV4p5TextToImageMaxImages { + One, + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueBytedanceSeedreamV4p5TextToImageSize { + // Square + Square, + SquareHd, + // Tall + PortraitFourThree, + PortraitSixteenNine, + // Wide + LandscapeFourThree, + LandscapeSixteenNine, + // Auto + Auto2k, + Auto4k, +} + +pub async fn enqueue_bytedance_seedream_v4p5_text_to_image_webhook( + args: EnqueueBytedanceSeedreamV4p5TextToImageArgs<'_, R> +) -> Result { + + let num_images = args.num_images + .map(|num_images| match num_images { + EnqueueBytedanceSeedreamV4p5TextToImageNumImages::One => 1, + EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Two => 2, + EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Three => 3, + EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Four => 4, + }); + + let max_images = args.max_images + .map(|num_images| match num_images { + EnqueueBytedanceSeedreamV4p5TextToImageMaxImages::One => 1, + EnqueueBytedanceSeedreamV4p5TextToImageMaxImages::Two => 2, + EnqueueBytedanceSeedreamV4p5TextToImageMaxImages::Three => 3, + EnqueueBytedanceSeedreamV4p5TextToImageMaxImages::Four => 4, + }); + + let image_size = args.image_size + .map(|image_size| match image_size { + EnqueueBytedanceSeedreamV4p5TextToImageSize::Square => "square", + EnqueueBytedanceSeedreamV4p5TextToImageSize::SquareHd => "square_hd", + EnqueueBytedanceSeedreamV4p5TextToImageSize::PortraitFourThree => "portrait_4_3", + EnqueueBytedanceSeedreamV4p5TextToImageSize::PortraitSixteenNine => "portrait_16_9", + EnqueueBytedanceSeedreamV4p5TextToImageSize::LandscapeFourThree => "landscape_4_3", + EnqueueBytedanceSeedreamV4p5TextToImageSize::LandscapeSixteenNine => "landscape_16_9", + EnqueueBytedanceSeedreamV4p5TextToImageSize::Auto2k => "auto_2K", + EnqueueBytedanceSeedreamV4p5TextToImageSize::Auto4k => "auto_4K", + }) + .map(|resolution| resolution.to_string()); + + let request = BytedanceSeedreamV4p5TextToImageInput { + prompt: args.prompt.to_string(), + // Optionals + num_images, + max_images, + image_size, + // Constants + enable_safety_checker: Some(false), + seed: None, + }; + + let result = bytedance_seedream_v4p5_text_to_image(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::image::text::enqueue_bytedance_seedream_v4p5_text_to_image_webhook::{enqueue_bytedance_seedream_v4p5_text_to_image_webhook, EnqueueBytedanceSeedreamV4p5TextToImageArgs, EnqueueBytedanceSeedreamV4p5TextToImageMaxImages, EnqueueBytedanceSeedreamV4p5TextToImageNumImages, EnqueueBytedanceSeedreamV4p5TextToImageSize}; + use errors::AnyhowResult; + use std::fs::read_to_string; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueBytedanceSeedreamV4p5TextToImageArgs { + prompt: "an anime girl is riding a t-rex in the forest", + num_images: Some(EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Two), + max_images: Some(EnqueueBytedanceSeedreamV4p5TextToImageMaxImages::Two), + image_size: Some(EnqueueBytedanceSeedreamV4p5TextToImageSize::LandscapeSixteenNine), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_bytedance_seedream_v4p5_text_to_image_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_nano_banana_pro_text_to_image_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_nano_banana_pro_text_to_image_webhook.rs new file mode 100644 index 0000000000..dcaf1fc7b3 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_nano_banana_pro_text_to_image_webhook.rs @@ -0,0 +1,139 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::nano_banana_pro::nano_banana_pro_text_to_image::{nano_banana_pro_text_to_image, NanoBananaProTextToImageInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueNanoBananaProTextToImageArgs<'a, R: IntoUrl> { + // Request required + pub prompt: &'a str, + pub num_images: EnqueueNanoBananaProTextToImageNumImages, + + // Optional args + pub resolution: Option, + pub aspect_ratio: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueNanoBananaProTextToImageNumImages { + One, + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueNanoBananaProTextToImageResolution { + OneK, + TwoK, + FourK, +} + +/// 21:9, 16:9, 3:2, 4:3, 5:4, 1:1, 4:5, 3:4, 2:3, 9:16 +/// Default is "1:1" +#[derive(Copy, Clone, Debug)] +pub enum EnqueueNanoBananaProTextToImageAspectRatio { + // Square + OneByOne, + // Wide + FiveByFour, + FourByThree, + ThreeByTwo, + SixteenByNine, + TwentyOneByNine, + // Tall + FourByFive, + ThreeByFour, + TwoByThree, + NineBySixteen, // NB: No NineByTwentyOne ? +} + +pub async fn enqueue_nano_banana_pro_text_to_image_webhook( + args: EnqueueNanoBananaProTextToImageArgs<'_, R> +) -> Result { + + let num_images = match args.num_images { + EnqueueNanoBananaProTextToImageNumImages::One => 1, + EnqueueNanoBananaProTextToImageNumImages::Two => 2, + EnqueueNanoBananaProTextToImageNumImages::Three => 3, + EnqueueNanoBananaProTextToImageNumImages::Four => 4, + }; + + let resolution = args.resolution + .map(|resolution| match resolution { + EnqueueNanoBananaProTextToImageResolution::OneK => "1K", + EnqueueNanoBananaProTextToImageResolution::TwoK => "2K", + EnqueueNanoBananaProTextToImageResolution::FourK => "4K", + }) + .map(|resolution| resolution.to_string()); + + let aspect_ratio = args.aspect_ratio + .map(|aspect_ratio| match aspect_ratio { + // Square + EnqueueNanoBananaProTextToImageAspectRatio::OneByOne => "1:1", + // Wide + EnqueueNanoBananaProTextToImageAspectRatio::FiveByFour => "5:4", + EnqueueNanoBananaProTextToImageAspectRatio::FourByThree => "4:3", + EnqueueNanoBananaProTextToImageAspectRatio::ThreeByTwo => "3:2", + EnqueueNanoBananaProTextToImageAspectRatio::SixteenByNine => "16:9", + EnqueueNanoBananaProTextToImageAspectRatio::TwentyOneByNine => "21:9", + // Tall + EnqueueNanoBananaProTextToImageAspectRatio::FourByFive => "4:5", + EnqueueNanoBananaProTextToImageAspectRatio::ThreeByFour => "3:4", + EnqueueNanoBananaProTextToImageAspectRatio::TwoByThree => "2:3", + EnqueueNanoBananaProTextToImageAspectRatio::NineBySixteen => "9:16", + }) + .map(|aspect_ratio| aspect_ratio.to_string()); + + let request = NanoBananaProTextToImageInput { + prompt: args.prompt.to_string(), + num_images: Some(num_images), + // Optionals + aspect_ratio, + resolution, + // Constants + output_format: Some("png".to_string()), + }; + + let result = nano_banana_pro_text_to_image(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::image::text::enqueue_nano_banana_pro_text_to_image_webhook::{enqueue_nano_banana_pro_text_to_image_webhook, EnqueueNanoBananaProTextToImageArgs, EnqueueNanoBananaProTextToImageAspectRatio, EnqueueNanoBananaProTextToImageNumImages, EnqueueNanoBananaProTextToImageResolution}; + use errors::AnyhowResult; + use std::fs::read_to_string; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueNanoBananaProTextToImageArgs { + prompt: "an anime girl riding on the back of a t-rex", + num_images: EnqueueNanoBananaProTextToImageNumImages::One, + aspect_ratio: Some(EnqueueNanoBananaProTextToImageAspectRatio::SixteenByNine), + resolution: Some(EnqueueNanoBananaProTextToImageResolution::TwoK), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_nano_banana_pro_text_to_image_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/image/text/mod.rs b/crates/api_clients/fal_client/src/requests/webhook/image/text/mod.rs new file mode 100644 index 0000000000..7bff55e3b3 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/image/text/mod.rs @@ -0,0 +1,3 @@ +pub mod enqueue_bytedance_seedream_v4_text_to_image_webhook; +pub mod enqueue_bytedance_seedream_v4p5_text_to_image_webhook; +pub mod enqueue_nano_banana_pro_text_to_image_webhook; \ No newline at end of file diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook.rs new file mode 100644 index 0000000000..1e3a72a1b4 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook.rs @@ -0,0 +1,70 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::minimax::hailuo::hailuo_v2p3_fast_pro_image_to_video::{hailuo_v2p3_fast_pro_image_to_video, HailuoV2p3FastProImageToVideoInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueHailuoV2p3FastProImageToVideoArgs<'a, R: IntoUrl> { + // Request required + pub prompt: String, + pub image_url: String, + + // Optional args + pub prompt_optimizer: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +pub async fn enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook( + args: EnqueueHailuoV2p3FastProImageToVideoArgs<'_, R> +) -> Result { + + let prompt_optimizer = args.prompt_optimizer.unwrap_or(true); + + let request = HailuoV2p3FastProImageToVideoInput { + prompt: args.prompt, + image_url: args.image_url, + // Optionals + prompt_optimizer: Some(prompt_optimizer), + }; + + let result = hailuo_v2p3_fast_pro_image_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::video::image::enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook::{enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook, EnqueueHailuoV2p3FastProImageToVideoArgs}; + use errors::AnyhowResult; + use std::fs::read_to_string; + use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueHailuoV2p3FastProImageToVideoArgs { + image_url: TREX_SKELETON_IMAGE_URL.to_string(), + prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(), + prompt_optimizer: Some(true), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_hailuo_v2p3_pro_image_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_hailuo_v2p3_pro_image_to_video_webhook.rs new file mode 100644 index 0000000000..ac1e05b8ca --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_hailuo_v2p3_pro_image_to_video_webhook.rs @@ -0,0 +1,70 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::minimax::hailuo::hailuo_v2p3_pro_image_to_video::{hailuo_v2p3_pro_image_to_video, HailuoV2p3ProImageToVideoInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueHailuoV2p3ProImageToVideoArgs<'a, R: IntoUrl> { + // Request required + pub prompt: String, + pub image_url: String, + + // Optional args + pub prompt_optimizer: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +pub async fn enqueue_hailuo_v2p3_pro_image_to_video_webhook( + args: EnqueueHailuoV2p3ProImageToVideoArgs<'_, R> +) -> Result { + + let prompt_optimizer = args.prompt_optimizer.unwrap_or(true); + + let request = HailuoV2p3ProImageToVideoInput { + prompt: args.prompt, + image_url: args.image_url, + // Optionals + prompt_optimizer: Some(prompt_optimizer), + }; + + let result = hailuo_v2p3_pro_image_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::video::image::enqueue_hailuo_v2p3_pro_image_to_video_webhook::{enqueue_hailuo_v2p3_pro_image_to_video_webhook, EnqueueHailuoV2p3ProImageToVideoArgs}; + use errors::AnyhowResult; + use std::fs::read_to_string; + use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueHailuoV2p3ProImageToVideoArgs { + image_url: TREX_SKELETON_IMAGE_URL.to_string(), + prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(), + prompt_optimizer: Some(true), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_hailuo_v2p3_pro_image_to_video_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_kling_v2p5_turbo_pro_image_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_kling_v2p5_turbo_pro_image_to_video_webhook.rs new file mode 100644 index 0000000000..af73bbe35f --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_kling_v2p5_turbo_pro_image_to_video_webhook.rs @@ -0,0 +1,90 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::kling_video::v2_5::kling_v2p5_turbo_pro_image_to_video::{kling_v2p5_turbo_pro_image_to_video, KlingV2p5TurboProImageToVideoInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueKlingV2p5TurboProImageToVideoArgs<'a, R: IntoUrl> { + // Request required + pub prompt: String, + pub image_url: String, + + // Optional args + pub tail_image_url: Option, + pub negative_prompt: Option, + + pub duration: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueKlingV2p5TurboProImageToVideoDurationSeconds { + Five, + Ten, +} + +pub async fn enqueue_kling_v2p5_turbo_pro_image_to_video_webhook( + args: EnqueueKlingV2p5TurboProImageToVideoArgs<'_, R> +) -> Result { + + let duration = args.duration + .map(|resolution| match resolution { + EnqueueKlingV2p5TurboProImageToVideoDurationSeconds::Five => "5", + EnqueueKlingV2p5TurboProImageToVideoDurationSeconds::Ten => "10", + }) + .map(|resolution| resolution.to_string()); + + let request = KlingV2p5TurboProImageToVideoInput { + prompt: args.prompt, + image_url: args.image_url, + // Optionals + duration, + tail_image_url: args.tail_image_url, + negative_prompt: args.negative_prompt, + // Constants + cfg_scale: None, + }; + + let result = kling_v2p5_turbo_pro_image_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::video::image::enqueue_kling_v2p5_turbo_pro_image_to_video_webhook::{enqueue_kling_v2p5_turbo_pro_image_to_video_webhook, EnqueueKlingV2p5TurboProImageToVideoArgs, EnqueueKlingV2p5TurboProImageToVideoDurationSeconds}; + use errors::AnyhowResult; + use std::fs::read_to_string; + use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueKlingV2p5TurboProImageToVideoArgs { + image_url: TREX_SKELETON_IMAGE_URL.to_string(), + prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(), + negative_prompt: None, + tail_image_url: None, + duration: Some(EnqueueKlingV2p5TurboProImageToVideoDurationSeconds::Five), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_kling_v2p5_turbo_pro_image_to_video_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_kling_v2p5_turbo_standard_image_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_kling_v2p5_turbo_standard_image_to_video_webhook.rs new file mode 100644 index 0000000000..497b37faed --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_kling_v2p5_turbo_standard_image_to_video_webhook.rs @@ -0,0 +1,86 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; +use fal::endpoints::fal_ai::kling_video::v2_5::kling_v2p5_turbo_standard_image_to_video::{kling_v2p5_turbo_standard_image_to_video, KlingV2p5TurboStandardImageToVideoInput}; + +pub struct EnqueueKlingV2p5TurboStandardImageToVideoArgs<'a, R: IntoUrl> { + // Request required + pub prompt: String, + pub image_url: String, + + // Optional args + pub negative_prompt: Option, + pub duration: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueKlingV2p5TurboStandardImageToVideoDurationSeconds { + Five, + Ten, +} + +pub async fn enqueue_kling_v2p5_turbo_standard_image_to_video_webhook( + args: EnqueueKlingV2p5TurboStandardImageToVideoArgs<'_, R> +) -> Result { + + let duration = args.duration + .map(|resolution| match resolution { + EnqueueKlingV2p5TurboStandardImageToVideoDurationSeconds::Five => "5", + EnqueueKlingV2p5TurboStandardImageToVideoDurationSeconds::Ten => "10", + }) + .map(|resolution| resolution.to_string()); + + let request = KlingV2p5TurboStandardImageToVideoInput { + prompt: args.prompt, + image_url: args.image_url, + // Optionals + duration, + negative_prompt: args.negative_prompt, + // Constants + cfg_scale: None, + }; + + let result = kling_v2p5_turbo_standard_image_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use errors::AnyhowResult; + use std::fs::read_to_string; + use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL; + use crate::requests::webhook::video::image::enqueue_kling_v2p5_turbo_standard_image_to_video_webhook::{enqueue_kling_v2p5_turbo_standard_image_to_video_webhook, EnqueueKlingV2p5TurboStandardImageToVideoArgs, EnqueueKlingV2p5TurboStandardImageToVideoDurationSeconds}; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueKlingV2p5TurboStandardImageToVideoArgs { + image_url: TREX_SKELETON_IMAGE_URL.to_string(), + prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(), + negative_prompt: None, + duration: Some(EnqueueKlingV2p5TurboStandardImageToVideoDurationSeconds::Five), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_kling_v2p5_turbo_standard_image_to_video_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_kling_v2p6_pro_image_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_kling_v2p6_pro_image_to_video_webhook.rs new file mode 100644 index 0000000000..7d8c3413b8 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_kling_v2p6_pro_image_to_video_webhook.rs @@ -0,0 +1,87 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::kling_video::v2_6::kling_v2p6_pro_image_to_video::{kling_v2p6_pro_image_to_video, KlingV2p6ProImageToVideoInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueKlingV2p6ProImageToVideoArgs<'a, R: IntoUrl> { + // Request required + pub prompt: String, + pub image_url: String, + + // Optional args + pub generate_audio: Option, + pub negative_prompt: Option, + pub duration: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueKlingV2p6ProImageToVideoDurationSeconds { + Five, + Ten, +} + +pub async fn enqueue_kling_v2p6_pro_image_to_video_webhook( + args: EnqueueKlingV2p6ProImageToVideoArgs<'_, R> +) -> Result { + + let duration = args.duration + .map(|resolution| match resolution { + EnqueueKlingV2p6ProImageToVideoDurationSeconds::Five => "5", + EnqueueKlingV2p6ProImageToVideoDurationSeconds::Ten => "10", + }) + .map(|resolution| resolution.to_string()); + + let request = KlingV2p6ProImageToVideoInput { + prompt: args.prompt, + image_url: args.image_url, + // Optionals + duration, + negative_prompt: args.negative_prompt, + generate_audio: args.generate_audio, + }; + + let result = kling_v2p6_pro_image_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::video::image::enqueue_kling_v2p6_pro_image_to_video_webhook::{enqueue_kling_v2p6_pro_image_to_video_webhook, EnqueueKlingV2p6ProImageToVideoArgs, EnqueueKlingV2p6ProImageToVideoDurationSeconds}; + use errors::AnyhowResult; + use std::fs::read_to_string; + use test_data::web::image_urls::ERNEST_SCARED_STUPID_IMAGE_URL; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueKlingV2p6ProImageToVideoArgs { + image_url: ERNEST_SCARED_STUPID_IMAGE_URL.to_string(), + prompt: "the man says, 'these ghosts sure are scary!' and then runs. behind him, a group of ghosts appear from the trees and chase him. the camera tracks the action. finally, the man yells, 'go away ghosts'.".to_string(), + negative_prompt: None, + generate_audio: Some(true), + duration: Some(EnqueueKlingV2p6ProImageToVideoDurationSeconds::Ten), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_kling_v2p6_pro_image_to_video_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_pixverse_v5_image_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_pixverse_v5_image_to_video_webhook.rs new file mode 100644 index 0000000000..d50f1b1cf4 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_pixverse_v5_image_to_video_webhook.rs @@ -0,0 +1,152 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::pixverse::v5::pixverse_v5_image_to_video::{pixverse_v5_image_to_video, PixverseV5ImageToVideoInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueuePixverseV5ImageToVideoArgs<'a, R: IntoUrl> { + // Request required + pub prompt: String, + pub image_url: String, + + // Optional args + pub negative_prompt: Option, + + // NB: eight-second videos do not work with 1080P + pub duration: Option, + pub aspect_ratio: Option, + pub resolution: Option, + pub style: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueuePixverseV5ImageToVideoDurationSeconds { + Five, + Eight, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueuePixverseV5ImageToVideoResolution { + ThreeSixtyP, + FiveFortyP, + SevenTwentyP, + TenEightyP, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueuePixverseV5ImageToVideoAspectRatio { + Square, + SixteenByNine, + FourByThree, + NineBySixteen, + ThreeByFour, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueuePixverseV5ImageToVideoStyle { + Anime, + Animation3d, + Clay, + Comic, + Cyberpunk, +} + +pub async fn enqueue_pixverse_v5_image_to_video_webhook( + args: EnqueuePixverseV5ImageToVideoArgs<'_, R> +) -> Result { + + let duration = args.duration + .map(|resolution| match resolution { + EnqueuePixverseV5ImageToVideoDurationSeconds::Five => "5", + EnqueuePixverseV5ImageToVideoDurationSeconds::Eight=> "8", + }) + .map(|s| s.to_string()); + + let aspect_ratio = args.aspect_ratio + .map(|aspect_ratio| match aspect_ratio { + EnqueuePixverseV5ImageToVideoAspectRatio::Square => "1:1", + EnqueuePixverseV5ImageToVideoAspectRatio::SixteenByNine => "16:9", + EnqueuePixverseV5ImageToVideoAspectRatio::FourByThree => "4:3", + EnqueuePixverseV5ImageToVideoAspectRatio::NineBySixteen => "9:16", + EnqueuePixverseV5ImageToVideoAspectRatio::ThreeByFour => "3:4", + }) + .map(|s| s.to_string()); + + let resolution = args.resolution + .map(|resolution| match resolution { + EnqueuePixverseV5ImageToVideoResolution::ThreeSixtyP => "360p", + EnqueuePixverseV5ImageToVideoResolution::FiveFortyP => "540p", + EnqueuePixverseV5ImageToVideoResolution::SevenTwentyP => "720p", + EnqueuePixverseV5ImageToVideoResolution::TenEightyP => "1080p", + }) + .map(|s| s.to_string()); + + let style = args.style + .map(|style| match style { + EnqueuePixverseV5ImageToVideoStyle::Anime => "anime", + EnqueuePixverseV5ImageToVideoStyle::Animation3d => "3d_animation", + EnqueuePixverseV5ImageToVideoStyle::Clay => "clay", + EnqueuePixverseV5ImageToVideoStyle::Comic => "comic", + EnqueuePixverseV5ImageToVideoStyle::Cyberpunk => "cyberpunk", + }) + .map(|s| s.to_string()); + + let request = PixverseV5ImageToVideoInput { + prompt: args.prompt, + image_url: args.image_url, + // Optionals + duration, + negative_prompt: args.negative_prompt, + aspect_ratio, + resolution, + style, + // Constant + seed: None, + }; + + let result = pixverse_v5_image_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::video::image::enqueue_pixverse_v5_image_to_video_webhook::{enqueue_pixverse_v5_image_to_video_webhook, EnqueuePixverseV5ImageToVideoArgs, EnqueuePixverseV5ImageToVideoAspectRatio, EnqueuePixverseV5ImageToVideoDurationSeconds, EnqueuePixverseV5ImageToVideoResolution, EnqueuePixverseV5ImageToVideoStyle}; + use errors::AnyhowResult; + use std::fs::read_to_string; + use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueuePixverseV5ImageToVideoArgs { + image_url: TREX_SKELETON_IMAGE_URL.to_string(), + prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(), + negative_prompt: None, + duration: Some(EnqueuePixverseV5ImageToVideoDurationSeconds::Five), + style: Some(EnqueuePixverseV5ImageToVideoStyle::Cyberpunk), + aspect_ratio: Some(EnqueuePixverseV5ImageToVideoAspectRatio::FourByThree), + resolution: Some(EnqueuePixverseV5ImageToVideoResolution::TenEightyP), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_pixverse_v5_image_to_video_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_sora_2_image_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_sora_2_image_to_video_webhook.rs new file mode 100644 index 0000000000..dae173616c --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_sora_2_image_to_video_webhook.rs @@ -0,0 +1,118 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::sora::sora2::sora_2_image_to_video::{sora_2_image_to_video, Sora2ImageToVideoInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueSora2ImageToVideoArgs<'a, R: IntoUrl> { + // Request required + pub prompt: String, + pub image_url: String, + + // Optional args + pub resolution: Option, + pub duration: Option, + pub aspect_ratio: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueSora2ImageToVideoDurationSeconds { + Four, + Eight, + Twelve, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueSora2ImageToVideoResolution { + Auto, + SevenTwentyP, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueSora2ImageToVideoAspectRatio { + Auto, + NineBySixteen, + SixteenByNine, +} + +pub async fn enqueue_sora_2_image_to_video_webhook( + args: EnqueueSora2ImageToVideoArgs<'_, R> +) -> Result { + + let duration = args.duration + .map(|resolution| match resolution { + EnqueueSora2ImageToVideoDurationSeconds::Four => 4, + EnqueueSora2ImageToVideoDurationSeconds::Eight => 8, + EnqueueSora2ImageToVideoDurationSeconds::Twelve => 12, + }); + + let resolution = args.resolution + .map(|resolution| match resolution { + EnqueueSora2ImageToVideoResolution::Auto => "auto", + EnqueueSora2ImageToVideoResolution::SevenTwentyP => "720p", + }) + .map(|resolution| resolution.to_string()); + + let aspect_ratio = args.aspect_ratio + .map(|aspect_ratio| match aspect_ratio { + EnqueueSora2ImageToVideoAspectRatio::Auto => "auto", + EnqueueSora2ImageToVideoAspectRatio::NineBySixteen => "9:16", + EnqueueSora2ImageToVideoAspectRatio::SixteenByNine => "16:9", + }) + .map(|resolution| resolution.to_string()); + + let request = Sora2ImageToVideoInput { + prompt: args.prompt, + image_url: args.image_url, + // Optionals + duration, + resolution, + aspect_ratio, + // Constants + delete_video: Some(false), + }; + + let result = sora_2_image_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::video::image::enqueue_sora_2_image_to_video_webhook::{enqueue_sora_2_image_to_video_webhook, EnqueueSora2ImageToVideoArgs, EnqueueSora2ImageToVideoAspectRatio, EnqueueSora2ImageToVideoDurationSeconds, EnqueueSora2ImageToVideoResolution}; + use errors::AnyhowResult; + use std::fs::read_to_string; + use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueSora2ImageToVideoArgs { + image_url: TREX_SKELETON_IMAGE_URL.to_string(), + prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(), + duration: Some(EnqueueSora2ImageToVideoDurationSeconds::Twelve), + aspect_ratio: Some(EnqueueSora2ImageToVideoAspectRatio::SixteenByNine), + resolution: Some(EnqueueSora2ImageToVideoResolution::SevenTwentyP), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_sora_2_image_to_video_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_sora_2_pro_image_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_sora_2_pro_image_to_video_webhook.rs new file mode 100644 index 0000000000..955c174ebb --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_sora_2_pro_image_to_video_webhook.rs @@ -0,0 +1,120 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::sora::sora2::sora_2_pro_image_to_video::{sora_2_pro_image_to_video, Sora2ProImageToVideoInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueSora2ProImageToVideoArgs<'a, R: IntoUrl> { + // Request required + pub prompt: String, + pub image_url: String, + + // Optional args + pub resolution: Option, + pub duration: Option, + pub aspect_ratio: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueSora2ProImageToVideoDurationSeconds { + Four, + Eight, + Twelve, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueSora2ProImageToVideoResolution { + Auto, + SevenTwentyP, + TenEightyP, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueSora2ProImageToVideoAspectRatio { + Auto, + NineBySixteen, + SixteenByNine, +} + +pub async fn enqueue_sora_2_pro_image_to_video_webhook( + args: EnqueueSora2ProImageToVideoArgs<'_, R> +) -> Result { + + let duration = args.duration + .map(|resolution| match resolution { + EnqueueSora2ProImageToVideoDurationSeconds::Four => 4, + EnqueueSora2ProImageToVideoDurationSeconds::Eight => 8, + EnqueueSora2ProImageToVideoDurationSeconds::Twelve => 12, + }); + + let resolution = args.resolution + .map(|resolution| match resolution { + EnqueueSora2ProImageToVideoResolution::Auto => "auto", + EnqueueSora2ProImageToVideoResolution::SevenTwentyP => "720p", + EnqueueSora2ProImageToVideoResolution::TenEightyP => "1080p", + }) + .map(|resolution| resolution.to_string()); + + let aspect_ratio = args.aspect_ratio + .map(|aspect_ratio| match aspect_ratio { + EnqueueSora2ProImageToVideoAspectRatio::Auto => "auto", + EnqueueSora2ProImageToVideoAspectRatio::NineBySixteen => "9:16", + EnqueueSora2ProImageToVideoAspectRatio::SixteenByNine => "16:9", + }) + .map(|resolution| resolution.to_string()); + + let request = Sora2ProImageToVideoInput { + prompt: args.prompt, + image_url: args.image_url, + // Optionals + duration, + resolution, + aspect_ratio, + // Constants + delete_video: Some(false), + }; + + let result = sora_2_pro_image_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::video::image::enqueue_sora_2_pro_image_to_video_webhook::{enqueue_sora_2_pro_image_to_video_webhook, EnqueueSora2ProImageToVideoArgs, EnqueueSora2ProImageToVideoAspectRatio, EnqueueSora2ProImageToVideoDurationSeconds, EnqueueSora2ProImageToVideoResolution}; + use errors::AnyhowResult; + use std::fs::read_to_string; + use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueSora2ProImageToVideoArgs { + image_url: TREX_SKELETON_IMAGE_URL.to_string(), + prompt: "the t-rex skeleton gets off the podium and begins walking to the camera. the camera orbits slightly. The t-rex gets close and then bites.".to_string(), + duration: Some(EnqueueSora2ProImageToVideoDurationSeconds::Twelve), + aspect_ratio: Some(EnqueueSora2ProImageToVideoAspectRatio::SixteenByNine), + resolution: Some(EnqueueSora2ProImageToVideoResolution::TenEightyP), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_sora_2_pro_image_to_video_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_veo_3p1_fast_first_last_frame_image_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_veo_3p1_fast_first_last_frame_image_to_video_webhook.rs new file mode 100644 index 0000000000..015bf5e071 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_veo_3p1_fast_first_last_frame_image_to_video_webhook.rs @@ -0,0 +1,129 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::veo::veo3_1::veo_3p1_fast_first_last_frame_image_to_video::{veo_3p1_fast_first_last_frame_image_to_video, Veo3p1FastFirstLastFrameImageToVideoInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueVeo3p1FastFirstLastFrameImageToVideoArgs<'a, R: IntoUrl> { + // Request required + pub prompt: String, + + // Starting frame + pub first_frame_url: String, + + // Ending frame + pub last_frame_url: String, + + // Optional args + pub duration: Option, + pub aspect_ratio: Option, + pub resolution: Option, + pub generate_audio: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds { + Four, + Six, + Eight, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio { + Auto, + Square, + SixteenByNine, + NineBySixteen, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueVeo3p1FastFirstLastFrameImageToVideoResolution { + SevenTwentyP, + TenEightyP, +} + +pub async fn enqueue_veo_3p1_fast_first_last_frame_image_to_video_webhook( + args: EnqueueVeo3p1FastFirstLastFrameImageToVideoArgs<'_, R> +) -> Result { + + let duration = args.duration + .map(|resolution| match resolution { + EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds::Four => "4s", + EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds::Six => "6s", + EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds::Eight=> "8s", + }) + .map(|s| s.to_string()); + + let aspect_ratio = args.aspect_ratio + .map(|aspect_ratio| match aspect_ratio { + EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio::Auto => "auto", + EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio::Square => "1:1", + EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio::SixteenByNine => "16:9", + EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio::NineBySixteen => "9:16", + }) + .map(|s| s.to_string()); + + let resolution = args.resolution + .map(|resolution| match resolution { + EnqueueVeo3p1FastFirstLastFrameImageToVideoResolution::SevenTwentyP => "720p", + EnqueueVeo3p1FastFirstLastFrameImageToVideoResolution::TenEightyP => "1080p", + }) + .map(|s| s.to_string()); + + let request = Veo3p1FastFirstLastFrameImageToVideoInput { + prompt: args.prompt, + first_frame_url: args.first_frame_url, + last_frame_url: args.last_frame_url, + generate_audio: args.generate_audio, + // Optionals + duration, + aspect_ratio, + resolution, + }; + + let result = veo_3p1_fast_first_last_frame_image_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::video::image::enqueue_veo_3p1_fast_first_last_frame_image_to_video_webhook::{enqueue_veo_3p1_fast_first_last_frame_image_to_video_webhook, EnqueueVeo3p1FastFirstLastFrameImageToVideoArgs, EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio, EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds, EnqueueVeo3p1FastFirstLastFrameImageToVideoResolution}; + use errors::AnyhowResult; + use std::fs::read_to_string; + use test_data::web::image_urls::{TALL_CORGI_SHIBA_TREASURE_OCEAN_URL, TALL_CORGI_SHIBA_TREASURE_SKY_URL}; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueVeo3p1FastFirstLastFrameImageToVideoArgs { + first_frame_url: TALL_CORGI_SHIBA_TREASURE_OCEAN_URL.to_string(), + last_frame_url: TALL_CORGI_SHIBA_TREASURE_SKY_URL.to_string(), + prompt: "There is a tiny ocean island with a corgi and shiba and treasure chest on it. The corgi and shiba are barking at the chest, when suddenly the island launches itself into the air. The camera tracks the island and follows it up high in the sky. The sun beams over the horizon. The dogs are happy and bark. The gold coins gleam in the sun.".to_string(), + duration: Some(EnqueueVeo3p1FastFirstLastFrameImageToVideoDurationSeconds::Six), + aspect_ratio: Some(EnqueueVeo3p1FastFirstLastFrameImageToVideoAspectRatio::NineBySixteen), + resolution: Some(EnqueueVeo3p1FastFirstLastFrameImageToVideoResolution::SevenTwentyP), + generate_audio: Some(true), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_veo_3p1_fast_first_last_frame_image_to_video_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_veo_3p1_fast_image_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_veo_3p1_fast_image_to_video_webhook.rs new file mode 100644 index 0000000000..3d135953c1 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_veo_3p1_fast_image_to_video_webhook.rs @@ -0,0 +1,124 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::veo::veo3_1::veo_3p1_fast_first_frame_image_to_video::{veo_3p1_fast_first_frame_image_to_video, Veo3p1FastFirstFrameImageToVideoInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueVeo3p1FastImageToVideoArgs<'a, R: IntoUrl> { + // Request required + pub prompt: String, + + // Starting frame + pub image_url: String, + + // Optional args + pub duration: Option, + pub aspect_ratio: Option, + pub resolution: Option, + pub generate_audio: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueVeo3p1FastImageToVideoDurationSeconds { + Four, + Six, + Eight, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueVeo3p1FastImageToVideoAspectRatio { + Auto, + Square, + SixteenByNine, + NineBySixteen, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueVeo3p1FastImageToVideoResolution { + SevenTwentyP, + TenEightyP, +} + +pub async fn enqueue_veo_3p1_fast_image_to_video_webhook( + args: EnqueueVeo3p1FastImageToVideoArgs<'_, R> +) -> Result { + + let duration = args.duration + .map(|resolution| match resolution { + EnqueueVeo3p1FastImageToVideoDurationSeconds::Four => "4s", + EnqueueVeo3p1FastImageToVideoDurationSeconds::Six => "6s", + EnqueueVeo3p1FastImageToVideoDurationSeconds::Eight=> "8s", + }) + .map(|s| s.to_string()); + + let aspect_ratio = args.aspect_ratio + .map(|aspect_ratio| match aspect_ratio { + EnqueueVeo3p1FastImageToVideoAspectRatio::Auto => "auto", + EnqueueVeo3p1FastImageToVideoAspectRatio::Square => "1:1", + EnqueueVeo3p1FastImageToVideoAspectRatio::SixteenByNine => "16:9", + EnqueueVeo3p1FastImageToVideoAspectRatio::NineBySixteen => "9:16", + }) + .map(|s| s.to_string()); + + let resolution = args.resolution + .map(|resolution| match resolution { + EnqueueVeo3p1FastImageToVideoResolution::SevenTwentyP => "720p", + EnqueueVeo3p1FastImageToVideoResolution::TenEightyP => "1080p", + }) + .map(|s| s.to_string()); + + let request = Veo3p1FastFirstFrameImageToVideoInput { + prompt: args.prompt, + image_url: args.image_url, + generate_audio: args.generate_audio, + // Optionals + duration, + aspect_ratio, + resolution, + }; + + let result = veo_3p1_fast_first_frame_image_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::video::image::enqueue_veo_3p1_fast_image_to_video_webhook::{enqueue_veo_3p1_fast_image_to_video_webhook, EnqueueVeo3p1FastImageToVideoArgs, EnqueueVeo3p1FastImageToVideoAspectRatio, EnqueueVeo3p1FastImageToVideoDurationSeconds, EnqueueVeo3p1FastImageToVideoResolution}; + use errors::AnyhowResult; + use std::fs::read_to_string; + use test_data::web::image_urls::TALL_CORGI_SHIBA_TREASURE_OCEAN_URL; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueVeo3p1FastImageToVideoArgs { + image_url: TALL_CORGI_SHIBA_TREASURE_OCEAN_URL.to_string(), + prompt: "There is a tiny ocean island with a corgi and shiba and treasure chest on it. The corgi and shiba are barking at the chest, when suddenly the island launches itself into the air. The camera tracks the island and follows it up high in the sky. The sun beams over the horizon. The dogs are happy and bark. The gold coins gleam in the sun.".to_string(), + duration: Some(EnqueueVeo3p1FastImageToVideoDurationSeconds::Four), + aspect_ratio: Some(EnqueueVeo3p1FastImageToVideoAspectRatio::SixteenByNine), + resolution: Some(EnqueueVeo3p1FastImageToVideoResolution::TenEightyP), + generate_audio: Some(true), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_veo_3p1_fast_image_to_video_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_veo_3p1_first_last_frame_image_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_veo_3p1_first_last_frame_image_to_video_webhook.rs new file mode 100644 index 0000000000..ee188ad6f0 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_veo_3p1_first_last_frame_image_to_video_webhook.rs @@ -0,0 +1,129 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::veo::veo3_1::veo_3p1_first_last_frame_image_to_video::{veo_3p1_first_last_frame_image_to_video, Veo3p1FirstLastFrameImageToVideoInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueVeo3p1FirstLastFrameImageToVideoArgs<'a, R: IntoUrl> { + // Request required + pub prompt: String, + + // Starting frame + pub first_frame_url: String, + + // Ending frame + pub last_frame_url: String, + + // Optional args + pub duration: Option, + pub aspect_ratio: Option, + pub resolution: Option, + pub generate_audio: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds { + Four, + Six, + Eight, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio { + Auto, + Square, + SixteenByNine, + NineBySixteen, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueVeo3p1FirstLastFrameImageToVideoResolution { + SevenTwentyP, + TenEightyP, +} + +pub async fn enqueue_veo_3p1_first_last_frame_image_to_video_webhook( + args: EnqueueVeo3p1FirstLastFrameImageToVideoArgs<'_, R> +) -> Result { + + let duration = args.duration + .map(|resolution| match resolution { + EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds::Four => "4s", + EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds::Six => "6s", + EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds::Eight=> "8s", + }) + .map(|s| s.to_string()); + + let aspect_ratio = args.aspect_ratio + .map(|aspect_ratio| match aspect_ratio { + EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio::Auto => "auto", + EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio::Square => "1:1", + EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio::SixteenByNine => "16:9", + EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio::NineBySixteen => "9:16", + }) + .map(|s| s.to_string()); + + let resolution = args.resolution + .map(|resolution| match resolution { + EnqueueVeo3p1FirstLastFrameImageToVideoResolution::SevenTwentyP => "720p", + EnqueueVeo3p1FirstLastFrameImageToVideoResolution::TenEightyP => "1080p", + }) + .map(|s| s.to_string()); + + let request = Veo3p1FirstLastFrameImageToVideoInput { + prompt: args.prompt, + first_frame_url: args.first_frame_url, + last_frame_url: args.last_frame_url, + generate_audio: args.generate_audio, + // Optionals + duration, + aspect_ratio, + resolution, + }; + + let result = veo_3p1_first_last_frame_image_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::video::image::enqueue_veo_3p1_first_last_frame_image_to_video_webhook::{enqueue_veo_3p1_first_last_frame_image_to_video_webhook, EnqueueVeo3p1FirstLastFrameImageToVideoArgs, EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio, EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds, EnqueueVeo3p1FirstLastFrameImageToVideoResolution}; + use errors::AnyhowResult; + use std::fs::read_to_string; + use test_data::web::image_urls::{TALL_CORGI_SHIBA_TREASURE_OCEAN_URL, TALL_CORGI_SHIBA_TREASURE_SKY_URL}; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueVeo3p1FirstLastFrameImageToVideoArgs { + first_frame_url: TALL_CORGI_SHIBA_TREASURE_OCEAN_URL.to_string(), + last_frame_url: TALL_CORGI_SHIBA_TREASURE_SKY_URL.to_string(), + prompt: "There is a tiny ocean island with a corgi and shiba and treasure chest on it. The corgi and shiba are barking at the chest, when suddenly the island launches itself into the air. The camera tracks the island and follows it up high in the sky. The sun beams over the horizon. The dogs are happy and bark. The gold coins gleam in the sun.".to_string(), + duration: Some(EnqueueVeo3p1FirstLastFrameImageToVideoDurationSeconds::Eight), + aspect_ratio: Some(EnqueueVeo3p1FirstLastFrameImageToVideoAspectRatio::NineBySixteen), + resolution: Some(EnqueueVeo3p1FirstLastFrameImageToVideoResolution::TenEightyP), + generate_audio: Some(true), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_veo_3p1_first_last_frame_image_to_video_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_veo_3p1_image_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_veo_3p1_image_to_video_webhook.rs new file mode 100644 index 0000000000..cd49031d6b --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/image/enqueue_veo_3p1_image_to_video_webhook.rs @@ -0,0 +1,124 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::veo::veo3_1::veo_3p1_first_frame_image_to_video::{veo_3p1_first_frame_image_to_video, Veo3p1FirstFrameImageToVideoInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueVeo3p1ImageToVideoArgs<'a, R: IntoUrl> { + // Request required + pub prompt: String, + + // Starting frame + pub image_url: String, + + // Optional args + pub duration: Option, + pub aspect_ratio: Option, + pub resolution: Option, + pub generate_audio: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueVeo3p1ImageToVideoDurationSeconds { + Four, + Six, + Eight, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueVeo3p1ImageToVideoAspectRatio { + Auto, + Square, + SixteenByNine, + NineBySixteen, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueVeo3p1ImageToVideoResolution { + SevenTwentyP, + TenEightyP, +} + +pub async fn enqueue_veo_3p1_image_to_video_webhook( + args: EnqueueVeo3p1ImageToVideoArgs<'_, R> +) -> Result { + + let duration = args.duration + .map(|resolution| match resolution { + EnqueueVeo3p1ImageToVideoDurationSeconds::Four => "4s", + EnqueueVeo3p1ImageToVideoDurationSeconds::Six => "6s", + EnqueueVeo3p1ImageToVideoDurationSeconds::Eight=> "8s", + }) + .map(|s| s.to_string()); + + let aspect_ratio = args.aspect_ratio + .map(|aspect_ratio| match aspect_ratio { + EnqueueVeo3p1ImageToVideoAspectRatio::Auto => "auto", + EnqueueVeo3p1ImageToVideoAspectRatio::Square => "1:1", + EnqueueVeo3p1ImageToVideoAspectRatio::SixteenByNine => "16:9", + EnqueueVeo3p1ImageToVideoAspectRatio::NineBySixteen => "9:16", + }) + .map(|s| s.to_string()); + + let resolution = args.resolution + .map(|resolution| match resolution { + EnqueueVeo3p1ImageToVideoResolution::SevenTwentyP => "720p", + EnqueueVeo3p1ImageToVideoResolution::TenEightyP => "1080p", + }) + .map(|s| s.to_string()); + + let request = Veo3p1FirstFrameImageToVideoInput { + prompt: args.prompt, + image_url: args.image_url, + generate_audio: args.generate_audio, + // Optionals + duration, + aspect_ratio, + resolution, + }; + + let result = veo_3p1_first_frame_image_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::video::image::enqueue_veo_3p1_image_to_video_webhook::{enqueue_veo_3p1_image_to_video_webhook, EnqueueVeo3p1ImageToVideoArgs, EnqueueVeo3p1ImageToVideoAspectRatio, EnqueueVeo3p1ImageToVideoDurationSeconds, EnqueueVeo3p1ImageToVideoResolution}; + use errors::AnyhowResult; + use std::fs::read_to_string; + use test_data::web::image_urls::TALL_CORGI_SHIBA_TREASURE_OCEAN_URL; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueVeo3p1ImageToVideoArgs { + image_url: TALL_CORGI_SHIBA_TREASURE_OCEAN_URL.to_string(), + prompt: "There is a tiny ocean island with a corgi and shiba and treasure chest on it. The corgi and shiba are barking at the chest, when suddenly the island launches itself into the air. The camera tracks the island and follows it up high in the sky. The sun beams over the horizon. The dogs are happy and bark. The gold coins gleam in the sun.".to_string(), + duration: Some(EnqueueVeo3p1ImageToVideoDurationSeconds::Eight), + aspect_ratio: Some(EnqueueVeo3p1ImageToVideoAspectRatio::NineBySixteen), + resolution: Some(EnqueueVeo3p1ImageToVideoResolution::TenEightyP), + generate_audio: Some(true), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_veo_3p1_image_to_video_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/image/mod.rs b/crates/api_clients/fal_client/src/requests/webhook/video/image/mod.rs new file mode 100644 index 0000000000..912e7c3c11 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/image/mod.rs @@ -0,0 +1,12 @@ +pub mod enqueue_hailuo_v2p3_fast_pro_image_to_video_webhook; +pub mod enqueue_hailuo_v2p3_pro_image_to_video_webhook; +pub mod enqueue_kling_v2p5_turbo_pro_image_to_video_webhook; +pub mod enqueue_kling_v2p5_turbo_standard_image_to_video_webhook; +pub mod enqueue_kling_v2p6_pro_image_to_video_webhook; +pub mod enqueue_pixverse_v5_image_to_video_webhook; +pub mod enqueue_sora_2_image_to_video_webhook; +pub mod enqueue_sora_2_pro_image_to_video_webhook; +pub mod enqueue_veo_3p1_fast_first_last_frame_image_to_video_webhook; +pub mod enqueue_veo_3p1_fast_image_to_video_webhook; +pub mod enqueue_veo_3p1_first_last_frame_image_to_video_webhook; +pub mod enqueue_veo_3p1_image_to_video_webhook; \ No newline at end of file diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/mod.rs b/crates/api_clients/fal_client/src/requests/webhook/video/mod.rs index 94ba9a5fb0..88bd9e5ee0 100644 --- a/crates/api_clients/fal_client/src/requests/webhook/video/mod.rs +++ b/crates/api_clients/fal_client/src/requests/webhook/video/mod.rs @@ -5,4 +5,6 @@ pub mod enqueue_seedance_1_lite_image_to_video_webhook; pub mod enqueue_seedance_1_pro_image_to_video_webhook; pub mod enqueue_veo_2_image_to_video_webhook; pub mod enqueue_veo_3_fast_image_to_video_webhook; -pub mod enqueue_veo_3_image_to_video_webhook; \ No newline at end of file +pub mod enqueue_veo_3_image_to_video_webhook; +pub mod image; +pub mod text; \ No newline at end of file diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_hailuo_v2p3_pro_text_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_hailuo_v2p3_pro_text_to_video_webhook.rs new file mode 100644 index 0000000000..409d3a805f --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_hailuo_v2p3_pro_text_to_video_webhook.rs @@ -0,0 +1,66 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::minimax::hailuo::hailuo_v2p3_pro_text_to_video::{hailuo_v2p3_pro_text_to_video, HailuoV2p3ProTextToVideoInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueHailuoV2p3ProTextToVideoArgs<'a, R: IntoUrl> { + // Request required + pub prompt: String, + + // Optional args + pub prompt_optimizer: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +pub async fn enqueue_hailuo_v2p3_pro_text_to_video_webhook( + args: EnqueueHailuoV2p3ProTextToVideoArgs<'_, R> +) -> Result { + + let prompt_optimizer = args.prompt_optimizer.unwrap_or(true); + + let request = HailuoV2p3ProTextToVideoInput { + prompt: args.prompt, + // Optionals + prompt_optimizer: Some(prompt_optimizer), + }; + + let result = hailuo_v2p3_pro_text_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::video::text::enqueue_hailuo_v2p3_pro_text_to_video_webhook::{enqueue_hailuo_v2p3_pro_text_to_video_webhook, EnqueueHailuoV2p3ProTextToVideoArgs}; + use errors::AnyhowResult; + use std::fs::read_to_string; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueHailuoV2p3ProTextToVideoArgs { + prompt: "a gray alien with big eyes dressed in an american flag tank top gives the peace symbol, it then barbecues some hot dogs on the grill".to_string(), + prompt_optimizer: Some(true), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_hailuo_v2p3_pro_text_to_video_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_kling_v2p5_turbo_pro_text_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_kling_v2p5_turbo_pro_text_to_video_webhook.rs new file mode 100644 index 0000000000..92812cb169 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_kling_v2p5_turbo_pro_text_to_video_webhook.rs @@ -0,0 +1,100 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::kling_video::v2_5::kling_v2p5_turbo_pro_text_to_video::{kling_v2p5_turbo_pro_text_to_video, KlingV2p5TurboProTextToVideoInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueKlingV2p5TurboProTextToVideoArgs<'a, R: IntoUrl> { + // Request required + pub prompt: String, + + // Optional args + pub negative_prompt: Option, + pub duration: Option, + pub aspect_ratio: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueKlingV2p5TurboProTextToVideoDurationSeconds { + Five, + Ten, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueKlingV2p5TurboProTextToVideoAspectRatio { + Square, + SixteenByNine, + NineBySixteen, +} + +pub async fn enqueue_kling_v2p5_turbo_pro_text_to_video_webhook( + args: EnqueueKlingV2p5TurboProTextToVideoArgs<'_, R> +) -> Result { + + let duration = args.duration + .map(|resolution| match resolution { + EnqueueKlingV2p5TurboProTextToVideoDurationSeconds::Five => "5", + EnqueueKlingV2p5TurboProTextToVideoDurationSeconds::Ten => "10", + }) + .map(|s| s.to_string()); + + let aspect_ratio = args.aspect_ratio + .map(|aspect| match aspect { + EnqueueKlingV2p5TurboProTextToVideoAspectRatio::Square => "1:1", + EnqueueKlingV2p5TurboProTextToVideoAspectRatio::SixteenByNine => "16:9", + EnqueueKlingV2p5TurboProTextToVideoAspectRatio::NineBySixteen => "9:16", + }) + .map(|s| s.to_string()); + + let request = KlingV2p5TurboProTextToVideoInput { + prompt: args.prompt, + // Optionals + duration, + aspect_ratio, + negative_prompt: args.negative_prompt, + // Constants + cfg_scale: None, + }; + + let result = kling_v2p5_turbo_pro_text_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::video::text::enqueue_kling_v2p5_turbo_pro_text_to_video_webhook::{enqueue_kling_v2p5_turbo_pro_text_to_video_webhook, EnqueueKlingV2p5TurboProTextToVideoArgs, EnqueueKlingV2p5TurboProTextToVideoAspectRatio, EnqueueKlingV2p5TurboProTextToVideoDurationSeconds}; + use errors::AnyhowResult; + use std::fs::read_to_string; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueKlingV2p5TurboProTextToVideoArgs { + prompt: "an owl flies by an abandoned castle at dusk. the camera tracks the flying owl. the castle stones are damp and gritty. there are lots of trees. fireflies dance over the fields".to_string(), + negative_prompt: None, + aspect_ratio: Some(EnqueueKlingV2p5TurboProTextToVideoAspectRatio::SixteenByNine), + duration: Some(EnqueueKlingV2p5TurboProTextToVideoDurationSeconds::Five), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_kling_v2p5_turbo_pro_text_to_video_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_kling_v2p6_pro_text_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_kling_v2p6_pro_text_to_video_webhook.rs new file mode 100644 index 0000000000..7089149daf --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_kling_v2p6_pro_text_to_video_webhook.rs @@ -0,0 +1,103 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::kling_video::v2_6::kling_v2p6_pro_text_to_video::{kling_v2p6_pro_text_to_video, KlingV2p6ProTextToVideoInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueKlingV2p6ProTextToVideoArgs<'a, R: IntoUrl> { + // Request required + pub prompt: String, + + // Optional args + pub generate_audio: Option, + pub negative_prompt: Option, + pub duration: Option, + pub aspect_ratio: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueKlingV2p6ProTextToVideoDurationSeconds { + Five, + Ten, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueKlingV2p6ProTextToVideoAspectRatio { + Square, + SixteenByNine, + NineBySixteen, +} + +pub async fn enqueue_kling_v2p6_pro_text_to_video_webhook( + args: EnqueueKlingV2p6ProTextToVideoArgs<'_, R> +) -> Result { + + let duration = args.duration + .map(|resolution| match resolution { + EnqueueKlingV2p6ProTextToVideoDurationSeconds::Five => "5", + EnqueueKlingV2p6ProTextToVideoDurationSeconds::Ten => "10", + }) + .map(|s| s.to_string()); + + let aspect_ratio = args.aspect_ratio + .map(|aspect| match aspect { + EnqueueKlingV2p6ProTextToVideoAspectRatio::Square => "1:1", + EnqueueKlingV2p6ProTextToVideoAspectRatio::SixteenByNine => "16:9", + EnqueueKlingV2p6ProTextToVideoAspectRatio::NineBySixteen => "9:16", + }) + .map(|s| s.to_string()); + + let request = KlingV2p6ProTextToVideoInput { + prompt: args.prompt, + // Optionals + generate_audio: args.generate_audio, + duration, + aspect_ratio, + negative_prompt: args.negative_prompt, + // Constants + cfg_scale: None, + }; + + let result = kling_v2p6_pro_text_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use errors::AnyhowResult; + use std::fs::read_to_string; + use crate::requests::webhook::video::text::enqueue_kling_v2p6_pro_text_to_video_webhook::{enqueue_kling_v2p6_pro_text_to_video_webhook, EnqueueKlingV2p6ProTextToVideoArgs, EnqueueKlingV2p6ProTextToVideoAspectRatio, EnqueueKlingV2p6ProTextToVideoDurationSeconds}; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueKlingV2p6ProTextToVideoArgs { + prompt: "a humanoid lizard creature walks around on the surface of an alien planet. the sky is purple and there are yellow plants. The lizard says, 'You should consider day trading'. Suddenly a giant t-rex comes in from off screen and eats the lizard creature.".to_string(), + negative_prompt: None, + aspect_ratio: Some(EnqueueKlingV2p6ProTextToVideoAspectRatio::SixteenByNine), + duration: Some(EnqueueKlingV2p6ProTextToVideoDurationSeconds::Ten), + generate_audio: Some(true), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_kling_v2p6_pro_text_to_video_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_pixverse_v5_text_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_pixverse_v5_text_to_video_webhook.rs new file mode 100644 index 0000000000..e9f0439ef6 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_pixverse_v5_text_to_video_webhook.rs @@ -0,0 +1,148 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::pixverse::v5::pixverse_v5_text_to_video::{pixverse_v5_text_to_video, PixverseV5TextToVideoInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueuePixverseV5TextToVideoArgs<'a, R: IntoUrl> { + // Request required + pub prompt: String, + + // Optional args + pub negative_prompt: Option, + + // NB: eight-second videos do not work with 1080P + pub duration: Option, + pub aspect_ratio: Option, + pub resolution: Option, + pub style: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueuePixverseV5TextToVideoDurationSeconds { + Five, + Eight, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueuePixverseV5TextToVideoResolution { + ThreeSixtyP, + FiveFortyP, + SevenTwentyP, + TenEightyP, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueuePixverseV5TextToVideoAspectRatio { + Square, + SixteenByNine, + FourByThree, + NineBySixteen, + ThreeByFour, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueuePixverseV5TextToVideoStyle { + Anime, + Animation3d, + Clay, + Comic, + Cyberpunk, +} + +pub async fn enqueue_pixverse_v5_text_to_video_webhook( + args: EnqueuePixverseV5TextToVideoArgs<'_, R> +) -> Result { + + let duration = args.duration + .map(|resolution| match resolution { + EnqueuePixverseV5TextToVideoDurationSeconds::Five => "5", + EnqueuePixverseV5TextToVideoDurationSeconds::Eight=> "8", + }) + .map(|s| s.to_string()); + + let aspect_ratio = args.aspect_ratio + .map(|aspect_ratio| match aspect_ratio { + EnqueuePixverseV5TextToVideoAspectRatio::Square => "1:1", + EnqueuePixverseV5TextToVideoAspectRatio::SixteenByNine => "16:9", + EnqueuePixverseV5TextToVideoAspectRatio::FourByThree => "4:3", + EnqueuePixverseV5TextToVideoAspectRatio::NineBySixteen => "9:16", + EnqueuePixverseV5TextToVideoAspectRatio::ThreeByFour => "3:4", + }) + .map(|s| s.to_string()); + + let resolution = args.resolution + .map(|resolution| match resolution { + EnqueuePixverseV5TextToVideoResolution::ThreeSixtyP => "360p", + EnqueuePixverseV5TextToVideoResolution::FiveFortyP => "540p", + EnqueuePixverseV5TextToVideoResolution::SevenTwentyP => "720p", + EnqueuePixverseV5TextToVideoResolution::TenEightyP => "1080p", + }) + .map(|s| s.to_string()); + + let style = args.style + .map(|style| match style { + EnqueuePixverseV5TextToVideoStyle::Anime => "anime", + EnqueuePixverseV5TextToVideoStyle::Animation3d => "3d_animation", + EnqueuePixverseV5TextToVideoStyle::Clay => "clay", + EnqueuePixverseV5TextToVideoStyle::Comic => "comic", + EnqueuePixverseV5TextToVideoStyle::Cyberpunk => "cyberpunk", + }) + .map(|s| s.to_string()); + + let request = PixverseV5TextToVideoInput { + prompt: args.prompt, + // Optionals + duration, + negative_prompt: args.negative_prompt, + aspect_ratio, + resolution, + style, + // Constant + seed: None, + }; + + let result = pixverse_v5_text_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::video::text::enqueue_pixverse_v5_text_to_video_webhook::{enqueue_pixverse_v5_text_to_video_webhook, EnqueuePixverseV5TextToVideoArgs, EnqueuePixverseV5TextToVideoAspectRatio, EnqueuePixverseV5TextToVideoDurationSeconds, EnqueuePixverseV5TextToVideoResolution, EnqueuePixverseV5TextToVideoStyle}; + use errors::AnyhowResult; + use std::fs::read_to_string; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueuePixverseV5TextToVideoArgs { + prompt: "an angry racoon shakes its fist at the garbage truck as it drives away, the camera orbits the racoon. the racoon sighs".to_string(), + negative_prompt: None, + duration: Some(EnqueuePixverseV5TextToVideoDurationSeconds::Eight), + style: Some(EnqueuePixverseV5TextToVideoStyle::Anime), + aspect_ratio: Some(EnqueuePixverseV5TextToVideoAspectRatio::FourByThree), + resolution: Some(EnqueuePixverseV5TextToVideoResolution::SevenTwentyP), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_pixverse_v5_text_to_video_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_sora_2_pro_text_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_sora_2_pro_text_to_video_webhook.rs new file mode 100644 index 0000000000..3b6e0b1ed1 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_sora_2_pro_text_to_video_webhook.rs @@ -0,0 +1,116 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; +use fal::endpoints::fal_ai::sora::sora2::sora_2_pro_text_to_video::{sora_2_pro_text_to_video, Sora2ProTextToVideoInput}; + +pub struct EnqueueSora2ProTextToVideoArgs<'a, R: IntoUrl> { + // Request required + pub prompt: String, + + // Optional args + pub resolution: Option, + pub duration: Option, + pub aspect_ratio: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueSora2ProTextToVideoDurationSeconds { + Four, + Eight, + Twelve, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueSora2ProTextToVideoResolution { + Auto, + SevenTwentyP, + TenEightyP, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueSora2ProTextToVideoAspectRatio { + Auto, + NineBySixteen, + SixteenByNine, +} + +pub async fn enqueue_sora_2_pro_text_to_video_webhook( + args: EnqueueSora2ProTextToVideoArgs<'_, R> +) -> Result { + + let duration = args.duration + .map(|resolution| match resolution { + EnqueueSora2ProTextToVideoDurationSeconds::Four => 4, + EnqueueSora2ProTextToVideoDurationSeconds::Eight => 8, + EnqueueSora2ProTextToVideoDurationSeconds::Twelve => 12, + }); + + let resolution = args.resolution + .map(|resolution| match resolution { + EnqueueSora2ProTextToVideoResolution::Auto => "auto", + EnqueueSora2ProTextToVideoResolution::SevenTwentyP => "720p", + EnqueueSora2ProTextToVideoResolution::TenEightyP => "1080p", + }) + .map(|resolution| resolution.to_string()); + + let aspect_ratio = args.aspect_ratio + .map(|aspect_ratio| match aspect_ratio { + EnqueueSora2ProTextToVideoAspectRatio::Auto => "auto", + EnqueueSora2ProTextToVideoAspectRatio::NineBySixteen => "9:16", + EnqueueSora2ProTextToVideoAspectRatio::SixteenByNine => "16:9", + }) + .map(|resolution| resolution.to_string()); + + let request = Sora2ProTextToVideoInput { + prompt: args.prompt, + // Optionals + duration, + resolution, + aspect_ratio, + // Constants + delete_video: Some(false), + }; + + let result = sora_2_pro_text_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use errors::AnyhowResult; + use std::fs::read_to_string; + use crate::requests::webhook::video::text::enqueue_sora_2_pro_text_to_video_webhook::{enqueue_sora_2_pro_text_to_video_webhook, EnqueueSora2ProTextToVideoArgs, EnqueueSora2ProTextToVideoAspectRatio, EnqueueSora2ProTextToVideoDurationSeconds, EnqueueSora2ProTextToVideoResolution}; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueSora2ProTextToVideoArgs { + prompt: "a dinosaur turns to the camera and asks, 'do you have adequate car insurance?' it then stomps off and attacks a brontosaurus".to_string(), + duration: Some(EnqueueSora2ProTextToVideoDurationSeconds::Eight), + aspect_ratio: Some(EnqueueSora2ProTextToVideoAspectRatio::NineBySixteen), + resolution: Some(EnqueueSora2ProTextToVideoResolution::SevenTwentyP), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_sora_2_pro_text_to_video_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_sora_2_text_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_sora_2_text_to_video_webhook.rs new file mode 100644 index 0000000000..5397997bc7 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_sora_2_text_to_video_webhook.rs @@ -0,0 +1,114 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::sora::sora2::sora_2_text_to_video::{sora_2_text_to_video, Sora2TextToVideoInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueSora2TextToVideoArgs<'a, R: IntoUrl> { + // Request required + pub prompt: String, + + // Optional args + pub resolution: Option, + pub duration: Option, + pub aspect_ratio: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueSora2TextToVideoDurationSeconds { + Four, + Eight, + Twelve, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueSora2TextToVideoResolution { + Auto, + SevenTwentyP, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueSora2TextToVideoAspectRatio { + Auto, + NineBySixteen, + SixteenByNine, +} + +pub async fn enqueue_sora_2_text_to_video_webhook( + args: EnqueueSora2TextToVideoArgs<'_, R> +) -> Result { + + let duration = args.duration + .map(|resolution| match resolution { + EnqueueSora2TextToVideoDurationSeconds::Four => 4, + EnqueueSora2TextToVideoDurationSeconds::Eight => 8, + EnqueueSora2TextToVideoDurationSeconds::Twelve => 12, + }); + + let resolution = args.resolution + .map(|resolution| match resolution { + EnqueueSora2TextToVideoResolution::Auto => "auto", + EnqueueSora2TextToVideoResolution::SevenTwentyP => "720p", + }) + .map(|resolution| resolution.to_string()); + + let aspect_ratio = args.aspect_ratio + .map(|aspect_ratio| match aspect_ratio { + EnqueueSora2TextToVideoAspectRatio::Auto => "auto", + EnqueueSora2TextToVideoAspectRatio::NineBySixteen => "9:16", + EnqueueSora2TextToVideoAspectRatio::SixteenByNine => "16:9", + }) + .map(|resolution| resolution.to_string()); + + let request = Sora2TextToVideoInput { + prompt: args.prompt, + // Optionals + duration, + resolution, + aspect_ratio, + // Constants + delete_video: Some(false), + }; + + let result = sora_2_text_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::video::text::enqueue_sora_2_text_to_video_webhook::{enqueue_sora_2_text_to_video_webhook, EnqueueSora2TextToVideoArgs, EnqueueSora2TextToVideoAspectRatio, EnqueueSora2TextToVideoDurationSeconds, EnqueueSora2TextToVideoResolution}; + use errors::AnyhowResult; + use std::fs::read_to_string; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueSora2TextToVideoArgs { + prompt: "a dinosaur turns to the camera and asks, 'do you have adequate car insurance?' it then stomps off and attacks a brontosaurus".to_string(), + duration: Some(EnqueueSora2TextToVideoDurationSeconds::Eight), + aspect_ratio: Some(EnqueueSora2TextToVideoAspectRatio::NineBySixteen), + resolution: Some(EnqueueSora2TextToVideoResolution::SevenTwentyP), + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_sora_2_text_to_video_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_veo_3p1_fast_text_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_veo_3p1_fast_text_to_video_webhook.rs new file mode 100644 index 0000000000..5a069bdcaf --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_veo_3p1_fast_text_to_video_webhook.rs @@ -0,0 +1,130 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::veo::veo3_1::veo_3p1_fast_text_to_video::{veo_3p1_fast_text_to_video, Veo3p1FastTextToVideoInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueVeo3p1FastTextToVideoArgs<'a, R: IntoUrl> { + // Request required + pub prompt: String, + + // Optional args + pub duration: Option, + pub aspect_ratio: Option, + pub resolution: Option, + pub generate_audio: Option, + pub enhance_prompt: Option, + pub negative_prompt: Option, + pub seed: Option, + pub auto_fix: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueVeo3p1FastTextToVideoDurationSeconds { + Four, + Six, + Eight, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueVeo3p1FastTextToVideoAspectRatio { + Auto, + Square, + SixteenByNine, + NineBySixteen, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueVeo3p1FastTextToVideoResolution { + SevenTwentyP, + TenEightyP, +} + +pub async fn enqueue_veo_3p1_fast_text_to_video_webhook( + args: EnqueueVeo3p1FastTextToVideoArgs<'_, R> +) -> Result { + + let duration = args.duration + .map(|resolution| match resolution { + EnqueueVeo3p1FastTextToVideoDurationSeconds::Four => "4s", + EnqueueVeo3p1FastTextToVideoDurationSeconds::Six => "6s", + EnqueueVeo3p1FastTextToVideoDurationSeconds::Eight=> "8s", + }) + .map(|s| s.to_string()); + + let aspect_ratio = args.aspect_ratio + .map(|aspect_ratio| match aspect_ratio { + EnqueueVeo3p1FastTextToVideoAspectRatio::Auto => "auto", + EnqueueVeo3p1FastTextToVideoAspectRatio::Square => "1:1", + EnqueueVeo3p1FastTextToVideoAspectRatio::SixteenByNine => "16:9", + EnqueueVeo3p1FastTextToVideoAspectRatio::NineBySixteen => "9:16", + }) + .map(|s| s.to_string()); + + let resolution = args.resolution + .map(|resolution| match resolution { + EnqueueVeo3p1FastTextToVideoResolution::SevenTwentyP => "720p", + EnqueueVeo3p1FastTextToVideoResolution::TenEightyP => "1080p", + }) + .map(|s| s.to_string()); + + let request = Veo3p1FastTextToVideoInput { + prompt: args.prompt, + // Optionals + duration, + aspect_ratio, + resolution, + negative_prompt: args.negative_prompt, + generate_audio: args.generate_audio, + enhance_prompt: args.enhance_prompt, + seed: args.seed, + auto_fix: args.auto_fix, + }; + + let result = veo_3p1_fast_text_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::video::text::enqueue_veo_3p1_fast_text_to_video_webhook::{enqueue_veo_3p1_fast_text_to_video_webhook, EnqueueVeo3p1FastTextToVideoArgs, EnqueueVeo3p1FastTextToVideoAspectRatio, EnqueueVeo3p1FastTextToVideoDurationSeconds, EnqueueVeo3p1FastTextToVideoResolution}; + use errors::AnyhowResult; + use std::fs::read_to_string; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueVeo3p1FastTextToVideoArgs { + prompt: "An alien space ship hovers over new york city. it looks ominous, ready to attack. suddenly, it drops a bunch of ping pong balls on the city".to_string(), + duration: Some(EnqueueVeo3p1FastTextToVideoDurationSeconds::Eight), + aspect_ratio: Some(EnqueueVeo3p1FastTextToVideoAspectRatio::SixteenByNine), + resolution: Some(EnqueueVeo3p1FastTextToVideoResolution::TenEightyP), + generate_audio: Some(true), + negative_prompt: None, + enhance_prompt: Some(true), + auto_fix: Some(true), + seed: None, + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_veo_3p1_fast_text_to_video_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_veo_3p1_text_to_video_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_veo_3p1_text_to_video_webhook.rs new file mode 100644 index 0000000000..676d6e832e --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/text/enqueue_veo_3p1_text_to_video_webhook.rs @@ -0,0 +1,130 @@ +use crate::creds::fal_api_key::FalApiKey; +use crate::error::classify_fal_error::classify_fal_error; +use crate::error::fal_error_plus::FalErrorPlus; +use fal::endpoints::fal_ai::veo::veo3_1::veo_3p1_text_to_video::{veo_3p1_text_to_video, Veo3p1TextToVideoInput}; +use fal::webhook::WebhookResponse; +use reqwest::IntoUrl; + +pub struct EnqueueVeo3p1TextToVideoArgs<'a, R: IntoUrl> { + // Request required + pub prompt: String, + + // Optional args + pub duration: Option, + pub aspect_ratio: Option, + pub resolution: Option, + pub generate_audio: Option, + pub enhance_prompt: Option, + pub negative_prompt: Option, + pub seed: Option, + pub auto_fix: Option, + + // Fulfillment + pub webhook_url: R, + pub api_key: &'a FalApiKey, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueVeo3p1TextToVideoDurationSeconds { + Four, + Six, + Eight, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueVeo3p1TextToVideoAspectRatio { + Auto, + Square, + SixteenByNine, + NineBySixteen, +} + +#[derive(Copy, Clone, Debug)] +pub enum EnqueueVeo3p1TextToVideoResolution { + SevenTwentyP, + TenEightyP, +} + +pub async fn enqueue_veo_3p1_text_to_video_webhook( + args: EnqueueVeo3p1TextToVideoArgs<'_, R> +) -> Result { + + let duration = args.duration + .map(|resolution| match resolution { + EnqueueVeo3p1TextToVideoDurationSeconds::Four => "4s", + EnqueueVeo3p1TextToVideoDurationSeconds::Six => "6s", + EnqueueVeo3p1TextToVideoDurationSeconds::Eight=> "8s", + }) + .map(|s| s.to_string()); + + let aspect_ratio = args.aspect_ratio + .map(|aspect_ratio| match aspect_ratio { + EnqueueVeo3p1TextToVideoAspectRatio::Auto => "auto", + EnqueueVeo3p1TextToVideoAspectRatio::Square => "1:1", + EnqueueVeo3p1TextToVideoAspectRatio::SixteenByNine => "16:9", + EnqueueVeo3p1TextToVideoAspectRatio::NineBySixteen => "9:16", + }) + .map(|s| s.to_string()); + + let resolution = args.resolution + .map(|resolution| match resolution { + EnqueueVeo3p1TextToVideoResolution::SevenTwentyP => "720p", + EnqueueVeo3p1TextToVideoResolution::TenEightyP => "1080p", + }) + .map(|s| s.to_string()); + + let request = Veo3p1TextToVideoInput { + prompt: args.prompt, + // Optionals + duration, + aspect_ratio, + resolution, + negative_prompt: args.negative_prompt, + generate_audio: args.generate_audio, + enhance_prompt: args.enhance_prompt, + seed: args.seed, + auto_fix: args.auto_fix, + }; + + let result = veo_3p1_text_to_video(request) + .with_api_key(&args.api_key.0) + .queue_webhook(args.webhook_url) + .await; + + result.map_err(|err| classify_fal_error(err)) +} + +#[cfg(test)] +mod tests { + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::webhook::video::text::enqueue_veo_3p1_text_to_video_webhook::{enqueue_veo_3p1_text_to_video_webhook, EnqueueVeo3p1TextToVideoArgs, EnqueueVeo3p1TextToVideoAspectRatio, EnqueueVeo3p1TextToVideoDurationSeconds, EnqueueVeo3p1TextToVideoResolution}; + use errors::AnyhowResult; + use std::fs::read_to_string; + + #[tokio::test] + #[ignore] + async fn test() -> AnyhowResult<()> { + // XXX: Don't commit secrets! + let secret = read_to_string("/home/bt/Artcraft/credentials/fal_api_key.txt")?; + + let api_key = FalApiKey::from_str(&secret); + + let args = EnqueueVeo3p1TextToVideoArgs { + prompt: "An alien space ship hovers over new york city. it looks ominous, ready to attack. suddenly, it drops a bunch of ping pong balls on the city".to_string(), + duration: Some(EnqueueVeo3p1TextToVideoDurationSeconds::Eight), + aspect_ratio: Some(EnqueueVeo3p1TextToVideoAspectRatio::SixteenByNine), + resolution: Some(EnqueueVeo3p1TextToVideoResolution::TenEightyP), + generate_audio: Some(true), + negative_prompt: None, + enhance_prompt: Some(true), + auto_fix: Some(true), + seed: None, + api_key: &api_key, + webhook_url: "https://example.com/webhook", + }; + + let result = enqueue_veo_3p1_text_to_video_webhook(args).await?; + + Ok(()) + } +} diff --git a/crates/api_clients/fal_client/src/requests/webhook/video/text/mod.rs b/crates/api_clients/fal_client/src/requests/webhook/video/text/mod.rs new file mode 100644 index 0000000000..a54ebd5f5a --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/webhook/video/text/mod.rs @@ -0,0 +1,8 @@ +pub mod enqueue_hailuo_v2p3_pro_text_to_video_webhook; +pub mod enqueue_kling_v2p5_turbo_pro_text_to_video_webhook; +pub mod enqueue_kling_v2p6_pro_text_to_video_webhook; +pub mod enqueue_pixverse_v5_text_to_video_webhook; +pub mod enqueue_sora_2_pro_text_to_video_webhook; +pub mod enqueue_sora_2_text_to_video_webhook; +pub mod enqueue_veo_3p1_fast_text_to_video_webhook; +pub mod enqueue_veo_3p1_text_to_video_webhook; \ No newline at end of file diff --git a/crates/api_clients/grok_client/src/datatypes/api/aspect_ratio.rs b/crates/api_clients/grok_client/src/datatypes/api/aspect_ratio.rs new file mode 100644 index 0000000000..614e451591 --- /dev/null +++ b/crates/api_clients/grok_client/src/datatypes/api/aspect_ratio.rs @@ -0,0 +1,13 @@ +use serde::Serialize; + +#[derive(Serialize, Clone, Copy, Debug)] +pub enum AspectRatio { + #[serde(rename = "2:3")] + TallTwoByThree, + + #[serde(rename = "3:2")] + WideThreeByTwo, + + #[serde(rename = "1:1")] + Square, +} diff --git a/crates/api_clients/grok_client/src/datatypes/api/mod.rs b/crates/api_clients/grok_client/src/datatypes/api/mod.rs index 77d75655b2..4fe3408938 100644 --- a/crates/api_clients/grok_client/src/datatypes/api/mod.rs +++ b/crates/api_clients/grok_client/src/datatypes/api/mod.rs @@ -1,5 +1,7 @@ +pub mod aspect_ratio; pub mod baggage; pub mod file_id; +pub mod video_generation_mode; pub mod post_id; pub mod request_id; pub mod sentry_trace; diff --git a/crates/api_clients/grok_client/src/datatypes/api/video_generation_mode.rs b/crates/api_clients/grok_client/src/datatypes/api/video_generation_mode.rs new file mode 100644 index 0000000000..3036b34114 --- /dev/null +++ b/crates/api_clients/grok_client/src/datatypes/api/video_generation_mode.rs @@ -0,0 +1,27 @@ + +/// The video generation mode selector. +#[derive(Copy, Clone, Debug)] +pub enum VideoGenerationMode { + /// Supposedly the "NSFW" mode. + Spicy, + + Fun, + + Normal, + + /// This might be the only mode that allows for a custom text prompt + Custom, +} + +impl VideoGenerationMode { + + /// This is the value for `--mode` in video generation chats. + pub fn as_api_mode_arg(&self) -> &'static str { + match self { + Self::Spicy => "extremely-spicy-or-crazy", + Self::Fun => "extremely-crazy", + Self::Normal => "normal", + Self::Custom => "custom", + } + } +} diff --git a/crates/api_clients/grok_client/src/error/grok_client_error.rs b/crates/api_clients/grok_client/src/error/grok_client_error.rs index 52f419f1ef..b33f902558 100644 --- a/crates/api_clients/grok_client/src/error/grok_client_error.rs +++ b/crates/api_clients/grok_client/src/error/grok_client_error.rs @@ -47,6 +47,18 @@ pub enum GrokClientError { /// Our script logic is out of date ScriptLogicOutOfDate, + /// Our script logic is out of date (SVG) + ScriptSvgLogicOutOfDate, + + /// Our script logic is out of date (script 1) + Script1LogicOutOfDate, + + /// Our script logic is out of date (script 2) + Script2LogicOutOfDate, + + /// Our script logic is out of date (xsid script) + ScriptXsidLogicOutOfDate, + /// Signature algorithm isn't working with inputs BadSignatureInputs, @@ -56,6 +68,9 @@ pub enum GrokClientError { /// Can't make request because cookies aren't present NoCookiesPresent, + /// Unknown error generating video + ErrorGeneratingVideo, + // /// An error reading the file for upload. // FileForUploadReadError(std::io::Error), // @@ -112,9 +127,14 @@ impl Display for GrokClientError { Self::FailedToDecodeVerificationToken(err) => write!(f, "Failed to decode verification token: {}", err), Self::InvalidVerificationTokenBytes => write!(f, "Invalid verification token bytes"), Self::ScriptLogicOutOfDate => write!(f, "Script logic out of date"), + Self::ScriptSvgLogicOutOfDate => write!(f, "Script logic out of date (SVG parsing)"), + Self::Script1LogicOutOfDate => write!(f, "Script logic out of date (script 1)"), + Self::Script2LogicOutOfDate => write!(f, "Script logic out of date (script 2)"), + Self::ScriptXsidLogicOutOfDate => write!(f, "Script logic out of date (xsid script)"), Self::BadSignatureInputs => write!(f, "Bad signature inputs"), Self::TimeoutMathBroken => write!(f, "Timeout math is broken"), Self::NoCookiesPresent => write!(f, "No cookies present"), + Self::ErrorGeneratingVideo => write!(f, "Error generating video"), //Self::FileForUploadReadError(err) => write!(f, "Error reading file for upload: {}", err), //Self::FileForUploadHasInvalidPath => write!(f, "The file path provided for upload is invalid."), diff --git a/crates/api_clients/grok_client/src/recipes/mod.rs b/crates/api_clients/grok_client/src/recipes/mod.rs index e774ecd05b..68bb011571 100644 --- a/crates/api_clients/grok_client/src/recipes/mod.rs +++ b/crates/api_clients/grok_client/src/recipes/mod.rs @@ -1,3 +1,4 @@ +pub mod prompt_websocket_image_with_retry; pub mod request_client_secrets; pub mod upload_image_and_generate_video; pub mod upload_image_and_generate_video_with_retry; \ No newline at end of file diff --git a/crates/api_clients/grok_client/src/recipes/prompt_websocket_image_with_retry.rs b/crates/api_clients/grok_client/src/recipes/prompt_websocket_image_with_retry.rs new file mode 100644 index 0000000000..f6cea7cb1f --- /dev/null +++ b/crates/api_clients/grok_client/src/recipes/prompt_websocket_image_with_retry.rs @@ -0,0 +1,201 @@ +use crate::credentials::grok_cookies::GrokCookies; +use crate::error::grok_client_error::GrokClientError; +use crate::error::grok_error::GrokError; +use crate::requests::image_websocket::create_listen_websocket::{create_listen_websocket, CreateListenWebsocketArgs}; +use crate::requests::image_websocket::grok_websocket::GrokWebsocket; +use crate::requests::image_websocket::messages::websocket_client_message::{ClientMessageAspectRatio, WebsocketClientMessage}; +use log::{error, info}; + +pub struct PromptWebsocketImageWithRetryArgs<'a> { + pub websocket: &'a mut GrokWebsocket, + pub prompt: &'a str, + pub aspect_ratio: ClientMessageAspectRatio, + pub cookies: &'a GrokCookies, +} + +pub struct PromptWebsocketImageWithRetryResult { + pub maybe_new_websocket: Option, +} + + +pub async fn prompt_websocket_image_with_retry(args: PromptWebsocketImageWithRetryArgs<'_>) -> Result +{ + let mut maybe_new_websocket : Option = None; + + for i in 0..3 { + info!("Starting Grok Websocket image generation (attempt {}) ...", (i+1)); + + let message = WebsocketClientMessage::new_image_prompt(args.prompt, args.aspect_ratio); + + let result = + if let Some(websocket) = maybe_new_websocket.as_mut() { + websocket.send_serializable(&message).await + } else { + args.websocket.send_serializable(&message).await + }; + + match result { + Ok(()) => { + break; + } + Err(GrokError::Client(GrokClientError::WreqClientError(err))) => { + error!("Error sending message to websocket (WreqClientError): {}", err); + } + Err(GrokError::Client(err)) => { + error!("Error sending message to websocket (GrokClientError): {}", err); + } + Err(GrokError::ApiSpecific(err)) => { + error!("Error sending message to websocket (GrokSpecificApiError): {}", err); + } + Err(GrokError::ApiGeneric(err)) => { + error!("Error sending message to websocket (GrokGenericApiError): {}", err); + } + } + + let result = create_listen_websocket(CreateListenWebsocketArgs { + cookies: args.cookies.as_str(), + }).await?; + + maybe_new_websocket = Some(GrokWebsocket::new(result)); + } + + Ok(PromptWebsocketImageWithRetryResult { + maybe_new_websocket, + }) +} + +#[cfg(test)] +mod tests { + use crate::recipes::prompt_websocket_image_with_retry::{prompt_websocket_image_with_retry, PromptWebsocketImageWithRetryArgs}; + use crate::requests::image_websocket::create_listen_websocket::{create_listen_websocket, CreateListenWebsocketArgs}; + use crate::requests::image_websocket::grok_websocket::GrokWebsocket; + use crate::requests::image_websocket::grok_wrapped_websocket::GrokWrappedWebsocket; + use crate::requests::image_websocket::messages::websocket_client_message::ClientMessageAspectRatio; + use crate::requests::image_websocket::messages::websocket_server_message::WebsocketServerMessage; + use crate::requests::image_websocket::prompt_websocket_image::{prompt_websocket_image, PromptWebsocketImageArgs}; + use crate::test_utils::get_test_cookies::{get_test_cookies, get_typed_test_cookies}; + use errors::AnyhowResult; + use futures::SinkExt; + use log::warn; + use std::io::Write; + use std::time::Duration; + use wreq::ws::message::{CloseCode, Message, Utf8Bytes}; + + #[tokio::test] + #[ignore] // manually test + async fn prompt() -> AnyhowResult<()> { + //setup_test_logging(LevelFilter::Trace); + + let cookies = get_typed_test_cookies()?; + + let websocket = create_listen_websocket(CreateListenWebsocketArgs { + cookies: cookies.as_str(), + }).await?; + + let prompt = "A dinosaur on stilts walking on the beach"; + + //let websocket = GrokWrappedWebsocket::new(websocket); + let mut websocket = GrokWebsocket::new(websocket); + + //println!("Sending..."); + //std::io::stdout().flush()?; + + //println!("For our test, we'll close the websocket and retry..."); + //websocket.websocket + // .close(CloseCode::NORMAL, Utf8Bytes::try_from(Vec::new())?).await?; + + //let mut websocket = GrokWebsocket::new(websocket.websocket); + + let result = prompt_websocket_image_with_retry(PromptWebsocketImageWithRetryArgs { + websocket: &mut websocket, + prompt, + aspect_ratio: ClientMessageAspectRatio::WideThreeByTwo, + cookies: &cookies, + }).await?; + + println!("Reading..."); + std::io::stdout().flush()?; + + let mut count = 0; + + println!("Polling."); + + loop { + //let maybe_message = + // websocket.get_response_with_timeout(Duration::from_millis(1000)).await?; + + let maybe_message = + websocket.try_next_timeout(Duration::from_millis(1000)).await?; + + let mut maybe_raw_text : Option = None; + + let maybe_message = match maybe_message { + None => None, + Some(Message::Text(text)) => { + let maybe_message = WebsocketServerMessage::from_json_str(text.as_str())?; + let mut text = text.as_str().to_string(); + text.truncate(100); + //println!("Received message: {}", text); + maybe_raw_text = Some(text); + Some(maybe_message) + }, + Some(_) => { + warn!("Received non-text websocket message."); + None + }, + }; + + match maybe_message { + None => { + println!("No message received within timeout."); + count = count + 1; + } + Some(message) => { + match message { + WebsocketServerMessage::Image(image) => { + println!("IMAGE: {:?}", image.percentage_complete); + if let Some(percent) = image.percentage_complete { + if percent > 90.0 { + println!("Image: {:?}", image); + } + } + } + WebsocketServerMessage::Json(json) => { + println!("JSON : {:?}", json.percentage_complete); + if let Some(percent) = json.percentage_complete { + if percent > 90.0 { + println!("Image: {:?}", json); + } + } + } + WebsocketServerMessage::Unknown(unknown) => { + //let typ = unknown.get("type"); + let unknown_string = unknown.as_str(); + println!("[UNKNOWN] websocket message: {:?}", maybe_raw_text); + } + } + count = 0; + } + } + + // TODO: Make sure we capture all the events for images + // TODO: Explore the other APIs + // TODO understand the IDs + // TODO: Video websocket APIs + // TODO: Upload APIs + + if count > 5 { + println!("No messages after 5 seconds"); + break; + } + } + + println!("Done polling."); + + log::logger().flush(); + + assert_eq!(1,2); + + Ok(()) + } +} diff --git a/crates/api_clients/grok_client/src/recipes/upload_image_and_generate_video.rs b/crates/api_clients/grok_client/src/recipes/upload_image_and_generate_video.rs index 187e5aa183..01e8421010 100644 --- a/crates/api_clients/grok_client/src/recipes/upload_image_and_generate_video.rs +++ b/crates/api_clients/grok_client/src/recipes/upload_image_and_generate_video.rs @@ -1,6 +1,8 @@ use crate::credentials::grok_full_credentials::GrokFullCredentials; +use crate::datatypes::api::aspect_ratio::AspectRatio; use crate::datatypes::api::file_id::FileId; use crate::datatypes::api::post_id::PostId; +use crate::datatypes::api::video_generation_mode::VideoGenerationMode; use crate::datatypes::file_upload_spec::FileUploadSpec; use crate::error::grok_error::GrokError; use crate::error::grok_generic_api_error::GrokGenericApiError; @@ -23,6 +25,12 @@ pub struct UploadImageAndGenerateVideo<'a, P: AsRef> { /// Video generation prompt pub prompt: Option<&'a str>, + /// Aspect ratio for the video. + pub aspect_ratio: Option, + + /// Mode to generate the video: normal, fun, spicy, etc. + pub mode: Option, + /// Wait for the full video to be generated before returning /// By default, the endpoint stays open for 30-ish seconds while /// the video generates and streams back JSON progress updates. @@ -101,6 +109,9 @@ pub async fn upload_image_and_generate_video>(args: UploadImageAn info!("Posted Image."); + info!("Video prompt: {:?}", args.prompt); + info!("Video aspect ratio: {:?}", args.aspect_ratio); + info!("Generate video..."); let request = GrokVideoGenChatConversationBuilder { @@ -109,6 +120,8 @@ pub async fn upload_image_and_generate_video>(args: UploadImageAn media_type: VideoMediaPostType::UserUploadedImage, cookie: args.full_credentials.cookies.as_str(), prompt: args.prompt, + mode: args.mode, + aspect_ratio: args.aspect_ratio, request_timeout: args.individual_request_timeout, baggage: &args.full_credentials.client_secrets.baggage, sentry_trace: &args.full_credentials.client_secrets.sentry_trace, @@ -170,6 +183,7 @@ pub async fn upload_image_and_generate_video>(args: UploadImageAn #[cfg(test)] mod tests { use crate::credentials::grok_full_credentials::GrokFullCredentials; + use crate::datatypes::api::aspect_ratio::AspectRatio; use crate::datatypes::file_upload_spec::FileUploadSpec; use crate::recipes::request_client_secrets::{request_client_secrets, RequestClientSecretsArgs}; use crate::recipes::upload_image_and_generate_video::{upload_image_and_generate_video, UploadImageAndGenerateVideo}; @@ -177,7 +191,6 @@ mod tests { use crate::test_utils::setup_test_logging::setup_test_logging; use errors::AnyhowResult; use log::LevelFilter; - // Result: GrokUploadFileResponse { file_metadata_id: // Some("acdee48f-9d6f-4bc6-9d06-fcc97dd4418a"), file_uri: // Some("users/85980643-ffab-4984-a3de-59a608c47d7f/acdee48f-9d6f-4bc6-9d06-fcc97dd4418a/content") } @@ -211,6 +224,8 @@ mod tests { full_credentials: &credentials, file: FileUploadSpec::Path(image_path), prompt: maybe_prompt, + aspect_ratio: Some(AspectRatio::Square), + mode: None, individual_request_timeout: None, wait_for_generation: false, }).await?; diff --git a/crates/api_clients/grok_client/src/recipes/upload_image_and_generate_video_with_retry.rs b/crates/api_clients/grok_client/src/recipes/upload_image_and_generate_video_with_retry.rs index b7f336515b..856d97338a 100644 --- a/crates/api_clients/grok_client/src/recipes/upload_image_and_generate_video_with_retry.rs +++ b/crates/api_clients/grok_client/src/recipes/upload_image_and_generate_video_with_retry.rs @@ -1,7 +1,23 @@ +use crate::credentials::grok_client_secrets::GrokClientSecrets; +use crate::credentials::grok_full_credentials::GrokFullCredentials; +use crate::datatypes::api::aspect_ratio::AspectRatio; +use crate::datatypes::api::video_generation_mode::VideoGenerationMode; +use crate::datatypes::file_upload_spec::FileUploadSpec; +use crate::error::grok_client_error::GrokClientError; +use crate::error::grok_error::GrokError; +use crate::error::grok_generic_api_error::GrokGenericApiError; +use crate::error::grok_specific_api_error::GrokSpecificApiError; +use crate::recipes::request_client_secrets::{request_client_secrets, RequestClientSecretsArgs}; +use crate::recipes::upload_image_and_generate_video::{ImageUploadAndGenerateVideoResult, UploadImageAndGenerateVideo}; +use crate::requests::media_posts::create_media_post::grok_create_media_post::{GrokCreateMediaPost, MediaPostType}; +use crate::requests::media_posts::like_media_post::grok_like_media::GrokLikeMediaPost; +use crate::requests::upload_file::grok_upload_file::GrokUploadFile; +use crate::requests::video_chat::grok_video_gen_chat_conversation::{GrokVideoGenChatConversationBuilder, VideoMediaPostType}; +use crate::utils::user_and_file_id_to_image_url::user_and_file_id_to_image_url; +use crate::utils::user_and_file_id_to_video_url::user_and_file_id_to_video_url; +use log::{error, info}; use std::path::Path; use std::time::Duration; -use crate::credentials::grok_full_credentials::GrokFullCredentials; -use crate::datatypes::file_upload_spec::FileUploadSpec; pub struct UploadImageAndGenerateVideoWithRetry<'a, P: AsRef> { pub credentials: &'a GrokFullCredentials, @@ -12,6 +28,12 @@ pub struct UploadImageAndGenerateVideoWithRetry<'a, P: AsRef> { /// Video generation prompt pub prompt: Option<&'a str>, + /// Aspect ratio for the video. + pub aspect_ratio: Option, + + /// Mode to generate the video: normal, fun, spicy, etc. + pub mode: Option, + /// Wait for the full video to be generated before returning /// By default, the endpoint stays open for 30-ish seconds while /// the video generates and streams back JSON progress updates. @@ -22,3 +44,305 @@ pub struct UploadImageAndGenerateVideoWithRetry<'a, P: AsRef> { pub individual_request_timeout: Option, } + +#[derive(Clone)] +pub struct ImageUploadAndGenerateVideoWithRetryResult { + /// If new secrets were generated, they're stored here. + pub maybe_new_client_secrets: Option, + + /// The video generation result. + pub upload_result: ImageUploadAndGenerateVideoResult, +} + +pub async fn upload_image_and_generate_video_with_retry>( + args: UploadImageAndGenerateVideoWithRetry<'_, P> +) -> Result { + + let mut current_full_credentials_ref = args.credentials; + let mut maybe_new_credentials = None; + + info!("Uploading file to Grok..."); + + let request = GrokUploadFile { + file: args.file, + cookie: current_full_credentials_ref.cookies.to_string(), + request_timeout: args.individual_request_timeout, + }; + + let upload_result = request.upload().await?; + + info!("File URI: {:?}", upload_result.file_uri); + info!("File Metadata ID: {:?}", upload_result.file_id); + + let upload_file_id = match upload_result.file_id { + Some(id) => id, + None => { + error!("Previous file upload failed. Cannot continue."); + return Err(GrokGenericApiError::UploadFailed.into()); + } + }; + + let url = user_and_file_id_to_image_url( + ¤t_full_credentials_ref.client_secrets.user_id, + &upload_file_id + ); + + info!("Uploaded URI: {:?}", url); + + info!("Creating media post..."); + + let request = GrokCreateMediaPost { + user_id: ¤t_full_credentials_ref.client_secrets.user_id, + file_id: &upload_file_id, + media_type: MediaPostType::UserUploadedImage, + cookie: current_full_credentials_ref.cookies.as_str(), + request_timeout: args.individual_request_timeout, + }; + + let post_result = request.send().await?; + + info!("Posted Image."); + + info!("Video prompt: {:?}", args.prompt); + info!("Video aspect ratio: {:?}", args.aspect_ratio); + + let mut video_enqueued_successfully = false; + let mut generation_is_complete= false; + let mut maybe_video_file_id = None; + let mut last_error = None; + + for i in 0..3 { + info!("Generate video... attempt {}", (i+1)); + + let request = GrokVideoGenChatConversationBuilder { + user_id: ¤t_full_credentials_ref.client_secrets.user_id, + file_id: &upload_file_id, + media_type: VideoMediaPostType::UserUploadedImage, + cookie: current_full_credentials_ref.cookies.as_str(), + prompt: args.prompt, + mode: args.mode, + aspect_ratio: args.aspect_ratio, + request_timeout: args.individual_request_timeout, + baggage: ¤t_full_credentials_ref.client_secrets.baggage, + sentry_trace: ¤t_full_credentials_ref.client_secrets.sentry_trace, + verification_token: ¤t_full_credentials_ref.client_secrets.verification_token, + svg_data: ¤t_full_credentials_ref.client_secrets.svg_path_data, + numbers: ¤t_full_credentials_ref.client_secrets.numbers, + }; + + let video_gen_result = + if args.wait_for_generation { + request.wait_for_video() + .await + .map(|res| res.video_file_id) + } else { + request.stream_only_video_id() + .await + .map(|res| res.video_file_id) + }; + + match video_gen_result { + Ok(res) => { + info!("Video Generation Enqueued!"); + video_enqueued_successfully = true; + generation_is_complete = args.wait_for_generation; // If we synchronously waited, it'll be complete. + maybe_video_file_id = res; + break; + } + Err(GrokError::ApiSpecific(GrokSpecificApiError::AutomationBlocked)) => { + info!("Grok automation blocked; renewing credentials."); + last_error = Some(GrokError::ApiSpecific(GrokSpecificApiError::AutomationBlocked)); + } + Err(err) => { + info!("Grok error; renewing credentials. Error = {:?}", err); + last_error = Some(err); + } + } + + info!("Refreshing Grok client secrets..."); + + let secrets = request_client_secrets(RequestClientSecretsArgs { + cookies: ¤t_full_credentials_ref.cookies, + }).await?; + + // NB: This is just to appease the borrow checker. It doesn't like that we're borrowing `maybe_new_credentials` + // in a loop and replacing it while borrowed, hence this hack. + current_full_credentials_ref = args.credentials; + + maybe_new_credentials = Some(GrokFullCredentials::from_cookies_and_client_secrets( + current_full_credentials_ref.cookies.clone(), + secrets, + )); + + if let Some(creds) = maybe_new_credentials.as_ref() { + current_full_credentials_ref = creds; + } + } + + if !video_enqueued_successfully { + if let Some(err) = last_error { + return Err(err); + } else { + // NB: This branch should be impossible as we should always set last_error. + return Err(GrokError::Client(GrokClientError::ErrorGeneratingVideo)) + } + } + + let mut video_liked_successfully = false; + let mut last_error = None; + + for i in 0..3 { + info!("Liking media ... attempt {}", (i+1)); + + let request = GrokLikeMediaPost { + file_id: &upload_file_id, + cookie: current_full_credentials_ref.cookies.as_str(), + request_timeout: args.individual_request_timeout, + baggage: ¤t_full_credentials_ref.client_secrets.baggage, + sentry_trace: ¤t_full_credentials_ref.client_secrets.sentry_trace, + verification_token: ¤t_full_credentials_ref.client_secrets.verification_token, + svg_data: ¤t_full_credentials_ref.client_secrets.svg_path_data, + numbers: ¤t_full_credentials_ref.client_secrets.numbers, + }; + + let like_result = request.send().await; + + match like_result { + Ok(_) => { + info!("Media Liked"); + video_liked_successfully = true; + break; + } + Err(GrokError::ApiSpecific(GrokSpecificApiError::AutomationBlocked)) => { + info!("Grok automation blocked liking media; renewing credentials."); + last_error = Some(GrokError::ApiSpecific(GrokSpecificApiError::AutomationBlocked)); + } + Err(err) => { + info!("Grok error liking media; renewing credentials. Error = {:?}", err); + last_error = Some(err); + } + } + + info!("Refreshing Grok client secrets..."); + + let secrets = request_client_secrets(RequestClientSecretsArgs { + cookies: ¤t_full_credentials_ref.cookies, + }).await?; + + // NB: This is just to appease the borrow checker. It doesn't like that we're borrowing `maybe_new_credentials` + // in a loop and replacing it while borrowed, hence this hack. + current_full_credentials_ref = args.credentials; + + maybe_new_credentials = Some(GrokFullCredentials::from_cookies_and_client_secrets( + current_full_credentials_ref.cookies.clone(), + secrets, + )); + + if let Some(creds) = maybe_new_credentials.as_ref() { + current_full_credentials_ref = creds; + } + } + + if !video_liked_successfully { + if let Some(err) = last_error { + return Err(err); + } else { + // NB: This branch should be impossible as we should always set last_error. + return Err(GrokError::Client(GrokClientError::ErrorGeneratingVideo)) + } + } + + let maybe_video_url = maybe_video_file_id + .as_ref() + .map(|file_id| { + user_and_file_id_to_video_url(¤t_full_credentials_ref.client_secrets.user_id, file_id, false) + }); + + Ok(ImageUploadAndGenerateVideoWithRetryResult { + maybe_new_client_secrets: maybe_new_credentials + .map(|secrets| secrets.client_secrets), + upload_result: ImageUploadAndGenerateVideoResult { + post_id: post_result.post_id, + image_file_id: upload_file_id, + video_file_id: maybe_video_file_id, + video_url: maybe_video_url, + generation_is_complete, + } + }) +} + + +#[cfg(test)] +mod tests { + use crate::credentials::grok_full_credentials::GrokFullCredentials; + use crate::datatypes::api::aspect_ratio::AspectRatio; + use crate::datatypes::api::video_generation_mode::VideoGenerationMode; + use crate::datatypes::file_upload_spec::FileUploadSpec; + use crate::recipes::request_client_secrets::{request_client_secrets, RequestClientSecretsArgs}; + use crate::recipes::upload_image_and_generate_video_with_retry::{upload_image_and_generate_video_with_retry, UploadImageAndGenerateVideoWithRetry}; + use crate::test_utils::get_test_cookies::get_typed_test_cookies; + use crate::test_utils::setup_test_logging::setup_test_logging; + use errors::AnyhowResult; + use log::LevelFilter; + + // Result: GrokUploadFileResponse { file_metadata_id: + // Some("acdee48f-9d6f-4bc6-9d06-fcc97dd4418a"), file_uri: + // Some("users/85980643-ffab-4984-a3de-59a608c47d7f/acdee48f-9d6f-4bc6-9d06-fcc97dd4418a/content") } + + #[tokio::test] + #[ignore] // Client side tests only + async fn test_upload_image_and_generate_video() -> AnyhowResult<()> { + setup_test_logging(LevelFilter::Info); + + //let image_path = "/Users/bt/Pictures/Zelda 64 Art/FCgYX6tWEAEhpsy.jpg"; + //let prompt = "our hero link plunges the sword into the pedestal, the temple is glowing with a blue aura"; + + let image_path = "/Users/bt/Pictures/Midjourney/hipster_girl.png"; + let maybe_prompt = Some("an anime girl turns and high fives a giant t-rex"); + + let cookies = get_typed_test_cookies()?; + + let mut bad_secrets = request_client_secrets(RequestClientSecretsArgs { + cookies: &cookies, + }).await?; + + // NB: We're purposely messing these up to trigger "retry". + bad_secrets.numbers.numbers = Vec::new(); + bad_secrets.sentry_trace.0 = "".to_string(); + + let credentials = GrokFullCredentials::from_cookies_and_client_secrets(cookies, bad_secrets); + + let result = upload_image_and_generate_video_with_retry(UploadImageAndGenerateVideoWithRetry { + credentials: &credentials, + file: FileUploadSpec::Path(image_path), + prompt: maybe_prompt, + mode: Some(VideoGenerationMode::Custom), + aspect_ratio: Some(AspectRatio::TallTwoByThree), + individual_request_timeout: None, + wait_for_generation: false, + }).await?; + + let new_secrets = result.maybe_new_client_secrets; + + println!("[test] New secrets ? : {:?}", new_secrets.is_some()); + + if let Some(secrets) = new_secrets { + println!("[test] Verification Token: {:?}", secrets.verification_token); + println!("[test] Sentry Trace: {:?}", secrets.sentry_trace); + println!("[test] Numbers: {:?}", secrets.numbers); + println!("[test] Svg Path: {:?}", secrets.svg_path_data); + println!("[test] Baggage: {:?}", secrets.baggage); + } + + let video_result = result.upload_result; + + println!("[test] Post ID: {:?}", video_result.post_id); + println!("[test] Image File ID: {:?}", video_result.image_file_id); + println!("[test] Video File ID: {:?}", video_result.video_file_id); + println!("[test] Video URL: {:?}", video_result.video_url); + + assert_eq!(1, 2); + + Ok(()) + } +} diff --git a/crates/api_clients/grok_client/src/requests/download_video_file/download_video_file.rs b/crates/api_clients/grok_client/src/requests/download_video_file/download_video_file.rs index c27f19d141..164c25aa77 100644 --- a/crates/api_clients/grok_client/src/requests/download_video_file/download_video_file.rs +++ b/crates/api_clients/grok_client/src/requests/download_video_file/download_video_file.rs @@ -83,6 +83,8 @@ pub async fn download_video_file(args: DownloadVideoFileArgs<'_>) -> Result Self { + pub fn new_image_prompt(prompt: &str, aspect_ratio: ClientMessageAspectRatio) -> Self { Self { r#type: "conversation.item.create".to_string(), timestamp: Utc::now().timestamp_millis() as u64, @@ -67,6 +80,7 @@ impl WebsocketClientMessage { enable_nsfw: true, skip_upsampler: false, is_initial: false, + aspect_ratio, }, } ], diff --git a/crates/api_clients/grok_client/src/requests/image_websocket/prompt_websocket_image.rs b/crates/api_clients/grok_client/src/requests/image_websocket/prompt_websocket_image.rs index 7b223f5e5e..10b9c859d9 100644 --- a/crates/api_clients/grok_client/src/requests/image_websocket/prompt_websocket_image.rs +++ b/crates/api_clients/grok_client/src/requests/image_websocket/prompt_websocket_image.rs @@ -1,15 +1,16 @@ use crate::error::grok_error::GrokError; use crate::requests::image_websocket::grok_websocket::GrokWebsocket; -use crate::requests::image_websocket::messages::websocket_client_message::WebsocketClientMessage; +use crate::requests::image_websocket::messages::websocket_client_message::{ClientMessageAspectRatio, WebsocketClientMessage}; pub struct PromptWebsocketImageArgs<'a> { pub websocket: &'a mut GrokWebsocket, pub prompt: &'a str, + pub aspect_ratio: ClientMessageAspectRatio, } pub async fn prompt_websocket_image(args: PromptWebsocketImageArgs<'_>) -> Result<(), GrokError> { - let message = WebsocketClientMessage::new_image_prompt(args.prompt); + let message = WebsocketClientMessage::new_image_prompt(args.prompt, args.aspect_ratio); args.websocket.send_serializable(&message).await?; @@ -21,6 +22,7 @@ mod tests { use crate::requests::image_websocket::create_listen_websocket::{create_listen_websocket, CreateListenWebsocketArgs}; use crate::requests::image_websocket::grok_websocket::GrokWebsocket; use crate::requests::image_websocket::grok_wrapped_websocket::GrokWrappedWebsocket; + use crate::requests::image_websocket::messages::websocket_client_message::ClientMessageAspectRatio; use crate::requests::image_websocket::messages::websocket_server_message::WebsocketServerMessage; use crate::requests::image_websocket::prompt_websocket_image::{prompt_websocket_image, PromptWebsocketImageArgs}; use crate::test_utils::get_test_cookies::get_test_cookies; @@ -50,6 +52,7 @@ mod tests { let result = prompt_websocket_image(PromptWebsocketImageArgs { websocket: &mut websocket, prompt: "a dog riding a motorcycle", + aspect_ratio: ClientMessageAspectRatio::WideThreeByTwo, }).await?; println!("Reading..."); diff --git a/crates/api_clients/grok_client/src/requests/index_page/parsers/script/parse_script_actions_and_xsid_script_path.rs b/crates/api_clients/grok_client/src/requests/index_page/parsers/script/parse_script_actions_and_xsid_script_path.rs index ae7fafa557..bd383bc1fa 100644 --- a/crates/api_clients/grok_client/src/requests/index_page/parsers/script/parse_script_actions_and_xsid_script_path.rs +++ b/crates/api_clients/grok_client/src/requests/index_page/parsers/script/parse_script_actions_and_xsid_script_path.rs @@ -2,14 +2,17 @@ use crate::error::grok_client_error::GrokClientError; use once_cell::sync::Lazy; use regex::Regex; use std::collections::HashMap; +use log::info; static ACTIONS_REGEX : Lazy = Lazy::new(|| { Regex::new(r#"createServerReference\)\("([a-f0-9]+)""#) .expect("Regex should parse") }); +/// 2025-10-01: "(static/chunks/[^"]+\.js)"[^}]*?a\(880932\)" +/// 2025-12-01: "(static/chunks/[^"]+\.js)"[^}]*?\(880932\)" static XSID_SCRIPT_REGEX : Lazy = Lazy::new(|| { - Regex::new(r#""(static/chunks/[^"]+\.js)"[^}]*?a\(880932\)"#) + Regex::new(r#""(static/chunks/[^"]+\.js)"[^}]*?\(880932\)"#) .expect("Regex should parse") }); @@ -36,33 +39,32 @@ pub fn parse_script_actions_and_xsid_script_path(scripts: &HashMap action_script_path, - None => return Err(GrokClientError::ScriptLogicOutOfDate), - }; + // NB: Commented out because we don't use it. + // let action_script_path = match action_script_path { + // Some(action_script_path) => action_script_path, + // None => return Err(GrokClientError::ScriptLogicOutOfDate), + // }; let script_content_1 = match script_content_1 { Some(script_content_1) => script_content_1, - None => return Err(GrokClientError::ScriptLogicOutOfDate), + None => return Err(GrokClientError::Script1LogicOutOfDate), }; let script_content_2 = match script_content_2 { Some(script_content_2) => script_content_2, - None => return Err(GrokClientError::ScriptLogicOutOfDate), + None => return Err(GrokClientError::Script2LogicOutOfDate), }; + //println!("Script content 1:"); + //println!("{}", script_content_1); + //println!("Script content 2:"); + //println!("{}", script_content_2); + let actions = ACTIONS_REGEX.captures_iter(&script_content_1) .flat_map(|captures| captures.get(1).map(|m| m.as_str().to_string())) .collect::>(); - let xsid_script_path = XSID_SCRIPT_REGEX.captures(&script_content_2) - .map(|captures| captures.get(1).map(|m| m.as_str().to_string())) - .flatten(); - - let xsid_script_path = match xsid_script_path { - Some(xsid_script) => xsid_script, - None => return Err(GrokClientError::ScriptLogicOutOfDate), - }; + let xsid_script_path = parse_xsid_script_path_from_turbopack_script(&script_content_2)?; Ok(ActionsAndXsid { actions, @@ -70,10 +72,23 @@ pub fn parse_script_actions_and_xsid_script_path(scripts: &HashMap Result { + let xsid_script_path = XSID_SCRIPT_REGEX.captures(&script_source) + .map(|captures| captures.get(1) + .map(|m| m.as_str() + .to_string())) + .flatten(); + + match xsid_script_path { + Some(xsid_script) => Ok(xsid_script), + None => Err(GrokClientError::ScriptXsidLogicOutOfDate), + } +} + #[cfg(test)] mod tests { use crate::requests::index_page::get_index_page_and_scripts::{get_index_page_and_scripts, GetIndexPageAndScriptsArgs}; - use crate::requests::index_page::parsers::script::parse_script_actions_and_xsid_script_path::parse_script_actions_and_xsid_script_path; + use crate::requests::index_page::parsers::script::parse_script_actions_and_xsid_script_path::{parse_script_actions_and_xsid_script_path, parse_xsid_script_path_from_turbopack_script}; use crate::test_utils::get_test_cookies::get_test_cookies; use errors::AnyhowResult; @@ -93,4 +108,17 @@ mod tests { assert_eq!(1, 2); Ok(()) } + + #[test] + fn parse_turbopack_script_2_for_xsid() { + // NB: The XSID parsing script broke on December 1st. + // The valid script to parse out is "static/chunks/6450f465e55b5c24.js" (as it contains the magic numbers). This comes before the string (880932) + // They probably just changed the TurboPack configs. + let example_script_2 = r#" + (globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,483347,a=>{a.v(s=>Promise.all(["static/chunks/6450f465e55b5c24.js"].map(s=>a.l(s))).then(()=>s(880932)))},330040,a=>{a.v(s=>Promise.all(["static/chunks/5cc0fc78839e38d4.js"].map(s=>a.l(s))).then(()=>s(921398)))},127660,a=>{a.v(s=>Promise.all(["static/chunks/fc24fa4ac57f0fa9.js"].map(s=>a.l(s))).then(()=>s(969999)))},307492,a=>{a.v(s=>Promise.all(["static/chunks/eb46b9f393cc69d9.js"].map(s=>a.l(s))).then(()=>s(529107)))},460973,a=>{a.v(s=>Promise.all(["static/chunks/7ff9ad06da182f94.js"].map(s=>a.l(s))).then(()=>s(296273)))},900864, + "#; + + let parsed = parse_xsid_script_path_from_turbopack_script(example_script_2).unwrap(); + assert_eq!(parsed, "static/chunks/6450f465e55b5c24.js".to_string()); + } } diff --git a/crates/api_clients/grok_client/src/requests/index_page/utils/select_svg_path_by_loading_anim.rs b/crates/api_clients/grok_client/src/requests/index_page/utils/select_svg_path_by_loading_anim.rs index 3603f82d26..999700cf08 100644 --- a/crates/api_clients/grok_client/src/requests/index_page/utils/select_svg_path_by_loading_anim.rs +++ b/crates/api_clients/grok_client/src/requests/index_page/utils/select_svg_path_by_loading_anim.rs @@ -11,7 +11,7 @@ pub fn select_svg_path_by_loading_anim( .map(|path| path.clone()) .ok_or_else(|| { error!("There was not an SVG path data at index {} (array len {})", loading_anim.0, svg_paths.len()); - GrokClientError::ScriptLogicOutOfDate + GrokClientError::ScriptSvgLogicOutOfDate }) } diff --git a/crates/api_clients/grok_client/src/requests/video_chat/_metadata/dec_1/fun_video_gen_request.txt b/crates/api_clients/grok_client/src/requests/video_chat/_metadata/dec_1/fun_video_gen_request.txt new file mode 100644 index 0000000000..42da28ae85 --- /dev/null +++ b/crates/api_clients/grok_client/src/requests/video_chat/_metadata/dec_1/fun_video_gen_request.txt @@ -0,0 +1,45 @@ +curl 'https://grok.com/rest/app-chat/conversations/new' \ + -H 'accept: */*' \ + -H 'accept-language: en-US,en;q=0.9' \ + -H 'baggage: sentry-environment=production,sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c,sentry-trace_id=7798e88fd61243ba8985ca9f589f5113,sentry-org_id=4508179396558848,sentry-transaction=%2F%3Aslug*%3F,sentry-sampled=false,sentry-sample_rand=0.01740155041905489,sentry-sample_rate=0' \ + -H 'cache-control: no-cache' \ + -H 'content-type: application/json' \ + -b '_ga=GA1.1.1232202746.1760710013; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; x-userid=85980643-ffab-4984-a3de-59a608c47d7f; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; cf_clearance=5QPOzLWH8eEniaDlJR23k9VB4XjRPHdR.kfVR15BRkI-1764645588-1.2.1.1-WBy8wpvS9.Q.ofWBIMTLHDrKRIPt0Xrn5kAQ6I7xf4u0JSbB.E4CRd.fK6kEZo8OTXMAdt.F0YkoLJzB0TufxM79dMu63TKLmorKB.gSgUWJcea1eQcLJ1WG80uhv4VCMrSJ_HHAaq6fqiNNvS7pArgMT4tvoaQXShfDuV8ivYsRp31LYMgNnRFdtiCiJ5mxhf7uUMpSS_UbvCSaYe7CysUr472alMjEYUd.QhKpb8E; _ga_8FEWB057YH=GS2.1.s1764645588$o22$g1$t1764646543$j57$l0$h0' \ + -H 'origin: https://grok.com' \ + -H 'pragma: no-cache' \ + -H 'priority: u=1, i' \ + -H 'referer: https://grok.com/imagine/post/463eb5f6-7485-495a-a344-70ab4fe454a9' \ + -H 'sec-ch-ua: "Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"' \ + -H 'sec-ch-ua-mobile: ?0' \ + -H 'sec-ch-ua-platform: "macOS"' \ + -H 'sec-fetch-dest: empty' \ + -H 'sec-fetch-mode: cors' \ + -H 'sec-fetch-site: same-origin' \ + -H 'sentry-trace: 7798e88fd61243ba8985ca9f589f5113-889cac2356ac270b-0' \ + -H 'traceparent: 00-9698441276035e5f4414a5c7b7cde4e7-043b158b0070e175-00' \ + -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36' \ + -H 'x-statsig-id: eVTlFkyAcCo5RWxUQOUfzewYmzWMb/GbAFj4n2ymSal+UQwbGGxtFMSDrd+DdsVB0EaFp33Dd+aDMEmMn9Ay0MCCa8Bqeg' \ + -H 'x-xai-request-id: ac5e2fc3-daed-4967-a9ff-1d12b60ee0e1' \ + --data-raw '{"temporary":true,"modelName":"grok-3","message":"https://imagine-public.x.ai/imagine-public/images/463eb5f6-7485-495a-a344-70ab4fe454a9.png --mode=extremely-crazy","toolOverrides":{"videoGen":true},"responseMetadata":{"modelConfigOverride":{"modelMap":{"videoGenModelConfig":{"parentPostId":"463eb5f6-7485-495a-a344-70ab4fe454a9","aspectRatio":"2:3","videoLength":6}}}}}' + + +--data-raw + { + "temporary":true, + "modelName":"grok-3", + "message":"https://imagine-public.x.ai/imagine-public/images/463eb5f6-7485-495a-a344-70ab4fe454a9.png --mode=extremely-crazy", + "toolOverrides": { + "videoGen":true + }, + "responseMetadata": { + "modelConfigOverride": { + "modelMap": { + "videoGenModelConfig": { + "parentPostId":"463eb5f6-7485-495a-a344-70ab4fe454a9", + "aspectRatio": "2:3", + "videoLength":6 + } + } + } + } + } diff --git a/crates/api_clients/grok_client/src/requests/video_chat/_metadata/dec_1/normal_video_gen_request.txt b/crates/api_clients/grok_client/src/requests/video_chat/_metadata/dec_1/normal_video_gen_request.txt new file mode 100644 index 0000000000..360d234a7a --- /dev/null +++ b/crates/api_clients/grok_client/src/requests/video_chat/_metadata/dec_1/normal_video_gen_request.txt @@ -0,0 +1,23 @@ +curl 'https://grok.com/rest/app-chat/conversations/new' \ + -H 'accept: */*' \ + -H 'accept-language: en-US,en;q=0.9' \ + -H 'baggage: sentry-environment=production,sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c,sentry-trace_id=7798e88fd61243ba8985ca9f589f5113,sentry-org_id=4508179396558848,sentry-transaction=%2F%3Aslug*%3F,sentry-sampled=false,sentry-sample_rand=0.01740155041905489,sentry-sample_rate=0' \ + -H 'cache-control: no-cache' \ + -H 'content-type: application/json' \ + -b '_ga=GA1.1.1232202746.1760710013; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; x-userid=85980643-ffab-4984-a3de-59a608c47d7f; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; cf_clearance=5QPOzLWH8eEniaDlJR23k9VB4XjRPHdR.kfVR15BRkI-1764645588-1.2.1.1-WBy8wpvS9.Q.ofWBIMTLHDrKRIPt0Xrn5kAQ6I7xf4u0JSbB.E4CRd.fK6kEZo8OTXMAdt.F0YkoLJzB0TufxM79dMu63TKLmorKB.gSgUWJcea1eQcLJ1WG80uhv4VCMrSJ_HHAaq6fqiNNvS7pArgMT4tvoaQXShfDuV8ivYsRp31LYMgNnRFdtiCiJ5mxhf7uUMpSS_UbvCSaYe7CysUr472alMjEYUd.QhKpb8E; _ga_8FEWB057YH=GS2.1.s1764645588$o22$g1$t1764646543$j57$l0$h0' \ + -H 'origin: https://grok.com' \ + -H 'pragma: no-cache' \ + -H 'priority: u=1, i' \ + -H 'referer: https://grok.com/imagine/post/463eb5f6-7485-495a-a344-70ab4fe454a9' \ + -H 'sec-ch-ua: "Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"' \ + -H 'sec-ch-ua-mobile: ?0' \ + -H 'sec-ch-ua-platform: "macOS"' \ + -H 'sec-fetch-dest: empty' \ + -H 'sec-fetch-mode: cors' \ + -H 'sec-fetch-site: same-origin' \ + -H 'sentry-trace: 7798e88fd61243ba8985ca9f589f5113-ac16ba34e7a9e3c2-0' \ + -H 'traceparent: 00-017b734e99d0585d4e05bf0376ef9ffb-c5854dfb0968df83-00' \ + -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36' \ + -H 'x-statsig-id: 8dxtnsQI+KKxzeTcyG2XRWSQE70E53kTiNBwF+QuwSH22YSTkOTlnEwLJVcL/k3JWIkNL/XNm7U/U87VShlyvfCIJ01k8g' \ + -H 'x-xai-request-id: c2eab60f-859f-4963-8ef8-b5eefaaf5966' \ + --data-raw '{"temporary":true,"modelName":"grok-3","message":"https://imagine-public.x.ai/imagine-public/images/463eb5f6-7485-495a-a344-70ab4fe454a9.png --mode=normal","toolOverrides":{"videoGen":true},"responseMetadata":{"modelConfigOverride":{"modelMap":{"videoGenModelConfig":{"parentPostId":"463eb5f6-7485-495a-a344-70ab4fe454a9","aspectRatio":"2:3","videoLength":6}}}}}' \ No newline at end of file diff --git a/crates/api_clients/grok_client/src/requests/video_chat/_metadata/dec_1/spicy_video_gen_request.txt b/crates/api_clients/grok_client/src/requests/video_chat/_metadata/dec_1/spicy_video_gen_request.txt new file mode 100644 index 0000000000..1922f7d4f1 --- /dev/null +++ b/crates/api_clients/grok_client/src/requests/video_chat/_metadata/dec_1/spicy_video_gen_request.txt @@ -0,0 +1,46 @@ +curl 'https://grok.com/rest/app-chat/conversations/new' \ + -H 'accept: */*' \ + -H 'accept-language: en-US,en;q=0.9' \ + -H 'baggage: sentry-environment=production,sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c,sentry-trace_id=277d6351c5063fd659b51b77b50bb453,sentry-org_id=4508179396558848,sentry-sampled=false,sentry-sample_rand=0.9765826569629004,sentry-sample_rate=0' \ + -H 'cache-control: no-cache' \ + -H 'content-type: application/json' \ + -b '_ga=GA1.1.1232202746.1760710013; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; x-userid=85980643-ffab-4984-a3de-59a608c47d7f; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; cf_clearance=5QPOzLWH8eEniaDlJR23k9VB4XjRPHdR.kfVR15BRkI-1764645588-1.2.1.1-WBy8wpvS9.Q.ofWBIMTLHDrKRIPt0Xrn5kAQ6I7xf4u0JSbB.E4CRd.fK6kEZo8OTXMAdt.F0YkoLJzB0TufxM79dMu63TKLmorKB.gSgUWJcea1eQcLJ1WG80uhv4VCMrSJ_HHAaq6fqiNNvS7pArgMT4tvoaQXShfDuV8ivYsRp31LYMgNnRFdtiCiJ5mxhf7uUMpSS_UbvCSaYe7CysUr472alMjEYUd.QhKpb8E; _ga_8FEWB057YH=GS2.1.s1764645588$o22$g1$t1764645617$j31$l0$h0' \ + -H 'origin: https://grok.com' \ + -H 'pragma: no-cache' \ + -H 'priority: u=1, i' \ + -H 'referer: https://grok.com/imagine/post/6136f31a-4aaf-48e0-ab86-fe9871feca91' \ + -H 'sec-ch-ua: "Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"' \ + -H 'sec-ch-ua-mobile: ?0' \ + -H 'sec-ch-ua-platform: "macOS"' \ + -H 'sec-fetch-dest: empty' \ + -H 'sec-fetch-mode: cors' \ + -H 'sec-fetch-site: same-origin' \ + -H 'sentry-trace: 277d6351c5063fd659b51b77b50bb453-928bfb8d1221fde1-0' \ + -H 'traceparent: 00-b0bd9846c2990e253ecca887fdfba8a2-c415282f4b74bf60-00' \ + -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36' \ + -H 'x-statsig-id: HjOCcSvnF01eIgszJ4J4qot//FLrCJb8Zz+f+AvBLs4ZNmt8fwsKc6PkyrjkEaImt9npwBpAUCrYQeeYTG/5jRWOKfLdHQ' \ + -H 'x-xai-request-id: d5ac2ce6-ed77-4a5d-8905-613a498035d3' \ + --data-raw '{"temporary":true,"modelName":"grok-3","message":"https://imagine-public.x.ai/imagine-public/images/6136f31a-4aaf-48e0-ab86-fe9871feca91.png --mode=extremely-spicy-or-crazy","toolOverrides":{"videoGen":true},"responseMetadata":{"modelConfigOverride":{"modelMap":{"videoGenModelConfig":{"parentPostId":"6136f31a-4aaf-48e0-ab86-fe9871feca91","aspectRatio":"2:3","videoLength":6}}}}}' + + +--data-raw + { + "temporary":true, + "modelName":"grok-3", + "message":"https://imagine-public.x.ai/imagine-public/images/6136f31a-4aaf-48e0-ab86-fe9871feca91.png --mode=extremely-spicy-or-crazy", + "toolOverrides": { + "videoGen":true + }, + "responseMetadata": { + "modelConfigOverride": { + "modelMap": { + "videoGenModelConfig": { + "parentPostId": "6136f31a-4aaf-48e0-ab86-fe9871feca91", + "aspectRatio": "2:3", + "videoLength":6 + } + } + } + } + } + diff --git a/crates/api_clients/grok_client/src/requests/video_chat/_metadata/nov_27/request_1x1.txt b/crates/api_clients/grok_client/src/requests/video_chat/_metadata/nov_27/request_1x1.txt new file mode 100644 index 0000000000..0d481adbaf --- /dev/null +++ b/crates/api_clients/grok_client/src/requests/video_chat/_metadata/nov_27/request_1x1.txt @@ -0,0 +1,23 @@ +curl 'https://grok.com/rest/app-chat/conversations/new' \ + -H 'accept: */*' \ + -H 'accept-language: en-US,en;q=0.9' \ + -H 'baggage: sentry-environment=production,sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c,sentry-trace_id=a0dfe5fb521c4aff81dc8e4c29ddf512,sentry-org_id=4508179396558848,sentry-transaction=%2F%3Aslug*%3F,sentry-sampled=false,sentry-sample_rand=0.2766847188981013,sentry-sample_rate=0' \ + -H 'cache-control: no-cache' \ + -H 'content-type: application/json' \ + -b '_ga=GA1.1.1232202746.1760710013; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; x-userid=85980643-ffab-4984-a3de-59a608c47d7f; cf_clearance=_kkd42wRt_Q2aVTB3qvDnUe8BZj7wJA5tYQ.SruABFE-1764222553-1.2.1.1-2U5UwW9vy4CXMEktGjlRBBtDSPkect5mmH5T98k7blNsLt_Mkxum6WZrSkwvTrujXz54E3k46_0elKO.37jq_sU2C.CoCmQ8FxU85JIPG05ybIx26PQkS54vvnnKDtvc7Jcat56FTRzKO8x._p4umwZZWsSSuthdvP9ogWyODnacVg7jyEnJ_KtZInJeLJOwUyL7qxLDTze0yIs9lfXuDzvCQrak4vl2P3FF9q0P1dA; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; _ga_8FEWB057YH=GS2.1.s1764228571$o20$g1$t1764228595$j36$l0$h0' \ + -H 'origin: https://grok.com' \ + -H 'pragma: no-cache' \ + -H 'priority: u=1, i' \ + -H 'referer: https://grok.com/imagine/post/51bc87b7-d985-486b-87f9-2345f9329675' \ + -H 'sec-ch-ua: "Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"' \ + -H 'sec-ch-ua-mobile: ?0' \ + -H 'sec-ch-ua-platform: "macOS"' \ + -H 'sec-fetch-dest: empty' \ + -H 'sec-fetch-mode: cors' \ + -H 'sec-fetch-site: same-origin' \ + -H 'sentry-trace: a0dfe5fb521c4aff81dc8e4c29ddf512-9498b36577293fef-0' \ + -H 'traceparent: 00-88e6010c9ecea790a91d0b27c38fd0e1-fac74f63c226ad4f-00' \ + -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36' \ + -H 'x-statsig-id: F6uioputjm5qWZ97AsfHQy+xxfuYSos+nBN7lvk1K0RxkSYiOP3iuyRLttV62pbJK5ONzxM3lf1JMZXxkVl8vNszvMiIFA' \ + -H 'x-xai-request-id: 1e445b21-9e73-4cb3-af6d-7918900fab62' \ + --data-raw '{"temporary":true,"modelName":"grok-3","message":"https://assets.grok.com/users/85980643-ffab-4984-a3de-59a608c47d7f/51bc87b7-d985-486b-87f9-2345f9329675/content --mode=normal","fileAttachments":["51bc87b7-d985-486b-87f9-2345f9329675"],"toolOverrides":{"videoGen":true},"responseMetadata":{"modelConfigOverride":{"modelMap":{"videoGenModelConfig":{"parentPostId":"51bc87b7-d985-486b-87f9-2345f9329675","aspectRatio":"1:1","videoLength":6}}}}}' \ No newline at end of file diff --git a/crates/api_clients/grok_client/src/requests/video_chat/_metadata/nov_27/request_tall_no_image.txt b/crates/api_clients/grok_client/src/requests/video_chat/_metadata/nov_27/request_tall_no_image.txt new file mode 100644 index 0000000000..d60f8d8c36 --- /dev/null +++ b/crates/api_clients/grok_client/src/requests/video_chat/_metadata/nov_27/request_tall_no_image.txt @@ -0,0 +1,23 @@ +curl 'https://grok.com/rest/app-chat/conversations/new' \ + -H 'accept: */*' \ + -H 'accept-language: en-US,en;q=0.9' \ + -H 'baggage: sentry-environment=production,sentry-public_key=b311e0f2690c81f25e2c4cf6d4f7ce1c,sentry-trace_id=a0dfe5fb521c4aff81dc8e4c29ddf512,sentry-org_id=4508179396558848,sentry-transaction=%2F%3Aslug*%3F,sentry-sampled=false,sentry-sample_rand=0.2766847188981013,sentry-sample_rate=0' \ + -H 'cache-control: no-cache' \ + -H 'content-type: application/json' \ + -b '_ga=GA1.1.1232202746.1760710013; i18nextLng=en; sso=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; sso-rw=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXNzaW9uX2lkIjoiOGU3MDFiNzctOTdkNC00ZjM0LWExOTctOWFmMDU1MzY3NDAwIn0.-a6x0InxbGzfTVfUlrdzxskxCnvMDI8lC90z4wHeGIk; stblid=b3331fc1-45d7-466b-83df-67427c0b2367; x-userid=85980643-ffab-4984-a3de-59a608c47d7f; cf_clearance=_kkd42wRt_Q2aVTB3qvDnUe8BZj7wJA5tYQ.SruABFE-1764222553-1.2.1.1-2U5UwW9vy4CXMEktGjlRBBtDSPkect5mmH5T98k7blNsLt_Mkxum6WZrSkwvTrujXz54E3k46_0elKO.37jq_sU2C.CoCmQ8FxU85JIPG05ybIx26PQkS54vvnnKDtvc7Jcat56FTRzKO8x._p4umwZZWsSSuthdvP9ogWyODnacVg7jyEnJ_KtZInJeLJOwUyL7qxLDTze0yIs9lfXuDzvCQrak4vl2P3FF9q0P1dA; mp_ea93da913ddb66b6372b89d97b1029ac_mixpanel=%7B%22distinct_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%2C%22%24device_id%22%3A%2279ce237a-a0f3-4913-bf4b-519ac8a98263%22%2C%22%24initial_referrer%22%3A%22%24direct%22%2C%22%24initial_referring_domain%22%3A%22%24direct%22%2C%22__mps%22%3A%7B%7D%2C%22__mpso%22%3A%7B%7D%2C%22__mpus%22%3A%7B%7D%2C%22__mpa%22%3A%7B%7D%2C%22__mpu%22%3A%7B%7D%2C%22__mpr%22%3A%5B%5D%2C%22__mpap%22%3A%5B%5D%2C%22%24user_id%22%3A%2285980643-ffab-4984-a3de-59a608c47d7f%22%7D; _ga_8FEWB057YH=GS2.1.s1764228571$o20$g1$t1764228668$j60$l0$h0' \ + -H 'origin: https://grok.com' \ + -H 'pragma: no-cache' \ + -H 'priority: u=1, i' \ + -H 'referer: https://grok.com/imagine/post/b0b88b45-9724-49bd-a82c-c83ddcd2937f' \ + -H 'sec-ch-ua: "Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"' \ + -H 'sec-ch-ua-mobile: ?0' \ + -H 'sec-ch-ua-platform: "macOS"' \ + -H 'sec-fetch-dest: empty' \ + -H 'sec-fetch-mode: cors' \ + -H 'sec-fetch-site: same-origin' \ + -H 'sentry-trace: a0dfe5fb521c4aff81dc8e4c29ddf512-8d3208b7533c8446-0' \ + -H 'traceparent: 00-46471c2129f7b3cac9db10d504ba132a-7851e787db05c657-00' \ + -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36' \ + -H 'x-statsig-id: T/P6+sP11jYyAccjWp+fG3fpnaPAEtNmxEsjzqFtcxwpyX56YKW643wT7o0igs6Rc6TVl0smbLBKnHfKsZhcEQxzyvdlTA' \ + -H 'x-xai-request-id: 9c853e9c-ce87-403b-a2c9-d073523b34f6' \ + --data-raw '{"temporary":true,"modelName":"grok-3","message":"A waterspout wrecks an oil tanker, misty, huge waves --mode=custom","toolOverrides":{"videoGen":true},"responseMetadata":{"modelConfigOverride":{"modelMap":{"videoGenModelConfig":{"parentPostId":"b0b88b45-9724-49bd-a82c-c83ddcd2937f","aspectRatio":"2:3","videoLength":6}}}}}' \ No newline at end of file diff --git a/crates/api_clients/grok_client/src/requests/video_chat/grok_video_gen_chat_conversation.rs b/crates/api_clients/grok_client/src/requests/video_chat/grok_video_gen_chat_conversation.rs index 6aa6913c83..10b7b50f47 100644 --- a/crates/api_clients/grok_client/src/requests/video_chat/grok_video_gen_chat_conversation.rs +++ b/crates/api_clients/grok_client/src/requests/video_chat/grok_video_gen_chat_conversation.rs @@ -1,10 +1,12 @@ use crate::client::browser_user_agents::FIREFOX_143_MAC_USER_AGENT; +use crate::datatypes::api::aspect_ratio::AspectRatio; use crate::datatypes::api::baggage::Baggage; use crate::datatypes::api::file_id::FileId; use crate::datatypes::api::sentry_trace::SentryTrace; use crate::datatypes::api::svg_path_data::SvgPathData; use crate::datatypes::api::user_id::UserId; use crate::datatypes::api::verification_token::VerificationToken; +use crate::datatypes::api::video_generation_mode::VideoGenerationMode; use crate::datatypes::api::xsid_numbers::XsidNumbers; use crate::error::categorize_grok_http_error::categorize_grok_http_error; use crate::error::grok_client_error::GrokClientError; @@ -14,7 +16,7 @@ use crate::requests::index_page::signature::generate_xsid::{generate_xsid, Gener use crate::requests::upload_file::grok_upload_file::{GrokUploadFile, GrokUploadFileResponse}; use crate::requests::video_chat::parse_video_id::parse_video_id; use crate::requests::video_chat::parse_video_id_from_partial_byte_stream_buffer::parse_video_id_from_partial_byte_stream_buffer; -use crate::requests::video_chat::request::{CreateChatConversationWireRequest, ModelConfigOverride, ModelMap, ResponseMetadata, ToolOverrides, VideoGenModelConfig}; +use crate::requests::video_chat::request::{CreateChatConversationWireRequest, ModelConfigOverride, ModelMap, ResponseMetadata, ToolOverrides, VideoGenAspectRatio, VideoGenModelConfig}; use crate::utils::user_and_file_id_to_image_url::user_and_file_id_to_image_url; use futures::StreamExt; use log::{debug, error, info, warn}; @@ -32,6 +34,8 @@ pub struct GrokVideoGenChatConversationBuilder<'a> { pub file_id: &'a FileId, pub media_type: VideoMediaPostType, pub prompt: Option<&'a str>, + pub mode: Option, + pub aspect_ratio: Option, pub cookie: &'a str, pub user_id: &'a UserId, @@ -230,12 +234,30 @@ impl <'a> GrokVideoGenChatConversationBuilder<'a> { VideoMediaPostType::Video => unimplemented!("implement for videos"), }; - let mut prompt = format!("{media_url} --mode=normal"); + let mode_string = self.mode + .unwrap_or(VideoGenerationMode::Normal) + .as_api_mode_arg(); + + let mut prompt = format!("{media_url} --mode={mode_string}"); if let Some(user_prompt) = self.prompt { - prompt = format!("{media_url} {user_prompt} --mode=custom"); + // If a text prompt is set, mode seems to typically be "custom". + let mode_string = self.mode + .unwrap_or(VideoGenerationMode::Custom) + .as_api_mode_arg(); + + prompt = format!("{media_url} {user_prompt} --mode={mode_string}"); } + info!("Grok video prompt: {prompt}"); + + let aspect_ratio = match self.aspect_ratio { + Some(AspectRatio::Square) => VideoGenAspectRatio::Square, + Some(AspectRatio::WideThreeByTwo) => VideoGenAspectRatio::WideThreeByTwo, + Some(AspectRatio::TallTwoByThree) => VideoGenAspectRatio::TallTwoByThree, + None => VideoGenAspectRatio::WideThreeByTwo, + }; + let request_body = CreateChatConversationWireRequest { temporary: true, model_name: "grok-3".to_string(), @@ -251,6 +273,8 @@ impl <'a> GrokVideoGenChatConversationBuilder<'a> { model_map: ModelMap { video_gen_model_config: VideoGenModelConfig { parent_post_id: self.file_id.0.to_string(), + video_length: 6, // NB: API defaults to "6" + aspect_ratio, }, }, }, @@ -340,6 +364,8 @@ mod tests { file_id: &file_id, media_type: VideoMediaPostType::UserUploadedImage, prompt: maybe_prompt, + mode: Some(VideoGenerationMode::Custom), + aspect_ratio: Some(AspectRatio::TallTwoByThree), cookie: cookies.as_str(), user_id: &secrets.user_id, @@ -402,6 +428,8 @@ mod tests { file_id: &file_id, media_type: VideoMediaPostType::UserUploadedImage, prompt: maybe_prompt, + mode: None, + aspect_ratio: Some(AspectRatio::Square), cookie: cookies.as_str(), user_id: &secrets.user_id, diff --git a/crates/api_clients/grok_client/src/requests/video_chat/request.rs b/crates/api_clients/grok_client/src/requests/video_chat/request.rs index 038a02dd83..2fa53073f2 100644 --- a/crates/api_clients/grok_client/src/requests/video_chat/request.rs +++ b/crates/api_clients/grok_client/src/requests/video_chat/request.rs @@ -51,10 +51,55 @@ pub (super) struct ModelMap { pub (super) struct VideoGenModelConfig { #[serde(rename = "parentPostId")] pub (super) parent_post_id: String, + + /// Newer field as of November + /// values: "1:1", "2:3" (tall), + #[serde(rename = "aspectRatio")] + pub (super) aspect_ratio: VideoGenAspectRatio, + + /// Newer field as of November + /// default seems to be "6" + #[serde(rename = "videoLength")] + pub (super) video_length: u32, } +#[derive(Serialize, Clone, Copy, Debug)] +pub (super) enum VideoGenAspectRatio { + #[serde(rename = "2:3")] + TallTwoByThree, + + #[serde(rename = "3:2")] + WideThreeByTwo, + + #[serde(rename = "1:1")] + Square, +} /* + +November 27 no image (text only) square aspect ratio - + +--data-raw + { + "temporary":true, + "modelName":"grok-3", + "message":"A waterspout wrecks an oil tanker, misty, huge waves --mode=custom", + "toolOverrides": { + "videoGen":true + }, + "responseMetadata": { + "modelConfigOverride": { + "modelMap": { + "videoGenModelConfig": { + "parentPostId":"b0b88b45-9724-49bd-a82c-c83ddcd2937f", + "aspectRatio":"2:3", + "videoLength":6 + } + } + } + } + } + October 29 request struct - --data-raw @@ -113,4 +158,6 @@ October 22 request struct - "videoGen":true } } - */ + +*/ + diff --git a/crates/api_clients/thumbnail_generator/Cargo.toml b/crates/api_clients/thumbnail_generator/Cargo.toml index 10c57d4786..4ffaa50502 100644 --- a/crates/api_clients/thumbnail_generator/Cargo.toml +++ b/crates/api_clients/thumbnail_generator/Cargo.toml @@ -15,5 +15,5 @@ workspace-build-acceleration.workspace = true # External easyenv = { path = "../../lib/easyenv" } log.workspace = true -reqwest = "0.12.4" +reqwest = { version = "0.12.4", default-features = false, features = [ "charset", "http2", "system-proxy", "rustls-tls" ] } serde_json.workspace = true diff --git a/crates/cli/devenv/dev-database-seed/Cargo.toml b/crates/cli/devenv/dev-database-seed/Cargo.toml index bb16ac8173..24731e8e6b 100644 --- a/crates/cli/devenv/dev-database-seed/Cargo.toml +++ b/crates/cli/devenv/dev-database-seed/Cargo.toml @@ -43,9 +43,9 @@ rand.workspace = true # NB: We switched to Tokio from `async-std` for broader ecosystem support. tokio = { version = "1.38.0", features = ["macros"] } -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] } +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] } -elasticsearch = "8.5.0-alpha.1" +elasticsearch = { version = "8.5.0-alpha.1", default-features = false, features = ["rustls-tls"] } [dev-dependencies] # None yet diff --git a/crates/cli/devenv/dev-upload-media-file/Cargo.toml b/crates/cli/devenv/dev-upload-media-file/Cargo.toml index a5615d548f..27bb5bda32 100644 --- a/crates/cli/devenv/dev-upload-media-file/Cargo.toml +++ b/crates/cli/devenv/dev-upload-media-file/Cargo.toml @@ -39,7 +39,7 @@ log.workspace = true # NB: We switched to Tokio from `async-std` for broader ecosystem support. tokio.workspace = true -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] } +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] } [dev-dependencies] # None yet diff --git a/crates/cli/production/db-backfill/Cargo.toml b/crates/cli/production/db-backfill/Cargo.toml index 5a15d89726..0c774d5a3e 100644 --- a/crates/cli/production/db-backfill/Cargo.toml +++ b/crates/cli/production/db-backfill/Cargo.toml @@ -36,7 +36,7 @@ log = "0.4.14" # Bigger pieces futures = { version = "0.3.17", features = ["thread-pool"] } -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] } +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] } tokio = { version = "1.38.0", features = ["macros"] } # Serialization diff --git a/crates/cli/production/db-cleanup/Cargo.toml b/crates/cli/production/db-cleanup/Cargo.toml index e25d1627ae..01018ad339 100644 --- a/crates/cli/production/db-cleanup/Cargo.toml +++ b/crates/cli/production/db-cleanup/Cargo.toml @@ -36,7 +36,7 @@ log = "0.4.14" # Bigger pieces tokio = { version = "1.38.0", features = ["macros"] } -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] } +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] } # Serialization serde = "1.0.125" diff --git a/crates/cli/production/elasticsearch-cli/Cargo.toml b/crates/cli/production/elasticsearch-cli/Cargo.toml index 4d8caaf96b..bf3b4e8a1d 100644 --- a/crates/cli/production/elasticsearch-cli/Cargo.toml +++ b/crates/cli/production/elasticsearch-cli/Cargo.toml @@ -38,8 +38,8 @@ strum = { version = "0.26", features = ["derive"] } # Bigger pieces tokio = { version = "1.38.0", features = ["macros"] } -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] } -elasticsearch = "8.5.0-alpha.1" +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] } +elasticsearch = { version = "8.5.0-alpha.1", default-features = false, features = ["rustls-tls"] } # Serialization serde = "1.0.125" diff --git a/crates/cli/production/migration-tool/Cargo.toml b/crates/cli/production/migration-tool/Cargo.toml index 3791f10171..da37d3d501 100644 --- a/crates/cli/production/migration-tool/Cargo.toml +++ b/crates/cli/production/migration-tool/Cargo.toml @@ -40,7 +40,7 @@ tempdir.workspace = true # Async + clients tokio.workspace = true -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] } +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] } # Serialization serde.workspace = true diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_object/enqueue_image_to_3d_object_command.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_object/enqueue_image_to_3d_object_command.rs index eeb23130ea..1010df4223 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_object/enqueue_image_to_3d_object_command.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_object/enqueue_image_to_3d_object_command.rs @@ -17,6 +17,7 @@ use crate::services::storyteller::state::storyteller_credential_manager::Storyte use log::{error, info, warn}; use serde_derive::{Deserialize, Serialize}; use tauri::{AppHandle, State}; +use enums::tauri::ux::tauri_command_caller::TauriCommandCaller; use tokens::tokens::media_files::MediaFileToken; #[derive(Deserialize)] @@ -27,6 +28,21 @@ pub struct EnqueueImageTo3dObjectRequest { /// The model to use. pub model: Option, + + /// OPTIONAL. + /// Name of the frontend caller. + /// We'll use this to selectively trigger events. + pub frontend_caller: Option, + + /// OPTIONAL. + /// A frontend-defined identifier that we'll send back to the frontend + /// as a Tauri event on task completion. + pub frontend_subscriber_id: Option, + + /// OPTIONAL. + /// A frontend-defined payload that we'll send back to the frontend + /// as a Tauri event on task completion. + pub frontend_subscriber_payload: Option, } #[derive(Deserialize, Debug, Copy, Clone)] @@ -159,7 +175,7 @@ pub async fn handle_request( ) -> Result { let result = handle_object( - request, + &request, &app, &app_env_configs, &app_data_root, @@ -173,7 +189,12 @@ pub async fn handle_request( }; let result = success_event - .insert_into_task_database(task_database) + .insert_into_task_database_with_frontend_payload( + task_database, + request.frontend_caller.clone(), + request.frontend_subscriber_id.as_deref(), + request.frontend_subscriber_payload.as_deref(), + ) .await; if let Err(err) = result { diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_object/generic/handle_object.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_object/generic/handle_object.rs index 9ff02721d1..1934c8cfc7 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_object/generic/handle_object.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_object/generic/handle_object.rs @@ -10,7 +10,7 @@ use crate::services::storyteller::state::storyteller_credential_manager::Storyte use tauri::AppHandle; pub async fn handle_object( - request: EnqueueImageTo3dObjectRequest, + request: &EnqueueImageTo3dObjectRequest, app: &AppHandle, app_env_configs: &AppEnvConfigs, app_data_root: &AppDataRoot, diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_object/generic/handle_object_artcraft.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_object/generic/handle_object_artcraft.rs index 21296aa980..558ae0a403 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_object/generic/handle_object_artcraft.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_object/generic/handle_object_artcraft.rs @@ -19,7 +19,7 @@ use storyteller_client::endpoints::generate::object::generate_hunyuan_3d_2_1_ima use tauri::AppHandle; pub async fn handle_object_artcraft( - request: EnqueueImageTo3dObjectRequest, + request: &EnqueueImageTo3dObjectRequest, app: &AppHandle, app_env_configs: &AppEnvConfigs, app_data_root: &AppDataRoot, @@ -59,7 +59,7 @@ pub async fn handle_object_artcraft( used_model = Some(GenerationModel::Hunyuan3d2_0); let request = GenerateHunyuan20ImageTo3dRequest { uuid_idempotency_token, - media_file_token: request.image_media_token, + media_file_token: request.image_media_token.clone(), }; let result = generate_hunyuan3d_2_0_image_to_3d( &app_env_configs.storyteller_host, @@ -82,7 +82,7 @@ pub async fn handle_object_artcraft( used_model = Some(GenerationModel::Hunyuan3d2_1); let request = GenerateHunyuan21ImageTo3dRequest { uuid_idempotency_token, - media_file_token: request.image_media_token, + media_file_token: request.image_media_token.clone(), }; let result = generate_hunyuan3d_2_1_image_to_3d( &app_env_configs.storyteller_host, diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/enqueue_image_to_video_command.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/enqueue_image_to_video_command.rs index d47bcb680d..f9f834a0c9 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/enqueue_image_to_video_command.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/enqueue_image_to_video_command.rs @@ -20,6 +20,7 @@ use crate::services::storyteller::state::storyteller_credential_manager::Storyte use log::{error, info, warn}; use serde_derive::{Deserialize, Serialize}; use tauri::{AppHandle, State}; +use enums::tauri::ux::tauri_command_caller::TauriCommandCaller; use tokens::tokens::media_files::MediaFileToken; use crate::core::events::generation_events::common::GenerationAction; use crate::core::events::generation_events::generation_enqueue_failure_event::GenerationEnqueueFailureEvent; @@ -78,9 +79,33 @@ pub struct EnqueueImageToVideoRequest { /// Text prompt used to direct the video. pub prompt: Option, + /// Optional. + /// Generate the video with audio, if there is an option. + /// Typically, this costs money when presented as an option, so we default to off. + pub generate_audio: Option, + /// OPTIONAL. /// Only for Sora2 model currently. pub sora_orientation: Option, + + /// OPTIONAL. + /// Only for Grok model currently + pub grok_aspect_ratio: Option, + + /// OPTIONAL. + /// Name of the frontend caller. + /// We'll use this to selectively trigger events. + pub frontend_caller: Option, + + /// OPTIONAL. + /// A frontend-defined identifier that we'll send back to the frontend + /// as a Tauri event on task completion. + pub frontend_subscriber_id: Option, + + /// OPTIONAL. + /// A frontend-defined payload that we'll send back to the frontend + /// as a Tauri event on task completion. + pub frontend_subscriber_payload: Option, } // TODO: Not sure how to handle so many different types of video (model) x (services). @@ -91,6 +116,15 @@ pub enum SoraOrientation { Landscape, } +// TODO: Not sure how to handle so many different types of video (model) x (services). +#[derive(Deserialize, Debug)] +#[serde(rename_all = "snake_case")] +pub enum GrokAspectRatio { + Portrait, + Landscape, + Square +} + #[derive(Serialize)] pub struct EnqueueImageToVideoSuccessResponse { } @@ -236,7 +270,7 @@ pub async fn handle_request( } _ => { handle_video( - request, + &request, app, app_env_configs, app_data_root, @@ -252,7 +286,12 @@ pub async fn handle_request( }; let result = success_event - .insert_into_task_database(task_database) + .insert_into_task_database_with_frontend_payload( + task_database, + request.frontend_caller, + request.frontend_subscriber_id.as_deref(), + request.frontend_subscriber_payload.as_deref() + ) .await; if let Err(err) = result { diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/generic/handle_video.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/generic/handle_video.rs index 58fda7b29d..3eedeceae9 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/generic/handle_video.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/generic/handle_video.rs @@ -9,7 +9,7 @@ use crate::services::storyteller::state::storyteller_credential_manager::Storyte use tauri::AppHandle; pub async fn handle_video( - request: EnqueueImageToVideoRequest, + request: &EnqueueImageToVideoRequest, app: &AppHandle, app_env_configs: &AppEnvConfigs, app_data_root: &AppDataRoot, @@ -25,7 +25,7 @@ pub async fn handle_video( Provider::Sora => {} // Fallthrough Provider::Artcraft => { return Ok(handle_video_artcraft( - request, + &request, &app, app_env_configs, app_data_root, diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/generic/handle_video_artcraft.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/generic/handle_video_artcraft.rs index a98156cc66..4a80d17a55 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/generic/handle_video_artcraft.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/generic/handle_video_artcraft.rs @@ -31,7 +31,7 @@ use storyteller_client::utils::api_host::ApiHost; use tauri::AppHandle; pub async fn handle_video_artcraft( - request: EnqueueImageToVideoRequest, + request: &EnqueueImageToVideoRequest, app: &AppHandle, app_env_configs: &AppEnvConfigs, app_data_root: &AppDataRoot, @@ -84,10 +84,10 @@ pub async fn handle_video_artcraft( selected_model = Some(GenerationModel::Kling1_6); let request = GenerateKling16ProImageToVideoRequest { uuid_idempotency_token, - media_file_token: request.image_media_token, - end_frame_image_media_token: request.end_frame_image_media_token, + media_file_token: request.image_media_token.clone(), + end_frame_image_media_token: request.end_frame_image_media_token.clone(), aspect_ratio: Some(GenerateKling16ProAspectRatio::WideSixteenNine), - prompt: request.prompt, + prompt: request.prompt.clone(), duration: None, }; let result = generate_kling_16_pro_image_to_video( @@ -111,9 +111,9 @@ pub async fn handle_video_artcraft( selected_model = Some(GenerationModel::Kling21Master); let request = GenerateKling21MasterImageToVideoRequest { uuid_idempotency_token, - media_file_token: request.image_media_token, + media_file_token: request.image_media_token.clone(), aspect_ratio: Some(GenerateKling21MasterAspectRatio::WideSixteenNine), - prompt: request.prompt, + prompt: request.prompt.clone(), duration: None, }; let result = generate_kling_21_master_image_to_video( @@ -137,10 +137,10 @@ pub async fn handle_video_artcraft( selected_model = Some(GenerationModel::Kling21Pro); let request = GenerateKling21ProImageToVideoRequest { uuid_idempotency_token, - media_file_token: request.image_media_token, - end_frame_image_media_token: request.end_frame_image_media_token, + media_file_token: request.image_media_token.clone(), + end_frame_image_media_token: request.end_frame_image_media_token.clone(), aspect_ratio: Some(GenerateKling21ProAspectRatio::WideSixteenNine), - prompt: request.prompt, + prompt: request.prompt.clone(), duration: None, }; let result = generate_kling_21_pro_image_to_video( @@ -164,9 +164,9 @@ pub async fn handle_video_artcraft( selected_model = Some(GenerationModel::Seedance10Lite); let request = GenerateSeedance10LiteImageToVideoRequest { uuid_idempotency_token, - media_file_token: request.image_media_token, - end_frame_image_media_token: request.end_frame_image_media_token, - prompt: request.prompt, + media_file_token: request.image_media_token.clone(), + end_frame_image_media_token: request.end_frame_image_media_token.clone(), + prompt: request.prompt.clone(), resolution: None, duration: None, }; @@ -191,9 +191,9 @@ pub async fn handle_video_artcraft( selected_model = Some(GenerationModel::Veo2); let request = GenerateVeo2ImageToVideoRequest { uuid_idempotency_token, - media_file_token: request.image_media_token, + media_file_token: request.image_media_token.clone(), aspect_ratio: Some(GenerateVeo2AspectRatio::WideSixteenNine), - prompt: request.prompt, + prompt: request.prompt.clone(), duration: None, }; let result = generate_veo_2_image_to_video( @@ -217,12 +217,12 @@ pub async fn handle_video_artcraft( selected_model = Some(GenerationModel::Veo3); let request = GenerateVeo3ImageToVideoRequest { uuid_idempotency_token, - media_file_token: request.image_media_token, + media_file_token: request.image_media_token.clone(), aspect_ratio: Some(GenerateVeo3AspectRatio::WideSixteenNine), - prompt: request.prompt, + prompt: request.prompt.clone(), duration: None, resolution: None, - generate_audio: None, + generate_audio: request.generate_audio, }; let result = generate_veo_3_image_to_video( &app_env_configs.storyteller_host, @@ -245,11 +245,11 @@ pub async fn handle_video_artcraft( selected_model = Some(GenerationModel::Veo3Fast); let request = GenerateVeo3FastImageToVideoRequest { uuid_idempotency_token, - media_file_token: request.image_media_token, - prompt: request.prompt, + media_file_token: request.image_media_token.clone(), + prompt: request.prompt.clone(), duration: None, resolution: None, - generate_audio: None, + generate_audio: request.generate_audio, }; let result = generate_veo_3_fast_image_to_video( &app_env_configs.storyteller_host, diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/grok/handle_grok_video.rs b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/grok/handle_grok_video.rs index c4810dd8d6..5ce8f3a275 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/grok/handle_grok_video.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/image_to_video/grok/handle_grok_video.rs @@ -1,7 +1,7 @@ use crate::core::commands::enqueue::generate_error::{BadInputReason, GenerateError}; use crate::core::commands::enqueue::image_edit::enqueue_contextual_edit_image_command::EnqueueContextualEditImageCommand; use crate::core::commands::enqueue::image_inpaint::enqueue_image_inpaint_command::EnqueueInpaintImageCommand; -use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, SoraOrientation}; +use crate::core::commands::enqueue::image_to_video::enqueue_image_to_video_command::{EnqueueImageToVideoRequest, GrokAspectRatio, SoraOrientation}; use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess; use crate::core::events::functional_events::show_provider_login_modal_event::ShowProviderLoginModalEvent; use crate::core::events::generation_events::common::GenerationModel; @@ -19,9 +19,11 @@ use enums::common::generation_provider::GenerationProvider; use enums::tauri::tasks::task_type::TaskType; use grok_client::credentials::grok_cookies::GrokCookies; use grok_client::credentials::grok_full_credentials::GrokFullCredentials; +use grok_client::datatypes::api::aspect_ratio::AspectRatio; use grok_client::datatypes::file_upload_spec::FileUploadSpec; use grok_client::recipes::request_client_secrets::{request_client_secrets, RequestClientSecretsArgs}; use grok_client::recipes::upload_image_and_generate_video::{upload_image_and_generate_video, UploadImageAndGenerateVideo}; +use grok_client::recipes::upload_image_and_generate_video_with_retry::{upload_image_and_generate_video_with_retry, UploadImageAndGenerateVideoWithRetry}; use log::{error, info, warn}; use openai_sora_client::recipes::generate_sora2_video::generate_sora2_video_with_session_auto_renew::generate_sora2_video_with_session_auto_renew; use openai_sora_client::requests::generate_sora2_video::generate_sora2_video::{GenerateSora2VideoArgs, Orientation}; @@ -55,24 +57,36 @@ pub async fn handle_grok_video( image_token, ).await?; + let aspect_ratio = match request.grok_aspect_ratio { + None => AspectRatio::WideThreeByTwo, + Some(GrokAspectRatio::Landscape) => AspectRatio::WideThreeByTwo, + Some(GrokAspectRatio::Portrait) => AspectRatio::TallTwoByThree, + Some(GrokAspectRatio::Square) => AspectRatio::Square, + }; + info!("Calling Grok Video generate..."); - let upload = upload_image_and_generate_video(UploadImageAndGenerateVideo { - full_credentials: &creds, + let upload_result = upload_image_and_generate_video_with_retry(UploadImageAndGenerateVideoWithRetry { + credentials: &creds, file: FileUploadSpec::Path(local_image_file.path()), prompt: request.prompt.as_deref(), + aspect_ratio: Some(aspect_ratio), wait_for_generation: false, individual_request_timeout: Some(GROK_IMAGE_UPLOAD_TIMEOUT) }).await; - let post_id = match upload { + let post_id = match upload_result { Err(err) => { error!("Failed to use Grok video: {:?}", err); return Err(GenerateError::from(err)); } Ok(result) => { - info!("Successfully enqueued Grok Video: post_id = {:?}", result.post_id); - result.post_id + if let Some(secrets) = result.maybe_new_client_secrets { + info!("New Grok secrets generated! Storing them in the credential manager..."); + grok_credential_manager.replace_client_secrets_only(secrets)?; + } + info!("Successfully enqueued Grok Video: post_id = {:?}", result.upload_result.post_id); + result.upload_result.post_id } }; diff --git a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/grok/handle_grok.rs b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/grok/handle_grok.rs index c00c03eac6..249f78aeec 100644 --- a/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/grok/handle_grok.rs +++ b/crates/desktop/artcraft/src/core/commands/enqueue/text_to_image/grok/handle_grok.rs @@ -1,12 +1,13 @@ use crate::core::commands::enqueue::generate_error::GenerateError; use crate::core::commands::enqueue::task_enqueue_success::TaskEnqueueSuccess; -use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::EnqueueTextToImageRequest; +use crate::core::commands::enqueue::text_to_image::enqueue_text_to_image_command::{EnqueueTextToImageRequest, TextToImageSize}; use crate::core::events::generation_events::common::GenerationModel; use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs; use crate::services::grok::state::grok_credential_manager::GrokCredentialManager; use crate::services::grok::state::grok_image_prompt_queue::{GrokImagePromptQueue, PromptItem}; use enums::common::generation_provider::GenerationProvider; use enums::tauri::tasks::task_type::TaskType; +use grok_client::requests::image_websocket::messages::websocket_client_message::ClientMessageAspectRatio; use idempotency::uuid::generate_random_uuid; use tauri::AppHandle; @@ -53,9 +54,18 @@ pub async fn handle_grok( .map(|prompt| prompt.trim().to_string()) .unwrap_or_else(|| "".to_string()); + let aspect_ratio = match request.aspect_ratio { + None => ClientMessageAspectRatio::Square, + Some(TextToImageSize::Auto) => ClientMessageAspectRatio::Square, + Some(TextToImageSize::Square) => ClientMessageAspectRatio::Square, + Some(TextToImageSize::Wide) => ClientMessageAspectRatio::WideThreeByTwo, + Some(TextToImageSize::Tall) => ClientMessageAspectRatio::TallTwoByThree, + }; + grok_image_prompt_queue.enqueue(PromptItem { task_id: job_id.clone(), prompt, + aspect_ratio, })?; Ok(TaskEnqueueSuccess { diff --git a/crates/desktop/artcraft/src/core/events/functional_events/mod.rs b/crates/desktop/artcraft/src/core/events/functional_events/mod.rs index 60db4d7b6c..605d0a82c2 100644 --- a/crates/desktop/artcraft/src/core/events/functional_events/mod.rs +++ b/crates/desktop/artcraft/src/core/events/functional_events/mod.rs @@ -1,8 +1,10 @@ pub mod canvas_background_removal_complete_event; pub mod credits_balance_changed_event; pub mod image_edit_complete_event; +pub mod object_generation_complete_event; pub mod refresh_account_state_event; pub mod show_provider_billing_modal_event; pub mod show_provider_login_modal_event; pub mod subscription_plan_changed_event; -pub mod text_to_image_generation_complete_event; \ No newline at end of file +pub mod text_to_image_generation_complete_event; +pub mod video_generation_complete_event; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/core/events/functional_events/object_generation_complete_event.rs b/crates/desktop/artcraft/src/core/events/functional_events/object_generation_complete_event.rs new file mode 100644 index 0000000000..43b2311886 --- /dev/null +++ b/crates/desktop/artcraft/src/core/events/functional_events/object_generation_complete_event.rs @@ -0,0 +1,26 @@ +use crate::core::events::basic_sendable_event_trait::{BasicEventStatus, BasicSendableEvent}; +use enums::tauri::ux::tauri_event_name::TauriEventName; +use serde_derive::Serialize; +use tokens::tokens::media_files::MediaFileToken; +use url::Url; + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct ObjectGenerationCompleteEvent { + pub generated_object: Option, + pub maybe_frontend_subscriber_id: Option, + pub maybe_frontend_subscriber_payload: Option, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct GeneratedObject { + pub media_token: MediaFileToken, + pub cdn_url: Url, + pub maybe_thumbnail_template: Option, +} + +impl BasicSendableEvent for ObjectGenerationCompleteEvent { + const FRONTEND_EVENT_NAME: TauriEventName = TauriEventName::ObjectGenerationCompleteEvent; + const EVENT_STATUS: BasicEventStatus = BasicEventStatus::Success; +} diff --git a/crates/desktop/artcraft/src/core/events/functional_events/video_generation_complete_event.rs b/crates/desktop/artcraft/src/core/events/functional_events/video_generation_complete_event.rs new file mode 100644 index 0000000000..5e20bcf548 --- /dev/null +++ b/crates/desktop/artcraft/src/core/events/functional_events/video_generation_complete_event.rs @@ -0,0 +1,26 @@ +use crate::core::events::basic_sendable_event_trait::{BasicEventStatus, BasicSendableEvent}; +use enums::tauri::ux::tauri_event_name::TauriEventName; +use serde_derive::Serialize; +use tokens::tokens::media_files::MediaFileToken; +use url::Url; + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct VideoGenerationCompleteEvent { + pub generated_video: Option, + pub maybe_frontend_subscriber_id: Option, + pub maybe_frontend_subscriber_payload: Option, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct GeneratedVideo { + pub media_token: MediaFileToken, + pub cdn_url: Url, + pub maybe_thumbnail_template: Option, +} + +impl BasicSendableEvent for VideoGenerationCompleteEvent { + const FRONTEND_EVENT_NAME: TauriEventName = TauriEventName::VideoGenerationCompleteEvent; + const EVENT_STATUS: BasicEventStatus = BasicEventStatus::Success; +} diff --git a/crates/desktop/artcraft/src/core/lifecycle/startup/setup_main_window.rs b/crates/desktop/artcraft/src/core/lifecycle/startup/setup_main_window.rs index 310433f191..ffbbbe4b05 100644 --- a/crates/desktop/artcraft/src/core/lifecycle/startup/setup_main_window.rs +++ b/crates/desktop/artcraft/src/core/lifecycle/startup/setup_main_window.rs @@ -25,9 +25,14 @@ pub async fn setup_main_window( .focusable(true) .focused(true); - #[cfg(not(target_os = "macos"))] + // NB: Setting decorations(false) "breaks" Mac, though I never documented how/why. + // NB: On Linux, setting decorations(false) will cause the window to be unable to be + // resized, despite the arrow cursor indicating resize is possible. This might be + // fixable another way, but for now it seems fine to disable this. There also seem to + // be some performance loss with decorations(false), but I might be imagining it. + #[cfg(target_os = "windows")] let win_builder = win_builder - .decorations(false); // NB: This breaks Mac! + .decorations(false); // NB: This breaks Mac! (And breaks resize on Linux) #[cfg(target_os = "windows")] let win_builder = win_builder diff --git a/crates/desktop/artcraft/src/services/grok/state/grok_credential_manager.rs b/crates/desktop/artcraft/src/services/grok/state/grok_credential_manager.rs index d76267b124..286d7b07f1 100644 --- a/crates/desktop/artcraft/src/services/grok/state/grok_credential_manager.rs +++ b/crates/desktop/artcraft/src/services/grok/state/grok_credential_manager.rs @@ -102,7 +102,7 @@ impl GrokCredentialManager { } } - pub fn replace_full_credentials(&self, creds: GrokFullCredentials) -> anyhow::Result<()> { + pub fn replace_full_credentials(&self, creds: GrokFullCredentials) -> anyhow::Result<()> { match self.credential_data.write() { Err(err) => Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err)), Ok(mut holder) => { @@ -112,6 +112,22 @@ impl GrokCredentialManager { } } + pub fn replace_client_secrets_only(&self, secrets: GrokClientSecrets) -> anyhow::Result<()> { + match self.credential_data.write() { + Err(err) => Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err)), + Ok(mut holder) => { + let mut maybe_full_creds = holder.grok_full_credentials.clone(); + if let Some(creds) = maybe_full_creds.as_mut() { + creds.client_secrets = secrets; + } else { + warn!("No existing credentials to replace secrets into."); + } + holder.grok_full_credentials = maybe_full_creds; + Ok(()) + } + } + } + // NB: This is just a heuristic. We'll add better checks later. pub fn do_task_polling(&self) -> anyhow::Result { self.session_appears_active() diff --git a/crates/desktop/artcraft/src/services/grok/state/grok_image_prompt_queue.rs b/crates/desktop/artcraft/src/services/grok/state/grok_image_prompt_queue.rs index 6bf31824f4..6e185aa2d9 100644 --- a/crates/desktop/artcraft/src/services/grok/state/grok_image_prompt_queue.rs +++ b/crates/desktop/artcraft/src/services/grok/state/grok_image_prompt_queue.rs @@ -2,6 +2,7 @@ use crate::core::artcraft_error::ArtcraftError; use log::error; use std::collections::VecDeque; use std::sync::{Arc, Mutex}; +use grok_client::requests::image_websocket::messages::websocket_client_message::ClientMessageAspectRatio; #[derive(Clone)] pub struct GrokImagePromptQueue { @@ -16,6 +17,9 @@ pub struct PromptItem { /// Text prompt pub prompt: String, + + /// The aspect ratio of the image + pub aspect_ratio: ClientMessageAspectRatio, } impl GrokImagePromptQueue { diff --git a/crates/desktop/artcraft/src/services/grok/threads/grok_image_websocket_thread/grok_image_websocket_thread.rs b/crates/desktop/artcraft/src/services/grok/threads/grok_image_websocket_thread/grok_image_websocket_thread.rs index fdf88e1f1e..69f22880ca 100644 --- a/crates/desktop/artcraft/src/services/grok/threads/grok_image_websocket_thread/grok_image_websocket_thread.rs +++ b/crates/desktop/artcraft/src/services/grok/threads/grok_image_websocket_thread/grok_image_websocket_thread.rs @@ -18,10 +18,12 @@ use enums::common::model_type::ModelType; use enums::tauri::tasks::task_media_file_class::TaskMediaFileClass; use enums::tauri::tasks::task_model_type::TaskModelType; use errors::AnyhowResult; +use grok_client::recipes::prompt_websocket_image_with_retry::{prompt_websocket_image_with_retry, PromptWebsocketImageWithRetryArgs}; use grok_client::requests::image_websocket::create_listen_websocket::{create_listen_websocket, CreateListenWebsocketArgs}; use grok_client::requests::image_websocket::grok_websocket::GrokWebsocket; use grok_client::requests::image_websocket::grok_wrapped_websocket::GrokWrappedWebsocket; use grok_client::requests::image_websocket::listen_for_websocket_images::{listen_for_websocket_images, ImageResults, ListenForWebsocketImagesArgs}; +use grok_client::requests::image_websocket::messages::websocket_client_message::ClientMessageAspectRatio; use grok_client::requests::image_websocket::prompt_websocket_image::{prompt_websocket_image, PromptWebsocketImageArgs}; use idempotency::uuid::generate_random_uuid; use log::{error, info, warn}; @@ -85,47 +87,33 @@ async fn inner_loop( continue; } - let maybe_cookie_header = grok_creds.maybe_copy_cookie_header_string()?; + let maybe_cookies = grok_creds.maybe_copy_typed_cookies()?; - let cookie_header = match maybe_cookie_header { - Some(cookie_header) => cookie_header, + //let maybe_cookie_header = grok_creds.maybe_copy_cookie_header_string()?; + + let cookies = match maybe_cookies { + Some(cookies) => cookies, None => { tokio::time::sleep(std::time::Duration::from_millis(10_000)).await; continue; - } + }, }; + //let cookie_header = match maybe_cookie_header { + // Some(cookie_header) => cookie_header, + // None => { + // tokio::time::sleep(std::time::Duration::from_millis(10_000)).await; + // continue; + // } + //}; + let websocket = create_listen_websocket(CreateListenWebsocketArgs { - cookies: &cookie_header, + cookies: cookies.as_str(), }).await?; let mut websocket = GrokWebsocket::new(websocket); - //poll_task_loop( - // &app_handle, - // &app_env_configs, - // &app_data_root, - // &task_database, - // &grok_creds, - // websocket, - // prompt_queue, - // &storyteller_creds_manager, - //).await?; - loop { - // let local_tasks = list_tasks_by_provider_and_status(ListTasksByProviderAndStatusArgs { - // db: task_database.get_connection(), - // provider: GenerationProvider::Grok, - // task_statuses: &TASK_DATABASE_PENDING_STATUSES, - // }).await?; - // // Only Grok images - // let local_tasks = local_tasks.tasks.into_iter() - // .filter(|task| match task.model_type { - // Some(TaskModelType::GrokImage) => true, - // _ => false, - // }) - // .collect::>(); - let maybe_prompt = prompt_queue.dequeue()?; let prompt_item = match maybe_prompt { @@ -136,13 +124,20 @@ async fn inner_loop( } }; - info!("Prompting Grok websocket: {}", prompt_item.prompt); + info!("Prompt received over in-memory queue. Prompting Grok websocket: {}", prompt_item.prompt); - let _result = prompt_websocket_image(PromptWebsocketImageArgs { + let result = prompt_websocket_image_with_retry(PromptWebsocketImageWithRetryArgs { websocket: &mut websocket, prompt: &prompt_item.prompt, + aspect_ratio: prompt_item.aspect_ratio, + cookies: &cookies, }).await?; + if let Some(new_websocket) = result.maybe_new_websocket { + info!("Replacing websocket with new one..."); + websocket = new_websocket; + } + let images = listen_for_websocket_images(ListenForWebsocketImagesArgs { websocket: &mut websocket, timeout: Duration::from_millis(30_000), @@ -162,19 +157,6 @@ async fn inner_loop( } } -//async fn poll_task_loop( -// app_handle: &AppHandle, -// app_env_configs: &AppEnvConfigs, -// app_data_root: &AppDataRoot, -// task_database: &TaskDatabase, -// grok_creds: &GrokCredentialManager, -// grok_websocket: GrokWebsocket, -// prompt_queue: &GrokImagePromptQueue, -// storyteller_creds_manager: &StorytellerCredentialManager, -//) -> AnyhowResult<()> { -// -//} - async fn upload_images_to_storyteller( app_handle: &AppHandle, app_env_configs: &AppEnvConfigs, diff --git a/crates/desktop/artcraft/src/services/storyteller/threads/events/maybe_handle_frontend_caller_notification.rs b/crates/desktop/artcraft/src/services/storyteller/threads/events/maybe_handle_frontend_caller_notification.rs new file mode 100644 index 0000000000..2484f32e1b --- /dev/null +++ b/crates/desktop/artcraft/src/services/storyteller/threads/events/maybe_handle_frontend_caller_notification.rs @@ -0,0 +1,244 @@ +use crate::core::events::basic_sendable_event_trait::BasicSendableEvent; +use crate::core::events::functional_events::image_edit_complete_event::{EditedImage, ImageEditCompleteEvent}; +use crate::core::events::functional_events::object_generation_complete_event::{GeneratedObject, ObjectGenerationCompleteEvent}; +use crate::core::events::functional_events::text_to_image_generation_complete_event::{GeneratedImage, TextToImageGenerationCompleteEvent}; +use crate::core::events::functional_events::video_generation_complete_event::{GeneratedVideo, VideoGenerationCompleteEvent}; +use crate::core::state::app_env_configs::app_env_configs::AppEnvConfigs; +use anyhow::anyhow; +use artcraft_api_defs::jobs::list_session_jobs::{ListSessionJobsItem, ListSessionResultDetailsResponse}; +use artcraft_api_defs::utils::media_links_to_thumbnail_template::media_links_to_thumbnail_template; +use enums::tauri::tasks::task_type::TaskType; +use enums::tauri::ux::tauri_command_caller::TauriCommandCaller; +use errors::AnyhowResult; +use log::{error, warn}; +use sqlite_tasks::queries::list_tasks_by_provider_and_tokens::{list_tasks_by_provider_and_tokens, ListTasksArgs}; +use sqlite_tasks::queries::task::Task; +use storyteller_client::credentials::storyteller_credential_set::StorytellerCredentialSet; +use storyteller_client::endpoints::media_files::list_batch_generated_redux_media_files::list_batch_generated_redux_media_files; +use tauri::AppHandle; +use tokens::tokens::batch_generations::BatchGenerationToken; +use tokens::tokens::media_files::MediaFileToken; +use crate::core::events::functional_events::canvas_background_removal_complete_event::CanvasBackgroundRemovalCompleteEvent; + +pub async fn maybe_handle_frontend_caller_notification( + app: &AppHandle, + app_env_configs: &AppEnvConfigs, + maybe_creds: Option<&StorytellerCredentialSet>, + task: &Task, + job: &ListSessionJobsItem, +) -> AnyhowResult<()> { + + let job_result = match job.maybe_result { + Some(ref res) => res, + None => { + warn!("Job result is None for task: {:?}", task); + return Ok(()); // No result, nothing to do + }, + }; + + match task.task_type { + TaskType::ImageGeneration => { + let _r = handle_image_generation( + app, + task, + job_result, + app_env_configs, + maybe_creds, + ).await?; + } + TaskType::ImageInpaintEdit => { + let _r = handle_inpaint_image_generation( + app, + task, + job_result, + app_env_configs, + maybe_creds, + ).await?; + } + TaskType::VideoGeneration => { + let _r = handle_video_generation( + app, + task, + job_result, + ).await?; + } + TaskType::ObjectGeneration => { + let _r = handle_object_generation( + app, + task, + job_result, + ).await?; + } + TaskType::BackgroundRemoval => { + let _r = handle_background_removal_generation( + app, + task, + job_result, + ).await?; + } + } + + Ok(()) +} + +async fn handle_image_generation( + app: &AppHandle, + task: &Task, + job_result: &ListSessionResultDetailsResponse, + app_env_configs: &AppEnvConfigs, + maybe_creds: Option<&StorytellerCredentialSet>, +) -> AnyhowResult<()> { + + let generated_images = match job_result.maybe_batch_token.as_ref() { + None => { + vec![GeneratedImage { + media_token: MediaFileToken::new_from_str(&job_result.entity_token), + cdn_url: job_result.media_links.cdn_url.clone(), + maybe_thumbnail_template: job_result.media_links.maybe_thumbnail_template.clone(), + }] + } + Some(batch_token) => { + let result = list_batch_generated_redux_media_files( + &app_env_configs.storyteller_host, + maybe_creds, + batch_token, + ).await?; + + if result.media_files.is_empty() { + return Err(anyhow!("No media files found for batch token: {}", batch_token)); + } + + result.media_files + .into_iter() + .map(|file| GeneratedImage { + media_token: file.token, + cdn_url: file.media_links.cdn_url, + maybe_thumbnail_template: file.media_links.maybe_thumbnail_template, + }) + .collect() + } + }; + + let event = TextToImageGenerationCompleteEvent { + generated_images, + maybe_frontend_subscriber_id: task.frontend_subscriber_id.clone(), + maybe_frontend_subscriber_payload: task.frontend_subscriber_payload.clone(), + }; + + event.send_infallible(&app); + + Ok(()) +} + +async fn handle_inpaint_image_generation( + app: &AppHandle, + task: &Task, + job_result: &ListSessionResultDetailsResponse, + app_env_configs: &AppEnvConfigs, + maybe_creds: Option<&StorytellerCredentialSet>, +) -> AnyhowResult<()> { + + let edited_images = match job_result.maybe_batch_token.as_ref() { + None => { + vec![EditedImage { + media_token: MediaFileToken::new_from_str(&job_result.entity_token), + cdn_url: job_result.media_links.cdn_url.clone(), + maybe_thumbnail_template: job_result.media_links.maybe_thumbnail_template.clone(), + }] + } + Some(batch_token) => { + let result = list_batch_generated_redux_media_files( + &app_env_configs.storyteller_host, + maybe_creds, + batch_token, + ).await?; + + if result.media_files.is_empty() { + return Err(anyhow!("No media files found for batch token: {}", batch_token)); + } + + result.media_files + .into_iter() + .map(|file| EditedImage { + media_token: file.token, + cdn_url: file.media_links.cdn_url, + maybe_thumbnail_template: file.media_links.maybe_thumbnail_template, + }) + .collect() + } + }; + + let event = ImageEditCompleteEvent { + edited_images, + maybe_frontend_subscriber_id: task.frontend_subscriber_id.clone(), + maybe_frontend_subscriber_payload: task.frontend_subscriber_payload.clone(), + }; + + event.send_infallible(&app); + + Ok(()) +} + +async fn handle_video_generation( + app: &AppHandle, + task: &Task, + job_result: &ListSessionResultDetailsResponse, +) -> AnyhowResult<()> { + + // NB: For now, we only generate one video at a time. + let event = VideoGenerationCompleteEvent { + generated_video: Some(GeneratedVideo { + media_token: MediaFileToken::new_from_str(&job_result.entity_token), + cdn_url: job_result.media_links.cdn_url.clone(), + maybe_thumbnail_template: media_links_to_thumbnail_template(&job_result.media_links) + .map(|s| s.to_owned()), + }), + maybe_frontend_subscriber_id: task.frontend_subscriber_id.clone(), + maybe_frontend_subscriber_payload: task.frontend_subscriber_payload.clone(), + }; + + event.send_infallible(&app); + + Ok(()) +} + +async fn handle_object_generation( + app: &AppHandle, + task: &Task, + job_result: &ListSessionResultDetailsResponse, +) -> AnyhowResult<()> { + + // NB: For now, we only generate one object (3d mesh) at a time. + let event = ObjectGenerationCompleteEvent { + generated_object: Some(GeneratedObject { + media_token: MediaFileToken::new_from_str(&job_result.entity_token), + cdn_url: job_result.media_links.cdn_url.clone(), + maybe_thumbnail_template: media_links_to_thumbnail_template(&job_result.media_links) + .map(|s| s.to_owned()), + }), + maybe_frontend_subscriber_id: task.frontend_subscriber_id.clone(), + maybe_frontend_subscriber_payload: task.frontend_subscriber_payload.clone(), + }; + + event.send_infallible(&app); + + Ok(()) +} + +async fn handle_background_removal_generation( + app: &AppHandle, + task: &Task, + job_result: &ListSessionResultDetailsResponse, +) -> AnyhowResult<()> { + + let event = CanvasBackgroundRemovalCompleteEvent { + media_token: MediaFileToken::new_from_str(&job_result.entity_token), + image_cdn_url: job_result.media_links.cdn_url.clone(), + maybe_frontend_subscriber_id: task.frontend_subscriber_id.clone(), + maybe_frontend_subscriber_payload: task.frontend_subscriber_payload.clone(), + }; + + event.send_infallible(&app); + + Ok(()) +} diff --git a/crates/desktop/artcraft/src/services/storyteller/threads/events/mod.rs b/crates/desktop/artcraft/src/services/storyteller/threads/events/mod.rs index fa576b10ce..c9ae78ea32 100644 --- a/crates/desktop/artcraft/src/services/storyteller/threads/events/mod.rs +++ b/crates/desktop/artcraft/src/services/storyteller/threads/events/mod.rs @@ -1,3 +1,4 @@ +pub (super) mod maybe_handle_frontend_caller_notification; pub (super) mod maybe_handle_inpainting_complete_event; pub (super) mod maybe_handle_text_to_image_complete_event; pub (super) mod maybe_send_background_removal_complete_event; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/services/storyteller/threads/storyteller_task_polling_thread.rs b/crates/desktop/artcraft/src/services/storyteller/threads/storyteller_task_polling_thread.rs index 3f9c8f7f63..7c67fb8a2c 100644 --- a/crates/desktop/artcraft/src/services/storyteller/threads/storyteller_task_polling_thread.rs +++ b/crates/desktop/artcraft/src/services/storyteller/threads/storyteller_task_polling_thread.rs @@ -12,6 +12,7 @@ use crate::core::utils::enum_conversion::task_type::to_generation_action; use crate::services::sora::state::sora_credential_manager::SoraCredentialManager; use crate::services::sora::state::sora_task_queue::SoraTaskQueue; use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager; +use crate::services::storyteller::threads::events::maybe_handle_frontend_caller_notification::maybe_handle_frontend_caller_notification; use crate::services::storyteller::threads::events::maybe_handle_inpainting_complete_event::maybe_handle_inpainting_complete_event; use crate::services::storyteller::threads::events::maybe_handle_text_to_image_complete_event::maybe_handle_text_to_image_complete_event; use crate::services::storyteller::threads::events::maybe_send_background_removal_complete_event::maybe_send_background_removal_complete_event; @@ -197,7 +198,7 @@ async fn send_additional_events( ) { info!("Attempting to dispatch events for completed Storyteller : {:?}", task); - let result = maybe_handle_text_to_image_complete_event( + let result = maybe_handle_frontend_caller_notification( app_handle, app_env_configs, creds, @@ -206,30 +207,42 @@ async fn send_additional_events( ).await; if let Err(err) = result { - error!("Failed to send text-to-image complete event: {:?}", err); + error!("Failed to send generation complete event: {:?}", err); } - let result = maybe_handle_inpainting_complete_event( - app_handle, - app_env_configs, - creds, - task, - job, - ).await; + //let result = maybe_handle_text_to_image_complete_event( + // app_handle, + // app_env_configs, + // creds, + // task, + // job, + //).await; - if let Err(err) = result { - error!("Failed to send image inpainting complete event: {:?}", err); - } + //if let Err(err) = result { + // error!("Failed to send text-to-image complete event: {:?}", err); + //} - let result = maybe_send_background_removal_complete_event( - app_handle, - task, - job, - ).await; + //let result = maybe_handle_inpainting_complete_event( + // app_handle, + // app_env_configs, + // creds, + // task, + // job, + //).await; - if let Err(err) = result { - error!("Failed to send background removal complete event: {:?}", err); - } + //if let Err(err) = result { + // error!("Failed to send image inpainting complete event: {:?}", err); + //} + + //let result = maybe_send_background_removal_complete_event( + // app_handle, + // task, + // job, + //).await; + + //if let Err(err) = result { + // error!("Failed to send background removal complete event: {:?}", err); + //} } fn get_thumbnail_template<'a>(job: &'a ListSessionJobsItem) -> Option<&'a str> { diff --git a/crates/lib/email_sender/Cargo.toml b/crates/lib/email_sender/Cargo.toml index 27e6c44c33..7365e9a3ff 100644 --- a/crates/lib/email_sender/Cargo.toml +++ b/crates/lib/email_sender/Cargo.toml @@ -20,5 +20,5 @@ errors = { path = "../errors" } workspace-build-acceleration = { version = "0.1", path = "../../workspace-build-acceleration" } # External -lettre = "0.11.2" +lettre = { version = "0.11.2", default-features = false, features = ["smtp-transport", "pool", "hostname", "builder", "rustls-tls"] } diff --git a/crates/lib/migration/Cargo.toml b/crates/lib/migration/Cargo.toml index c7e4cc4604..1ef9fdc46e 100644 --- a/crates/lib/migration/Cargo.toml +++ b/crates/lib/migration/Cargo.toml @@ -42,7 +42,7 @@ toml.workspace = true # Runtime + Clients tokio = { version = "1.38.0", features = ["full"] } -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] } +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] } [dev-dependencies] # None yet diff --git a/crates/lib/sqlx_mysql_helpers/Cargo.toml b/crates/lib/sqlx_mysql_helpers/Cargo.toml index 47efd613e8..41523916d4 100644 --- a/crates/lib/sqlx_mysql_helpers/Cargo.toml +++ b/crates/lib/sqlx_mysql_helpers/Cargo.toml @@ -23,7 +23,7 @@ errors = { path = "../errors" } workspace-build-acceleration = { version = "0.1", path = "../../workspace-build-acceleration" } # External -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] } +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] } [dev-dependencies] # None yet diff --git a/crates/lib/test_data/src/web/image_urls.rs b/crates/lib/test_data/src/web/image_urls.rs index d4eb40880a..074f3d8f60 100644 --- a/crates/lib/test_data/src/web/image_urls.rs +++ b/crates/lib/test_data/src/web/image_urls.rs @@ -35,3 +35,12 @@ pub const TALL_MOCHI_WITH_GLASSES_GLASSES_MASK_IMAGE_URL : &str = "https://cdn-2 // media token: m_77cpdyaby60z0qv9g6tv67gw2a1337 pub const TREX_SKELETON_IMAGE_URL : &str = "https://cdn-2.fakeyou.com/media/f/4/j/5/3/f4j53296sc35wqs3csgw4j4vvjf5yyxe/image_f4j53296sc35wqs3csgw4j4vvjf5yyxe.png"; + +// media token: m_zyh17gn6prfp4xs6z76mqkz9k0sr0j +pub const TALL_CORGI_SHIBA_TREASURE_OCEAN_URL: &str = "https://cdn-2.fakeyou.com/media/j/z/7/2/p/jz72pgwb6fegem74pr36yy88hsshwx4m/artcraft_jz72pgwb6fegem74pr36yy88hsshwx4m.png"; + +// media token: m_a9c4g43nw87y6mebjammrqz849c5xe +pub const WIDE_CORGI_SHIBA_TREASURE_OCEAN_URL: &str = "https://cdn-2.fakeyou.com/media/v/a/g/g/a/vaggan4137tqsz173432cjjg33xkrxpc/artcraft_vaggan4137tqsz173432cjjg33xkrxpc.png"; + +// media token: m_ytkp3tk2ptvr03p75fwqvend0m3qsg +pub const TALL_CORGI_SHIBA_TREASURE_SKY_URL: &str = "https://cdn-2.fakeyou.com/media/a/4/s/x/x/a4sxxecwenrbta5kx5jk34dp2z9apxqv/artcraft_a4sxxecwenrbta5kx5jk34dp2z9apxqv.png"; diff --git a/crates/lib/web_scrapers/Cargo.toml b/crates/lib/web_scrapers/Cargo.toml index d3c02481c6..7e593e1106 100644 --- a/crates/lib/web_scrapers/Cargo.toml +++ b/crates/lib/web_scrapers/Cargo.toml @@ -36,7 +36,7 @@ serde_derive = "1.0.125" serde_json = "1.0.108" # HTTP -reqwest = { version = "0.12", features = ["json"] } # HTTP Client +reqwest = { version = "0.12", default-features = false, features = ["charset", "http2", "system-proxy", "rustls-tls", "json"] } # HTTP Client # Scraping rss = "2.0.2" diff --git a/crates/schema/database/elasticsearch_schema/Cargo.toml b/crates/schema/database/elasticsearch_schema/Cargo.toml index 8999e0f664..579b092880 100644 --- a/crates/schema/database/elasticsearch_schema/Cargo.toml +++ b/crates/schema/database/elasticsearch_schema/Cargo.toml @@ -25,7 +25,7 @@ errors = { workspace = true } tokens = { workspace = true } # External -elasticsearch = "8.5.0-alpha.1" +elasticsearch = { version = "8.5.0-alpha.1", default-features = false, features = ["rustls-tls"] } jsonpath_lib = "0.3.0" log = "0.4.14" once_cell = "1.19.0" diff --git a/crates/schema/database/mysql_queries/Cargo.toml b/crates/schema/database/mysql_queries/Cargo.toml index afc91167a5..7c747fb33d 100644 --- a/crates/schema/database/mysql_queries/Cargo.toml +++ b/crates/schema/database/mysql_queries/Cargo.toml @@ -47,7 +47,7 @@ num-traits = "0.2.19" tokio = { version = "1.38.0", features = ["full"] } rand.workspace = true -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono", "bigdecimal" ] } +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono", "bigdecimal" ] } [dev-dependencies] serial_test = "3.0.0" diff --git a/crates/schema/database/sqlite_queries/Cargo.toml b/crates/schema/database/sqlite_queries/Cargo.toml index 281f916a47..f10e5902e8 100644 --- a/crates/schema/database/sqlite_queries/Cargo.toml +++ b/crates/schema/database/sqlite_queries/Cargo.toml @@ -1,3 +1,5 @@ +# NOTE: THIS IS NOT THE ACTIVE `sqlite` package! + [package] name = "sqlite_queries" edition = "2018" @@ -28,7 +30,7 @@ log = "0.4.14" once_cell = "1.9.0" regex = "1.5.4" -sqlx = { version = "0.7.4", features = [ "sqlite", "runtime-tokio-native-tls", "chrono" ] } +sqlx = { version = "0.7.4", features = [ "sqlite", "runtime-tokio-rustls", "chrono" ] } [dev-dependencies] # None yet diff --git a/crates/schema/database/sqlite_tasks/Cargo.toml b/crates/schema/database/sqlite_tasks/Cargo.toml index cb0fa81c2c..7937087d01 100644 --- a/crates/schema/database/sqlite_tasks/Cargo.toml +++ b/crates/schema/database/sqlite_tasks/Cargo.toml @@ -27,7 +27,7 @@ log.workspace = true # TODO(bt,2025-07-09): Update sqlx ASAP. We're stuck on 0.7 because of the complex use of internal types and macros in the database crate. # We're going to embed in-memory SQLite -sqlx = { package = "sqlx", version = "0.7.4", features = ["runtime-tokio", "sqlite", "chrono"] } +sqlx = { package = "sqlx", version = "0.7.4", features = ["runtime-tokio-rustls", "sqlite", "chrono"] } # Needed to safety check some queries we can't use escaping for once_cell.workspace = true @@ -43,5 +43,5 @@ serde_json.workspace = true [build-dependencies] tempfile.workspace = true -sqlx = { package = "sqlx", version = "0.7.4", features = ["runtime-tokio", "sqlite", "chrono"] } +sqlx = { package = "sqlx", version = "0.7.4", features = ["runtime-tokio-rustls", "sqlite", "chrono"] } #rusqlite = "0.25.4" # NB: For the build.rs file diff --git a/crates/schema/public/composite_identifiers/Cargo.toml b/crates/schema/public/composite_identifiers/Cargo.toml index 928b33d346..d7f0a4a792 100644 --- a/crates/schema/public/composite_identifiers/Cargo.toml +++ b/crates/schema/public/composite_identifiers/Cargo.toml @@ -28,7 +28,7 @@ tokens = { workspace = true } serde = "1.0.125" serde_derive = "1.0.125" -sqlx = { version = "0.7.4", features = [ "mysql", "sqlite", "runtime-tokio-native-tls", "chrono" ] } +sqlx = { version = "0.7.4", features = [ "mysql", "sqlite", "runtime-tokio-rustls", "chrono" ] } sqlx-core = { version = "0.7.4" } sqlx-mysql= { version = "0.7.4" } diff --git a/crates/schema/public/enums/Cargo.toml b/crates/schema/public/enums/Cargo.toml index 26d92963c0..8a6b03ce32 100644 --- a/crates/schema/public/enums/Cargo.toml +++ b/crates/schema/public/enums/Cargo.toml @@ -31,7 +31,7 @@ serde.workspace = true serde_derive.workspace = true # Database conversion of enum types is optional -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-native-tls", "chrono" ], optional = true } +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ], optional = true } sqlx-core = { version = "0.7.4" , optional = true } sqlx-mysql = { version = "0.7.4" , optional = true } diff --git a/crates/schema/public/enums/src/tauri/ux/tauri_command_caller.rs b/crates/schema/public/enums/src/tauri/ux/tauri_command_caller.rs index f745f91280..82b8d07993 100644 --- a/crates/schema/public/enums/src/tauri/ux/tauri_command_caller.rs +++ b/crates/schema/public/enums/src/tauri/ux/tauri_command_caller.rs @@ -21,6 +21,8 @@ pub enum TauriCommandCaller { TextToImage, /// The image-to-video page ImageToVideo, + /// A mini-app (doesn't specify which one) + MiniApp, } impl_enum_display_and_debug_using_to_str!(TauriCommandCaller); @@ -36,6 +38,7 @@ impl TauriCommandCaller { Self::ImageEditor => "image_editor", Self::TextToImage => "text_to_image", Self::ImageToVideo => "image_to_video", + Self::MiniApp => "mini_app", } } @@ -45,6 +48,7 @@ impl TauriCommandCaller { "image_editor" => Ok(Self::ImageEditor), "text_to_image" => Ok(Self::TextToImage), "image_to_video" => Ok(Self::ImageToVideo), + "mini_app" => Ok(Self::MiniApp), _ => Err(EnumError::CouldNotConvertFromString(value.to_string())), } } @@ -57,6 +61,7 @@ impl TauriCommandCaller { Self::ImageEditor, Self::TextToImage, Self::ImageToVideo, + Self::MiniApp, ]) } } @@ -76,6 +81,7 @@ mod tests { assert_serialization(TauriCommandCaller::ImageEditor, "image_editor"); assert_serialization(TauriCommandCaller::TextToImage, "text_to_image"); assert_serialization(TauriCommandCaller::ImageToVideo, "image_to_video"); + assert_serialization(TauriCommandCaller::MiniApp, "mini_app"); } #[test] @@ -84,6 +90,7 @@ mod tests { assert_eq!(TauriCommandCaller::ImageEditor.to_str(), "image_editor"); assert_eq!(TauriCommandCaller::TextToImage.to_str(), "text_to_image"); assert_eq!(TauriCommandCaller::ImageToVideo.to_str(), "image_to_video"); + assert_eq!(TauriCommandCaller::MiniApp.to_str(), "mini_app"); } #[test] @@ -92,6 +99,7 @@ mod tests { assert_eq!(TauriCommandCaller::from_str("image_editor").unwrap(), TauriCommandCaller::ImageEditor); assert_eq!(TauriCommandCaller::from_str("text_to_image").unwrap(), TauriCommandCaller::TextToImage); assert_eq!(TauriCommandCaller::from_str("image_to_video").unwrap(), TauriCommandCaller::ImageToVideo); + assert_eq!(TauriCommandCaller::from_str("mini_app").unwrap(), TauriCommandCaller::MiniApp); } #[test] @@ -108,11 +116,12 @@ mod tests { #[test] fn all_variants() { let mut variants = TauriCommandCaller::all_variants(); - assert_eq!(variants.len(), 4); + assert_eq!(variants.len(), 5); assert_eq!(variants.pop_first(), Some(TauriCommandCaller::Canvas)); assert_eq!(variants.pop_first(), Some(TauriCommandCaller::ImageEditor)); assert_eq!(variants.pop_first(), Some(TauriCommandCaller::TextToImage)); assert_eq!(variants.pop_first(), Some(TauriCommandCaller::ImageToVideo)); + assert_eq!(variants.pop_first(), Some(TauriCommandCaller::MiniApp)); assert_eq!(variants.pop_first(), None); } } diff --git a/crates/schema/public/enums/src/tauri/ux/tauri_event_name.rs b/crates/schema/public/enums/src/tauri/ux/tauri_event_name.rs index fd6b8bf44b..3c61656bfb 100644 --- a/crates/schema/public/enums/src/tauri/ux/tauri_event_name.rs +++ b/crates/schema/public/enums/src/tauri/ux/tauri_event_name.rs @@ -54,7 +54,7 @@ pub enum TauriEventName { CanvasBgRemovedEvent, /// Special event: - /// Image edit is complete + /// Image generation is complete #[serde(rename = "text_to_image_generation_complete_event")] TextToImageGenerationCompleteEvent, @@ -62,6 +62,16 @@ pub enum TauriEventName { /// Image edit is complete #[serde(rename = "image_edit_complete_event")] ImageEditCompleteEvent, + + /// Special event: + /// Object (3D mesh) generation is complete + #[serde(rename = "object_generation_complete_event")] + ObjectGenerationCompleteEvent, + + /// Special event: + /// Video generation is complete + #[serde(rename = "video_generation_complete_event")] + VideoGenerationCompleteEvent, /// Special event: /// Refresh account states @@ -102,6 +112,8 @@ impl TauriEventName { Self::CanvasBgRemovedEvent => "canvas_bg_removed_event", Self::TextToImageGenerationCompleteEvent => "text_to_image_generation_complete_event", Self::ImageEditCompleteEvent => "image_edit_complete_event", + Self::ObjectGenerationCompleteEvent => "object_generation_complete_event", + Self::VideoGenerationCompleteEvent => "video_generation_complete_event", Self::RefreshAccountStateEvent => "refresh_account_state_event", Self::ShowProviderBillingModalEvent => "show_provider_billing_modal_event", Self::ShowProviderLoginModalEvent => "show_provider_login_modal_event", @@ -120,6 +132,8 @@ impl TauriEventName { "canvas_bg_removed_event" => Ok(Self::CanvasBgRemovedEvent), "text_to_image_generation_complete_event" => Ok(Self::TextToImageGenerationCompleteEvent), "image_edit_complete_event" => Ok(Self::ImageEditCompleteEvent), + "object_generation_complete_event" => Ok(Self::ObjectGenerationCompleteEvent), + "video_generation_complete_event" => Ok(Self::VideoGenerationCompleteEvent), "refresh_account_state_event" => Ok(Self::RefreshAccountStateEvent), "show_provider_billing_modal_event" => Ok(Self::ShowProviderBillingModalEvent), "show_provider_login_modal_event" => Ok(Self::ShowProviderLoginModalEvent), @@ -141,6 +155,8 @@ impl TauriEventName { Self::CanvasBgRemovedEvent, Self::TextToImageGenerationCompleteEvent, Self::ImageEditCompleteEvent, + Self::ObjectGenerationCompleteEvent, + Self::VideoGenerationCompleteEvent, Self::RefreshAccountStateEvent, Self::ShowProviderBillingModalEvent, Self::ShowProviderLoginModalEvent, @@ -168,6 +184,8 @@ mod tests { assert_serialization(TauriEventName::CanvasBgRemovedEvent, "canvas_bg_removed_event"); assert_serialization(TauriEventName::TextToImageGenerationCompleteEvent, "text_to_image_generation_complete_event"); assert_serialization(TauriEventName::ImageEditCompleteEvent, "image_edit_complete_event"); + assert_serialization(TauriEventName::ObjectGenerationCompleteEvent, "object_generation_complete_event"); + assert_serialization(TauriEventName::VideoGenerationCompleteEvent, "video_generation_complete_event"); assert_serialization(TauriEventName::RefreshAccountStateEvent, "refresh_account_state_event"); assert_serialization(TauriEventName::ShowProviderBillingModalEvent, "show_provider_billing_modal_event"); assert_serialization(TauriEventName::ShowProviderLoginModalEvent, "show_provider_login_modal_event"); @@ -185,6 +203,8 @@ mod tests { assert_eq!(TauriEventName::CanvasBgRemovedEvent.to_str(), "canvas_bg_removed_event"); assert_eq!(TauriEventName::TextToImageGenerationCompleteEvent.to_str(), "text_to_image_generation_complete_event"); assert_eq!(TauriEventName::ImageEditCompleteEvent.to_str(), "image_edit_complete_event"); + assert_eq!(TauriEventName::ObjectGenerationCompleteEvent.to_str(), "object_generation_complete_event"); + assert_eq!(TauriEventName::VideoGenerationCompleteEvent.to_str(), "video_generation_complete_event"); assert_eq!(TauriEventName::RefreshAccountStateEvent.to_str(), "refresh_account_state_event"); assert_eq!(TauriEventName::ShowProviderBillingModalEvent.to_str(), "show_provider_billing_modal_event"); assert_eq!(TauriEventName::ShowProviderLoginModalEvent.to_str(), "show_provider_login_modal_event"); @@ -202,6 +222,8 @@ mod tests { assert_eq!(TauriEventName::from_str("canvas_bg_removed_event").unwrap(), TauriEventName::CanvasBgRemovedEvent); assert_eq!(TauriEventName::from_str("text_to_image_generation_complete_event").unwrap(), TauriEventName::TextToImageGenerationCompleteEvent); assert_eq!(TauriEventName::from_str("image_edit_complete_event").unwrap(), TauriEventName::ImageEditCompleteEvent); + assert_eq!(TauriEventName::from_str("object_generation_complete_event").unwrap(), TauriEventName::ObjectGenerationCompleteEvent); + assert_eq!(TauriEventName::from_str("video_generation_complete_event").unwrap(), TauriEventName::VideoGenerationCompleteEvent); assert_eq!(TauriEventName::from_str("refresh_account_state_event").unwrap(), TauriEventName::RefreshAccountStateEvent); assert_eq!(TauriEventName::from_str("show_provider_billing_modal_event").unwrap(), TauriEventName::ShowProviderBillingModalEvent); assert_eq!(TauriEventName::from_str("show_provider_login_modal_event").unwrap(), TauriEventName::ShowProviderLoginModalEvent); @@ -211,7 +233,7 @@ mod tests { #[test] fn all_variants() { let mut variants = TauriEventName::all_variants(); - assert_eq!(variants.len(), 13); + assert_eq!(variants.len(), 15); assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationEnqueueSuccessEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationEnqueueFailureEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::GenerationCompleteEvent)); @@ -221,6 +243,8 @@ mod tests { assert_eq!(variants.pop_first(), Some(TauriEventName::CanvasBgRemovedEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::TextToImageGenerationCompleteEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::ImageEditCompleteEvent)); + assert_eq!(variants.pop_first(), Some(TauriEventName::ObjectGenerationCompleteEvent)); + assert_eq!(variants.pop_first(), Some(TauriEventName::VideoGenerationCompleteEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::RefreshAccountStateEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::ShowProviderBillingModalEvent)); assert_eq!(variants.pop_first(), Some(TauriEventName::ShowProviderLoginModalEvent)); diff --git a/crates/schema/public/reusable_types/Cargo.toml b/crates/schema/public/reusable_types/Cargo.toml index 8cbb361db6..b51a3c7073 100644 --- a/crates/schema/public/reusable_types/Cargo.toml +++ b/crates/schema/public/reusable_types/Cargo.toml @@ -30,7 +30,8 @@ serde = { workspace = true } serde_derive = { workspace = true } # MySql Database -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] } +sqlx = { version = "0.7.4", default-features = false, features = [ "mysql", "runtime-tokio-rustls", "chrono" ] } +sqlx-core = { version = "0.7.4", features = ["rustls"] } [dev-dependencies] # Test serialization diff --git a/crates/schema/public/tokens/Cargo.toml b/crates/schema/public/tokens/Cargo.toml index 5fb5b2ff20..acc0a95215 100644 --- a/crates/schema/public/tokens/Cargo.toml +++ b/crates/schema/public/tokens/Cargo.toml @@ -36,7 +36,7 @@ rand = { version = "0.8.5", features = ["std_rng"] } once_cell.workspace = true # Database conversion of enum types is optional -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ], optional = true } +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ], optional = true } sqlx-core = { version = "0.7.4", optional = true } sqlx-mysql= { version = "0.7.4", optional = true } diff --git a/crates/service/job/analytics_job/Cargo.toml b/crates/service/job/analytics_job/Cargo.toml index 0f34ef9361..b297a3ab91 100644 --- a/crates/service/job/analytics_job/Cargo.toml +++ b/crates/service/job/analytics_job/Cargo.toml @@ -38,7 +38,7 @@ hostname = "0.4.0" log = "0.4.14" rand = "0.8.5" -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] } +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] } # Runtime # NB: We switched to Tokio from `async-std` for broader ecosystem support. diff --git a/crates/service/job/download_job/Cargo.toml b/crates/service/job/download_job/Cargo.toml index b5a7e56842..ea28b53308 100644 --- a/crates/service/job/download_job/Cargo.toml +++ b/crates/service/job/download_job/Cargo.toml @@ -50,7 +50,7 @@ subprocess = "0.2.9" tempdir.workspace = true zip = "0.6.6" -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] } +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] } # Redis r2d2_redis = "0.14.0" diff --git a/crates/service/job/email_sender_job/Cargo.toml b/crates/service/job/email_sender_job/Cargo.toml index 000f05abb8..de13a90f33 100644 --- a/crates/service/job/email_sender_job/Cargo.toml +++ b/crates/service/job/email_sender_job/Cargo.toml @@ -37,7 +37,7 @@ server_environment = { path = "../../../lib/server_environment" } anyhow = { workspace = true } log = { workspace = true } -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] } +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] } # Redis r2d2_redis = "0.14.0" diff --git a/crates/service/job/es-update-job/Cargo.toml b/crates/service/job/es-update-job/Cargo.toml index 2249954685..188ba7c11c 100644 --- a/crates/service/job/es-update-job/Cargo.toml +++ b/crates/service/job/es-update-job/Cargo.toml @@ -43,9 +43,9 @@ log = "0.4.14" once_cell = "1.18.0" rand = "0.8.5" -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] } +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] } -elasticsearch = "8.5.0-alpha.1" +elasticsearch = { version = "8.5.0-alpha.1", default-features = false, features = ["rustls-tls"] } # Runtime # NB: We switched to Tokio from `async-std` for broader ecosystem support. diff --git a/crates/service/job/inference_job/Cargo.toml b/crates/service/job/inference_job/Cargo.toml index e9c6bc0d80..111d533d9c 100644 --- a/crates/service/job/inference_job/Cargo.toml +++ b/crates/service/job/inference_job/Cargo.toml @@ -62,7 +62,7 @@ once_cell.workspace = true subprocess = "0.2.9" tempdir.workspace = true -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] } +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] } # Redis r2d2_redis = "0.14.0" @@ -74,7 +74,7 @@ tokio-util = "0.7.12" tokio-stream = "0.1.16" # HTTP Client -reqwest = { version = "0.12.4", features = ["blocking"] } +reqwest = { version = "0.12.4", default-features = false, features = ["charset", "http2", "system-proxy", "blocking", "rustls-tls"] } # HTTP Server actix-web.workspace = true diff --git a/crates/service/job/tts_download_job/Cargo.toml b/crates/service/job/tts_download_job/Cargo.toml index 40addd43dc..15ea96ced9 100644 --- a/crates/service/job/tts_download_job/Cargo.toml +++ b/crates/service/job/tts_download_job/Cargo.toml @@ -42,7 +42,7 @@ log.workspace = true subprocess = "0.2.9" tempdir.workspace = true -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] } +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] } # Redis r2d2_redis = "0.14.0" diff --git a/crates/service/job/tts_inference_job/Cargo.toml b/crates/service/job/tts_inference_job/Cargo.toml index 5cec6c1050..80033e1a56 100644 --- a/crates/service/job/tts_inference_job/Cargo.toml +++ b/crates/service/job/tts_inference_job/Cargo.toml @@ -42,7 +42,7 @@ lfu = "0.2.5" log = "0.4.14" tempdir.workspace = true -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] } +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] } # Redis r2d2_redis = "0.14.0" @@ -51,7 +51,7 @@ r2d2_redis = "0.14.0" tokio.workspace = true # HTTP -reqwest = { version = "0.12.4", features = ["blocking"] } +reqwest = { version = "0.12.4", default-features = false, features = ["charset", "http2", "system-proxy", "blocking", "rustls-tls"] } # Serialization serde.workspace = true diff --git a/crates/service/job/w2l_download_job/Cargo.toml b/crates/service/job/w2l_download_job/Cargo.toml index 926a657ca4..0d867a1d85 100644 --- a/crates/service/job/w2l_download_job/Cargo.toml +++ b/crates/service/job/w2l_download_job/Cargo.toml @@ -40,7 +40,7 @@ log.workspace = true subprocess = "0.2.9" tempdir.workspace = true -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] } +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] } # Redis r2d2_redis = "0.14.0" diff --git a/crates/service/job/w2l_inference_job/Cargo.toml b/crates/service/job/w2l_inference_job/Cargo.toml index b952621d5c..f53e98fa62 100644 --- a/crates/service/job/w2l_inference_job/Cargo.toml +++ b/crates/service/job/w2l_inference_job/Cargo.toml @@ -40,7 +40,7 @@ log.workspace = true subprocess = "0.2.9" tempdir.workspace = true -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] } +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] } # Redis r2d2_redis = "0.14.0" diff --git a/crates/service/plugins/billing/Cargo.toml b/crates/service/plugins/billing/Cargo.toml index 709f946e28..8551aff99c 100644 --- a/crates/service/plugins/billing/Cargo.toml +++ b/crates/service/plugins/billing/Cargo.toml @@ -45,7 +45,7 @@ serde_derive = { workspace = true } serde_json = { workspace = true } # Money -async-stripe = { version = "0.26.0", default-features = false, features = ["runtime-tokio-hyper", "connect", "billing", "checkout", "webhook-events"] } +async-stripe = { version = "0.26.0", default-features = false, features = ["runtime-tokio-hyper-rustls", "connect", "billing", "checkout", "webhook-events"] } # Async traits # NB: note: `async` trait functions are not currently supported @@ -53,7 +53,7 @@ async-stripe = { version = "0.26.0", default-features = false, features = ["runt # include support for async fn in traits. async-trait = "0.1.58" -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] } +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] } utoipa = { version = "4", features = ["chrono","actix_extras"] } diff --git a/crates/service/plugins/billing_artcraft/Cargo.toml b/crates/service/plugins/billing_artcraft/Cargo.toml index b1e56ad52a..60b67b8bc5 100644 --- a/crates/service/plugins/billing_artcraft/Cargo.toml +++ b/crates/service/plugins/billing_artcraft/Cargo.toml @@ -41,10 +41,10 @@ once_cell.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] } +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] } # Money -async-stripe = { version = "1.0.0-alpha.2", features = [] } +async-stripe = { version = "1.0.0-alpha.2", default-features = false, features = ["rustls-tls-native"] } async-stripe-checkout = { version = "1.0.0-alpha.2", features = ["checkout_session"] } async-stripe-core = { version = "1.0.0-alpha.2", features = ["customer"] } async-stripe-shared= { version = "1.0.0-alpha.2", features = [] } diff --git a/crates/service/plugins/component_traits/Cargo.toml b/crates/service/plugins/component_traits/Cargo.toml index 411f57fd3b..429224f2b0 100644 --- a/crates/service/plugins/component_traits/Cargo.toml +++ b/crates/service/plugins/component_traits/Cargo.toml @@ -47,7 +47,7 @@ serde_json.workspace = true # include support for async fn in traits. async-trait = "0.1.58" -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] } +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] } #sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] } #utoipa = { version = "4", features = ["chrono","actix_extras"] } diff --git a/crates/service/web/storyteller_web/Cargo.toml b/crates/service/web/storyteller_web/Cargo.toml index dcb91b6613..1c054fb944 100644 --- a/crates/service/web/storyteller_web/Cargo.toml +++ b/crates/service/web/storyteller_web/Cargo.toml @@ -101,13 +101,8 @@ strum_macros = "0.26.4" time = "0.3.30" url.workspace = true -# Stripe for ArtCraft -async_stripe_artcraft = { package = "async-stripe", version = "0.41.0", default-features = false, features = ["runtime-tokio-hyper", "connect", "billing", "checkout", "webhook-events"] } -#async_stripe_artcraft = { git = "https://github.com/arlyon/async-stripe", branch = "openapi-1720743905", default-features = false, features = ["runtime-tokio-hyper", "connect", "billing", "checkout", "webhook-events"] } -#async_stripe_artcraft = { git = "https://github.com/arlyon/async-stripe", branch = "next", default-features = false, features = ["runtime-tokio-hyper", "connect", "billing", "checkout", "webhook-events"] } - # Money -async-stripe = { version = "0.26.0", default-features = false, features = ["runtime-tokio-hyper", "connect", "billing", "checkout", "webhook-events"] } +async-stripe = { version = "0.26.0", default-features = false, features = ["runtime-tokio-hyper-rustls", "connect", "billing", "checkout", "webhook-events"] } # Language tag parsing # TODO: Move to language support crate @@ -125,14 +120,14 @@ futures = { version = "0.3.17", features = ["thread-pool"] } # feature for Threa futures-util = "0.3.29" futures_old_for_limiter = { package = "futures", version = "0.1.29" } # Older version of futures for `limiter` crate -sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio", "tls-rustls", "chrono" ] } +sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] } # Redis Database limitation = { path = "../../../vendor/limitation" } r2d2_redis = "0.14.0" # Elasticsearch -elasticsearch = "8.5.0-alpha.1" +elasticsearch = { version = "8.5.0-alpha.1", default-features = false, features = ["rustls-tls"] } # Files infer = "0.16.0" @@ -142,7 +137,7 @@ tempfile.workspace = true # The "never-encode-slash" feature seems to have been removed: # https://github.com/durch/rust-s3/pull/254 -rust-s3 = { version = "0.36.0-beta.2" } +rust-s3 = { version = "0.36.0-beta.2", default-features = false, features = ["fail-on-err", "tags", "tokio-rustls-tls"] } actix.workspace = true actix-http.workspace = true diff --git a/crates/service/web/storyteller_web/src/main.rs b/crates/service/web/storyteller_web/src/main.rs index dea71b8c90..29cf3859a0 100644 --- a/crates/service/web/storyteller_web/src/main.rs +++ b/crates/service/web/storyteller_web/src/main.rs @@ -82,7 +82,7 @@ use crate::http_server::web_utils::handle_multipart_error::handle_multipart_erro use crate::http_server::web_utils::scoped_temp_dir_creator::ScopedTempDirCreator; use crate::state::certs::google_sign_in_cert::GoogleSignInCert; use crate::state::memory_cache::model_token_to_info_cache::ModelTokenToInfoCache; -use crate::state::server_state::{DurableInMemoryCaches, EnvConfig, EphemeralInMemoryCaches, FalData, InMemoryCaches, OpenAiData, ServerInfo, ServerState, StaticFeatureFlags, StripeArtcraftSettings, StripeSettings, TrollBans}; +use crate::state::server_state::{DurableInMemoryCaches, EnvConfig, EphemeralInMemoryCaches, FalData, InMemoryCaches, OpenAiData, ServerInfo, ServerState, StaticFeatureFlags, StripeSettings, TrollBans}; use crate::threads::db_health_checker_thread::db_health_check_status::HealthCheckStatus; use crate::threads::db_health_checker_thread::db_health_checker_thread::db_health_checker_thread; use crate::threads::poll_ip_banlist_thread::poll_ip_bans; diff --git a/crates/service/web/storyteller_web/src/state/server_state.rs b/crates/service/web/storyteller_web/src/state/server_state.rs index a1cf4c2028..07412551bd 100644 --- a/crates/service/web/storyteller_web/src/state/server_state.rs +++ b/crates/service/web/storyteller_web/src/state/server_state.rs @@ -227,15 +227,6 @@ pub struct StripeSettings { pub client: stripe::Client, } -#[derive(Clone)] -pub struct StripeArtcraftSettings { - pub secret_key: String, - pub secret_webhook_signing_key: String, - pub checkout_success_url: String, - pub checkout_cancel_url: String, - pub client: async_stripe_artcraft::Client, -} - /// Flags set at service startup #[derive(Clone)] pub struct StaticFeatureFlags { diff --git a/crates/vendor/fal-rs/Cargo.toml b/crates/vendor/fal-rs/Cargo.toml index 302d80a9e4..1ac27851f8 100644 --- a/crates/vendor/fal-rs/Cargo.toml +++ b/crates/vendor/fal-rs/Cargo.toml @@ -213,6 +213,7 @@ endpoints_fal-ai_sana = [] endpoints_fal-ai_sd15-depth-controlnet = [] endpoints_fal-ai_sdxl-controlnet-union = [] endpoints_fal-ai_skyreels-i2v = [] +endpoints_fal-ai_sora = [] endpoints_fal-ai_stable-audio = [] endpoints_fal-ai_stable-cascade = [] endpoints_fal-ai_stable-diffusion-v15 = [] @@ -230,7 +231,9 @@ endpoints_fal-ai_topaz = [] endpoints_fal-ai_transpixar = [] endpoints_fal-ai_trellis = [] endpoints_fal-ai_triposr = [] +endpoints_fal-ai_veo = [] endpoints_fal-ai_veo2 = [] +endpoints_fal-ai_veo3 = [] endpoints_fal-ai_video-prompt-generator = [] endpoints_fal-ai_video-upscaler = [] endpoints_fal-ai_vidu = [] diff --git a/crates/vendor/fal-rs/generate/Cargo.toml b/crates/vendor/fal-rs/generate/Cargo.toml index 51c8df20b4..6385014938 100644 --- a/crates/vendor/fal-rs/generate/Cargo.toml +++ b/crates/vendor/fal-rs/generate/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false [dependencies] -reqwest = { version = "0.12", features = ["json", "stream", "multipart"] } +reqwest = { version = "0.12", default-features = false, features = ["charset", "http2", "system-proxy", "json", "stream", "multipart", "rustls-tls"] } tokio = { version = "1", features = ["full"] } serde = { version = "1", features = ["derive"] } serde_json = { version = "1" } diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/bytedance/mod.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/bytedance/mod.rs index 48e1746b4e..074e0f5e54 100644 --- a/crates/vendor/fal-rs/src/endpoints/fal_ai/bytedance/mod.rs +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/bytedance/mod.rs @@ -25,4 +25,18 @@ pub mod seedance; feature = "endpoints_fal-ai_bytedance" ))) )] -pub mod seededit; \ No newline at end of file +pub mod seededit; +#[cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_bytedance" +))] +#[cfg_attr( + docsrs, + doc(cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_bytedance" + ))) +)] +pub mod seedream; \ No newline at end of file diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/bytedance/seedream/bytedance_seedream_v4_edit_image.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/bytedance/seedream/bytedance_seedream_v4_edit_image.rs new file mode 100644 index 0000000000..b4ec161493 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/bytedance/seedream/bytedance_seedream_v4_edit_image.rs @@ -0,0 +1,52 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct BytedanceSeedreamV4EditImageInput { + pub prompt: String, + + pub image_urls: Vec, + + /// Supports a custom height/width (which we didn't implement) OR an enum value - + /// square_hd, square, portrait_4_3, portrait_16_9, landscape_4_3, landscape_16_9, auto, auto_2K, auto_4K + #[serde(skip_serializing_if = "Option::is_none")] + pub image_size: Option, + + /// Number of generations + /// 1 - 4 + #[serde(skip_serializing_if = "Option::is_none")] + pub num_images: Option, + + /// 1 - 4 + /// If set to a number greater than one, enables multi-image generation. + /// The model will potentially return up to max_images images every generation, + /// and in total, num_images generations will be carried out. In total, the number + /// of images generated will be between num_images and max_images*num_images. + /// Default value: 1 + #[serde(skip_serializing_if = "Option::is_none")] + pub max_images: Option, + + /// Possible enum values: standard, fast (default "standard") + #[serde(skip_serializing_if = "Option::is_none")] + pub enhance_prompt_mode: Option, + + /// Defaults to true + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_safety_checker: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct BytedanceSeedreamV4EditImageOutput { + pub images: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct ImageFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn bytedance_seedream_v4_edit_image( + params: BytedanceSeedreamV4EditImageInput, +) -> FalRequest { + FalRequest::new("fal-ai/bytedance/seedream/v4/edit", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/bytedance/seedream/bytedance_seedream_v4_text_to_image.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/bytedance/seedream/bytedance_seedream_v4_text_to_image.rs new file mode 100644 index 0000000000..2287e7d947 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/bytedance/seedream/bytedance_seedream_v4_text_to_image.rs @@ -0,0 +1,50 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct BytedanceSeedreamV4TextToImageInput { + pub prompt: String, + + /// Supports a custom height/width (which we didn't implement) OR an enum value - + /// square_hd, square, portrait_4_3, portrait_16_9, landscape_4_3, landscape_16_9, auto, auto_2K, auto_4K + #[serde(skip_serializing_if = "Option::is_none")] + pub image_size: Option, + + /// Number of generations + /// 1 - 4 + #[serde(skip_serializing_if = "Option::is_none")] + pub num_images: Option, + + /// 1 - 4 + /// If set to a number greater than one, enables multi-image generation. + /// The model will potentially return up to max_images images every generation, + /// and in total, num_images generations will be carried out. In total, the number + /// of images generated will be between num_images and max_images*num_images. + /// Default value: 1 + #[serde(skip_serializing_if = "Option::is_none")] + pub max_images: Option, + + /// Possible enum values: standard, fast (default "standard") + #[serde(skip_serializing_if = "Option::is_none")] + pub enhance_prompt_mode: Option, + + /// Defaults to true + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_safety_checker: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct BytedanceSeedreamV4TextToImageOutput { + pub images: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct ImageFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn bytedance_seedream_v4_text_to_image( + params: BytedanceSeedreamV4TextToImageInput, +) -> FalRequest { + FalRequest::new("fal-ai/bytedance/seedream/v4/text-to-image", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/bytedance/seedream/bytedance_seedream_v4p5_edit_image.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/bytedance/seedream/bytedance_seedream_v4p5_edit_image.rs new file mode 100644 index 0000000000..e1791856ba --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/bytedance/seedream/bytedance_seedream_v4p5_edit_image.rs @@ -0,0 +1,51 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct BytedanceSeedreamV4p5EditImageInput { + pub prompt: String, + + pub image_urls: Vec, + + /// Supports a custom height/width (which we didn't implement) OR an enum value - + /// square_hd, square, portrait_4_3, portrait_16_9, landscape_4_3, landscape_16_9, auto_2K, auto_4K (no more "auto") + #[serde(skip_serializing_if = "Option::is_none")] + pub image_size: Option, + + /// Number of generations + /// 1 - 4 + #[serde(skip_serializing_if = "Option::is_none")] + pub num_images: Option, + + /// 1 - 4 + /// If set to a number greater than one, enables multi-image generation. + /// The model will potentially return up to max_images images every generation, + /// and in total, num_images generations will be carried out. In total, the number + /// of images generated will be between num_images and max_images*num_images. + /// Default value: 1 + #[serde(skip_serializing_if = "Option::is_none")] + pub max_images: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub seed: Option, + + /// Defaults to true + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_safety_checker: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct BytedanceSeedreamV4p5EditImageOutput { + pub images: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct ImageFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn bytedance_seedream_v4p5_edit_image( + params: BytedanceSeedreamV4p5EditImageInput, +) -> FalRequest { + FalRequest::new("fal-ai/bytedance/seedream/v4.5/edit", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/bytedance/seedream/bytedance_seedream_v4p5_text_to_image.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/bytedance/seedream/bytedance_seedream_v4p5_text_to_image.rs new file mode 100644 index 0000000000..8e72cab789 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/bytedance/seedream/bytedance_seedream_v4p5_text_to_image.rs @@ -0,0 +1,49 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct BytedanceSeedreamV4p5TextToImageInput { + pub prompt: String, + + /// Supports a custom height/width (which we didn't implement) OR an enum value - + /// square_hd, square, portrait_4_3, portrait_16_9, landscape_4_3, landscape_16_9, auto_2K, auto_4K (no more auto) + #[serde(skip_serializing_if = "Option::is_none")] + pub image_size: Option, + + /// Number of generations + /// 1 - 4 + #[serde(skip_serializing_if = "Option::is_none")] + pub num_images: Option, + + /// 1 - 4 + /// If set to a number greater than one, enables multi-image generation. + /// The model will potentially return up to max_images images every generation, + /// and in total, num_images generations will be carried out. In total, the number + /// of images generated will be between num_images and max_images*num_images. + /// Default value: 1 + #[serde(skip_serializing_if = "Option::is_none")] + pub max_images: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub seed: Option, + + /// Defaults to true + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_safety_checker: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct BytedanceSeedreamV4p5TextToImageOutput { + pub images: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct ImageFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn bytedance_seedream_v4p5_text_to_image( + params: BytedanceSeedreamV4p5TextToImageInput, +) -> FalRequest { + FalRequest::new("fal-ai/bytedance/seedream/v4.5/text-to-image", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/bytedance/seedream/mod.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/bytedance/seedream/mod.rs new file mode 100644 index 0000000000..18f67cd257 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/bytedance/seedream/mod.rs @@ -0,0 +1,4 @@ +pub mod bytedance_seedream_v4_edit_image; +pub mod bytedance_seedream_v4_text_to_image; +pub mod bytedance_seedream_v4p5_edit_image; +pub mod bytedance_seedream_v4p5_text_to_image; \ No newline at end of file diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/mod.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/mod.rs index 56863d4604..df04d9cb04 100644 --- a/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/mod.rs +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/mod.rs @@ -54,3 +54,31 @@ pub mod v1_6; ))) )] pub mod v2_1; +#[cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_kling-video" +))] +#[cfg_attr( + docsrs, + doc(cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_kling-video" + ))) +)] +pub mod v2_5; +#[cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_kling-video" +))] +#[cfg_attr( + docsrs, + doc(cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_kling-video" + ))) +)] +pub mod v2_6; diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_5/kling_v2p5_turbo_pro_image_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_5/kling_v2p5_turbo_pro_image_to_video.rs new file mode 100644 index 0000000000..3a461a7164 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_5/kling_v2p5_turbo_pro_image_to_video.rs @@ -0,0 +1,45 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct KlingV2p5TurboProImageToVideoInput { + pub prompt: String, + + /// Starting frame + pub image_url: String, + + /// Optional ending frame + #[serde(skip_serializing_if = "Option::is_none")] + pub tail_image_url: Option, + + /// Optional negative prompt + #[serde(skip_serializing_if = "Option::is_none")] + pub negative_prompt: Option, + + /// Duration in seconds + /// Options: "5", "10" + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// The CFG (Classifier Free Guidance) scale is a measure of how close you want the model to + /// stick to your prompt. + /// Default value: 0.5 + #[serde(skip_serializing_if = "Option::is_none")] + pub cfg_scale: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct KlingV2p5TurboProImageToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn kling_v2p5_turbo_pro_image_to_video( + params: KlingV2p5TurboProImageToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/kling-video/v2.5-turbo/pro/image-to-video", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_5/kling_v2p5_turbo_pro_text_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_5/kling_v2p5_turbo_pro_text_to_video.rs new file mode 100644 index 0000000000..070d1c9fa8 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_5/kling_v2p5_turbo_pro_text_to_video.rs @@ -0,0 +1,42 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct KlingV2p5TurboProTextToVideoInput { + pub prompt: String, + + /// Aspect ratio + /// Possible enum values: "16:9", "9:16", "1:1" + pub aspect_ratio: Option, + + /// Optional negative prompt + #[serde(skip_serializing_if = "Option::is_none")] + pub negative_prompt: Option, + + /// Duration in seconds + /// Options: "5", "10" + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// The CFG (Classifier Free Guidance) scale is a measure of how close you want the model to + /// stick to your prompt. + /// Default value: 0.5 + #[serde(skip_serializing_if = "Option::is_none")] + pub cfg_scale: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct KlingV2p5TurboProTextToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn kling_v2p5_turbo_pro_text_to_video( + params: KlingV2p5TurboProTextToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/kling-video/v2.5-turbo/pro/text-to-video", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_5/kling_v2p5_turbo_standard_image_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_5/kling_v2p5_turbo_standard_image_to_video.rs new file mode 100644 index 0000000000..60f4e45932 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_5/kling_v2p5_turbo_standard_image_to_video.rs @@ -0,0 +1,41 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct KlingV2p5TurboStandardImageToVideoInput { + pub prompt: String, + + /// Starting frame + pub image_url: String, + + /// Optional negative prompt + #[serde(skip_serializing_if = "Option::is_none")] + pub negative_prompt: Option, + + /// Duration in seconds + /// Options: "5", "10" + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// The CFG (Classifier Free Guidance) scale is a measure of how close you want the model to + /// stick to your prompt. + /// Default value: 0.5 + #[serde(skip_serializing_if = "Option::is_none")] + pub cfg_scale: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct KlingV2p5TurboStandardImageToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn kling_v2p5_turbo_standard_image_to_video( + params: KlingV2p5TurboStandardImageToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/kling-video/v2.5-turbo/standard/image-to-video", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_5/mod.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_5/mod.rs new file mode 100644 index 0000000000..6a63891597 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_5/mod.rs @@ -0,0 +1,3 @@ +pub mod kling_v2p5_turbo_pro_image_to_video; +pub mod kling_v2p5_turbo_pro_text_to_video; +pub mod kling_v2p5_turbo_standard_image_to_video; \ No newline at end of file diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_6/kling_v2p6_pro_image_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_6/kling_v2p6_pro_image_to_video.rs new file mode 100644 index 0000000000..be86b18aad --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_6/kling_v2p6_pro_image_to_video.rs @@ -0,0 +1,39 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct KlingV2p6ProImageToVideoInput { + pub prompt: String, + + /// Starting frame + pub image_url: String, + + /// Generate audio + #[serde(skip_serializing_if = "Option::is_none")] + pub generate_audio: Option, + + /// Optional negative prompt + #[serde(skip_serializing_if = "Option::is_none")] + pub negative_prompt: Option, + + /// Duration in seconds + /// Options: "5", "10" + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct KlingV2p6ProImageToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn kling_v2p6_pro_image_to_video( + params: KlingV2p6ProImageToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/kling-video/v2.6/pro/image-to-video", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_6/kling_v2p6_pro_text_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_6/kling_v2p6_pro_text_to_video.rs new file mode 100644 index 0000000000..5649bca3f9 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_6/kling_v2p6_pro_text_to_video.rs @@ -0,0 +1,46 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct KlingV2p6ProTextToVideoInput { + pub prompt: String, + + /// Aspect ratio + /// Possible enum values: "16:9", "9:16", "1:1" + pub aspect_ratio: Option, + + /// Generate audio + #[serde(skip_serializing_if = "Option::is_none")] + pub generate_audio: Option, + + /// Optional negative prompt + #[serde(skip_serializing_if = "Option::is_none")] + pub negative_prompt: Option, + + /// Duration in seconds + /// Options: "5", "10" + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// The CFG (Classifier Free Guidance) scale is a measure of how close you want the model to + /// stick to your prompt. + /// Default value: 0.5 + #[serde(skip_serializing_if = "Option::is_none")] + pub cfg_scale: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct KlingV2p6ProTextToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn kling_v2p6_pro_text_to_video( + params: KlingV2p6ProTextToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/kling-video/v2.6/pro/text-to-video", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_6/mod.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_6/mod.rs new file mode 100644 index 0000000000..6b45e041e4 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/kling_video/v2_6/mod.rs @@ -0,0 +1,2 @@ +pub mod kling_v2p6_pro_image_to_video; +pub mod kling_v2p6_pro_text_to_video; \ No newline at end of file diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/minimax/hailuo/hailuo_v2p3_fast_pro_image_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/minimax/hailuo/hailuo_v2p3_fast_pro_image_to_video.rs new file mode 100644 index 0000000000..5e1a3a95a2 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/minimax/hailuo/hailuo_v2p3_fast_pro_image_to_video.rs @@ -0,0 +1,30 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct HailuoV2p3FastProImageToVideoInput { + pub prompt: String, + + /// Starting frame + pub image_url: String, + + /// Default value: true + #[serde(skip_serializing_if = "Option::is_none")] + pub prompt_optimizer: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct HailuoV2p3FastProImageToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn hailuo_v2p3_fast_pro_image_to_video( + params: HailuoV2p3FastProImageToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/minimax/hailuo-2.3-fast/pro/image-to-video", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/minimax/hailuo/hailuo_v2p3_pro_image_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/minimax/hailuo/hailuo_v2p3_pro_image_to_video.rs new file mode 100644 index 0000000000..a7a5a411eb --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/minimax/hailuo/hailuo_v2p3_pro_image_to_video.rs @@ -0,0 +1,30 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct HailuoV2p3ProImageToVideoInput { + pub prompt: String, + + /// Starting frame + pub image_url: String, + + /// Default value: true + #[serde(skip_serializing_if = "Option::is_none")] + pub prompt_optimizer: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct HailuoV2p3ProImageToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn hailuo_v2p3_pro_image_to_video( + params: HailuoV2p3ProImageToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/minimax/hailuo-2.3/pro/image-to-video", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/minimax/hailuo/hailuo_v2p3_pro_text_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/minimax/hailuo/hailuo_v2p3_pro_text_to_video.rs new file mode 100644 index 0000000000..a728c7e693 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/minimax/hailuo/hailuo_v2p3_pro_text_to_video.rs @@ -0,0 +1,27 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct HailuoV2p3ProTextToVideoInput { + pub prompt: String, + + /// Default value: true + #[serde(skip_serializing_if = "Option::is_none")] + pub prompt_optimizer: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct HailuoV2p3ProTextToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn hailuo_v2p3_pro_text_to_video( + params: HailuoV2p3ProTextToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/minimax/hailuo-2.3/pro/text-to-video", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/minimax/hailuo/mod.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/minimax/hailuo/mod.rs new file mode 100644 index 0000000000..d25d64c427 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/minimax/hailuo/mod.rs @@ -0,0 +1,3 @@ +pub mod hailuo_v2p3_pro_image_to_video; +pub mod hailuo_v2p3_pro_text_to_video; +pub mod hailuo_v2p3_fast_pro_image_to_video; \ No newline at end of file diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/minimax/mod.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/minimax/mod.rs index 2c946ccd0a..d314f0fba1 100644 --- a/crates/vendor/fal-rs/src/endpoints/fal_ai/minimax/mod.rs +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/minimax/mod.rs @@ -75,6 +75,20 @@ pub mod video_01_live; ))) )] pub mod video_01_subject_reference; +#[cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_minimax" +))] +#[cfg_attr( + docsrs, + doc(cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_minimax" + ))) +)] +pub mod hailuo; #[derive(Debug, Serialize, Deserialize, Default)] pub struct File { diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/mod.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/mod.rs index 1374972ee0..11fb3d905a 100644 --- a/crates/vendor/fal-rs/src/endpoints/fal_ai/mod.rs +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/mod.rs @@ -1930,6 +1930,20 @@ pub mod sdxl_controlnet_union; ))) )] pub mod skyreels_i2v; +#[cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_sora" +))] +#[cfg_attr( + docsrs, + doc(cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_sora" + ))) +)] +pub mod sora; #[cfg(any( feature = "endpoints", feature = "endpoints_fal-ai", @@ -2196,6 +2210,20 @@ pub mod veo2; ))) )] pub mod veo3; +#[cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_veo" +))] +#[cfg_attr( + docsrs, + doc(cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_veo" + ))) +)] +pub mod veo; #[cfg(any( feature = "endpoints", feature = "endpoints_fal-ai", @@ -2408,3 +2436,4 @@ pub mod yue; pub mod zonos; pub mod qwen_image_edit; +pub mod nano_banana_pro; diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/nano_banana_pro/mod.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/nano_banana_pro/mod.rs new file mode 100644 index 0000000000..d3ef4e8e3c --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/nano_banana_pro/mod.rs @@ -0,0 +1,2 @@ +pub mod nano_banana_pro_text_to_image; +pub mod nano_banana_pro_image_edit; \ No newline at end of file diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/nano_banana_pro/nano_banana_pro_image_edit.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/nano_banana_pro/nano_banana_pro_image_edit.rs new file mode 100644 index 0000000000..9526de616a --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/nano_banana_pro/nano_banana_pro_image_edit.rs @@ -0,0 +1,55 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct NanoBananaProImageEditInput { + pub prompt: String, + + pub image_urls: Vec, + + /// Eg. "16:9", "1:1", and a ton of other options. So many options. + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Eg. "1K", "2K", "4K" + #[serde(skip_serializing_if = "Option::is_none")] + pub resolution: Option, + + /// 1 - 4 + #[serde(skip_serializing_if = "Option::is_none")] + pub num_images: Option, + + /// "jpeg", "png", "webp" + #[serde(skip_serializing_if = "Option::is_none")] + pub output_format: Option, +} + +/* +{ + "prompt": "make a photo of the man driving the car down the california coastline", + "num_images": 3, + "aspect_ratio": "16:9", + "output_format": "png", + "image_urls": [ + "https://storage.googleapis.com/falserverless/example_inputs/nano-banana-edit-input.png", + "https://storage.googleapis.com/falserverless/example_inputs/nano-banana-edit-input-2.png" + ], + "resolution": "1K" +} +*/ + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct ImageFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct NanoBananaProImageEditOutput { + pub images: Vec, +} + +pub fn nano_banana_pro_image_edit( + params: NanoBananaProImageEditInput, +) -> FalRequest { + FalRequest::new("fal-ai/nano-banana-pro/edit", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/nano_banana_pro/nano_banana_pro_text_to_image.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/nano_banana_pro/nano_banana_pro_text_to_image.rs new file mode 100644 index 0000000000..b35cab3dfa --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/nano_banana_pro/nano_banana_pro_text_to_image.rs @@ -0,0 +1,39 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct NanoBananaProTextToImageInput { + pub prompt: String, + + /// Eg. "16:9", "1:1", and a ton of other options. So many options. + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Eg. "1K", "2K", "4K" + #[serde(skip_serializing_if = "Option::is_none")] + pub resolution: Option, + + /// 1 - 4 + #[serde(skip_serializing_if = "Option::is_none")] + pub num_images: Option, + + /// "jpeg", "png", "webp" + #[serde(skip_serializing_if = "Option::is_none")] + pub output_format: Option, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct ImageFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct NanoBananaProTextToImageOutput { + pub images: Vec, +} + +pub fn nano_banana_pro_text_to_image( + params: NanoBananaProTextToImageInput, +) -> FalRequest { + FalRequest::new("fal-ai/nano-banana-pro", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/pixverse/mod.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/pixverse/mod.rs index 6e8f6f7066..b32a9d21fe 100644 --- a/crates/vendor/fal-rs/src/endpoints/fal_ai/pixverse/mod.rs +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/pixverse/mod.rs @@ -12,3 +12,17 @@ ))) )] pub mod v3_5; +#[cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_pixverse" +))] +#[cfg_attr( + docsrs, + doc(cfg(any( + feature = "endpoints", + feature = "endpoints_fal-ai", + feature = "endpoints_fal-ai_pixverse" + ))) +)] +pub mod v5; diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/pixverse/v5/mod.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/pixverse/v5/mod.rs new file mode 100644 index 0000000000..fd2e473d3d --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/pixverse/v5/mod.rs @@ -0,0 +1,2 @@ +pub mod pixverse_v5_image_to_video; +pub mod pixverse_v5_text_to_video; \ No newline at end of file diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/pixverse/v5/pixverse_v5_image_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/pixverse/v5/pixverse_v5_image_to_video.rs new file mode 100644 index 0000000000..0d3a4e2ac7 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/pixverse/v5/pixverse_v5_image_to_video.rs @@ -0,0 +1,56 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct PixverseV5ImageToVideoInput { + pub prompt: String, + + /// Starting frame + pub image_url: String, + + /// Resolution + /// Possible enum values: 360p, 540p, 720p, 1080p + /// Default is 720p + #[serde(skip_serializing_if = "Option::is_none")] + pub resolution: Option, + + /// The aspect ratio of the generated video + /// Default value: "16:9" + /// Possible enum values: 16:9, 4:3, 1:1, 3:4, 9:16 + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Optional negative prompt + #[serde(skip_serializing_if = "Option::is_none")] + pub negative_prompt: Option, + + /// Duration in seconds + /// Options: "5", "8" + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// Optional style + /// Possible enum values: anime, 3d_animation, clay, comic, cyberpunk + #[serde(skip_serializing_if = "Option::is_none")] + pub style: Option, + + /// Optional seed + #[serde(skip_serializing_if = "Option::is_none")] + pub seed: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PixverseV5ImageToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn pixverse_v5_image_to_video( + params: PixverseV5ImageToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/pixverse/v5/image-to-video", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/pixverse/v5/pixverse_v5_text_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/pixverse/v5/pixverse_v5_text_to_video.rs new file mode 100644 index 0000000000..92709e53f8 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/pixverse/v5/pixverse_v5_text_to_video.rs @@ -0,0 +1,53 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct PixverseV5TextToVideoInput { + pub prompt: String, + + /// Resolution + /// Possible enum values: 360p, 540p, 720p, 1080p + /// Default is 720p + #[serde(skip_serializing_if = "Option::is_none")] + pub resolution: Option, + + /// The aspect ratio of the generated video + /// Default value: "16:9" + /// Possible enum values: 16:9, 4:3, 1:1, 3:4, 9:16 + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Optional negative prompt + #[serde(skip_serializing_if = "Option::is_none")] + pub negative_prompt: Option, + + /// Duration in seconds + /// Options: "5", "8" + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// Optional style + /// Possible enum values: anime, 3d_animation, clay, comic, cyberpunk + #[serde(skip_serializing_if = "Option::is_none")] + pub style: Option, + + /// Optional seed + #[serde(skip_serializing_if = "Option::is_none")] + pub seed: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PixverseV5TextToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn pixverse_v5_text_to_video( + params: PixverseV5TextToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/pixverse/v5/text-to-video", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/sora/mod.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/sora/mod.rs new file mode 100644 index 0000000000..baeee15332 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/sora/mod.rs @@ -0,0 +1 @@ +pub mod sora2; \ No newline at end of file diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/sora/sora2/mod.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/sora/sora2/mod.rs new file mode 100644 index 0000000000..95a3fd25c5 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/sora/sora2/mod.rs @@ -0,0 +1,4 @@ +pub mod sora_2_image_to_video; +pub mod sora_2_pro_image_to_video; +pub mod sora_2_pro_text_to_video; +pub mod sora_2_text_to_video; \ No newline at end of file diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/sora/sora2/sora_2_image_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/sora/sora2/sora_2_image_to_video.rs new file mode 100644 index 0000000000..df124a9200 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/sora/sora2/sora_2_image_to_video.rs @@ -0,0 +1,48 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Sora2ImageToVideoInput { + pub prompt: String, + + /// Starting frame + pub image_url: String, + + /// Possible enum values: auto, 720p + /// Default value auto + #[serde(skip_serializing_if = "Option::is_none")] + pub resolution: Option, + + /// Possible enum values: auto, 9:16, 16:9 + /// Default value "auto" + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Duration in seconds + /// Possible enum values: 4, 8, 12 + /// Default value 4 + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// Whether to delete the video after generation for privacy reasons. + /// If True, the video cannot be used for remixing and will be permanently deleted. + /// Default value: true + #[serde(skip_serializing_if = "Option::is_none")] + pub delete_video: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Sora2ImageToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn sora_2_image_to_video( + params: Sora2ImageToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/sora-2/image-to-video", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/sora/sora2/sora_2_pro_image_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/sora/sora2/sora_2_pro_image_to_video.rs new file mode 100644 index 0000000000..b9fb092cb9 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/sora/sora2/sora_2_pro_image_to_video.rs @@ -0,0 +1,48 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Sora2ProImageToVideoInput { + pub prompt: String, + + /// Starting frame + pub image_url: String, + + /// Possible enum values: auto, 720p, 1080p + /// Default value auto + #[serde(skip_serializing_if = "Option::is_none")] + pub resolution: Option, + + /// Possible enum values: auto, 9:16, 16:9 + /// Default value "auto" + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Duration in seconds + /// Possible enum values: 4, 8, 12 + /// Default value 4 + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// Whether to delete the video after generation for privacy reasons. + /// If True, the video cannot be used for remixing and will be permanently deleted. + /// Default value: true + #[serde(skip_serializing_if = "Option::is_none")] + pub delete_video: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Sora2ProImageToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn sora_2_pro_image_to_video( + params: Sora2ProImageToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/sora-2/image-to-video/pro", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/sora/sora2/sora_2_pro_text_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/sora/sora2/sora_2_pro_text_to_video.rs new file mode 100644 index 0000000000..8c23940cb2 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/sora/sora2/sora_2_pro_text_to_video.rs @@ -0,0 +1,45 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Sora2ProTextToVideoInput { + pub prompt: String, + + /// Possible enum values: auto, 720p, 1080p + /// Default value auto + #[serde(skip_serializing_if = "Option::is_none")] + pub resolution: Option, + + /// Possible enum values: auto, 9:16, 16:9 + /// Default value "auto" + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Duration in seconds + /// Possible enum values: 4, 8, 12 + /// Default value 4 + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// Whether to delete the video after generation for privacy reasons. + /// If True, the video cannot be used for remixing and will be permanently deleted. + /// Default value: true + #[serde(skip_serializing_if = "Option::is_none")] + pub delete_video: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Sora2ProTextToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn sora_2_pro_text_to_video( + params: Sora2ProTextToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/sora-2/text-to-video/pro", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/sora/sora2/sora_2_text_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/sora/sora2/sora_2_text_to_video.rs new file mode 100644 index 0000000000..fb3cf830b9 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/sora/sora2/sora_2_text_to_video.rs @@ -0,0 +1,45 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Sora2TextToVideoInput { + pub prompt: String, + + /// Possible enum values: 720p + /// Default value 720p + #[serde(skip_serializing_if = "Option::is_none")] + pub resolution: Option, + + /// Possible enum values: auto, 9:16, 16:9 + /// Default value "auto" + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Duration in seconds + /// Possible enum values: 4, 8, 12 + /// Default value 4 + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// Whether to delete the video after generation for privacy reasons. + /// If True, the video cannot be used for remixing and will be permanently deleted. + /// Default value: true + #[serde(skip_serializing_if = "Option::is_none")] + pub delete_video: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Sora2TextToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn sora_2_text_to_video( + params: Sora2TextToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/sora-2/text-to-video", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/mod.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/mod.rs new file mode 100644 index 0000000000..aac3d4e058 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/mod.rs @@ -0,0 +1 @@ +pub mod veo3_1; \ No newline at end of file diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/veo3_1/mod.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/veo3_1/mod.rs new file mode 100644 index 0000000000..92c85b788d --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/veo3_1/mod.rs @@ -0,0 +1,6 @@ +pub mod veo_3p1_fast_first_frame_image_to_video; +pub mod veo_3p1_fast_first_last_frame_image_to_video; +pub mod veo_3p1_fast_text_to_video; +pub mod veo_3p1_first_frame_image_to_video; +pub mod veo_3p1_first_last_frame_image_to_video; +pub mod veo_3p1_text_to_video; \ No newline at end of file diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/veo3_1/veo_3p1_fast_first_frame_image_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/veo3_1/veo_3p1_fast_first_frame_image_to_video.rs new file mode 100644 index 0000000000..78e271a2b9 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/veo3_1/veo_3p1_fast_first_frame_image_to_video.rs @@ -0,0 +1,47 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Veo3p1FastFirstFrameImageToVideoInput { + pub prompt: String, + + /// Starting frame + pub image_url: String, + + /// Possible enum values: auto, 9:16, 16:9, 1:1 + /// Default value "16:9" + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Duration in seconds + /// Possible enum values: 4s, 6s, 8s + /// Default value 8s + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// Generate audio + /// Default value "true" + #[serde(skip_serializing_if = "Option::is_none")] + pub generate_audio: Option, + + /// Possible enum values: 720p, 1080p + /// Default value 720p + #[serde(skip_serializing_if = "Option::is_none")] + pub resolution: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Veo3p1FastFirstFrameImageToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn veo_3p1_fast_first_frame_image_to_video( + params: Veo3p1FastFirstFrameImageToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/veo3.1/fast/image-to-video", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/veo3_1/veo_3p1_fast_first_last_frame_image_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/veo3_1/veo_3p1_fast_first_last_frame_image_to_video.rs new file mode 100644 index 0000000000..4ae6b25e9d --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/veo3_1/veo_3p1_fast_first_last_frame_image_to_video.rs @@ -0,0 +1,50 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Veo3p1FastFirstLastFrameImageToVideoInput { + pub prompt: String, + + /// Starting frame + pub first_frame_url: String, + + /// Ending frame + pub last_frame_url: String, + + /// Duration in seconds + /// Possible enum values: 4s, 6s, 8s + /// Default value 8s + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// Possible enum values: auto, 9:16, 16:9, 1:1 + /// Default value "auto" + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Possible enum values: 720p, 1080p + /// Default value 720p + #[serde(skip_serializing_if = "Option::is_none")] + pub resolution: Option, + + /// Generate audio + /// Defaults to "true" + #[serde(skip_serializing_if = "Option::is_none")] + pub generate_audio: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Veo3p1FastFirstLastFrameImageToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn veo_3p1_fast_first_last_frame_image_to_video( + params: Veo3p1FastFirstLastFrameImageToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/veo3.1/fast/first-last-frame-to-video", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/veo3_1/veo_3p1_fast_text_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/veo3_1/veo_3p1_fast_text_to_video.rs new file mode 100644 index 0000000000..e6b838c3d7 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/veo3_1/veo_3p1_fast_text_to_video.rs @@ -0,0 +1,58 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Veo3p1FastTextToVideoInput { + pub prompt: String, + + /// Possible enum values: 9:16, 16:9, 1:1 + /// Default value "16:9" + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Duration in seconds + /// Possible enum values: 4s, 6s, 8s + /// Default value 8s + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub negative_prompt: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub enhance_prompt: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub seed: Option, + + /// Whether to automatically attempt to fix prompts that fail content policy or other validation checks by rewriting them + /// Default value: true + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_fix: Option, + + /// Possible enum values: 720p, 1080p + /// Default value 720p + #[serde(skip_serializing_if = "Option::is_none")] + pub resolution: Option, + + /// Generate audio + /// Default value "true" + #[serde(skip_serializing_if = "Option::is_none")] + pub generate_audio: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Veo3p1FastTextToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn veo_3p1_fast_text_to_video( + params: Veo3p1FastTextToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/veo3.1/fast", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/veo3_1/veo_3p1_first_frame_image_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/veo3_1/veo_3p1_first_frame_image_to_video.rs new file mode 100644 index 0000000000..6f126a8b73 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/veo3_1/veo_3p1_first_frame_image_to_video.rs @@ -0,0 +1,47 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Veo3p1FirstFrameImageToVideoInput { + pub prompt: String, + + /// Starting frame + pub image_url: String, + + /// Possible enum values: 9:16, 16:9 (NB: This differs from first-frame/last-frame) + /// Default value "16:9" + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Duration in seconds + /// Possible enum values: 4s, 6s, 8s + /// Default value 8s + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// Generate audio + /// Default value "true" + #[serde(skip_serializing_if = "Option::is_none")] + pub generate_audio: Option, + + /// Possible enum values: 720p, 1080p + /// Default value 720p + #[serde(skip_serializing_if = "Option::is_none")] + pub resolution: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Veo3p1FirstFrameImageToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn veo_3p1_first_frame_image_to_video( + params: Veo3p1FirstFrameImageToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/veo3.1/image-to-video", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/veo3_1/veo_3p1_first_last_frame_image_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/veo3_1/veo_3p1_first_last_frame_image_to_video.rs new file mode 100644 index 0000000000..ac87f57e60 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/veo3_1/veo_3p1_first_last_frame_image_to_video.rs @@ -0,0 +1,50 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Veo3p1FirstLastFrameImageToVideoInput { + pub prompt: String, + + /// Starting frame + pub first_frame_url: String, + + /// Ending frame + pub last_frame_url: String, + + /// Duration in seconds + /// Possible enum values: 4s, 6s, 8s + /// Default value 8s + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + /// Possible enum values: auto, 9:16, 16:9, 1:1 + /// Default value "auto" + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Possible enum values: 720p, 1080p + /// Default value 720p + #[serde(skip_serializing_if = "Option::is_none")] + pub resolution: Option, + + /// Generate audio + /// Defaults to "true" + #[serde(skip_serializing_if = "Option::is_none")] + pub generate_audio: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Veo3p1FirstLastFrameImageToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn veo_3p1_first_last_frame_image_to_video( + params: Veo3p1FirstLastFrameImageToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/veo3.1/first-last-frame-to-video", params) +} diff --git a/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/veo3_1/veo_3p1_text_to_video.rs b/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/veo3_1/veo_3p1_text_to_video.rs new file mode 100644 index 0000000000..c26f896357 --- /dev/null +++ b/crates/vendor/fal-rs/src/endpoints/fal_ai/veo/veo3_1/veo_3p1_text_to_video.rs @@ -0,0 +1,58 @@ +use crate::prelude::{Deserialize, FalRequest, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct Veo3p1TextToVideoInput { + pub prompt: String, + + /// Possible enum values: 9:16, 16:9, 1:1 + /// Default value "16:9" + #[serde(skip_serializing_if = "Option::is_none")] + pub aspect_ratio: Option, + + /// Duration in seconds + /// Possible enum values: 4s, 6s, 8s + /// Default value 8s + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub negative_prompt: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub enhance_prompt: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub seed: Option, + + /// Whether to automatically attempt to fix prompts that fail content policy or other validation checks by rewriting them + /// Default value: true + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_fix: Option, + + /// Possible enum values: 720p, 1080p + /// Default value 720p + #[serde(skip_serializing_if = "Option::is_none")] + pub resolution: Option, + + /// Generate audio + /// Default value "true" + #[serde(skip_serializing_if = "Option::is_none")] + pub generate_audio: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Veo3p1TextToVideoOutput { + pub video: VideoFile, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct VideoFile { + /// The URL where the file can be downloaded from. + pub url: String, +} + +pub fn veo_3p1_text_to_video( + params: Veo3p1TextToVideoInput, +) -> FalRequest { + FalRequest::new("fal-ai/veo3.1", params) +} diff --git a/crates/workspace-build-acceleration/Cargo.toml b/crates/workspace-build-acceleration/Cargo.toml index 52c59b1e87..55229bc891 100644 --- a/crates/workspace-build-acceleration/Cargo.toml +++ b/crates/workspace-build-acceleration/Cargo.toml @@ -50,7 +50,6 @@ generic-array = { version = "0.14.7", default-features = false, features = ["mor getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2.14", default-features = false, features = ["js", "std"] } hashbrown = { version = "0.14.5" } hmac = { version = "0.12.1", default-features = false, features = ["reset"] } -hyper-582f2526e08bb6a0 = { package = "hyper", version = "0.14.28", features = ["full"] } idna = { version = "1.0.3" } indexmap-dff4ba8e3ae991db = { package = "indexmap", version = "1.9.3", default-features = false, features = ["serde-1"] } indexmap-f595c2ba2a3f28df = { package = "indexmap", version = "2.9.0", features = ["serde"] } @@ -60,7 +59,6 @@ log = { version = "0.4.27", default-features = false, features = ["kv_unstable", md-5 = { version = "0.10.6" } memchr = { version = "2.7.4", features = ["use_std"] } mime_guess = { version = "2.0.4" } -native-tls = { version = "0.2.14", default-features = false, features = ["alpn", "vendored"] } num-bigint = { version = "0.4.6" } num-integer = { version = "0.1.46", features = ["i128"] } num-iter = { version = "0.1.45", default-features = false, features = ["i128", "std"] } @@ -78,7 +76,6 @@ rand_core = { version = "0.6.4", default-features = false, features = ["std"] } regex = { version = "1.11.1" } regex-automata = { version = "0.4.9", default-features = false, features = ["dfa-onepass", "hybrid", "meta", "nfa-backtrack", "perf-inline", "perf-literal", "unicode"] } regex-syntax = { version = "0.8.5" } -reqwest = { version = "0.12.15", features = ["blocking", "brotli", "cookies", "deflate", "gzip", "json", "multipart", "native-tls-alpn", "native-tls-vendored", "rustls-tls", "rustls-tls-manual-roots", "rustls-tls-native-roots", "socks", "stream", "trust-dns", "zstd"] } rsa = { version = "0.9.6", features = ["getrandom", "hazmat", "sha2"] } schemars = { version = "0.8.21", features = ["preserve_order", "url", "uuid1"] } scopeguard = { version = "1.2.0" } @@ -89,8 +86,6 @@ sha1 = { version = "0.10.6" } sha2 = { version = "0.10.8", features = ["oid"] } smallvec = { version = "1.13.2", default-features = false, features = ["const_new", "union"] } socket2 = { version = "0.5.7", default-features = false, features = ["all"] } -sqlx = { version = "0.7.3", features = ["bigdecimal", "chrono", "mysql", "runtime-tokio-native-tls", "sqlite", "tls-rustls"] } -sqlx-core = { version = "0.7.3", features = ["_rt-tokio", "_tls-native-tls", "_tls-rustls", "any", "bigdecimal", "chrono", "json", "migrate", "offline"] } sqlx-mysql = { version = "0.7.3", default-features = false, features = ["any", "bigdecimal", "chrono", "json", "migrate", "offline"] } sqlx-sqlite = { version = "0.7.3", default-features = false, features = ["any", "chrono", "json", "migrate", "offline"] } stable_deref_trait = { version = "1.2.0" } @@ -145,7 +140,6 @@ libc = { version = "0.2.170", features = ["extra_traits"] } log = { version = "0.4.27", default-features = false, features = ["kv_unstable", "std"] } md-5 = { version = "0.10.6" } memchr = { version = "2.7.4", features = ["use_std"] } -native-tls = { version = "0.2.14", default-features = false, features = ["alpn", "vendored"] } num-bigint = { version = "0.4.6" } num-integer = { version = "0.1.46", features = ["i128"] } num-iter = { version = "0.1.45", default-features = false, features = ["i128", "std"] } @@ -171,9 +165,6 @@ serde_json = { version = "1.0.140", features = ["alloc", "preserve_order", "raw_ sha1 = { version = "0.10.6" } sha2 = { version = "0.10.8", features = ["oid"] } smallvec = { version = "1.13.2", default-features = false, features = ["const_new", "union"] } -sqlx-core = { version = "0.7.3", features = ["_rt-tokio", "_tls-native-tls", "_tls-rustls", "any", "bigdecimal", "chrono", "json", "migrate", "offline"] } -sqlx-macros = { version = "0.7.3", features = ["_rt-tokio", "_tls-native-tls", "_tls-rustls", "bigdecimal", "chrono", "json", "migrate", "mysql", "sqlite"] } -sqlx-macros-core = { version = "0.7.3", features = ["_rt-tokio", "_tls-native-tls", "_tls-rustls", "bigdecimal", "chrono", "json", "migrate", "mysql", "sqlite"] } sqlx-mysql = { version = "0.7.3", default-features = false, features = ["any", "bigdecimal", "chrono", "json", "migrate", "offline"] } sqlx-sqlite = { version = "0.7.3", default-features = false, features = ["any", "chrono", "json", "migrate", "offline"] } stable_deref_trait = { version = "1.2.0" } @@ -195,12 +186,6 @@ zeroize = { version = "1.8.1", features = ["zeroize_derive"] } [target.x86_64-unknown-linux-gnu.dependencies] async-compression = { version = "0.4.5", default-features = false, features = ["brotli", "gzip", "tokio", "zlib", "zstd"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3.1", default-features = false, features = ["std"] } -hyper-dff4ba8e3ae991db = { package = "hyper", version = "1.6.0", features = ["client", "http1", "http2"] } -hyper-rustls = { version = "0.27.3", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12", "webpki-tokio"] } -hyper-tls = { version = "0.6.0", default-features = false, features = ["alpn"] } -hyper-util = { version = "0.1.10", features = ["client-legacy", "http1", "http2"] } -openssl = { version = "0.10.71", features = ["vendored"] } -openssl-sys = { version = "0.9.106", default-features = false, features = ["vendored"] } rustls-pki-types = { version = "1.7.0", features = ["std"] } spin = { version = "0.9.8" } tokio-util = { version = "0.7.12", features = ["codec", "io"] } @@ -212,8 +197,6 @@ zstd-sys = { version = "2.0.10", default-features = false, features = ["legacy", [target.x86_64-unknown-linux-gnu.build-dependencies] getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3.1", default-features = false, features = ["std"] } heck = { version = "0.4.1", features = ["unicode"] } -openssl = { version = "0.10.71", features = ["vendored"] } -openssl-sys = { version = "0.9.106", default-features = false, features = ["vendored"] } socket2 = { version = "0.5.7", default-features = false, features = ["all"] } spin = { version = "0.9.8" } toml_edit = { version = "0.19.15", features = ["serde"] } @@ -225,10 +208,6 @@ zstd-sys = { version = "2.0.10", default-features = false, features = ["legacy", async-compression = { version = "0.4.5", default-features = false, features = ["brotli", "gzip", "tokio", "zlib", "zstd"] } core-foundation-sys = { version = "0.8.7" } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3.1", default-features = false, features = ["std"] } -hyper-dff4ba8e3ae991db = { package = "hyper", version = "1.6.0", features = ["client", "http1", "http2"] } -hyper-rustls = { version = "0.27.3", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12", "webpki-tokio"] } -hyper-tls = { version = "0.6.0", default-features = false, features = ["alpn"] } -hyper-util = { version = "0.1.10", features = ["client-legacy", "http1", "http2"] } quick-xml = { version = "0.32.0", features = ["serialize"] } rustls-pki-types = { version = "1.7.0", features = ["std"] } security-framework = { version = "2.10.0", features = ["alpn"] } @@ -254,10 +233,6 @@ zstd-sys = { version = "2.0.10", default-features = false, features = ["legacy", [target.x86_64-pc-windows-msvc.dependencies] async-compression = { version = "0.4.5", default-features = false, features = ["brotli", "gzip", "tokio", "zlib", "zstd"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3.1", default-features = false, features = ["std"] } -hyper-dff4ba8e3ae991db = { package = "hyper", version = "1.6.0", features = ["client", "http1", "http2"] } -hyper-rustls = { version = "0.27.3", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12", "webpki-tokio"] } -hyper-tls = { version = "0.6.0", default-features = false, features = ["alpn"] } -hyper-util = { version = "0.1.10", features = ["client-legacy", "http1", "http2"] } rustls-pki-types = { version = "1.7.0", features = ["std"] } spin = { version = "0.9.8" } tokio-util = { version = "0.7.12", features = ["codec", "io"] } @@ -274,12 +249,7 @@ async-compression = { version = "0.4.5", default-features = false, features = [" cookie = { version = "0.18.1", default-features = false, features = ["percent-encode"] } deranged = { version = "0.3.11", default-features = false, features = ["powerfmt", "serde", "std"] } getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3.1", default-features = false, features = ["std"] } -hyper-dff4ba8e3ae991db = { package = "hyper", version = "1.6.0", features = ["client", "http1", "http2"] } -hyper-rustls = { version = "0.27.3", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12", "webpki-tokio"] } -hyper-tls = { version = "0.6.0", default-features = false, features = ["alpn"] } -hyper-util = { version = "0.1.10", features = ["client-legacy", "http1", "http2"] } mime_guess = { version = "2.0.4" } -reqwest = { version = "0.12.15", features = ["blocking", "brotli", "cookies", "deflate", "gzip", "json", "multipart", "native-tls-alpn", "native-tls-vendored", "rustls-tls", "rustls-tls-manual-roots", "rustls-tls-native-roots", "socks", "stream", "trust-dns", "zstd"] } rustls-pki-types = { version = "1.7.0", features = ["std"] } socket2 = { version = "0.5.7", default-features = false, features = ["all"] } spin = { version = "0.9.8" } diff --git a/frontend/apps/artcraft-website/src/config/downloads.ts b/frontend/apps/artcraft-website/src/config/downloads.ts index 0f28362963..7210bfde75 100644 --- a/frontend/apps/artcraft-website/src/config/downloads.ts +++ b/frontend/apps/artcraft-website/src/config/downloads.ts @@ -8,7 +8,8 @@ // const MAC_VERSION = "2025.09.23"; // const MAC_VERSION = "2025.09.24"; // Works! Good release. // const MAC_VERSION = "2025.09.27"; -export const MAC_VERSION = "2025.10.04"; // Sora 2 +//export const MAC_VERSION = "2025.10.04"; // Sora 2 +export const MAC_VERSION = "2025.12.01"; // Grok // Current Windows version (update this when a new version is released) // If a rollback is necessary, roll back to the last known good date. @@ -20,7 +21,7 @@ export const MAC_VERSION = "2025.10.04"; // Sora 2 // const WINDOWS_VERSION = "2025.09.23"; // const WINDOWS_VERSION = "2025.09.24"; // Works! Good release. // const WINDOWS_VERSION = "2025.09.27"; -export const WINDOWS_VERSION = "2025.10.04"; // Sora 2 +export const WINDOWS_VERSION = "2025.12.01"; // Grok const R2_BASE = "https://pub-3b58c874772a4e04b9c291815224128c.r2.dev"; diff --git a/frontend/apps/artcraft-website/src/pages/media/media.tsx b/frontend/apps/artcraft-website/src/pages/media/media.tsx index 1ac3c66522..85edcc0fc1 100644 --- a/frontend/apps/artcraft-website/src/pages/media/media.tsx +++ b/frontend/apps/artcraft-website/src/pages/media/media.tsx @@ -265,6 +265,8 @@ export default function MediaPage() { onError={(e) => { (e.currentTarget as HTMLImageElement).src = PLACEHOLDER_IMAGES.DEFAULT; (e.currentTarget as HTMLImageElement).style.opacity = "0.3"; + // Set the `data-brokenurl` property for debugging the broken images: + (e.currentTarget as HTMLImageElement).dataset.brokenurl = media.url || ""; setMedia(prev => ({ ...prev, isLoaded: true })); }} onLoad={() => setMedia(prev => ({ ...prev, isLoaded: true }))} diff --git a/frontend/apps/editor3d/app/public/resources/images/floating-cubes.png b/frontend/apps/editor3d/app/public/resources/images/floating-cubes.png new file mode 100644 index 0000000000..a568e7ec0e Binary files /dev/null and b/frontend/apps/editor3d/app/public/resources/images/floating-cubes.png differ diff --git a/frontend/apps/editor3d/app/src/components/experiences/ImageTo3DExperience.tsx b/frontend/apps/editor3d/app/src/components/experiences/ImageTo3DExperience.tsx index 78f5968f7e..f213f23d65 100644 --- a/frontend/apps/editor3d/app/src/components/experiences/ImageTo3DExperience.tsx +++ b/frontend/apps/editor3d/app/src/components/experiences/ImageTo3DExperience.tsx @@ -1,12 +1,13 @@ import { useEffect, useMemo, useRef, useState } from "react"; +import { animated, useSpring } from "@react-spring/web"; import { Button, ToggleButton } from "@storyteller/ui-button"; import { TabSelector } from "@storyteller/ui-tab-selector"; import { Tooltip } from "@storyteller/ui-tooltip"; +import { Viewer3D } from "@storyteller/ui-viewer-3d"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCube, faCubes, - faGlobe, faImages, faPlus, faSparkles, @@ -14,6 +15,20 @@ import { faXmark, } from "@fortawesome/pro-solid-svg-icons"; import { twMerge } from "tailwind-merge"; +import { useImageTo3DStore } from "../../pages/PageImageTo3DObject/ImageTo3DStore"; +import { MediaUploadApi, downloadFileFromUrl } from "@storyteller/api"; +import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal"; +import { + EnqueueImageTo3dObject, + EnqueueImageTo3dObjectModel, +} from "@storyteller/tauri-api"; +import { toast } from "react-hot-toast"; +import { v4 as uuidv4 } from "uuid"; +import { useTabStore } from "../../pages/Stores/TabState"; +import { addObject } from "../../pages/PageEnigma/signals/objectGroup/addObject"; +import { set3DPageMounted } from "../../pages/PageEnigma/Editor/editor"; +import { AssetType } from "~/enums"; +import type { MediaItem } from "../../pages/PageEnigma/models"; type Mode = "image" | "text"; type Variant = "object" | "world"; @@ -22,20 +37,12 @@ interface ImageTo3DExperienceProps { title: string; subtitle: string; variant: Variant; -} - -interface GeneratedResult { - id: string; - mode: Mode; - timestamp: number; - note?: string; - previewUrl?: string; - meshOnly?: boolean; + backgroundImage?: string; } const MODE_TABS = [ { id: "image", label: "Image to 3D" }, - { id: "text", label: "Text to 3D" }, + // { id: "text", label: "Text to 3D" }, ] satisfies { id: Mode; label: string }[]; const formatTime = (timestamp: number) => { @@ -52,31 +59,54 @@ export const ImageTo3DExperience = ({ title, subtitle, variant, + backgroundImage, }: ImageTo3DExperienceProps) => { const [activeMode, setActiveMode] = useState("image"); const [uploadedPreview, setUploadedPreview] = useState(null); const [uploadedName, setUploadedName] = useState(null); const [prompt, setPrompt] = useState(""); - const [results, setResults] = useState([]); const [isGenerating, setIsGenerating] = useState(false); + const [isUploading, setIsUploading] = useState(false); const [dragActive, setDragActive] = useState(false); const [meshOnly, setMeshOnly] = useState(false); - const timeoutRef = useRef(null); const fileInputRef = useRef(null); const textareaRef = useRef(null); - useEffect( - () => () => { - if (timeoutRef.current) { - window.clearTimeout(timeoutRef.current); - } - if (uploadedPreview && uploadedPreview.startsWith("blob:")) { - URL.revokeObjectURL(uploadedPreview); - } - }, - [uploadedPreview], + const [selectedResultId, setSelectedResultId] = useState(null); + const promptContentRef = useRef(null); + const [promptHeight, setPromptHeight] = useState(400); + const [vh, setVh] = useState( + typeof window !== "undefined" ? window.innerHeight : 800, ); + const results = useImageTo3DStore((s) => s.results); + const startGeneration = useImageTo3DStore((s) => s.startGeneration); + const resetResults = useImageTo3DStore((s) => s.reset); + const [uploadedMediaToken, setUploadedMediaToken] = useState( + null, + ); + const [isGalleryModalOpen, setIsGalleryModalOpen] = useState(false); + const [selectedGalleryImages, setSelectedGalleryImages] = useState( + [], + ); + + useEffect(() => { + const onResize = () => setVh(window.innerHeight); + window.addEventListener("resize", onResize); + return () => window.removeEventListener("resize", onResize); + }, []); + + useEffect(() => { + const el = promptContentRef.current; + if (!el || typeof ResizeObserver === "undefined") return; + const update = () => setPromptHeight(el.offsetHeight); + update(); + const ro = new ResizeObserver(() => update()); + ro.observe(el); + return () => ro.disconnect(); + }, []); + + useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = "auto"; @@ -84,20 +114,50 @@ export const ImageTo3DExperience = ({ } }); - const handleFiles = (files?: FileList | null) => { + + const handleFiles = async (files?: FileList | null) => { if (!files || files.length === 0) return; const file = files[0]; if (!file.type.startsWith("image/")) return; - if (uploadedPreview && uploadedPreview.startsWith("blob:")) { - URL.revokeObjectURL(uploadedPreview); - } - const objectUrl = URL.createObjectURL(file); - setUploadedPreview(objectUrl); + setUploadedName(file.name); + setUploadedMediaToken(null); + setIsUploading(true); + + const reader = new FileReader(); + reader.onload = (e) => { + const dataUrl = e.target?.result as string; + setUploadedPreview(dataUrl); + }; + reader.readAsDataURL(file); + + try { + const mediaUploadApi = new MediaUploadApi(); + const uuid = uuidv4(); + + const uploadResult = await mediaUploadApi.UploadImage({ + blob: file, + fileName: file.name, + uuid: uuid, + }); + + if (!uploadResult.success || !uploadResult.data) { + throw new Error("Failed to upload image"); + } + + setUploadedMediaToken(uploadResult.data); + } catch (error) { + toast.error("Failed to upload image"); + setUploadedPreview(null); + setUploadedName(null); + } finally { + setIsUploading(false); + } }; const handleFileChange = (event: React.ChangeEvent) => { handleFiles(event.target.files); + event.target.value = ""; }; const handleDrop = (event: React.DragEvent) => { @@ -108,45 +168,115 @@ export const ImageTo3DExperience = ({ }; const handlePickFromLibrary = () => { - console.info("TODO: integrate gallery modal for ImageTo3DExperience"); + setIsGalleryModalOpen(true); }; - const handleGenerate = () => { - if (isGenerating) return; + const handleImageSelect = (id: string) => { + setSelectedGalleryImages((prev) => { + if (prev.includes(id)) return prev.filter((x) => x !== id); + return [id]; + }); + }; + + const handleGallerySelect = async (selectedItems: GalleryItem[]) => { + const item = selectedItems[0]; + if (!item || !item.fullImage) { + toast.error("No image selected"); + return; + } + + if (isUploading) return; + + setIsGalleryModalOpen(false); + setSelectedGalleryImages([]); + + setUploadedName(item.label || "Library Image"); + setUploadedPreview(item.fullImage); + setUploadedMediaToken(item.id); + }; + + const handleGenerate = async () => { + if (isGenerating || isUploading) return; + if (activeMode === "image" && !uploadedMediaToken) return; + if (activeMode === "text" && prompt.trim().length <= 3) return; + setIsGenerating(true); const snapshotPrompt = prompt.trim(); const snapshotPreview = uploadedPreview || undefined; - const newResult: GeneratedResult = { - id: generateId(), - mode: activeMode, - timestamp: Date.now(), - note: activeMode === "text" ? snapshotPrompt : uploadedName || undefined, - previewUrl: snapshotPreview, - meshOnly, - }; + const snapshotName = uploadedName; + + try { + const subscriberId = generateId(); + const note = + activeMode === "text" + ? snapshotPrompt + : snapshotName || "Generated Model"; + + startGeneration( + activeMode, + note, + snapshotPreview, + meshOnly, + subscriberId, + ); + setSelectedResultId(subscriberId); + + const result = await EnqueueImageTo3dObject({ + image_media_token: uploadedMediaToken || undefined, + model: EnqueueImageTo3dObjectModel.Hunyuan3d2, + frontend_caller: "mini_app", + frontend_subscriber_id: subscriberId, + }); + + if ("error_type" in result) { + throw new Error(result.error_message || result.error_type); + } - timeoutRef.current = window.setTimeout(() => { - setResults((prev) => [newResult, ...prev]); - setIsGenerating(false); if (activeMode === "text") { setPrompt(""); + } else { + setUploadedPreview(null); + setUploadedName(null); + setUploadedMediaToken(null); } - }, 1200); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : "An unexpected error occurred"; + toast.error(`Failed to generate 3D model: ${errorMessage}`); + } finally { + setIsGenerating(false); + } }; const canGenerate = useMemo(() => { - if (isGenerating) return false; + if (isGenerating || isUploading) return false; if (activeMode === "image") { - return Boolean(uploadedPreview); + return Boolean(uploadedMediaToken); } if (activeMode === "text") { return prompt.trim().length > 3; } return true; - }, [activeMode, uploadedPreview, prompt, isGenerating]); + }, [activeMode, uploadedMediaToken, prompt, isGenerating, isUploading]); - const resultTitle = - variant === "object" ? "3D Object Preview" : "3D World Preview"; + const hasResults = results.length > 0; + const showPromptAtBottom = hasResults; + + // Animation logic + const bottomMarginPx = 24; + const bottomOffsetPx = promptHeight + bottomMarginPx; + + const centerTop = vh / 2 - promptHeight / 2 + 80; + const bottomTop = vh - bottomOffsetPx; + + const targetTop = showPromptAtBottom + ? Math.max(0, bottomTop) + : Math.max(0, centerTop); + + const promptAnim = useSpring({ + top: targetTop, + config: { tension: 200, friction: 28, mass: 1.1 }, + }); const renderAddImageTile = () => ( { @@ -209,11 +340,16 @@ export const ImageTo3DExperience = ({ > - - Add Image - + {!hasResults && ( + + Add Image + + )} ); @@ -222,28 +358,39 @@ export const ImageTo3DExperience = ({
{uploadedPreview ? (
fileInputRef.current?.click()} + className={twMerge( + "group relative cursor-pointer overflow-hidden rounded-2xl border-[3px] border-primary/40 bg-black/30 transition-all", + hasResults ? "aspect-square w-24" : "aspect-square w-48", + )} + onClick={() => !isUploading && fileInputRef.current?.click()} > Reference - + {isUploading && ( +
+
+
+ )} + {!isUploading && ( + + )}
) : ( renderAddImageTile() @@ -272,112 +419,201 @@ export const ImageTo3DExperience = ({ return renderImageMode(); }; - const renderResults = () => ( -
- {results.map((result) => ( -
-
-
- {resultTitle} -
-
- {formatTime(result.timestamp)} -
-
-
-
-
- {result.previewUrl ? ( - Result preview - ) : ( -
- - Preview ready -
- )} -
-
-
-
-

- {result.mode} input - {result.meshOnly ? " · Mesh only" : ""} -

-

- {result.note ?? "Configuration snapshot saved."} -

-
- - -
-
-
- ))} -
- ); + const activeResult = + results.find((r) => r.id === selectedResultId) || results[0]; + + useEffect(() => { + console.log("[ImageTo3D] Active result changed:", activeResult); + }, [activeResult]); return ( -
-
-
-

{title}

-

{subtitle}

-
+
+ {backgroundImage && !hasResults && ( + <> +
+
+
+
+ + )} - {results.length > 0 && ( -
- {renderResults()} +
+ {!hasResults && ( +
+

{title}

+

{subtitle}

)} -
-
{renderActiveMode()}
+ {/* Split View: Viewer + History */} + {hasResults && ( +
+ {/* Left: Viewer */} +
+ + {activeResult?.modelUrl && activeResult?.mediaToken && ( +
+ + +
+ )} +
-
- setMeshOnly((prev) => !prev)} - /> -
- + {/* Right: History List */} +
+
+

History

+ {results.length > 0 && ( + + )} +
+
+
+ {results.map((result) => ( + + ))} +
+
-
+ )} -
- setActiveMode(tabId as Mode)} - className="w-full max-w-md" - indicatorClassName="bg-primary/25" - /> -
+ {/* Animated Input Area */} + +
+
+
{renderActiveMode()}
+ +
+ setMeshOnly((prev) => !prev)} + /> +
+ +
+
+
+ + {MODE_TABS.length > 1 && ( +
+ setActiveMode(tabId as Mode)} + className="w-fit" + indicatorClassName="bg-primary/25" + /> +
+ )} +
+
+ + { + setIsGalleryModalOpen(false); + setSelectedGalleryImages([]); + }} + mode="select" + selectedItemIds={selectedGalleryImages} + onSelectItem={handleImageSelect} + maxSelections={1} + onUseSelected={handleGallerySelect} + onDownloadClicked={downloadFileFromUrl} + forceFilter="image" + />
); }; diff --git a/frontend/apps/editor3d/app/src/components/experiences/ImageTo3DStore.ts b/frontend/apps/editor3d/app/src/components/experiences/ImageTo3DStore.ts new file mode 100644 index 0000000000..9af3897109 --- /dev/null +++ b/frontend/apps/editor3d/app/src/components/experiences/ImageTo3DStore.ts @@ -0,0 +1,34 @@ +import { create } from "zustand"; + +export type ImageTo3DMode = "image" | "text"; + +export interface ImageTo3DResult { + id: string; + mode: ImageTo3DMode; + timestamp: number; + note?: string; + previewUrl?: string; + meshOnly?: boolean; + status?: "pending" | "completed"; +} + +interface ImageTo3DState { + results: ImageTo3DResult[]; + addResult: (result: ImageTo3DResult) => void; + updateResult: (id: string, updates: Partial) => void; + reset: () => void; +} + +export const useImageTo3DStore = create((set) => ({ + results: [], + addResult: (result) => + set((state) => ({ results: [result, ...state.results] })), + updateResult: (id, updates) => + set((state) => ({ + results: state.results.map((r) => + r.id === id ? { ...r, ...updates } : r, + ), + })), + reset: () => set({ results: [] }), +})); + diff --git a/frontend/apps/editor3d/app/src/components/signaled/TopBar/TaskQueue.tsx b/frontend/apps/editor3d/app/src/components/signaled/TopBar/TaskQueue.tsx index aa1e7ecee3..f3a227d9f8 100644 --- a/frontend/apps/editor3d/app/src/components/signaled/TopBar/TaskQueue.tsx +++ b/frontend/apps/editor3d/app/src/components/signaled/TopBar/TaskQueue.tsx @@ -25,10 +25,7 @@ import { ModelPage, } from "@storyteller/ui-model-selector"; import { Button } from "@storyteller/ui-button"; -import { - getProviderDisplayName, - getModelDisplayName, -} from "@storyteller/model-list"; +import { getProviderDisplayName } from "@storyteller/model-list"; import { CloseButton } from "@storyteller/ui-close-button"; import { ActionReminderModal } from "@storyteller/ui-action-reminder-modal"; import { TaskMediaFileClass } from "@storyteller/api-enums"; @@ -37,6 +34,7 @@ import { THUMBNAIL_SIZES, getPlaceholderForMediaClass, } from "@storyteller/common"; +import { coverImageCache } from "~/pages/PageImageTo3DObject/ImageTo3DStore"; type InProgressTask = { id: string; @@ -68,7 +66,7 @@ const InProgressCard = ({ onDismiss?: () => void; }) => { return ( -
+
{ return (
@@ -212,13 +213,37 @@ export const TaskQueue = () => { ? getProviderDisplayName(String(t.provider).toLowerCase()) : undefined; const taskTypeStr = t.task_type ? String(t.task_type) : ""; - const kind = taskTypeStr.includes("video") - ? "Video" - : taskTypeStr.includes("image") - ? "Image" - : undefined; + const is3DModel = + taskTypeStr.includes("3d") || + taskTypeStr.includes("object") || + taskTypeStr.includes("dimensional"); + const kind = is3DModel + ? "3D Model" + : taskTypeStr.includes("video") + ? "Video" + : taskTypeStr.includes("image") + ? "Image" + : undefined; + + const formatModelName = (modelType: string): string => { + const formatted = modelType + .replace(/_/g, " ") + .replace(/(\d)([a-zA-Z])/g, "$1 $2") + .replace(/([a-zA-Z])(\d)/g, "$1 $2") + .split(" ") + .map((word) => + word.length <= 2 + ? word.toUpperCase() + : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(), + ) + .join(" ") + .replace(/\s+/g, " ") + .trim(); + return formatted; + }; + const modelDisplay = t.model_type - ? getModelDisplayName(String(t.model_type)) + ? formatModelName(String(t.model_type)) : undefined; const title = kind || "Task"; @@ -287,28 +312,33 @@ export const TaskQueue = () => { (b.completed_at?.getTime() || b.updated_at.getTime()) - (a.completed_at?.getTime() || a.updated_at.getTime()), ) - .map((t: TaskQueueItem) => ({ - id: t.id, - ...formatTitleParts(t), - thumbnailUrl: - getThumbnailUrl( - t.completed_item?.primary_media_file - ?.maybe_thumbnail_url_template, - { width: THUMBNAIL_SIZES.MEDIUM }, - ) || undefined, - imageUrls: t.completed_item?.primary_media_file?.cdn_url - ? [t.completed_item?.primary_media_file?.cdn_url] - : [], - mediaTokens: (() => { - const primaryToken = t.completed_item?.primary_media_file?.token; - const tokens: string[] = primaryToken ? [primaryToken] : []; - return tokens; - })(), - mediaFileClass: t.completed_item?.media_file_class, - batchImageToken: t.completed_item?.maybe_batch_token, - completedAt: t.completed_at, - updatedAt: t.updated_at, - })); + .map((t: TaskQueueItem) => { + const mediaToken = t.completed_item?.primary_media_file?.token; + // Try server thumbnail first, then fall back to local cache + const serverThumbnail = getThumbnailUrl( + t.completed_item?.primary_media_file?.maybe_thumbnail_url_template, + { width: THUMBNAIL_SIZES.MEDIUM }, + ); + const cachedThumbnail = mediaToken ? coverImageCache.get(mediaToken) : undefined; + + return { + id: t.id, + ...formatTitleParts(t), + thumbnailUrl: serverThumbnail || cachedThumbnail || undefined, + imageUrls: t.completed_item?.primary_media_file?.cdn_url + ? [t.completed_item?.primary_media_file?.cdn_url] + : [], + mediaTokens: (() => { + const primaryToken = t.completed_item?.primary_media_file?.token; + const tokens: string[] = primaryToken ? [primaryToken] : []; + return tokens; + })(), + mediaFileClass: t.completed_item?.media_file_class, + batchImageToken: t.completed_item?.maybe_batch_token, + completedAt: t.completed_at, + updatedAt: t.updated_at, + }; + }); setInProgress(inProg); setCompleted(done); @@ -334,6 +364,16 @@ export const TaskQueue = () => { load(); const id = setInterval(load, 5000); + + // Listen for cover image uploads to refresh and show new thumbnails + const handleCoverUploaded = () => { + if (!cancelled) { + // Small delay to allow server to process + setTimeout(load, 1000); + } + }; + window.addEventListener("cover-image-uploaded", handleCoverUploaded); + let unlisten: Promise | null = null; (async () => { // Update immediately when Tauri signals a generation completion @@ -346,6 +386,7 @@ export const TaskQueue = () => { return () => { cancelled = true; clearInterval(id); + window.removeEventListener("cover-image-uploaded", handleCoverUploaded); if (unlisten) { unlisten.then((f) => f()); } diff --git a/frontend/apps/editor3d/app/src/config/appMenu.tsx b/frontend/apps/editor3d/app/src/config/appMenu.tsx index 671a09cde4..313fb9e83c 100644 --- a/frontend/apps/editor3d/app/src/config/appMenu.tsx +++ b/frontend/apps/editor3d/app/src/config/appMenu.tsx @@ -10,6 +10,7 @@ import { faGlobe, } from "@fortawesome/pro-solid-svg-icons"; import { useTabStore, TabId } from "~/pages/Stores/TabState"; +import { set3DPageMounted } from "~/pages/PageEnigma/Editor/editor"; export type AppId = | "IMAGE" @@ -105,9 +106,8 @@ export const ALL_APPS: FullAppItem[] = [ description: "Convert references into textured assets", icon: faCube, category: "generate", - // action: "IMAGE_TO_3D_OBJECT", + action: "IMAGE_TO_3D_OBJECT", color: "bg-emerald-500/40", - badge: "SOON", }, { id: "image-to-3d-world", @@ -211,6 +211,11 @@ export const goToApp = (action?: string) => { "IMAGE_TO_3D_WORLD", ].includes(action) ) { + if (action === "3D") { + set3DPageMounted(true); + } else { + set3DPageMounted(false); + } useTabStore.getState().setActiveTab(action as TabId); } }; diff --git a/frontend/apps/editor3d/app/src/pages/PageEdit/BaseImageSelector.tsx b/frontend/apps/editor3d/app/src/pages/PageEdit/BaseImageSelector.tsx index e70efe26b8..d3ee771d6f 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEdit/BaseImageSelector.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageEdit/BaseImageSelector.tsx @@ -1,15 +1,13 @@ -import { faImage, faUpload, faImages } from "@fortawesome/pro-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faImage, faImages } from "@fortawesome/pro-solid-svg-icons"; import { downloadFileFromUrl } from "libs/api/src/lib/LocalApi"; -import { Button } from "@storyteller/ui-button"; import { GalleryModal, GalleryItem } from "@storyteller/ui-gallery-modal"; -import { LoadingSpinner } from "@storyteller/ui-loading-spinner"; -import { useRef, useState } from "react"; +import { useState } from "react"; import toast from "react-hot-toast"; import { uploadImage } from "~/components/reusable/UploadModalMedia/uploadImage"; import { UploaderStates } from "~/enums"; import { MediaFilesApi } from "@storyteller/api"; import { TutorialModalButton } from "@storyteller/ui-tutorial-modal"; +import { UploadEntryCard } from "~/components/media/UploadEntryCard"; export interface BaseSelectorImage { url: string; @@ -34,7 +32,6 @@ export const BaseImageSelector = ({ onImageSelect, showLoading = false, }: BaseImageSelectorProps) => { - const fileInputRef = useRef(null); const [isGalleryModalOpen, setIsGalleryModalOpen] = useState(false); const [selectedGalleryImages, setSelectedGalleryImages] = useState( [], @@ -49,24 +46,24 @@ export const BaseImageSelector = ({ }; const handleImageSelect = (mediaToken: string) => { - // If already selected, deselect it - // Else if not selected and under max limit, select it - if (selectedGalleryImages.includes(mediaToken)) { - setSelectedGalleryImages([]); - } else { - setSelectedGalleryImages([mediaToken]); - } + setSelectedGalleryImages((prev) => { + if (prev.includes(mediaToken)) + return prev.filter((x) => x !== mediaToken); + const maxSelections = 1; + if (prev.length >= maxSelections) { + return maxSelections === 1 ? [mediaToken] : prev; + } + return [...prev, mediaToken]; + }); }; const handleUseGalleryImages = (selectedItems: GalleryItem[]) => { - // We only want one file - if (selectedItems.length !== 1) { - return; - } const item = selectedItems[0]; - if (!item.fullImage) { + if (!item || !item.fullImage) { + toast.error("No image selected"); return; } + const referenceImage: BaseSelectorImage = { url: item.fullImage, mediaToken: item.id, @@ -75,83 +72,75 @@ export const BaseImageSelector = ({ sendImageEvent(referenceImage); }; - const handleUploadClick = () => { - if (fileInputRef.current) { - fileInputRef.current.click(); - } - }; + const handleFileUpload = (files: FileList) => { + setIsLoading(true); - const handleFileUpload = (event: React.ChangeEvent) => { - const files = event.target.files; - if (files) { - setIsLoading(true); - - Array.from(files).forEach((file) => { - const reader = new FileReader(); - reader.onloadend = () => { - uploadImage({ - title: `reference-image-${Math.random() - .toString(36) - .substring(2, 15)}`, - assetFile: file, - progressCallback: (newState) => { - console.debug("Upload progress:", newState.data); - if (newState.status === UploaderStates.success && newState.data) { - const mediaToken = newState.data || ""; - // Attempt to resolve the CDN URL for the uploaded image token; fallback to data URL - (async () => { - let finalUrl = reader.result as string; - let thumbnailUrlTemplate = undefined; - try { - const api = new MediaFilesApi(); - const result = await api.GetMediaFileByToken({ - mediaFileToken: mediaToken, - }); - if (result.success && result.data) { - finalUrl = - result.data.media_links?.cdn_url || - result.data.public_bucket_url || - finalUrl; - // NB(bt,2025-10-09): I think `thumbnail_template` is wrong and that - // `maybe_thumbnail_template` is the correct API field, but upstream - // seems to imply otherwise. Let's simply check both keys for now. - thumbnailUrlTemplate = - result.data.media_links?.thumbnail_template || - (result.data.media_links as any) - ?.maybe_thumbnail_template; - } - } catch (e) { - console.warn( - "Falling back to data URL for uploaded image", - e, - ); + Array.from(files).forEach((file) => { + const reader = new FileReader(); + reader.onloadend = () => { + uploadImage({ + title: `reference-image-${Math.random() + .toString(36) + .substring(2, 15)}`, + assetFile: file, + progressCallback: (newState) => { + console.debug("Upload progress:", newState.data); + if (newState.status === UploaderStates.success && newState.data) { + const mediaToken = newState.data || ""; + (async () => { + let finalUrl = reader.result as string; + let thumbnailUrlTemplate = undefined; + try { + const api = new MediaFilesApi(); + const result = await api.GetMediaFileByToken({ + mediaFileToken: mediaToken, + }); + if (result.success && result.data) { + finalUrl = + result.data.media_links?.cdn_url || + result.data.public_bucket_url || + finalUrl; + const mediaLinks = result.data.media_links as + | { + thumbnail_template?: string; + maybe_thumbnail_template?: string; + } + | undefined; + thumbnailUrlTemplate = + mediaLinks?.thumbnail_template || + mediaLinks?.maybe_thumbnail_template; } + } catch (e) { + console.warn( + "Falling back to data URL for uploaded image", + e, + ); + } - const referenceImage: BaseSelectorImage = { - mediaToken, - url: finalUrl, - fullImageUrl: finalUrl, - thumbnailUrlTemplate: thumbnailUrlTemplate, - }; + const referenceImage: BaseSelectorImage = { + mediaToken, + url: finalUrl, + fullImageUrl: finalUrl, + thumbnailUrlTemplate: thumbnailUrlTemplate, + }; - toast.success("Image uploaded successfully!"); - sendImageEvent(referenceImage); - setIsLoading(false); - })(); - } else if ( - newState.status === UploaderStates.assetError || - newState.status === UploaderStates.imageCreateError - ) { - toast.error("Upload failed. Please try again."); + toast.success("Image uploaded successfully!"); + sendImageEvent(referenceImage); setIsLoading(false); - } - }, - }); - }; + })(); + } else if ( + newState.status === UploaderStates.assetError || + newState.status === UploaderStates.imageCreateError + ) { + toast.error("Upload failed. Please try again."); + setIsLoading(false); + } + }, + }); + }; - reader.readAsDataURL(file); - }); - } + reader.readAsDataURL(file); + }); }; const sendImageEvent = (image: BaseSelectorImage) => { @@ -161,60 +150,24 @@ export const BaseImageSelector = ({ return ( <> -
- - {isLoading || showLoading ? ( -
- Uploading image... - -
- ) : ( - <> -
-
-
- -
-
-
-

- Edit Image -

-

- Click to upload or drag and drop an image here to edit -

-
-
-
- - -
- - )} +
+
+ +
diff --git a/frontend/apps/editor3d/app/src/pages/PageEdit/DrawToolControlBar.tsx b/frontend/apps/editor3d/app/src/pages/PageEdit/DrawToolControlBar.tsx deleted file mode 100644 index 641e4e4724..0000000000 --- a/frontend/apps/editor3d/app/src/pages/PageEdit/DrawToolControlBar.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { faPlusCircle, faMinusCircle, faDrawCircle, faEraser, faPaintbrush } from "@fortawesome/pro-regular-svg-icons"; -import { EditOperation } from "./stores/EditState"; -import { ButtonIconSelect } from "@storyteller/ui-button-icon-select"; -import { SliderV2 } from "@storyteller/ui-sliderv2"; -import { faPaintBrush } from "@fortawesome/pro-solid-svg-icons"; - -export interface DrawToolControlBarProps { - currentMode: EditOperation; - currentSize: number; - onModeChange?: (mode: EditOperation) => void; - onSizeChange?: (size: number) => void; -} - -const DrawToolControlBar = ({ - currentMode, - currentSize, - onModeChange, - onSizeChange = () => { } -}: DrawToolControlBarProps) => { - - const modes = [ - { - value: "add", - icon: faPaintBrush, - text: "Draw", - tooltip: "Draw strokes", - }, - { - value: "minus", - icon: faEraser, - text: "Erase", - tooltip: "Erases strokes", - }, - ]; - - return ( -
-
- - -
-
- ) -} - -export default DrawToolControlBar; diff --git a/frontend/apps/editor3d/app/src/pages/PageEdit/EditPaintSurface.tsx b/frontend/apps/editor3d/app/src/pages/PageEdit/EditPaintSurface.tsx index bfad38216e..1658faf76b 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEdit/EditPaintSurface.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageEdit/EditPaintSurface.tsx @@ -37,6 +37,9 @@ export type EditPaintSurfaceProps = { activeTool?: ActiveEditTool; brushColor?: string; brushSize?: number; + markerBrushSize?: number; + eraserBrushSize?: number; + markerColor?: string; onSelectionChange?: (isSelecting: boolean) => void; stageRef: React.RefObject; transformerRefs: React.RefObject<{ [key: string]: Konva.Transformer }>; @@ -52,6 +55,9 @@ export const EditPaintSurface = ({ activeTool = "select", brushColor = "#000000", brushSize = 5, + markerBrushSize = 5, + eraserBrushSize = 25, + markerColor = "#FF0000", onSelectionChange, stageRef, transformerRefs, @@ -94,7 +100,9 @@ export const EditPaintSurface = ({ endY: number; } | null>(null); const [isSelecting, setIsSelecting] = useState(false); - const [isDragging, setIsDragging] = useState(undefined); + const [isDragging, setIsDragging] = useState( + undefined, + ); // Add a ref to track if we're currently selecting const isSelectingRef = React.useRef(false); @@ -167,7 +175,13 @@ export const EditPaintSurface = ({ activeTool: string, nodeDraggable: boolean, ): boolean => { - return activeTool !== "edit" && nodeDraggable && !isMiddleMousePressed; + return ( + activeTool !== "edit" && + activeTool !== "marker" && + activeTool !== "eraser" && + nodeDraggable && + !isMiddleMousePressed + ); }; // Add state for middle mouse button @@ -220,28 +234,52 @@ export const EditPaintSurface = ({ } // Handle drawing tools - only start if within bounds - if (activeTool === "edit" && isWithinLeftPanel(stagePoint)) { + if ( + (activeTool === "edit" || + activeTool === "marker" || + activeTool === "eraser") && + isWithinLeftPanel(stagePoint) + ) { const lineId = `line-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const opacity = activeTool === "edit" ? store.brushOpacity : 1; const composite = - store.editOperation === "add" ? "source-over" : "destination-out"; + activeTool === "eraser" + ? "destination-out" + : activeTool === "marker" + ? "source-over" + : store.editOperation === "add" + ? "source-over" + : "destination-out"; const strokeColor = - store.editOperation === "add" ? brushColor : "#FFFFFF"; // Eraser uses transparent stroke + activeTool === "eraser" + ? "#FFFFFF" + : activeTool === "marker" + ? markerColor + : store.editOperation === "add" + ? brushColor + : "#FFFFFF"; + + const lineStrokeWidth = + activeTool === "eraser" + ? eraserBrushSize / stage.scaleX() + : activeTool === "marker" + ? markerBrushSize / stage.scaleX() + : brushSize / stage.scaleX(); const newLineNode: LineNode = { id: lineId, type: "line", points: [stagePoint.x, stagePoint.y], stroke: strokeColor, - strokeWidth: brushSize / stage.scaleX(), - draggable: true, + strokeWidth: lineStrokeWidth, + draggable: activeTool === "marker", opacity: opacity, locked: false, - zIndex: 0, // Default zIndex, can be adjusted later + zIndex: 0, globalCompositeOperation: composite, }; store.selectNode(null); - store.addLineNode(newLineNode, false); // Don't save state when starting line + store.addLineNode(newLineNode, false); setCurrentLineId(lineId); setIsDrawing(true); setLastPoint(stagePoint); @@ -291,14 +329,67 @@ export const EditPaintSurface = ({ const isWithinCanvas = isWithinLeftPanel(stagePoint); // Update cursor position and visibility based on tool and canvas position - if (activeTool === "edit") { + if ( + activeTool === "edit" || + activeTool === "marker" || + activeTool === "eraser" + ) { if (isWithinCanvas || isDrawing) { stage.container().style.cursor = "none"; - store.setCursorPosition(pointer); - store.setCursorVisible(true); + + // Update cursor directly without triggering React re-render + const cursorNode = cursorShapeRef.current; + const cursorLayer = cursorLayerRef.current; + if (cursorNode && cursorLayer) { + const stageX = stage.x(); + const stageY = stage.y(); + const scaleX = stage.scaleX(); + const scaleY = stage.scaleY(); + + cursorLayer.x(-stageX / scaleX); + cursorLayer.y(-stageY / scaleY); + cursorLayer.scaleX(1 / scaleX); + cursorLayer.scaleY(1 / scaleY); + + cursorNode.visible(true); + cursorNode.position(pointer); + + const cursorRadius = + activeTool === "eraser" + ? eraserBrushSize / 2 + : activeTool === "marker" + ? markerBrushSize / 2 + : brushSize / 2; + cursorNode.radius(cursorRadius); + + if (activeTool === "marker") { + cursorNode.fill(markerColor); + cursorNode.stroke("rgba(255, 255, 255, 0.7)"); + cursorNode.strokeWidth(1); + } else if (activeTool === "eraser") { + cursorNode.fill("rgba(255, 255, 255, 0.3)"); + cursorNode.stroke("rgba(0, 0, 0, 0.7)"); + cursorNode.strokeWidth(1); + } else if (store.editOperation === "add") { + cursorNode.fill(brushColor); + cursorNode.stroke("rgba(255, 255, 255, 0.7)"); + cursorNode.strokeWidth(1); + } else { + cursorNode.fill("rgba(255, 255, 255, 0.3)"); + cursorNode.stroke("rgba(0, 0, 0, 0.7)"); + cursorNode.strokeWidth(1); + } + + cursorLayer.batchDraw(); + } } else { stage.container().style.cursor = "grab"; - store.setCursorVisible(false); + const cursorNode = cursorShapeRef.current; + const cursorLayer = cursorLayerRef.current; + if (cursorNode && cursorLayer) { + cursorNode.visible(false); + cursorLayer.batchDraw(); + } } } else { if (isWithinCanvas) { @@ -306,10 +397,14 @@ export const EditPaintSurface = ({ } else { stage.container().style.cursor = "grab"; } - store.setCursorVisible(false); + const cursorNode = cursorShapeRef.current; + const cursorLayer = cursorLayerRef.current; + if (cursorNode && cursorLayer) { + cursorNode.visible(false); + cursorLayer.batchDraw(); + } } - // Handle panning with improved dragging if (isDragging) { const currentStage = e.target.getStage(); @@ -319,11 +414,11 @@ export const EditPaintSurface = ({ const displacement = { x: pointer.x - isDragging.x, - y: pointer.y - isDragging.y - } + y: pointer.y - isDragging.y, + }; const newPos = { x: isDragging.anchorX + displacement.x, - y: isDragging.anchorY + displacement.y + y: isDragging.anchorY + displacement.y, }; console.log(isDragging, displacement, newPos); currentStage.position(newPos); @@ -333,7 +428,13 @@ export const EditPaintSurface = ({ } // Handle drawing - only add points if within bounds - if (isDrawing && currentLineId && activeTool === "edit") { + if ( + isDrawing && + currentLineId && + (activeTool === "edit" || + activeTool === "marker" || + activeTool === "eraser") + ) { const point = stage.getPointerPosition(); if (!point) return; @@ -342,18 +443,15 @@ export const EditPaintSurface = ({ y: (point.y - stage.y()) / stage.scaleY(), }; - // Continue drawing regardless of canvas boundaries - const currentLine = store.lineNodes.find( - (line) => line.id === currentLineId, - ); - if (currentLine) { - const updatedPoints = [ - ...currentLine.points, - stagePoint.x, - stagePoint.y, - ]; - store.updateLineNode(currentLineId, { points: updatedPoints }, false); // Don't save state while drawing + // Update line directly on the Konva layer without React + const lineNode = stage.findOne(`#${currentLineId}`) as Konva.Line; + if (lineNode) { + const currentPoints = lineNode.points(); + currentPoints.push(stagePoint.x, stagePoint.y); + lineNode.points(currentPoints); + lineNode.getLayer()?.batchDraw(); } + setLastPoint(stagePoint); } @@ -383,18 +481,27 @@ export const EditPaintSurface = ({ setSelectionRect((prev) => prev ? { - // Ensure prev is not null - ...prev, - endX: clampedPoint.x, - endY: clampedPoint.y, - } + // Ensure prev is not null + ...prev, + endX: clampedPoint.x, + endY: clampedPoint.y, + } : null, ); } }; const handleStageMouseUp = () => { - if (isDrawing) { + if (isDrawing && currentLineId) { + // Update store with final line points after drawing is complete + const stage = stageRef.current; + if (stage) { + const lineNode = stage.findOne(`#${currentLineId}`) as Konva.Line; + if (lineNode) { + const finalPoints = lineNode.points(); + store.updateLineNode(currentLineId, { points: finalPoints }, false); + } + } store.saveState(); // Save state only when the stroke is complete } @@ -530,7 +637,9 @@ export const EditPaintSurface = ({ const container = e.target.getStage()?.container(); if (!container) return; const defaultCursor = - activeTool === "edit" + activeTool === "edit" || + activeTool === "marker" || + activeTool === "eraser" ? "none" : activeTool === "select" ? "grab" @@ -546,16 +655,14 @@ export const EditPaintSurface = ({ return; } - if (activeTool === "edit") { + if ( + activeTool === "edit" || + activeTool === "marker" || + activeTool === "eraser" + ) { stage.container().style.cursor = "none"; - const pointer = stage.getPointerPosition(); - if (pointer) { - store.setCursorPosition(pointer); - store.setCursorVisible(true); - } } else { stage.container().style.cursor = "default"; - store.setCursorVisible(false); } }; @@ -564,8 +671,12 @@ export const EditPaintSurface = ({ if (stage) { stage.container().style.cursor = "default"; } - store.setCursorVisible(false); - store.setCursorPosition(null); + const cursorNode = cursorShapeRef.current; + const cursorLayer = cursorLayerRef.current; + if (cursorNode && cursorLayer) { + cursorNode.visible(false); + cursorLayer.batchDraw(); + } }; // Update cursor appearance @@ -582,7 +693,7 @@ export const EditPaintSurface = ({ const touch2 = touches[1]; return Math.sqrt( Math.pow(touch2.clientX - touch1.clientX, 2) + - Math.pow(touch2.clientY - touch1.clientY, 2), + Math.pow(touch2.clientY - touch1.clientY, 2), ); }; @@ -649,59 +760,63 @@ export const EditPaintSurface = ({ } }; + // Only update cursor appearance when tool or size changes, not on every mouse move useLayoutEffect(() => { const cursorNode = cursorShapeRef.current; const cursorLayer = cursorLayerRef.current; - const stage = stageRef.current; - if (!cursorNode || !cursorLayer || !stage) return; + if (!cursorNode || !cursorLayer) return; - if (store.cursorVisible && store.cursorPosition && activeTool === "edit") { - // Counteract stage transforms to position cursor in screen space - const stageX = stage.x(); - const stageY = stage.y(); - const scaleX = stage.scaleX(); - const scaleY = stage.scaleY(); + const cursorRadius = + activeTool === "eraser" + ? eraserBrushSize / 2 + : activeTool === "marker" + ? markerBrushSize / 2 + : brushSize / 2; + cursorNode.radius(cursorRadius); - cursorLayer.x(-stageX / scaleX); - cursorLayer.y(-stageY / scaleY); - cursorLayer.scaleX(1 / scaleX); - cursorLayer.scaleY(1 / scaleY); - - cursorNode.visible(true); - cursorNode.position(store.cursorPosition); - cursorNode.radius(brushSize / 2); - - if (store.editOperation === "add") { - cursorNode.fill(brushColor); - cursorNode.stroke("rgba(255, 255, 255, 0.7)"); - cursorNode.strokeWidth(1); - } else { - cursorNode.fill("rgba(255, 255, 255, 0.3)"); - cursorNode.stroke("rgba(0, 0, 0, 0.7)"); - cursorNode.strokeWidth(1); - } + if (activeTool === "marker") { + cursorNode.fill(markerColor); + cursorNode.stroke("rgba(255, 255, 255, 0.7)"); + cursorNode.strokeWidth(1); + } else if (activeTool === "eraser") { + cursorNode.fill("rgba(255, 255, 255, 0.3)"); + cursorNode.stroke("rgba(0, 0, 0, 0.7)"); + cursorNode.strokeWidth(1); + } else if (store.editOperation === "add") { + cursorNode.fill(brushColor); + cursorNode.stroke("rgba(255, 255, 255, 0.7)"); + cursorNode.strokeWidth(1); } else { - cursorNode.visible(false); + cursorNode.fill("rgba(255, 255, 255, 0.3)"); + cursorNode.stroke("rgba(0, 0, 0, 0.7)"); + cursorNode.strokeWidth(1); + } + + if (cursorNode.visible()) { + cursorLayer.batchDraw(); } - cursorLayer.batchDraw(); }, [ - store.cursorVisible, - store.cursorPosition, activeTool, brushColor, brushSize, - stageRef, + markerBrushSize, + eraserBrushSize, + markerColor, store.editOperation, ]); const renderNode = (node: Node | LineNode) => { // Node Callbacks const handleNodeMouseDown = ( - e: Konva.KonvaEventObject, + e: Konva.KonvaEventObject, nodeId: string, ) => { - // Don't select nodes when edit tool is active - if (activeTool === "edit") { + // Don't select nodes when edit, marker, or eraser tool is active + if ( + activeTool === "edit" || + activeTool === "marker" || + activeTool === "eraser" + ) { return; } @@ -755,7 +870,7 @@ export const EditPaintSurface = ({ }; const handleNodeDragStart = ( - e: Konva.KonvaEventObject, + e: Konva.KonvaEventObject, nodeId: string, ) => { const targetNode = e.target as Konva.Node & { @@ -769,7 +884,7 @@ export const EditPaintSurface = ({ }; const handleNodeDragMove = ( - e: Konva.KonvaEventObject, + e: Konva.KonvaEventObject, nodeId: string, ) => { const targetNode = e.target as Konva.Node & { @@ -789,7 +904,7 @@ export const EditPaintSurface = ({ }; const handleNodeDragEnd = ( - e: Konva.KonvaEventObject, + e: Konva.KonvaEventObject, nodeId: string, ) => { const targetNode = e.target as Konva.Node & { @@ -902,7 +1017,7 @@ export const EditPaintSurface = ({ const lineNode = node as LineNode; const handleLineDragStart = ( - e: Konva.KonvaEventObject, + e: Konva.KonvaEventObject, // eslint-disable-next-line @typescript-eslint/no-unused-vars _nodeId: string, // NodeId is not used but kept for function signature compatibility ) => { @@ -914,7 +1029,7 @@ export const EditPaintSurface = ({ targetNode.lastY = targetNode.y(); }; const handleLineDragMove = ( - e: Konva.KonvaEventObject, + e: Konva.KonvaEventObject, // eslint-disable-next-line @typescript-eslint/no-unused-vars _nodeId: string, // NodeId is not used but kept for function signature compatibility ) => { @@ -926,7 +1041,7 @@ export const EditPaintSurface = ({ targetNode.lastY = targetNode.y(); // but is set for consistency or future use }; const handleLineDragEnd = ( - e: Konva.KonvaEventObject, + e: Konva.KonvaEventObject, nodeId: string, ) => { // Move the line node in the store @@ -1188,6 +1303,37 @@ export const EditPaintSurface = ({ loadImageFromUrl(checkerboard).then(setCheckerImage); }, []); + // Smooth fade transition when base image changes + useEffect(() => { + if (!store.baseImageBitmap) return; + + const imageNode = baseImageRef.current; + if (!imageNode) return; + + // Fade out + const fadeOut = new Konva.Tween({ + node: imageNode, + opacity: 0, + duration: 0.08, + onFinish: () => { + // Fade in + const fadeIn = new Konva.Tween({ + node: imageNode, + opacity: 1, + duration: 0.15, + }); + fadeIn.play(); + }, + }); + + fadeOut.play(); + + return () => { + fadeOut.destroy(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [store.baseImageBitmap]); + return ( void; + onColorChange?: (color: string) => void; +} + +function useDebounced void, A extends unknown[]>( + fn: T, + ms = 75, +) { + const timer = useRef | null>(null); + return (...args: A) => { + if (timer.current) clearTimeout(timer.current); + timer.current = setTimeout(() => fn(...args), ms); + }; +} + +const MarkerToolControlBar = ({ + currentSize, + currentColor, + activeTool, + showColorPicker: showColorPickerProp, + onSizeChange, + onColorChange, +}: MarkerToolControlBarProps) => { + const [showColorPickerPanel, setShowColorPickerPanel] = useState(false); + const [hsva, setHsva] = useState(hexToHsva(currentColor)); + + const isEraser = activeTool === "eraser"; + const shouldShowColorPicker = showColorPickerProp && !isEraser; + const maxSize = isEraser ? 50 : 20; + + const sendColor = useDebounced<(hex: string) => void, [string]>( + (hex: string) => { + onColorChange?.(hex); + }, + 75, + ); + + return ( +
+
+ {shouldShowColorPicker && ( +
+ + + {showColorPickerPanel && ( +
setShowColorPickerPanel(false)} + > +
+ + + { + setHsva(c); + const hex = hsvaToHex(c); + sendColor(hex); + }} + className="brush-picker" + /> +
+
+ )} +
+ )} + + onSizeChange?.(size)} + step={1} + innerLabel={"Size " + currentSize + "pt"} + showDecrement + showIncrement + /> +
+
+ ); +}; + +export default MarkerToolControlBar; diff --git a/frontend/apps/editor3d/app/src/pages/PageEdit/PageEdit.tsx b/frontend/apps/editor3d/app/src/pages/PageEdit/PageEdit.tsx index 4f040e7e3b..5aab907f16 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEdit/PageEdit.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageEdit/PageEdit.tsx @@ -10,7 +10,7 @@ import { ActiveEditTool, useEditStore } from "./stores/EditState"; import { EditPaintSurface } from "./EditPaintSurface"; import { normalizeCanvas } from "../../Helpers/CanvasHelpers"; import { BaseImageSelector, BaseSelectorImage } from "./BaseImageSelector"; -import DrawToolControlBar from "./DrawToolControlBar"; +import MarkerToolControlBar from "./MarkerToolControlBar"; import { ClassyModelSelector, useSelectedImageModel, @@ -142,14 +142,30 @@ const PageEdit = () => { const canvasW = store.getAspectRatioDimensions().width; const canvasH = store.getAspectRatioDimensions().height; + // Account for top toolbar and bottom prompt box + // Top toolbar (MarkerToolControlBar): ~140px from top (toolbar + padding) + // Bottom prompt box: ~200px from bottom (can vary with reference images) + // Mode selector above prompt: ~60px + const topToolbarHeight = + store.activeTool === "marker" || + store.activeTool === "eraser" || + store.activeTool === "edit" + ? 140 + : 0; + + // Check if model supports masking or is nano banana (has mode selector) + const hasModeSelectorUI = + selectedImageModel?.usesInpaintingMask || + selectedImageModel?.id === "gemini_25_flash"; + const bottomUIHeight = hasModeSelectorUI ? 260 : 200; + const totalVerticalReserved = topToolbarHeight + bottomUIHeight; + // Calculate optimal scale to fit the canvas in the container - // Leave horizontal margin via factor, and explicit vertical padding const paddingFactor = 0.95; // horizontal padding (left/right) - const verticalPaddingPx = 128; // explicit top/bottom margin in pixels const scaleX = (containerWidth * paddingFactor) / canvasW; const availableHeight = Math.max( 0, - containerHeight - verticalPaddingPx * 2, + containerHeight - totalVerticalReserved, ); const scaleY = availableHeight / canvasH; @@ -162,20 +178,20 @@ const PageEdit = () => { // Apply the calculated scale stage.scale({ x: boundedScale, y: boundedScale }); - // Calculate position to center canvas in container (creates ~verticalPaddingPx margins) + // Calculate position to center canvas in available space (between toolbars) + const verticalOffset = (topToolbarHeight - bottomUIHeight) / 2; stage.position({ x: (containerWidth - canvasW * boundedScale) / 2, - y: (containerHeight - canvasH * boundedScale) / 2, + y: (containerHeight - canvasH * boundedScale) / 2 + verticalOffset, }); - // NOTE: Store isn't used as dependency because it's a different const reference each time - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [selectedImageModel, store]); // Auto-fit when the base image becomes available and stage is ready + // Also re-fit when active tool or model changes (as UI layout changes) useEffect(() => { if (!store.baseImageBitmap) return; if (!stageRef.current) return; - // Defer to ensure container has laid out with correct size + // Defer to ensure container has laid out with correct size and toolbars have rendered const id = requestAnimationFrame(() => { const id2 = requestAnimationFrame(() => { void onFitPressed(); @@ -184,7 +200,12 @@ const PageEdit = () => { }); return () => cancelAnimationFrame(id); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [store.baseImageBitmap, stageRef.current]); + }, [ + store.baseImageBitmap, + stageRef.current, + store.activeTool, + selectedImageModel, + ]); // Re-center on window resize when an image is loaded useEffect(() => { @@ -289,24 +310,28 @@ const PageEdit = () => { [generationCount, selectedImageModel, store.baseImageInfo?.mediaToken], ); - //const modelConfig = lookupModelByTauriId(selectedImageModel!.tauriId); - //const supportsMaskedInpainting = modelConfig?.tags?.includes(ModelTag.MaskedInpainting) ?? false; + const isNanoBananaModel = selectedImageModel?.id === "gemini_25_flash"; const supportsMaskedInpainting = selectedImageModel?.usesInpaintingMask ?? false; useEffect(() => { - if ( + if (isNanoBananaModel) { + if (store.activeTool !== "marker" && store.activeTool !== "eraser") { + store.setActiveTool("marker"); + } + } else if ( !supportsMaskedInpainting && (store.activeTool !== "select" || store.lineNodes.length > 0) ) { - // TODO: Implement a new mode for unsupported masking and hide the nodes layer instead of clearing store.setActiveTool("select"); store.clearLineNodes(); - } else if (supportsMaskedInpainting && store.activeTool === "select") { - // Switch to edit mode if supported and currently in select mode + } else if ( + supportsMaskedInpainting && + (store.activeTool === "select" || store.activeTool === "marker") + ) { store.setActiveTool("edit"); } - }, [store, supportsMaskedInpainting]); + }, [store, supportsMaskedInpainting, isNanoBananaModel]); /* * @@ -323,7 +348,7 @@ const PageEdit = () => { return (
@@ -346,19 +371,56 @@ const PageEdit = () => { return ( <>
-
- -
+ {(store.activeTool === "marker" || store.activeTool === "eraser") && + isNanoBananaModel && ( +
+ +
+ )} + {(store.activeTool === "edit" || store.activeTool === "eraser") && + supportsMaskedInpainting && ( +
+ +
+ )}
{ generationCount={generationCount} onGenerationCountChange={setGenerationCount} supportsMaskedInpainting={supportsMaskedInpainting} + isNanoBananaModel={isNanoBananaModel} + onUndo={store.undo} + onRedo={store.redo} />
@@ -461,6 +526,9 @@ const PageEdit = () => { activeTool={store.activeTool} brushColor={"rgba(39, 187, 245, 0.54)"} brushSize={store.brushSize} + markerBrushSize={store.markerBrushSize} + eraserBrushSize={store.eraserBrushSize} + markerColor={store.markerColor} onSelectionChange={setIsSelecting} stageRef={stageRef} transformerRefs={transformerRefs} diff --git a/frontend/apps/editor3d/app/src/pages/PageEdit/PromptEditor/PromptEditor.tsx b/frontend/apps/editor3d/app/src/pages/PageEdit/PromptEditor/PromptEditor.tsx index 7c04a2bdfd..92cda47ecf 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEdit/PromptEditor/PromptEditor.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageEdit/PromptEditor/PromptEditor.tsx @@ -1,30 +1,10 @@ import React from "react"; import { PromptBoxEdit, PromptBoxEditProps } from "@storyteller/ui-promptbox"; -const PromptEditor: React.FC = ({ - onModeChange, - selectedMode, - onGenerateClick, - isDisabled, - onFitPressed, - selectedImageModel, - generationCount, - onGenerationCountChange, - supportsMaskedInpainting -}) => { +const PromptEditor: React.FC = (props) => { return (
- +
); }; diff --git a/frontend/apps/editor3d/app/src/pages/PageEdit/stores/EditState.ts b/frontend/apps/editor3d/app/src/pages/PageEdit/stores/EditState.ts index 2ef250ac64..38e79bfc04 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEdit/stores/EditState.ts +++ b/frontend/apps/editor3d/app/src/pages/PageEdit/stores/EditState.ts @@ -5,7 +5,7 @@ import { BaseSelectorImage } from "../BaseImageSelector"; import { FetchProxy } from "libs/tauri-utils/src/lib/FetchProxy"; import { ImageBundle } from "../HistoryStack"; -export type ActiveEditTool = "select" | "edit" | "expand"; +export type ActiveEditTool = "select" | "edit" | "expand" | "marker" | "eraser"; export type EditOperation = "add" | "minus"; interface EditState { @@ -22,8 +22,11 @@ interface EditState { editOperation: EditOperation; brushColor: string; brushSize: number; + markerBrushSize: number; + eraserBrushSize: number; brushOpacity: number; fillColor: string; + markerColor: string; // Cursor state cursorPosition: { x: number; y: number } | null; @@ -104,7 +107,10 @@ interface EditState { setBrushColor: (color: string) => void; setBrushOpacity: (opacity: number) => void; setBrushSize: (size: number) => void; + setMarkerBrushSize: (size: number) => void; + setEraserBrushSize: (size: number) => void; setFillColor: (color: string) => void; + setMarkerColor: (color: string) => void; // Cursor actions setCursorPosition: (position: { x: number; y: number } | null) => void; @@ -168,8 +174,11 @@ export const useEditStore = create((set, get, store) => ({ editOperation: "add", // Default to 'add' operation brushColor: "#000000", brushSize: 30, + markerBrushSize: 5, + eraserBrushSize: 25, brushOpacity: 1, fillColor: "rgba(0, 0, 255, 0.5)", + markerColor: "#FF0000", // Cursor initial state cursorPosition: null, @@ -833,8 +842,11 @@ export const useEditStore = create((set, get, store) => ({ setEditOperation: (mode: EditOperation) => set({ editOperation: mode }), setBrushColor: (color: string) => set({ brushColor: color }), setBrushSize: (size: number) => set({ brushSize: size }), + setMarkerBrushSize: (size: number) => set({ markerBrushSize: size }), + setEraserBrushSize: (size: number) => set({ eraserBrushSize: size }), setFillColor: (color: string) => set({ fillColor: color }), setBrushOpacity: (opacity: number) => set({ brushOpacity: opacity }), + setMarkerColor: (color: string) => set({ markerColor: color }), // Cursor actions setCursorPosition: (position: { x: number; y: number } | null) => diff --git a/frontend/apps/editor3d/app/src/pages/PageEnigma/Editor/timeline.ts b/frontend/apps/editor3d/app/src/pages/PageEnigma/Editor/timeline.ts index 7e35604743..56c6664d75 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEnigma/Editor/timeline.ts +++ b/frontend/apps/editor3d/app/src/pages/PageEnigma/Editor/timeline.ts @@ -373,17 +373,13 @@ export class TimeLine { return new THREE.Vector3(0, 0, 0); } - public async addObject(data: MediaItem) { - const pos = this.getPos(); + public async addObject(data: MediaItem & { position?: { x: number; y: number; z: number } }) { + const dataPos = data.position; + const pos = dataPos + ? new THREE.Vector3(dataPos.x, dataPos.y, dataPos.z) + : this.getPos(); const media_id = data.media_id; const name = data.name; - // const obj = await this.scene.loadGlbWithPlaceholder( - // media_id, - // name, - // true, - // pos, - // this.editorEngine.version, - // ); const obj = await this.editorEngine.sceneManager?.create( media_id, diff --git a/frontend/apps/editor3d/app/src/pages/PageEnigma/comps/Controls3D/Controls3D.tsx b/frontend/apps/editor3d/app/src/pages/PageEnigma/comps/Controls3D/Controls3D.tsx index 31898ad82d..40e291764d 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEnigma/comps/Controls3D/Controls3D.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageEnigma/comps/Controls3D/Controls3D.tsx @@ -11,23 +11,14 @@ import { import { ButtonIconSelect } from "@storyteller/ui-button-icon-select"; import { Button } from "@storyteller/ui-button"; import { Tooltip } from "@storyteller/ui-tooltip"; -import { - showActionReminder, - isActionReminderOpen, -} from "@storyteller/ui-action-reminder-modal"; import { SettingsModal } from "@storyteller/ui-settings-modal"; import { EngineContext } from "../../contexts/EngineContext"; -import { useContext, useEffect, useState, useRef, useCallback } from "react"; +import { useContext, useEffect, useState } from "react"; import { assetModalVisibleDuringDrag, assetModalVisible } from "../../signals"; -import { - Create3dModal, - useCreate3dModalStore, - // eslint-disable-next-line import/no-unresolved -} from "@storyteller/ui-create-3d-modal"; +import { useTabStore } from "~/pages/Stores/TabState"; // import { v4 as uuidv4 } from "uuid"; // import { addObject } from "../../signals/objectGroup/addObject"; // import { MediaItem } from "../../models/assets"; -import { GetFalApiKey } from "@storyteller/tauri-api"; // Fix import path // eslint-disable-next-line import/no-unresolved // import { AssetType } from "~/enums"; import { AssetModal } from "../AssetMenu/AssetModal"; @@ -58,8 +49,6 @@ export const Controls3D = () => { const [uploadImageIsShowing, setUploadImageIsShowing] = useState(false); const [uploadSplatIsShowing, setUploadSplatIsShowing] = useState(false); - // Track processed 3D models by their media token to prevent duplicates - const processedModelsRef = useRef>({}); useEffect(() => { // Check if scene is empty and onboarding helper is not visible @@ -161,37 +150,8 @@ export const Controls3D = () => { assetModalVisible.value = true; }; - const handleOpenCreate3dModal = async () => { - try { - const falApiKeyResult = await GetFalApiKey(); - - if ( - falApiKeyResult.status === "success" && - "payload" in falApiKeyResult && - falApiKeyResult.payload?.key - ) { - // API key exists, open the create 3D modal - useCreate3dModalStore.getState().open(); - } else { - // No API key, show the action reminder modal - showActionReminder({ - reminderType: "default", - title: "FAL API Key Required", - message: - "To generate 3D models, you need to add a valid FAL API key in your settings.", - primaryActionText: "Add now", - secondaryActionText: "Cancel", - onPrimaryAction: () => { - // Close the action reminder modal first - isActionReminderOpen.value = false; - // Then open the settings modal - openSettingsModal(); - }, - }); - } - } catch (error) { - console.error("Error checking FAL API key:", error); - } + const handleOpenCreate3dModal = () => { + useTabStore.getState().setActiveTab("IMAGE_TO_3D_OBJECT"); }; // Open global Gallery modal @@ -222,22 +182,6 @@ export const Controls3D = () => { } }; - const openSettingsModal = () => { - setIsSettingsModalOpen(true); - }; - - // Handle completed models from the Create3dModal component - const handleModelComplete = useCallback((mediaToken: string) => { - // Check if we've already processed this model - if (mediaToken && !processedModelsRef.current[mediaToken]) { - // Mark this model as processed to avoid duplicates - processedModelsRef.current[mediaToken] = true; - - // Add the generated 3D model to the scene - // addGeneratedModelToScene(mediaToken); - } - }, []); - const modes = [ { value: "move", @@ -382,8 +326,6 @@ export const Controls3D = () => { - - setIsSettingsModalOpen(false)} diff --git a/frontend/apps/editor3d/app/src/pages/PageEnigma/signals/objectGroup/addObject.ts b/frontend/apps/editor3d/app/src/pages/PageEnigma/signals/objectGroup/addObject.ts index de14516086..c94d101931 100644 --- a/frontend/apps/editor3d/app/src/pages/PageEnigma/signals/objectGroup/addObject.ts +++ b/frontend/apps/editor3d/app/src/pages/PageEnigma/signals/objectGroup/addObject.ts @@ -4,7 +4,9 @@ import { QueueNames } from "~/pages/PageEnigma/Queue/QueueNames"; import { toEngineActions } from "~/pages/PageEnigma/Queue/toEngineActions"; import { objectGroup } from "~/pages/PageEnigma/signals"; -export function addObject(object: MediaItem) { +export function addObject( + object: MediaItem & { position?: { x: number; y: number; z: number } } +) { Queue.publish({ queueName: QueueNames.TO_ENGINE, action: toEngineActions.ADD_OBJECT, diff --git a/frontend/apps/editor3d/app/src/pages/PageImageTo3DObject/ImageTo3DObject.tsx b/frontend/apps/editor3d/app/src/pages/PageImageTo3DObject/ImageTo3DObject.tsx index 54b0a75287..114063f1e6 100644 --- a/frontend/apps/editor3d/app/src/pages/PageImageTo3DObject/ImageTo3DObject.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageImageTo3DObject/ImageTo3DObject.tsx @@ -6,8 +6,10 @@ export const ImageTo3DObject = () => { title="Generate 3D Object" subtitle="Transform your image into a 3D object with textures." variant="object" + backgroundImage="/resources/images/floating-cubes.png" /> ); }; export default ImageTo3DObject; + diff --git a/frontend/apps/editor3d/app/src/pages/PageImageTo3DObject/ImageTo3DStore.ts b/frontend/apps/editor3d/app/src/pages/PageImageTo3DObject/ImageTo3DStore.ts new file mode 100644 index 0000000000..0cefc52522 --- /dev/null +++ b/frontend/apps/editor3d/app/src/pages/PageImageTo3DObject/ImageTo3DStore.ts @@ -0,0 +1,376 @@ +import { create } from "zustand"; +import { listen } from "@tauri-apps/api/event"; +import { MediaFilesApi, MediaUploadApi } from "@storyteller/api"; +import { v4 as uuidv4 } from "uuid"; +import * as THREE from "three"; +import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; + +// Offscreen thumbnail capture function - produces a square image +async function captureModelThumbnail(modelUrl: string): Promise { + return new Promise((resolve) => { + try { + // Square dimensions for thumbnail + const size = 512; + const fov = 50; + + const scene = new THREE.Scene(); + scene.background = new THREE.Color(0x282828); + + const camera = new THREE.PerspectiveCamera(fov, 1, 0.1, 1000); // Aspect ratio 1 for square + + const renderer = new THREE.WebGLRenderer({ + antialias: true, + alpha: false, + preserveDrawingBuffer: true, + }); + renderer.setSize(size, size); + renderer.setPixelRatio(1); + + // Add lights + const ambientLight = new THREE.AmbientLight(0xffffff, 2); + scene.add(ambientLight); + + const hemisphereLight = new THREE.HemisphereLight( + 0xffffff, + 0x888888, + 1.2, + ); + scene.add(hemisphereLight); + + const keyLight = new THREE.DirectionalLight(0xffffff, 2); + keyLight.position.set(2, 10, 8); + scene.add(keyLight); + + const fillLight = new THREE.DirectionalLight(0xffffff, 1.2); + fillLight.position.set(-6, 6, -4); + scene.add(fillLight); + + const frontLight = new THREE.DirectionalLight(0xffffff, 1); + frontLight.position.set(0, 4, 10); + scene.add(frontLight); + + const loader = new GLTFLoader(); + loader.load( + modelUrl, + (gltf) => { + const model = gltf.scene; + + const box = new THREE.Box3().setFromObject(model); + const modelSize = box.getSize(new THREE.Vector3()); + + const maxDim = Math.max(modelSize.x, modelSize.y, modelSize.z); + const scale = 2 / maxDim; + model.scale.multiplyScalar(scale); + + const scaledBox = new THREE.Box3().setFromObject(model); + const scaledCenter = scaledBox.getCenter(new THREE.Vector3()); + const scaledSize = scaledBox.getSize(new THREE.Vector3()); + + model.position.x = -scaledCenter.x; + model.position.z = -scaledCenter.z; + model.position.y = -scaledBox.min.y; + + scene.add(model); + + // Calculate camera distance to fit model in view with padding + const modelHeight = scaledSize.y; + const maxModelDim = Math.max( + scaledSize.x, + scaledSize.y, + scaledSize.z, + ); + + // Calculate distance needed to fit the model based on FOV + // For a 45-degree camera angle, we view from a diagonal + const fovRad = (fov * Math.PI) / 180; + const fitDistance = maxModelDim / 2 / Math.tan(fovRad / 2); + + // Add some padding (1.3x) and account for diagonal viewing angle + const cameraDistance = fitDistance * 1.3; + + // Position camera at 45-degree angle + const angle = Math.PI / 4; // 45 degrees + camera.position.set( + Math.sin(angle) * cameraDistance, + modelHeight * 0.5 + cameraDistance * 0.4, + Math.cos(angle) * cameraDistance, + ); + camera.lookAt(0, modelHeight * 0.4, 0); + + // Render and capture + renderer.render(scene, camera); + const dataUrl = renderer.domElement.toDataURL("image/png"); + + // Cleanup + renderer.dispose(); + scene.clear(); + + resolve(dataUrl); + }, + undefined, + (error) => { + console.error("[captureModelThumbnail] Error loading model:", error); + renderer.dispose(); + scene.clear(); + resolve(null); + }, + ); + + // Timeout after 30 seconds + setTimeout(() => { + renderer.dispose(); + scene.clear(); + resolve(null); + }, 30000); + } catch (error) { + console.error("[captureModelThumbnail] Error:", error); + resolve(null); + } + }); +} + +export type ImageTo3DResult = { + id: string; + mode: "image" | "text"; + timestamp: number; + note?: string; + previewUrl?: string; + meshOnly?: boolean; + status: "pending" | "completed"; + subscriberId: string; + modelUrl?: string; + mediaToken?: string; + coverImageUploaded?: boolean; +}; + +// Global cache for cover image URLs - TaskQueue can use this as fallback +// Maps mediaToken -> cover image data URL +export const coverImageCache = new Map(); + +type ImageTo3DState = { + results: ImageTo3DResult[]; + startGeneration: ( + mode: "image" | "text", + note: string, + previewUrl: string | undefined, + meshOnly: boolean, + subscriberId?: string, + ) => string; + completeGeneration: ( + modelUrl: string, + mediaToken: string, + maybeSubscriberId?: string, + ) => void; + uploadCoverImage: ( + mediaToken: string, + thumbnailDataUrl: string, + ) => Promise; + captureAndUploadCover: ( + modelUrl: string, + mediaToken: string, + ) => Promise; + reset: () => void; +}; + +export const useImageTo3DStore = create((set, get) => ({ + results: [], + startGeneration: ( + mode: "image" | "text", + note: string, + previewUrl: string | undefined, + meshOnly: boolean, + subscriberId?: string, + ) => { + const id = subscriberId + ? subscriberId + : crypto.randomUUID + ? crypto.randomUUID() + : Math.random().toString(36).slice(2); + const result: ImageTo3DResult = { + id, + mode, + timestamp: Date.now(), + note, + previewUrl, + meshOnly, + status: "pending", + subscriberId: id, + }; + set((s) => ({ results: [result, ...s.results] })); + return id; + }, + completeGeneration: ( + modelUrl: string, + mediaToken: string, + maybeSubscriberId?: string, + ) => { + console.log("[ImageTo3DStore] completeGeneration", { + modelUrl, + mediaToken, + maybeSubscriberId, + }); + const pending = maybeSubscriberId + ? get().results.find((r) => r.subscriberId === maybeSubscriberId) + : get().results.find((r) => r.status === "pending"); + + set((s) => { + const results = [...s.results]; + const targetIdx = pending + ? results.findIndex((r) => r.subscriberId === pending.subscriberId) + : -1; + + if (targetIdx === -1) { + const generatedId = + crypto.randomUUID?.() ?? Math.random().toString(36).slice(2); + const result: ImageTo3DResult = { + id: generatedId, + subscriberId: generatedId, + mode: "image", + timestamp: Date.now(), + note: "Generated Model", + status: "completed", + modelUrl, + mediaToken, + }; + return { results: [result, ...results] }; + } + + results[targetIdx] = { + ...results[targetIdx], + status: "completed", + modelUrl, + mediaToken, + }; + + return { results }; + }); + }, + uploadCoverImage: async (mediaToken: string, thumbnailDataUrl: string) => { + try { + // Check if already uploaded + const result = get().results.find((r) => r.mediaToken === mediaToken); + if (result?.coverImageUploaded) { + console.log( + "[ImageTo3DStore] Cover image already uploaded for:", + mediaToken, + ); + return; + } + + console.log("[ImageTo3DStore] Uploading cover image for:", mediaToken); + + // Convert data URL to blob + const response = await fetch(thumbnailDataUrl); + const blob = await response.blob(); + + // Upload as image + const mediaUploadApi = new MediaUploadApi(); + const uploadResult = await mediaUploadApi.UploadImage({ + blob, + fileName: `cover-${mediaToken}.png`, + uuid: uuidv4(), + }); + + if (!uploadResult.success || !uploadResult.data) { + console.error("[ImageTo3DStore] Failed to upload cover image"); + return; + } + + const coverImageToken = uploadResult.data; + console.log("[ImageTo3DStore] Cover image uploaded:", coverImageToken); + + // Set as cover image for the 3D model + const mediaFilesApi = new MediaFilesApi(); + const setCoverResult = await mediaFilesApi.UpdateCoverImage({ + mediaFileToken: mediaToken, + imageToken: coverImageToken, + }); + + if (setCoverResult.success) { + console.log("[ImageTo3DStore] Cover image set successfully"); + + // Store in global cache for TaskQueue to use as fallback + coverImageCache.set(mediaToken, thumbnailDataUrl); + + // Mark as uploaded + set((s) => ({ + results: s.results.map((r) => + r.mediaToken === mediaToken + ? { ...r, coverImageUploaded: true } + : r, + ), + })); + // Emit event to trigger task queue refresh with the cover data + window.dispatchEvent( + new CustomEvent("cover-image-uploaded", { + detail: { mediaToken, thumbnailDataUrl }, + }), + ); + } else { + console.error( + "[ImageTo3DStore] Failed to set cover image:", + setCoverResult.errorMessage, + ); + } + } catch (error) { + console.error("[ImageTo3DStore] Error uploading cover image:", error); + } + }, + captureAndUploadCover: async (modelUrl: string, mediaToken: string) => { + try { + // Check if already uploaded + const result = get().results.find((r) => r.mediaToken === mediaToken); + if (result?.coverImageUploaded) { + console.log("[ImageTo3DStore] Cover already uploaded for:", mediaToken); + return; + } + + console.log("[ImageTo3DStore] Capturing thumbnail for:", modelUrl); + const thumbnailDataUrl = await captureModelThumbnail(modelUrl); + + if (thumbnailDataUrl) { + await get().uploadCoverImage(mediaToken, thumbnailDataUrl); + } else { + console.error("[ImageTo3DStore] Failed to capture thumbnail"); + } + } catch (error) { + console.error("[ImageTo3DStore] Error in captureAndUploadCover:", error); + } + }, + reset: () => set({ results: [] }), +})); + +interface ObjectGenerationEvent { + data: { + generated_object?: { + cdn_url: string; + media_token: string; + }; + maybe_frontend_subscriber_id?: string; + }; +} + +listen("object_generation_complete_event", (event) => { + const payload = event.payload?.data; + if (payload?.maybe_frontend_subscriber_id && payload?.generated_object) { + console.log( + "[ImageTo3DStore] Global event received for subscriber:", + payload.maybe_frontend_subscriber_id, + ); + useImageTo3DStore + .getState() + .completeGeneration( + payload.generated_object.cdn_url, + payload.generated_object.media_token, + payload.maybe_frontend_subscriber_id, + ); + + // Automatically capture and upload cover image in background + useImageTo3DStore + .getState() + .captureAndUploadCover( + payload.generated_object.cdn_url, + payload.generated_object.media_token, + ); + } +}); diff --git a/frontend/apps/editor3d/app/src/pages/PageImageTo3DObject/index.ts b/frontend/apps/editor3d/app/src/pages/PageImageTo3DObject/index.ts index a002f1863f..3c9c2ee75d 100644 --- a/frontend/apps/editor3d/app/src/pages/PageImageTo3DObject/index.ts +++ b/frontend/apps/editor3d/app/src/pages/PageImageTo3DObject/index.ts @@ -1,2 +1,3 @@ export * from "./ImageTo3DObject"; + diff --git a/frontend/apps/editor3d/app/src/pages/PageImageTo3DWorld/ImageTo3DWorld.tsx b/frontend/apps/editor3d/app/src/pages/PageImageTo3DWorld/ImageTo3DWorld.tsx index 57c321c514..851bd57774 100644 --- a/frontend/apps/editor3d/app/src/pages/PageImageTo3DWorld/ImageTo3DWorld.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageImageTo3DWorld/ImageTo3DWorld.tsx @@ -12,3 +12,4 @@ export const ImageTo3DWorld = () => { export default ImageTo3DWorld; + diff --git a/frontend/apps/editor3d/app/src/pages/PageImageTo3DWorld/index.ts b/frontend/apps/editor3d/app/src/pages/PageImageTo3DWorld/index.ts index 8d3188d488..587035eed4 100644 --- a/frontend/apps/editor3d/app/src/pages/PageImageTo3DWorld/index.ts +++ b/frontend/apps/editor3d/app/src/pages/PageImageTo3DWorld/index.ts @@ -1,2 +1,3 @@ export * from "./ImageTo3DWorld"; + diff --git a/frontend/apps/editor3d/app/src/pages/PageVideo/ImageToVideo.tsx b/frontend/apps/editor3d/app/src/pages/PageVideo/ImageToVideo.tsx index ecd8d10618..9e2705cf48 100644 --- a/frontend/apps/editor3d/app/src/pages/PageVideo/ImageToVideo.tsx +++ b/frontend/apps/editor3d/app/src/pages/PageVideo/ImageToVideo.tsx @@ -14,8 +14,8 @@ import { VideoModel } from "@storyteller/model-list"; import { animated, useSpring } from "@react-spring/web"; import { useImageToVideoStore } from "./ImageToVideoStore"; import { - useImageToVideoGenerationCompleteEvent, - ImageToVideoGenerationCompleteEvent, + useVideoGenerationCompleteEvent, + VideoGenerationCompleteEvent, } from "@storyteller/tauri-events"; // import { Badge } from "@storyteller/ui-badge"; import { twMerge } from "tailwind-merge"; @@ -49,8 +49,8 @@ const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => { clearJobTokens: () => {}, }; - useImageToVideoGenerationCompleteEvent( - async (event: ImageToVideoGenerationCompleteEvent) => { + useVideoGenerationCompleteEvent( + async (event: VideoGenerationCompleteEvent) => { if (!event.generated_video) return; completeBatch( { diff --git a/frontend/apps/editor3d/tsconfig.json b/frontend/apps/editor3d/tsconfig.json index 1d8e2a55e5..f0f1338dbb 100644 --- a/frontend/apps/editor3d/tsconfig.json +++ b/frontend/apps/editor3d/tsconfig.json @@ -67,14 +67,11 @@ { "path": "../../libs/components/loading-spinner" }, - { - "path": "../../libs/components/promptbox" - }, { "path": "../../libs/components/tutorial-modal" }, { - "path": "../../libs/common" + "path": "../../libs/components/promptbox" }, { "path": "../../libs/state/subscription" @@ -103,6 +100,9 @@ { "path": "../../libs/components/settings-modal" }, + { + "path": "../../libs/common" + }, { "path": "../../libs/api-enums" }, @@ -121,9 +121,6 @@ { "path": "../../libs/components/popover" }, - { - "path": "../../libs/components/tooltip" - }, { "path": "../../libs/components/gravatar" }, @@ -148,6 +145,15 @@ { "path": "../../libs/components/list-dropdown" }, + { + "path": "../../libs/components/viewer-3d" + }, + { + "path": "../../libs/components/tooltip" + }, + { + "path": "../../libs/components/tab-selector" + }, { "path": "../../libs/components/button" }, diff --git a/frontend/build_modules.sh b/frontend/build_modules.sh new file mode 100755 index 0000000000..f90aff6e32 --- /dev/null +++ b/frontend/build_modules.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +set -euxo pipefail + +# +# If you get errors like this: +# +# x Build failed in 342ms +# error during build: +# [commonjs--resolver] Failed to resolve entry for package "@storyteller/api". +# The package may have incorrect main/module/exports specified in its package.json. +# ... +# +# Run this script! +# + +echo 'TEMPORARY HACK: Build internal dependencies of editor3d since nx is not finding them...' + +# A 'clean' will remove nx +npm install + +# TODO: Temp commented out +nx build "@storyteller/api" +nx build "@storyteller/ui-gallery-modal" +nx build "@storyteller/tauri-events" +nx build "@storyteller/ui-pricing-modal" +nx build "@storyteller/ui-loading" +nx build "@storyteller/ui-promptbox" +nx build "@storyteller/ui-model-selector" +nx build "@storyteller/ui-login-modal" +nx build "@storyteller/ui-badge" +nx build "@storyteller/ui-settings-modal" +nx build "@storyteller/ui-menu-icon-selector" +nx build "@storyteller/provider-billing-modal" +nx build "@storyteller/provider-setup-modal" +nx build "@storyteller/ui-button-dropdown" +nx build "@storyteller/ui-create-3d-modal" +nx build "@storyteller/ui-button-icon" +nx build "@storyteller/ui-upload-modal" + +# CANNOT FIND +nx build "@sparkjsdev/spark" + + + + + + + + + + + + + + diff --git a/frontend/libs/components/create-3d-modal/src/lib/create-3d-modal.tsx b/frontend/libs/components/create-3d-modal/src/lib/create-3d-modal.tsx index d779a667ed..50099ef5ac 100644 --- a/frontend/libs/components/create-3d-modal/src/lib/create-3d-modal.tsx +++ b/frontend/libs/components/create-3d-modal/src/lib/create-3d-modal.tsx @@ -48,38 +48,48 @@ export const Create3dModal = ({ onModelComplete }: Create3dModalProps = {}) => { const eventData = event.payload?.data; console.log("[DEBUG] eventData:", eventData); - console.log("[DEBUG] Current activeToastIds:", {...activeToastIds.current}); + console.log("[DEBUG] Current activeToastIds:", { + ...activeToastIds.current, + }); if (eventData?.action === GenerationAction.ImageTo3d) { console.log("[DEBUG] This is an ImageTo3d completion event"); - + // Find any active toasts for 3D model generation and update them const entries = Object.entries(activeToastIds.current); - console.log("[DEBUG] Processing", entries.length, "active toast entries"); - - entries.forEach( - ([mediaToken, toastId]) => { - console.log("[DEBUG] Processing toast for mediaToken:", mediaToken); - - // Dismiss the loading toast - toast.dismiss(toastId); - console.log("[DEBUG] Toast dismissed"); - - // Call the callback if provided - if (onModelComplete) { - console.log("[DEBUG] Calling onModelComplete with mediaToken:", mediaToken); - onModelComplete(mediaToken); - } else { - console.log("[DEBUG] onModelComplete callback is not provided"); - } - - // Remove from active toasts - delete activeToastIds.current[mediaToken]; - console.log("[DEBUG] Removed mediaToken from activeToastIds"); - } + console.log( + "[DEBUG] Processing", + entries.length, + "active toast entries" ); + + entries.forEach(([mediaToken, toastId]) => { + console.log("[DEBUG] Processing toast for mediaToken:", mediaToken); + + // Dismiss the loading toast + toast.dismiss(toastId); + console.log("[DEBUG] Toast dismissed"); + + // Call the callback if provided + if (onModelComplete) { + console.log( + "[DEBUG] Calling onModelComplete with mediaToken:", + mediaToken + ); + onModelComplete(mediaToken); + } else { + console.log("[DEBUG] onModelComplete callback is not provided"); + } + + // Remove from active toasts + delete activeToastIds.current[mediaToken]; + console.log("[DEBUG] Removed mediaToken from activeToastIds"); + }); } else { - console.log("[DEBUG] Not an ImageTo3d event, action:", eventData?.action); + console.log( + "[DEBUG] Not an ImageTo3d event, action:", + eventData?.action + ); } }); @@ -174,6 +184,7 @@ export const Create3dModal = ({ onModelComplete }: Create3dModalProps = {}) => { const result = await EnqueueImageTo3dObject({ image_media_token: mediaToken, model: EnqueueImageTo3dObjectModel.Hunyuan3d2, + frontend_caller: "mini_app", }); if ("error_type" in result) { diff --git a/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx b/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx index d1bffeb7d2..df464e18f9 100644 --- a/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx +++ b/frontend/libs/components/gallery-modal/src/lib/gallery-modal.tsx @@ -663,6 +663,8 @@ export const GalleryModal = React.memo( e.currentTarget.src = PLACEHOLDER_IMAGES.DEFAULT; e.currentTarget.style.opacity = "0.3"; + // Set the `data-brokenurl` property for debugging the broken images: + (e.currentTarget as HTMLImageElement).dataset.brokenurl = item.thumbnail || ""; handleImageError(item.thumbnail!); }} disableTooltipAndBadge={mode === "select"} diff --git a/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx b/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx index 0ccbaaec1b..a793c87c96 100644 --- a/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx +++ b/frontend/libs/components/lightbox-modal/src/lib/lightbox-modal.tsx @@ -12,7 +12,15 @@ import { EnqueueImageTo3dObjectModel, } from "@storyteller/tauri-api"; import { LoadingSpinner } from "@storyteller/ui-loading-spinner"; -import { useEffect, useState, ReactNode, useMemo, useCallback, useRef } from "react"; +import { Viewer3D } from "@storyteller/ui-viewer-3d"; +import { + useEffect, + useState, + ReactNode, + useMemo, + useCallback, + useRef, +} from "react"; import { gtagEvent } from "@storyteller/google-analytics"; import { MediaFilesApi, PromptsApi } from "@storyteller/api"; import { toast } from "@storyteller/ui-toaster"; @@ -373,6 +381,15 @@ export function LightboxModal({
Image not available
+ ) : mediaClass === "dimensional" ? ( +
+ +
) : mediaClass === "video" ? (
)} - {!mediaLoaded && selectedImageUrl && ( + {!mediaLoaded && selectedImageUrl && mediaClass !== "dimensional" && (
@@ -661,18 +680,25 @@ export function LightboxModal({ toast.success("Share link copied"); setShareCopied(true); if (shareCopiedTimeoutRef.current) { - window.clearTimeout(shareCopiedTimeoutRef.current); + window.clearTimeout( + shareCopiedTimeoutRef.current + ); } - shareCopiedTimeoutRef.current = window.setTimeout(() => { - setShareCopied(false); - shareCopiedTimeoutRef.current = null; - }, 1500); + shareCopiedTimeoutRef.current = window.setTimeout( + () => { + setShareCopied(false); + shareCopiedTimeoutRef.current = null; + }, + 1500 + ); } catch (err) { toast.error("Unable to copy link"); } }} > - {shareCopied ? "Share link copied" : "Copy Share Link"} + {shareCopied + ? "Share link copied" + : "Copy Share Link"} )} {onEditClicked && @@ -742,6 +768,7 @@ export function LightboxModal({ await EnqueueImageTo3dObject({ image_media_token: selectedMediaToken, model: EnqueueImageTo3dObjectModel.Hunyuan3d2_0, + frontend_caller: "mini_app", }); }} > diff --git a/frontend/libs/components/model-selector/src/lib/classy-model-selector.tsx b/frontend/libs/components/model-selector/src/lib/classy-model-selector.tsx index 2d4c23a986..434aedb881 100644 --- a/frontend/libs/components/model-selector/src/lib/classy-model-selector.tsx +++ b/frontend/libs/components/model-selector/src/lib/classy-model-selector.tsx @@ -25,11 +25,7 @@ interface ClassyModelSelectorProps { providerTooltipDelayMs?: number; } -const DEFAULT_PROVIDER_OPTIONS: Provider[] = [ - Provider.ArtCraft, - Provider.Fal, - Provider.Sora, -]; +const DEFAULT_PROVIDER_OPTIONS: Provider[] = [Provider.ArtCraft]; function ProviderTooltipContent({ page, diff --git a/frontend/libs/components/model-selector/src/lib/provider-lookup.ts b/frontend/libs/components/model-selector/src/lib/provider-lookup.ts index 6ca3b855d8..6cd7a1b3d8 100644 --- a/frontend/libs/components/model-selector/src/lib/provider-lookup.ts +++ b/frontend/libs/components/model-selector/src/lib/provider-lookup.ts @@ -6,19 +6,14 @@ export type ProvidersByModel = Partial>; // Centralized lookup: page (product/tab) → modelId → allowed providers export const PROVIDER_LOOKUP_BY_PAGE: Record = { [ModelPage.TextToImage]: { - midjourney: [Provider.ArtCraft], - flux_pro_1_1: [Provider.ArtCraft, Provider.Fal], - gpt_image_1: [Provider.Sora], + gpt_image_1: [Provider.ArtCraft, Provider.Sora], }, [ModelPage.ImageToVideo]: {}, [ModelPage.Canvas2D]: { - gpt_image_1: [Provider.Sora], - flux_pro_1_1: [Provider.ArtCraft, Provider.Fal], - midjourney: [Provider.ArtCraft], + gpt_image_1: [Provider.ArtCraft, Provider.Sora], }, - [ModelPage.Stage3D]: {}, + [ModelPage.Stage3D]: { gpt_image_1: [Provider.ArtCraft, Provider.Sora] }, [ModelPage.ImageEditor]: { - gpt_image_1: [Provider.Sora], - flux_pro_1_1: [Provider.ArtCraft, Provider.Fal], + gpt_image_1: [Provider.ArtCraft, Provider.Sora], }, }; diff --git a/frontend/libs/components/promptbox/src/lib/PromptBoxEdit.tsx b/frontend/libs/components/promptbox/src/lib/PromptBoxEdit.tsx index 196f439912..fda94a17ce 100644 --- a/frontend/libs/components/promptbox/src/lib/PromptBoxEdit.tsx +++ b/frontend/libs/components/promptbox/src/lib/PromptBoxEdit.tsx @@ -8,6 +8,10 @@ import { faSpinnerThird, faFrame, faCopy, + faPen, + faEraser, + faUndo, + faRedo, } from "@fortawesome/pro-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Button, ToggleButton } from "@storyteller/ui-button"; @@ -32,6 +36,9 @@ export interface PromptBoxEditProps { supportsMaskedInpainting?: boolean; isEnqueueing?: boolean; uploadImage?: UploadImageFn; + isNanoBananaModel?: boolean; + onUndo?: () => void; + onRedo?: () => void; } export const PromptBoxEdit = ({ @@ -46,6 +53,9 @@ export const PromptBoxEdit = ({ onGenerationCountChange, supportsMaskedInpainting = false, uploadImage, + isNanoBananaModel = false, + onUndo, + onRedo, }: PromptBoxEditProps) => { const [prompt, setPrompt] = useState(""); const [useSystemPrompt, setUseSystemPrompt] = useState(true); @@ -179,27 +189,44 @@ export const PromptBoxEdit = ({ } }; - const modes = [ - { - value: "edit", - icon: faEdit, - text: "Edit Region", - tooltip: "Edit area for inpainting", - }, - { - value: "select", - icon: faMousePointer, - text: "Select", - tooltip: "Selection mode", - }, - // Commented out for now - BFL-1000 - // { - // value: "expand", - // icon: faExpand, - // text: "Expand", - // tooltip: "Expand area for outpainting", - // }, - ]; + const modes = isNanoBananaModel + ? [ + { + value: "marker", + icon: faPen, + text: "Marker", + }, + { + value: "eraser", + icon: faEraser, + text: "Eraser", + }, + ] + : supportsMaskedInpainting + ? [ + { + value: "edit", + icon: faEdit, + text: "Edit Region", + }, + { + value: "eraser", + icon: faEraser, + text: "Eraser", + }, + ] + : [ + { + value: "edit", + icon: faEdit, + text: "Edit Region", + }, + { + value: "select", + icon: faMousePointer, + text: "Select", + }, + ]; return ( <> @@ -218,6 +245,36 @@ export const PromptBoxEdit = ({ onImageClick={() => {}} /> )} + {(supportsMaskedInpainting || isNanoBananaModel) && ( +
+ +
+
+ + + + + + +
+
+ )}
- {supportsMaskedInpainting && ( - - )} - { + console.log(">>>> handleResolutionSelect", selectedItem); setResolution(selectedItem.label as any); setResolutionList((prev) => resolutionOptions.map((item) => ({ @@ -280,17 +283,19 @@ export const PromptBoxVideo = ({ frontend_subscriber_id: subscriberId, }; - if (selectedModel?.tauriId === "sora_2") { - request.sora_orientation = - resolution === "720p" ? "landscape" : "portrait"; + if (selectedModel.generateWithSound) { + request.generate_audio = !!generateWithSound; } - if (selectedModel?.tauriId === "veo_2") { - request.generate_with_sound = !!generateWithSound; - } + switch (selectedModel?.tauriId) { + case "grok_video": + request.grok_aspect_ratio = getGrokAspectRatio(); + break; - if (selectedModel?.tauriId === "veo_3") { - request.generate_with_sound = !!generateWithSound; + case "sora_2": + request.sora_orientation = + resolution === "720p" ? "landscape" : "portrait"; + break; } await EnqueueImageToVideo(request); @@ -323,6 +328,26 @@ export const PromptBoxVideo = ({ } }; + const getGrokAspectRatio = (): GROK_ASPECT_RATIO => { + // NB: This function was just written to give us better type safety. + // There has to be a cleaner appraoach. + const maybeAspectRatio = selectedModel?. + sizeOptions?. + find((option) => option.textLabel === resolution)?. + tauriValue; + + switch (maybeAspectRatio) { + case "landscape": + return "landscape"; + case "portrait": + return "portrait"; + case "square": + return "square"; + default: + return "landscape"; + } + } + const [isGalleryModalOpen, setIsGalleryModalOpen] = useState(false); const modelNeedsAnImageButNoneAreSelected = diff --git a/frontend/libs/components/viewer-3d/.babelrc b/frontend/libs/components/viewer-3d/.babelrc new file mode 100644 index 0000000000..1ea870ead4 --- /dev/null +++ b/frontend/libs/components/viewer-3d/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + [ + "@nx/react/babel", + { + "runtime": "automatic", + "useBuiltIns": "usage" + } + ] + ], + "plugins": [] +} diff --git a/frontend/libs/components/viewer-3d/README.md b/frontend/libs/components/viewer-3d/README.md new file mode 100644 index 0000000000..ebc60a9ed2 --- /dev/null +++ b/frontend/libs/components/viewer-3d/README.md @@ -0,0 +1,7 @@ +# @storyteller/ui-viewer-3d + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test @storyteller/ui-viewer-3d` to execute the unit tests via [Vitest](https://vitest.dev/). diff --git a/frontend/libs/components/viewer-3d/package.json b/frontend/libs/components/viewer-3d/package.json new file mode 100644 index 0000000000..c19f0c2f06 --- /dev/null +++ b/frontend/libs/components/viewer-3d/package.json @@ -0,0 +1,17 @@ +{ + "name": "@storyteller/ui-viewer-3d", + "version": "0.0.1", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "development": "./src/index.ts", + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + } +} diff --git a/frontend/libs/components/viewer-3d/src/index.ts b/frontend/libs/components/viewer-3d/src/index.ts new file mode 100644 index 0000000000..64b7f004b4 --- /dev/null +++ b/frontend/libs/components/viewer-3d/src/index.ts @@ -0,0 +1,2 @@ + +export * from './lib/viewer-3d'; diff --git a/frontend/libs/components/viewer-3d/src/lib/viewer-3d.module.css b/frontend/libs/components/viewer-3d/src/lib/viewer-3d.module.css new file mode 100644 index 0000000000..d0246c74b9 --- /dev/null +++ b/frontend/libs/components/viewer-3d/src/lib/viewer-3d.module.css @@ -0,0 +1,7 @@ +/* + * Replace this with your own classes + * + * e.g. + * .container { + * } +*/ \ No newline at end of file diff --git a/frontend/libs/components/viewer-3d/src/lib/viewer-3d.spec.tsx b/frontend/libs/components/viewer-3d/src/lib/viewer-3d.spec.tsx new file mode 100644 index 0000000000..c449c2683a --- /dev/null +++ b/frontend/libs/components/viewer-3d/src/lib/viewer-3d.spec.tsx @@ -0,0 +1,12 @@ +import { render } from '@testing-library/react'; + +import StorytellerUiViewer3d from './viewer-3d'; + +describe('StorytellerUiViewer3d', () => { + + it('should render successfully', () => { + const { baseElement } = render(); + expect(baseElement).toBeTruthy(); + }); + +}); diff --git a/frontend/libs/components/viewer-3d/src/lib/viewer-3d.tsx b/frontend/libs/components/viewer-3d/src/lib/viewer-3d.tsx new file mode 100644 index 0000000000..7840aa2aae --- /dev/null +++ b/frontend/libs/components/viewer-3d/src/lib/viewer-3d.tsx @@ -0,0 +1,333 @@ +import { useEffect, useRef, useState } from "react"; +import * as THREE from "three"; +import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; +import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; + +export interface Viewer3DProps { + modelUrl?: string; + previewUrl?: string; + isActive?: boolean; + className?: string; + showGrid?: boolean; + onThumbnailCapture?: (dataUrl: string) => void; +} + +export function Viewer3D({ + modelUrl, + previewUrl, + isActive, + className = "", + showGrid = true, + onThumbnailCapture, +}: Viewer3DProps) { + const containerRef = useRef(null); + const sceneRef = useRef(null); + const cameraRef = useRef(null); + const rendererRef = useRef(null); + const controlsRef = useRef(null); + const animationFrameRef = useRef(null); + const cubeRef = useRef(null); + const loadedModelRef = useRef(null); + const gridRef = useRef(null); + const [isModelLoaded, setIsModelLoaded] = useState(false); + const thumbnailCapturedRef = useRef(false); + + useEffect(() => { + if (!containerRef.current) return; + + const container = containerRef.current; + + // Use ResizeObserver for more reliable size detection + const initScene = () => { + const width = container.clientWidth; + const height = container.clientHeight; + + if (width === 0 || height === 0) return false; + + const scene = new THREE.Scene(); + scene.background = new THREE.Color(0x282828); + sceneRef.current = scene; + + const camera = new THREE.PerspectiveCamera(50, width / height, 0.1, 1000); + camera.position.set(3, 3, 3); + camera.lookAt(0, 0.5, 0); + cameraRef.current = camera; + + const renderer = new THREE.WebGLRenderer({ + antialias: true, + alpha: false, + }); + renderer.setSize(width, height); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + rendererRef.current = renderer; + + container.appendChild(renderer.domElement); + + const controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.dampingFactor = 0.05; + controls.target.set(0, 0.5, 0); + controls.update(); + controlsRef.current = controls; + + const ambientLight = new THREE.AmbientLight(0xffffff, 2); + scene.add(ambientLight); + + const hemisphereLight = new THREE.HemisphereLight( + 0xffffff, + 0x888888, + 1.2 + ); + scene.add(hemisphereLight); + + const keyLight = new THREE.DirectionalLight(0xffffff, 2); + keyLight.position.set(2, 10, 8); + scene.add(keyLight); + + const fillLight = new THREE.DirectionalLight(0xffffff, 1.2); + fillLight.position.set(-6, 6, -4); + scene.add(fillLight); + + const frontLight = new THREE.DirectionalLight(0xffffff, 1); + frontLight.position.set(0, 4, 10); + scene.add(frontLight); + + const gridHelper = new THREE.GridHelper(10, 10, 0x444444, 0x333333); + gridHelper.visible = showGrid; + scene.add(gridHelper); + gridRef.current = gridHelper; + + const geometry = new THREE.BoxGeometry(1, 1, 1); + const material = new THREE.MeshStandardMaterial({ + color: 0x4a9eff, + roughness: 0.5, + metalness: 0.5, + }); + const cube = new THREE.Mesh(geometry, material); + cube.position.y = 0.5; + scene.add(cube); + cubeRef.current = cube; + + const animate = () => { + animationFrameRef.current = requestAnimationFrame(animate); + + if (cubeRef.current) { + cubeRef.current.rotation.y += 0.01; + } + + controls.update(); + renderer.render(scene, camera); + }; + animate(); + + return true; + }; + + // Try to initialize, retry if container not ready + let initialized = initScene(); + let retryTimeout: number | null = null; + + if (!initialized) { + retryTimeout = window.setTimeout(() => { + initialized = initScene(); + }, 100); + } + + const handleResize = () => { + if (!container || !cameraRef.current || !rendererRef.current) return; + const newWidth = container.clientWidth; + const newHeight = container.clientHeight; + if (newWidth === 0 || newHeight === 0) return; + cameraRef.current.aspect = newWidth / newHeight; + cameraRef.current.updateProjectionMatrix(); + rendererRef.current.setSize(newWidth, newHeight); + }; + + // Use ResizeObserver for container size changes + const resizeObserver = new ResizeObserver(() => { + handleResize(); + }); + resizeObserver.observe(container); + + // Also listen to window resize as backup + window.addEventListener("resize", handleResize); + + return () => { + if (retryTimeout) window.clearTimeout(retryTimeout); + window.removeEventListener("resize", handleResize); + resizeObserver.disconnect(); + + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } + if ( + rendererRef.current && + container.contains(rendererRef.current.domElement) + ) { + container.removeChild(rendererRef.current.domElement); + } + rendererRef.current?.dispose(); + controlsRef.current?.dispose(); + + if (cubeRef.current) { + const cube = cubeRef.current; + if (cube.geometry) cube.geometry.dispose(); + if (cube.material) { + if (Array.isArray(cube.material)) { + cube.material.forEach((m) => m.dispose()); + } else { + cube.material.dispose(); + } + } + } + }; + }, []); + + useEffect(() => { + if (!modelUrl || !sceneRef.current) return; + + console.log("[Viewer3D] Loading model from URL:", modelUrl); + setIsModelLoaded(false); + thumbnailCapturedRef.current = false; + + const scene = sceneRef.current; + const loader = new GLTFLoader(); + + if (loadedModelRef.current) { + console.log("[Viewer3D] Removing previous model"); + scene.remove(loadedModelRef.current); + loadedModelRef.current = null; + } + + if (cubeRef.current) { + cubeRef.current.visible = false; + } + + loader.load( + modelUrl, + (gltf) => { + console.log("[Viewer3D] Model loaded successfully"); + const model = gltf.scene; + + const box = new THREE.Box3().setFromObject(model); + const size = box.getSize(new THREE.Vector3()); + + const maxDim = Math.max(size.x, size.y, size.z); + const scale = 2 / maxDim; + model.scale.multiplyScalar(scale); + + const scaledBox = new THREE.Box3().setFromObject(model); + const scaledCenter = scaledBox.getCenter(new THREE.Vector3()); + const scaledSize = scaledBox.getSize(new THREE.Vector3()); + + model.position.x = -scaledCenter.x; + model.position.z = -scaledCenter.z; + model.position.y = -scaledBox.min.y; + + scene.add(model); + loadedModelRef.current = model; + setIsModelLoaded(true); + + const modelHeight = scaledSize.y; + const maxModelDim = Math.max(scaledSize.x, scaledSize.y, scaledSize.z); + + // Calculate distance needed to fit the model based on camera FOV (50 degrees) + const fov = 50; + const fovRad = (fov * Math.PI) / 180; + const fitDistance = maxModelDim / 2 / Math.tan(fovRad / 2); + + // Add padding (1.4x) to ensure model isn't touching edges + const cameraDistance = fitDistance * 1.4; + + if (cameraRef.current && controlsRef.current) { + // Position camera at 45-degree angle + const angle = Math.PI / 4; + cameraRef.current.position.set( + Math.sin(angle) * cameraDistance, + modelHeight * 0.5 + cameraDistance * 0.35, + Math.cos(angle) * cameraDistance + ); + controlsRef.current.target.set(0, modelHeight * 0.4, 0); + controlsRef.current.update(); + } + + // Capture thumbnail after a short delay to ensure rendering is complete + if (onThumbnailCapture && !thumbnailCapturedRef.current) { + thumbnailCapturedRef.current = true; + setTimeout(() => { + if (rendererRef.current && sceneRef.current && cameraRef.current) { + // Temporarily hide grid for thumbnail + if (gridRef.current) { + gridRef.current.visible = false; + } + // Also hide placeholder cube + if (cubeRef.current) { + cubeRef.current.visible = false; + } + + // Render and capture + rendererRef.current.render(sceneRef.current, cameraRef.current); + const dataUrl = + rendererRef.current.domElement.toDataURL("image/png"); + onThumbnailCapture(dataUrl); + + // Restore grid visibility + if (gridRef.current) { + gridRef.current.visible = showGrid; + } + } + }, 100); + } + }, + (progress) => { + console.log( + "[Viewer3D] Loading progress:", + ((progress.loaded / progress.total) * 100).toFixed(2) + "%" + ); + }, + (error) => { + console.error("[Viewer3D] Error loading model:", error); + if (cubeRef.current) { + cubeRef.current.visible = true; + } + } + ); + + return () => { + if (loadedModelRef.current && sceneRef.current) { + sceneRef.current.remove(loadedModelRef.current); + loadedModelRef.current = null; + } + }; + }, [modelUrl, onThumbnailCapture, showGrid]); + + // Update grid visibility when prop changes + useEffect(() => { + if (gridRef.current) { + gridRef.current.visible = showGrid; + } + }, [showGrid]); + + const showViewer = modelUrl && isModelLoaded; + const showSpinner = !showViewer; + + return ( +
+ {showSpinner && ( +
+
+
+ )} + +
+
+ ); +} + +export default Viewer3D; diff --git a/frontend/libs/components/viewer-3d/tsconfig.json b/frontend/libs/components/viewer-3d/tsconfig.json new file mode 100644 index 0000000000..b3d19d8f5e --- /dev/null +++ b/frontend/libs/components/viewer-3d/tsconfig.json @@ -0,0 +1,13 @@ +{ + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json" +} diff --git a/frontend/libs/components/viewer-3d/tsconfig.lib.json b/frontend/libs/components/viewer-3d/tsconfig.lib.json new file mode 100644 index 0000000000..f0782e3dd8 --- /dev/null +++ b/frontend/libs/components/viewer-3d/tsconfig.lib.json @@ -0,0 +1,45 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "types": [ + "node", + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts", + "vite/client" + ], + "rootDir": "src", + "jsx": "react-jsx", + "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo" + }, + "exclude": [ + "out-tsc", + "dist", + "**/*.spec.ts", + "**/*.test.ts", + "**/*.spec.tsx", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.test.js", + "**/*.spec.jsx", + "**/*.test.jsx", + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx" + ], + "include": [ + "src/**/*.js", + "src/**/*.jsx", + "src/**/*.ts", + "src/**/*.tsx" + ] +} diff --git a/frontend/libs/components/viewer-3d/tsconfig.spec.json b/frontend/libs/components/viewer-3d/tsconfig.spec.json new file mode 100644 index 0000000000..18b7de51b8 --- /dev/null +++ b/frontend/libs/components/viewer-3d/tsconfig.spec.json @@ -0,0 +1,34 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out-tsc/vitest", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ], + "jsx": "react-jsx" + }, + "include": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/frontend/libs/components/viewer-3d/vite.config.ts b/frontend/libs/components/viewer-3d/vite.config.ts new file mode 100644 index 0000000000..b8522a20e9 --- /dev/null +++ b/frontend/libs/components/viewer-3d/vite.config.ts @@ -0,0 +1,49 @@ +/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import dts from 'vite-plugin-dts'; +import * as path from 'path'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../../node_modules/.vite/libs/components/viewer-3d', + plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })], + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + // Configuration for building your library. + // See: https://vitejs.dev/guide/build.html#library-mode + build: { + outDir: './dist', + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + lib: { + // Could also be a dictionary or array of multiple entry points. + entry: 'src/index.ts', + name: '@storyteller/ui-viewer-3d', + fileName: 'index', + // Change this to the formats you want to support. + // Don't forget to update your package.json as well. + formats: ['es' as const] + }, + rollupOptions: { + // External packages that should not be bundled into your library. + external: ['react','react-dom','react/jsx-runtime'] + }, + }, + test: { + watch: false, + globals: true, + environment: 'jsdom', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: './test-output/vitest/coverage', + provider: 'v8' as const, + } + }, +})); diff --git a/frontend/libs/model-list/src/lib/classes/VideoModel.ts b/frontend/libs/model-list/src/lib/classes/VideoModel.ts index d055f4fa85..771e35f375 100644 --- a/frontend/libs/model-list/src/lib/classes/VideoModel.ts +++ b/frontend/libs/model-list/src/lib/classes/VideoModel.ts @@ -47,6 +47,6 @@ export class VideoModel extends Model { this.endFrame = args.endFrame; this.requiresImage = args.requiresImage; this.sizeOptions = args.sizeOptions ?? []; - this.generateWithSound = args.generateWithSound; + this.generateWithSound = args.generateWithSound || false; } } diff --git a/frontend/libs/model-list/src/lib/lists/VideoModels.ts b/frontend/libs/model-list/src/lib/lists/VideoModels.ts index 7335d27dda..82d2e7cb89 100644 --- a/frontend/libs/model-list/src/lib/lists/VideoModels.ts +++ b/frontend/libs/model-list/src/lib/lists/VideoModels.ts @@ -16,6 +16,23 @@ export const VIDEO_MODELS: VideoModel[] = [ endFrame: false, requiresImage: true, progressBarTime: 50000, + sizeOptions: [ + { + tauriValue: "landscape", + textLabel: "Landscape", + icon: SizeIconOption.Landscape, + }, + { + tauriValue: "portrait", + textLabel: "Portrait", + icon: SizeIconOption.Portrait, + }, + { + tauriValue: "square", + textLabel: "Square", + icon: SizeIconOption.Square, + }, + ], }), new VideoModel({ id: "kling_1_6_pro", @@ -30,6 +47,7 @@ export const VIDEO_MODELS: VideoModel[] = [ endFrame: true, requiresImage: true, progressBarTime: 100000, + generateWithSound: false, }), new VideoModel({ id: "kling_2_1_pro", @@ -44,6 +62,7 @@ export const VIDEO_MODELS: VideoModel[] = [ endFrame: true, requiresImage: true, progressBarTime: 100000, + generateWithSound: false, }), new VideoModel({ id: "kling_2_1_master", @@ -58,6 +77,7 @@ export const VIDEO_MODELS: VideoModel[] = [ endFrame: false, requiresImage: true, progressBarTime: 100000, + generateWithSound: false, }), new VideoModel({ id: "seedance_1_0_lite", @@ -111,7 +131,7 @@ export const VIDEO_MODELS: VideoModel[] = [ startFrame: true, endFrame: false, requiresImage: true, - generateWithSound: true, + generateWithSound: false, progressBarTime: 100000, }), new VideoModel({ diff --git a/frontend/libs/state/subscription/src/lib/subscription-plans.ts b/frontend/libs/state/subscription/src/lib/subscription-plans.ts index 9cd7ea5986..8b12330089 100644 --- a/frontend/libs/state/subscription/src/lib/subscription-plans.ts +++ b/frontend/libs/state/subscription/src/lib/subscription-plans.ts @@ -9,7 +9,6 @@ export const FREE_PLAN: SubscriptionPlanDetails = { features: [ { text: "Free daily generations", included: true }, { text: "Limited access to ArtCraft tools", included: true }, - { text: "~15 Midjourney images", included: true }, { text: "~10 GPT-Image-1 images", included: true }, { text: "~1 minute Kling video", included: true }, ], @@ -28,7 +27,6 @@ export const SUBSCRIPTION_PLANS: SubscriptionPlanDetails[] = [ originalYearlyPrice: 120, features: [ { text: "~1,000 Flux images", included: true }, - { text: "~300 Midjourney images", included: true }, { text: "~100 GPT-Image-1 images", included: true }, { text: "~10 minutes Kling video", included: true }, { text: "Commercial license", included: true }, @@ -45,7 +43,6 @@ export const SUBSCRIPTION_PLANS: SubscriptionPlanDetails[] = [ originalYearlyPrice: 420, features: [ { text: "~5,000 Flux images", included: true }, - { text: "~1,000 Midjourney images", included: true }, { text: "~300 GPT-Image-1 images", included: true }, { text: "~30 minutes Kling video", included: true }, { text: "Commercial license", included: true }, @@ -62,7 +59,6 @@ export const SUBSCRIPTION_PLANS: SubscriptionPlanDetails[] = [ originalYearlyPrice: 720, features: [ { text: "~15,000 Flux images", included: true }, - { text: "~3,000 Midjourney images", included: true }, { text: "~500 GPT-Image-1 images", included: true }, { text: "~60 minutes Kling video", included: true }, { text: "Commercial license", included: true }, diff --git a/frontend/libs/tauri-api/src/lib/enqueue/EnqueueImageTo3dObject.ts b/frontend/libs/tauri-api/src/lib/enqueue/EnqueueImageTo3dObject.ts index 459985ef15..3f93cdafe6 100644 --- a/frontend/libs/tauri-api/src/lib/enqueue/EnqueueImageTo3dObject.ts +++ b/frontend/libs/tauri-api/src/lib/enqueue/EnqueueImageTo3dObject.ts @@ -17,6 +17,19 @@ export enum EnqueueImageTo3dObjectErrorType { export interface EnqueueImageTo3dObjectRequest { image_media_token?: string; model?: EnqueueImageTo3dObjectModel; + + // TODO: Actual enum. + frontend_caller?: string; + + // Optional frontend state to return later. + frontend_subscriber_id?: string; +} + +interface EnqueueImageTo3dObjectRequestRaw { + image_media_token?: string; + model?: EnqueueImageTo3dObjectModel; + frontend_caller?: string; + frontend_subscriber_id?: string; } export enum EnqueueImageTo3dObjectModel { @@ -30,21 +43,35 @@ export interface EnqueueImageTo3dObjectError extends CommandResult { error_message?: string; } -export interface EnqueueImageTo3dObjectPayload { -} +export interface EnqueueImageTo3dObjectPayload {} export interface EnqueueImageTo3dObjectSuccess extends CommandResult { payload: EnqueueImageTo3dObjectPayload; } -export type EnqueueImageTo3dObjectResult = EnqueueImageTo3dObjectSuccess | EnqueueImageTo3dObjectError; +export type EnqueueImageTo3dObjectResult = + | EnqueueImageTo3dObjectSuccess + | EnqueueImageTo3dObjectError; -export const EnqueueImageTo3dObject = async (request: EnqueueImageTo3dObjectRequest) : Promise => { - let result = await invoke("enqueue_image_to_3d_object_command", { - request: { - image_media_token: request.image_media_token, - model: request.model, - } +export const EnqueueImageTo3dObject = async ( + request: EnqueueImageTo3dObjectRequest +): Promise => { + let mutableRequest: EnqueueImageTo3dObjectRequestRaw = { + image_media_token: request.image_media_token, + model: request.model, + }; + + if (!!request.frontend_caller) { + mutableRequest.frontend_caller = request.frontend_caller; + } + + if (!!request.frontend_subscriber_id) { + mutableRequest.frontend_subscriber_id = request.frontend_subscriber_id; + } + + let result = await invoke("enqueue_image_to_3d_object_command", { + request: mutableRequest, }); - return (result as EnqueueImageTo3dObjectResult); -} + + return result as EnqueueImageTo3dObjectResult; +}; diff --git a/frontend/libs/tauri-api/src/lib/enqueue/EnqueueImageToVideo.ts b/frontend/libs/tauri-api/src/lib/enqueue/EnqueueImageToVideo.ts index 4f60c891c8..c1777bc6db 100644 --- a/frontend/libs/tauri-api/src/lib/enqueue/EnqueueImageToVideo.ts +++ b/frontend/libs/tauri-api/src/lib/enqueue/EnqueueImageToVideo.ts @@ -35,8 +35,11 @@ export interface EnqueueImageToVideoRequest { // Optional. Orientation of the video for Sora 2. sora_orientation?: "portrait" | "landscape"; + // Optional. Aspect Ratio for the video for Grok Video. + grok_aspect_ratio?: "portrait" | "landscape" | "square"; + // Optional. Whether to generate audio alongside the video (used by some models like Veo2) - generate_with_sound?: boolean; + generate_audio?: boolean; } interface RawEnqueueImageToVideoRequest { @@ -47,7 +50,8 @@ interface RawEnqueueImageToVideoRequest { frontend_caller?: string; frontend_subscriber_id?: string; sora_orientation?: "portrait" | "landscape"; - generate_with_sound?: boolean; + grok_aspect_ratio?: "portrait" | "landscape" | "square"; + generate_audio?: boolean; } export interface EnqueueImageToVideoError extends CommandResult { @@ -94,8 +98,12 @@ export const EnqueueImageToVideo = async ( mutableRequest.sora_orientation = request.sora_orientation; } - if (typeof request.generate_with_sound === "boolean") { - mutableRequest.generate_with_sound = request.generate_with_sound; + if (request.grok_aspect_ratio) { + mutableRequest.grok_aspect_ratio = request.grok_aspect_ratio; + } + + if (typeof request.generate_audio === "boolean") { + mutableRequest.generate_audio = request.generate_audio; } const result = await invoke("enqueue_image_to_video_command", { diff --git a/frontend/libs/tauri-events/src/index.ts b/frontend/libs/tauri-events/src/index.ts index 170d20d67b..4dd36cea48 100644 --- a/frontend/libs/tauri-events/src/index.ts +++ b/frontend/libs/tauri-events/src/index.ts @@ -1,11 +1,12 @@ export * from "./lib/common/BasicEventWrapper"; export * from "./lib/events/functional/CreditsBalanceChangedEvent"; export * from "./lib/events/functional/ImageEditCompleteEvent"; -export * from "./lib/events/functional/ImageToVideoGenerationCompleteEvent"; export * from "./lib/events/functional/RefreshAccountStateEvent"; export * from "./lib/events/functional/ShowProviderBillingModalEvent"; export * from "./lib/events/functional/ShowProviderLoginModalEvent"; export * from "./lib/events/functional/TextToImageGenerationCompleteEvent"; +export * from "./lib/events/functional/ObjectGenerationCompleteEvent"; +export * from "./lib/events/functional/VideoGenerationCompleteEvent"; export * from "./lib/events/functional/SubscriptionPlanChangedEvent"; export * from "./lib/events/generation/useGenerationCompleteEvent"; export * from "./lib/events/generation/useGenerationEnqueueFailureEvent"; diff --git a/frontend/libs/tauri-events/src/lib/events/functional/ImageToVideoGenerationCompleteEvent.ts b/frontend/libs/tauri-events/src/lib/events/functional/ObjectGenerationCompleteEvent.ts similarity index 65% rename from frontend/libs/tauri-events/src/lib/events/functional/ImageToVideoGenerationCompleteEvent.ts rename to frontend/libs/tauri-events/src/lib/events/functional/ObjectGenerationCompleteEvent.ts index d106941d69..aac39c01a5 100644 --- a/frontend/libs/tauri-events/src/lib/events/functional/ImageToVideoGenerationCompleteEvent.ts +++ b/frontend/libs/tauri-events/src/lib/events/functional/ObjectGenerationCompleteEvent.ts @@ -2,28 +2,29 @@ import { listen, UnlistenFn } from "@tauri-apps/api/event"; import { BasicEventWrapper } from "../../common/BasicEventWrapper"; import { useEffect } from "react"; -const EVENT_NAME: string = "image_to_video_generation_complete_event"; +const EVENT_NAME: string = "object_generation_complete_event"; -export interface ImageToVideoGenerationCompleteEvent { - generated_video: GeneratedVideo; +export interface ObjectGenerationCompleteEvent { + generated_object?: GeneratedObject; maybe_frontend_subscriber_id?: string; maybe_frontend_subscriber_payload?: string; } -export interface GeneratedVideo { +export interface GeneratedObject { media_token: string; cdn_url: string; + maybe_thumbnail_template?: string; } -export const useImageToVideoGenerationCompleteEvent = ( - asyncCallback: (event: ImageToVideoGenerationCompleteEvent) => Promise +export const useObjectGenerationCompleteEvent = ( + asyncCallback: (event: ObjectGenerationCompleteEvent) => Promise ) => { useEffect(() => { let isUnmounted = false; let unlisten: Promise; const setup = async () => { - unlisten = listen>( + unlisten = listen>( EVENT_NAME, async (wrappedEvent) => { await asyncCallback(wrappedEvent.payload.data); diff --git a/frontend/libs/tauri-events/src/lib/events/functional/VideoGenerationCompleteEvent.ts b/frontend/libs/tauri-events/src/lib/events/functional/VideoGenerationCompleteEvent.ts new file mode 100644 index 0000000000..68aca6f3c7 --- /dev/null +++ b/frontend/libs/tauri-events/src/lib/events/functional/VideoGenerationCompleteEvent.ts @@ -0,0 +1,42 @@ +import { listen, UnlistenFn } from '@tauri-apps/api/event'; +import { BasicEventWrapper } from '../../common/BasicEventWrapper'; +import { useEffect } from 'react'; + +const EVENT_NAME : string = 'video_generation_complete_event'; + +export interface VideoGenerationCompleteEvent { + generated_video?: GeneratedVideo, + maybe_frontend_subscriber_id?: string, + maybe_frontend_subscriber_payload?: string, +} + +export interface GeneratedVideo { + media_token: string, + cdn_url: string, + maybe_thumbnail_template?: string, +} + +export const useVideoGenerationCompleteEvent = (asyncCallback: (event: VideoGenerationCompleteEvent) => Promise) => { + useEffect(() => { + let isUnmounted = false; + let unlisten: Promise; + + const setup = async () => { + unlisten = listen>(EVENT_NAME, async (wrappedEvent) => { + await asyncCallback(wrappedEvent.payload.data); + }); + + if (isUnmounted) { + unlisten.then(f => f()); // Unsubscribe if unmounted early. + } + }; + + setup(); + + return () => { + isUnmounted = true; + unlisten.then(f => f()); + }; + + }, []); +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index db2b9ad7ef..587765d731 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1365,6 +1365,10 @@ "name": "@storyteller/ui-upload-modal", "version": "0.0.1" }, + "libs/components/viewer-3d": { + "name": "@storyteller/ui-viewer-3d", + "version": "0.0.1" + }, "libs/login": { "name": "@frontend/login", "version": "0.0.1" @@ -1517,13 +1521,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", - "license": "MIT", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -1652,6 +1655,7 @@ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -1781,9 +1785,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "engines": { "node": ">=6.9.0" } @@ -1826,12 +1830,11 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", - "license": "MIT", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dependencies": { - "@babel/types": "^7.28.4" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -1960,6 +1963,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1972,6 +1976,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -1984,6 +1989,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, @@ -1996,6 +2002,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -2058,6 +2065,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -2070,6 +2078,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -2098,6 +2107,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -2110,6 +2120,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -2122,6 +2133,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -2134,6 +2146,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -2146,6 +2159,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -2158,6 +2172,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -2170,6 +2185,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -2185,6 +2201,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -3429,17 +3446,17 @@ }, "node_modules/@babel/traverse--for-generate-function-map": { "name": "@babel/traverse", - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", - "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", - "license": "MIT", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", + "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.4", + "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4", + "@babel/types": "^7.28.5", "debug": "^4.3.1" }, "engines": { @@ -3447,13 +3464,12 @@ } }, "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", - "license": "MIT", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -4449,6 +4465,7 @@ "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -4458,6 +4475,7 @@ "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "license": "ISC", + "peer": true, "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", @@ -4474,6 +4492,7 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "license": "MIT", + "peer": true, "dependencies": { "sprintf-js": "~1.0.2" } @@ -4483,6 +4502,7 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -4492,6 +4512,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "license": "MIT", + "peer": true, "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -4501,10 +4522,10 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "license": "MIT", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "peer": true, "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -4518,6 +4539,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "license": "MIT", + "peer": true, "dependencies": { "p-locate": "^4.1.0" }, @@ -4530,6 +4552,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "license": "MIT", + "peer": true, "dependencies": { "p-try": "^2.0.0" }, @@ -4545,6 +4568,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "license": "MIT", + "peer": true, "dependencies": { "p-limit": "^2.2.0" }, @@ -4557,6 +4581,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "license": "MIT", + "peer": true, "engines": { "node": ">=8" } @@ -4575,6 +4600,7 @@ "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==", "license": "MIT", + "peer": true, "dependencies": { "@jest/types": "^29.6.3" }, @@ -4587,6 +4613,7 @@ "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", "license": "MIT", + "peer": true, "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", @@ -4615,6 +4642,7 @@ "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", "license": "MIT", + "peer": true, "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", @@ -4643,7 +4671,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", - "license": "MIT", "peer": true, "dependencies": { "@babel/core": "^7.11.6", @@ -7116,19 +7143,19 @@ } }, "node_modules/@react-native/assets-registry": { - "version": "0.82.0", - "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.82.0.tgz", - "integrity": "sha512-SHRZxH+VHb6RwcHNskxyjso6o91Lq0DPgOpE5cDrppn1ziYhI723rjufFgh59RcpH441eci0/cXs/b0csXTtnw==", - "license": "MIT", + "version": "0.82.1", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.82.1.tgz", + "integrity": "sha512-B1SRwpntaAcckiatxbjzylvNK562Ayza05gdJCjDQHTiDafa1OABmyB5LHt7qWDOpNkaluD+w11vHF7pBmTpzQ==", + "peer": true, "engines": { "node": ">= 20.19.4" } }, "node_modules/@react-native/codegen": { - "version": "0.82.0", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.82.0.tgz", - "integrity": "sha512-DJKDwyr6s0EtoPKmAaOsx2EnS2sV/qICNWn/KA+8lohSY6gJF1wuA+DOjitivBfU0soADoo8tqGq50C5rlzmCA==", - "license": "MIT", + "version": "0.82.1", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.82.1.tgz", + "integrity": "sha512-ezXTN70ygVm9l2m0i+pAlct0RntoV4afftWMGUIeAWLgaca9qItQ54uOt32I/9dBJvzBibT33luIR/pBG0dQvg==", + "peer": true, "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.25.3", @@ -7150,6 +7177,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7161,6 +7189,7 @@ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", "license": "ISC", + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -7181,6 +7210,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -7189,12 +7219,12 @@ } }, "node_modules/@react-native/community-cli-plugin": { - "version": "0.82.0", - "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.82.0.tgz", - "integrity": "sha512-n5dxQowsRAjruG5sNl6MEPUzANUiVERaL7w4lHGmm/pz/ey1JOM9sFxL6RpZp1FJSVu4QKmbx6sIHrKb2MCekg==", - "license": "MIT", + "version": "0.82.1", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.82.1.tgz", + "integrity": "sha512-H/eMdtOy9nEeX7YVeEG1N2vyCoifw3dr9OV8++xfUElNYV7LtSmJ6AqxZUUfxGJRDFPQvaU/8enmJlM/l11VxQ==", + "peer": true, "dependencies": { - "@react-native/dev-middleware": "0.82.0", + "@react-native/dev-middleware": "0.82.1", "debug": "^4.4.0", "invariant": "^2.2.4", "metro": "^0.83.1", @@ -7219,19 +7249,19 @@ } }, "node_modules/@react-native/debugger-frontend": { - "version": "0.82.0", - "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.82.0.tgz", - "integrity": "sha512-rlTDcjf0ecjOHmygdBACAQCqPG0z/itAxnbhk8ZiQts7m4gRJiA/iCGFyC8/T9voUA0azAX6QCl4tHlnuUy7mQ==", - "license": "BSD-3-Clause", + "version": "0.82.1", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.82.1.tgz", + "integrity": "sha512-a2O6M7/OZ2V9rdavOHyCQ+10z54JX8+B+apYKCQ6a9zoEChGTxUMG2YzzJ8zZJVvYf1ByWSNxv9Se0dca1hO9A==", + "peer": true, "engines": { "node": ">= 20.19.4" } }, "node_modules/@react-native/debugger-shell": { - "version": "0.82.0", - "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.82.0.tgz", - "integrity": "sha512-XbXABIMzaH7SvapNWcW+zix1uHeSX/MoXYKKWWTs99a12TgwNuTeLKKTEj/ZkAjWtaCCqb/sMI4aJDLXKppCGg==", - "license": "MIT", + "version": "0.82.1", + "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.82.1.tgz", + "integrity": "sha512-fdRHAeqqPT93bSrxfX+JHPpCXHApfDUdrXMXhoxlPgSzgXQXJDykIViKhtpu0M6slX6xU/+duq+AtP/qWJRpBw==", + "peer": true, "dependencies": { "cross-spawn": "^7.0.6", "fb-dotslash": "0.5.8" @@ -7241,14 +7271,14 @@ } }, "node_modules/@react-native/dev-middleware": { - "version": "0.82.0", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.82.0.tgz", - "integrity": "sha512-SHvpo89RSzH06yZCmY3Xwr1J82EdUljC2lcO4YvXfHmytFG453Nz6kyZQcDEqGCfWDjznIUFUyT2UcLErmRWQg==", - "license": "MIT", + "version": "0.82.1", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.82.1.tgz", + "integrity": "sha512-wuOIzms/Qg5raBV6Ctf2LmgzEOCqdP3p1AYN4zdhMT110c39TVMbunpBaJxm0Kbt2HQ762MQViF9naxk7SBo4w==", + "peer": true, "dependencies": { "@isaacs/ttlcache": "^1.4.1", - "@react-native/debugger-frontend": "0.82.0", - "@react-native/debugger-shell": "0.82.0", + "@react-native/debugger-frontend": "0.82.1", + "@react-native/debugger-shell": "0.82.1", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", @@ -7268,6 +7298,7 @@ "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", "license": "MIT", + "peer": true, "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" @@ -7284,33 +7315,34 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", "license": "MIT", + "peer": true, "dependencies": { "async-limiter": "~1.0.0" } }, "node_modules/@react-native/gradle-plugin": { - "version": "0.82.0", - "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.82.0.tgz", - "integrity": "sha512-PTfmQ6cYsJgMXJ49NzB4Sz/DmRUtwatGtcA6MuskRvQpSinno/00Sns7wxphkTVMHGAwk3Xh0t0SFNd1d1HCyw==", - "license": "MIT", + "version": "0.82.1", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.82.1.tgz", + "integrity": "sha512-KkF/2T1NSn6EJ5ALNT/gx0MHlrntFHv8YdooH9OOGl9HQn5NM0ZmQSr86o5utJsGc7ME3R6p3SaQuzlsFDrn8Q==", + "peer": true, "engines": { "node": ">= 20.19.4" } }, "node_modules/@react-native/js-polyfills": { - "version": "0.82.0", - "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.82.0.tgz", - "integrity": "sha512-7K1K64rfq0sKoGxB2DTsZROxal0B04Q+ftia0JyCOGOto/tyBQIQqiQgVtMVEBZSEXZyXmGx3HzF4EEPlvrEtw==", - "license": "MIT", + "version": "0.82.1", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.82.1.tgz", + "integrity": "sha512-tf70X7pUodslOBdLN37J57JmDPB/yiZcNDzS2m+4bbQzo8fhx3eG9QEBv5n4fmzqfGAgSB4BWRHgDMXmmlDSVA==", + "peer": true, "engines": { "node": ">= 20.19.4" } }, "node_modules/@react-native/normalize-colors": { - "version": "0.82.0", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.82.0.tgz", - "integrity": "sha512-oinsK6TYEz5RnFTSk9P+hJ/N/E0pOG76O0euU0Gf3BlXArDpS8m3vrGcTjqeQvajRIaYVHIRAY9hCO6q+exyLg==", - "license": "MIT" + "version": "0.82.1", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.82.1.tgz", + "integrity": "sha512-CCfTR1uX+Z7zJTdt3DNX9LUXr2zWXsNOyLbwupW2wmRzrxlHRYfmLgTABzRL/cKhh0Ubuwn15o72MQChvCRaHw==", + "peer": true }, "node_modules/@react-spring/animated": { "version": "10.0.1", @@ -9106,6 +9138,7 @@ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "type-detect": "4.0.8" } @@ -9115,6 +9148,7 @@ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@sinonjs/commons": "^3.0.0" } @@ -9340,6 +9374,10 @@ "resolved": "libs/components/upload-modal", "link": true }, + "node_modules/@storyteller/ui-viewer-3d": { + "resolved": "libs/components/viewer-3d", + "link": true + }, "node_modules/@svgr/babel-plugin-add-jsx-attribute": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", @@ -10306,6 +10344,7 @@ "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*" } @@ -10480,10 +10519,10 @@ } }, "node_modules/@types/react-reconciler": { - "version": "0.32.1", - "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.32.1.tgz", - "integrity": "sha512-RsqPttsBQ+6af0nATFXJJpemYQH7kL9+xLNm1z+0MjQFDKBZDM2R6SBrjdvRmHu9i9fM6povACj57Ft+pKRNOA==", - "license": "MIT", + "version": "0.32.3", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.32.3.tgz", + "integrity": "sha512-cMi5ZrLG7UtbL7LTK6hq9w/EZIRk4Mf1Z5qHoI+qBh7/WkYkFXQ7gOto2yfUvPzF5ERMAhaXS5eTQ2SAnHjLzA==", + "peer": true, "peerDependencies": { "@types/react": "*" } @@ -12722,6 +12761,7 @@ "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", "license": "MIT" + "peer": true }, "node_modules/ansi-colors": { "version": "4.1.3", @@ -13000,6 +13040,7 @@ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "license": "MIT" + "peer": true }, "node_modules/assertion-error": { "version": "2.0.1", @@ -13050,6 +13091,7 @@ "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", "license": "MIT" + "peer": true }, "node_modules/asynckit": { "version": "0.4.0", @@ -13151,7 +13193,6 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", - "license": "MIT", "peer": true, "dependencies": { "@jest/transform": "^29.7.0", @@ -13189,6 +13230,7 @@ "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", @@ -13205,6 +13247,7 @@ "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", @@ -13315,6 +13358,7 @@ "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.32.0.tgz", "integrity": "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==", "license": "MIT", + "peer": true, "dependencies": { "hermes-parser": "0.32.0" } @@ -13334,6 +13378,7 @@ "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", @@ -13360,6 +13405,7 @@ "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", "license": "MIT", + "peer": true, "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" @@ -13599,6 +13645,7 @@ "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "node-int64": "^0.4.0" } @@ -13987,6 +14034,7 @@ "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", @@ -14005,6 +14053,7 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -14027,6 +14076,7 @@ "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz", "integrity": "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", @@ -14041,6 +14091,7 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -14369,6 +14420,7 @@ "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", "license": "MIT", + "peer": true, "dependencies": { "debug": "2.6.9", "finalhandler": "1.1.2", @@ -14384,6 +14436,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", + "peer": true, "dependencies": { "ms": "2.0.0" } @@ -14393,6 +14446,7 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.8" } @@ -14402,6 +14456,7 @@ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", "license": "MIT", + "peer": true, "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", @@ -14420,12 +14475,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" + "peer": true }, "node_modules/connect/node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", "license": "MIT", + "peer": true, "dependencies": { "ee-first": "1.1.1" }, @@ -14438,6 +14495,7 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -15570,6 +15628,7 @@ "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", "license": "MIT", + "peer": true, "dependencies": { "stackframe": "^1.3.4" } @@ -17113,10 +17172,10 @@ } }, "node_modules/exponential-backoff": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz", - "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA==", - "license": "Apache-2.0" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "peer": true }, "node_modules/express": { "version": "4.21.2", @@ -17277,6 +17336,7 @@ "resolved": "https://registry.npmjs.org/fb-dotslash/-/fb-dotslash-0.5.8.tgz", "integrity": "sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA==", "license": "(MIT OR Apache-2.0)", + "peer": true, "bin": { "dotslash": "bin/dotslash" }, @@ -17289,6 +17349,7 @@ "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "bser": "2.1.1" } @@ -17510,6 +17571,7 @@ "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz", "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==", "license": "MIT" + "peer": true }, "node_modules/follow-redirects": { "version": "1.15.9", @@ -17871,6 +17933,7 @@ "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -18280,18 +18343,21 @@ "resolved": "https://registry.npmjs.org/hermes-compiler/-/hermes-compiler-0.0.0.tgz", "integrity": "sha512-boVFutx6ME/Km2mB6vvsQcdnazEYYI/jV1pomx1wcFUG/EVqTkr5CU0CW9bKipOA/8Hyu3NYwW3THg2Q1kNCfA==", "license": "MIT" + "peer": true }, "node_modules/hermes-estree": { "version": "0.32.0", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.0.tgz", "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==", "license": "MIT" + "peer": true }, "node_modules/hermes-parser": { "version": "0.32.0", "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz", "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", "license": "MIT", + "peer": true, "dependencies": { "hermes-estree": "0.32.0" } @@ -18596,6 +18662,7 @@ "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", "license": "MIT", + "peer": true, "dependencies": { "queue": "6.0.2" }, @@ -18702,6 +18769,7 @@ "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.0.0" } @@ -19384,6 +19452,7 @@ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", @@ -19400,6 +19469,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "license": "ISC", + "peer": true, "bin": { "semver": "bin/semver.js" } @@ -19537,6 +19607,7 @@ "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", "license": "MIT", + "peer": true, "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -19563,6 +19634,7 @@ "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "license": "MIT", + "peer": true, "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", @@ -19588,6 +19660,7 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", @@ -19603,6 +19676,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -19654,6 +19728,7 @@ "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", "license": "MIT", + "peer": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -19668,6 +19743,7 @@ "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "license": "MIT", + "peer": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -19706,6 +19782,7 @@ "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", "license": "MIT", + "peer": true, "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", @@ -19791,6 +19868,7 @@ "resolved": "https://registry.npmjs.org/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz", "integrity": "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==", "license": "0BSD" + "peer": true }, "node_modules/jsdom": { "version": "22.1.0", @@ -20167,6 +20245,7 @@ "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -20190,6 +20269,7 @@ "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", "license": "Apache-2.0", + "peer": true, "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" @@ -20200,6 +20280,7 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", + "peer": true, "dependencies": { "ms": "2.0.0" } @@ -20209,6 +20290,7 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" + "peer": true }, "node_modules/lilconfig": { "version": "3.1.3", @@ -20641,6 +20723,7 @@ "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", "license": "MIT" + "peer": true }, "node_modules/log-symbols": { "version": "4.1.0", @@ -21026,6 +21109,7 @@ "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "tmpl": "1.0.5" } @@ -21045,6 +21129,7 @@ "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", "license": "Apache-2.0" + "peer": true }, "node_modules/math-intrinsics": { "version": "1.1.0", @@ -21293,6 +21378,7 @@ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", "license": "MIT" + "peer": true }, "node_modules/merge-descriptors": { "version": "1.0.3", @@ -21340,6 +21426,7 @@ "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.3.tgz", "integrity": "sha512-+rP+/GieOzkt97hSJ0MrPOuAH/jpaS21ZDvL9DJ35QYRDlQcwzcvUlGUf79AnQxq/2NPiS/AULhhM4TKutIt8Q==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/core": "^7.25.2", @@ -21394,6 +21481,7 @@ "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.83.3.tgz", "integrity": "sha512-1vxlvj2yY24ES1O5RsSIvg4a4WeL7PFXgKOHvXTXiW0deLvQr28ExXj6LjwCCDZ4YZLhq6HddLpZnX4dEdSq5g==", "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", @@ -21409,6 +21497,7 @@ "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.83.3.tgz", "integrity": "sha512-3jo65X515mQJvKqK3vWRblxDEcgY55Sk3w4xa6LlfEXgQ9g1WgMh9m4qVZVwgcHoLy0a2HENTPCCX4Pk6s8c8Q==", "license": "MIT", + "peer": true, "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", @@ -21424,6 +21513,7 @@ "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.83.3.tgz", "integrity": "sha512-59ZO049jKzSmvBmG/B5bZ6/dztP0ilp0o988nc6dpaDsU05Cl1c/lRf+yx8m9WW/JVgbmfO5MziBU559XjI5Zw==", "license": "MIT", + "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6" }, @@ -21436,6 +21526,7 @@ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "license": "MIT", + "peer": true, "engines": { "node": ">= 14" } @@ -21445,6 +21536,7 @@ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "license": "MIT", + "peer": true, "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -21458,6 +21550,7 @@ "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.83.3.tgz", "integrity": "sha512-mTel7ipT0yNjKILIan04bkJkuCzUUkm2SeEaTads8VfEecCh+ltXchdq6DovXJqzQAXuR2P9cxZB47Lg4klriA==", "license": "MIT", + "peer": true, "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", @@ -21477,6 +21570,7 @@ "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.83.3.tgz", "integrity": "sha512-M+X59lm7oBmJZamc96usuF1kusd5YimqG/q97g4Ac7slnJ3YiGglW5CsOlicTR5EWf8MQFxxjDoB6ytTqRe8Hw==", "license": "MIT", + "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", @@ -21491,6 +21585,7 @@ "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.83.3.tgz", "integrity": "sha512-jg5AcyE0Q9Xbbu/4NAwwZkmQn7doJCKGW0SLeSJmzNB9Z24jBe0AL2PHNMy4eu0JiKtNWHz9IiONGZWq7hjVTA==", "license": "MIT", + "peer": true, "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", @@ -21511,6 +21606,7 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", @@ -21526,6 +21622,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -21541,6 +21638,7 @@ "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.83.3.tgz", "integrity": "sha512-O2BmfWj6FSfzBLrNCXt/rr2VYZdX5i6444QJU0fFoc7Ljg+Q+iqebwE3K0eTvkI6TRjELsXk1cjU+fXwAR4OjQ==", "license": "MIT", + "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" @@ -21554,6 +21652,7 @@ "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.83.3.tgz", "integrity": "sha512-0js+zwI5flFxb1ktmR///bxHYg7OLpRpWZlBBruYG8OKYxeMP7SV0xQ/o/hUelrEMdK4LJzqVtHAhBm25LVfAQ==", "license": "MIT", + "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6" }, @@ -21566,6 +21665,7 @@ "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.83.3.tgz", "integrity": "sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" @@ -21579,6 +21679,7 @@ "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.83.3.tgz", "integrity": "sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", @@ -21600,6 +21701,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", "license": "BSD-3-Clause", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -21609,6 +21711,7 @@ "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.83.3.tgz", "integrity": "sha512-F/YChgKd6KbFK3eUR5HdUsfBqVsanf5lNTwFd4Ca7uuxnHgBC3kR/Hba/RGkenR3pZaGNp5Bu9ZqqP52Wyhomw==", "license": "MIT", + "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", @@ -21629,6 +21732,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", "license": "BSD-3-Clause", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -21638,6 +21742,7 @@ "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.83.3.tgz", "integrity": "sha512-eRGoKJU6jmqOakBMH5kUB7VitEWiNrDzBHpYbkBXW7C5fUGeOd2CyqrosEzbMK5VMiZYyOcNFEphvxk3OXey2A==", "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", @@ -21655,6 +21760,7 @@ "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.83.3.tgz", "integrity": "sha512-Ztekew9t/gOIMZX1tvJOgX7KlSLL5kWykl0Iwu2cL2vKMKVALRl1hysyhUw0vjpAvLFx+Kfq9VLjnHIkW32fPA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", @@ -21679,12 +21785,14 @@ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "license": "MIT" + "peer": true }, "node_modules/metro/node_modules/jest-worker": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", @@ -21700,6 +21808,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", "license": "BSD-3-Clause", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -21709,6 +21818,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -21724,6 +21834,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=8.3.0" }, @@ -22875,6 +22986,7 @@ "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "license": "MIT" + "peer": true }, "node_modules/node-machine-id": { "version": "1.1.12", @@ -23085,6 +23197,7 @@ "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", "license": "MIT" + "peer": true }, "node_modules/nwsapi": { "version": "2.2.20", @@ -23171,6 +23284,7 @@ "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.83.3.tgz", "integrity": "sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA==", "license": "MIT", + "peer": true, "dependencies": { "flow-enums-runtime": "^0.0.6" }, @@ -23519,6 +23633,7 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -24341,6 +24456,7 @@ "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", "license": "MIT", + "peer": true, "dependencies": { "asap": "~2.0.6" } @@ -24503,6 +24619,7 @@ "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", "license": "MIT", + "peer": true, "dependencies": { "inherits": "~2.0.3" } @@ -24634,6 +24751,7 @@ "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-6.1.5.tgz", "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", "license": "MIT", + "peer": true, "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" @@ -24644,6 +24762,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=8.3.0" }, @@ -24742,9 +24861,9 @@ "license": "MIT" }, "node_modules/react-konva": { - "version": "19.0.10", - "resolved": "https://registry.npmjs.org/react-konva/-/react-konva-19.0.10.tgz", - "integrity": "sha512-tJID7bungYs8ri5McjjlHWCniQMAasiNkTgATd8OJEnLrz3B7S5JN7nFkNfWETIHPx9rEN3gc+kQg32RhdZJcw==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-konva/-/react-konva-19.2.0.tgz", + "integrity": "sha512-Ofifq/rdNvff50+Lj8x86WSfoeQDvdysOlsXMMrpD2uWmDxUPrEYSRLt27iCfdovQZL6xinKRpX9VaL9xDwXDQ==", "funding": [ { "type": "patreon", @@ -24760,16 +24879,17 @@ } ], "license": "MIT", + "peer": true, "dependencies": { - "@types/react-reconciler": "^0.32.1", + "@types/react-reconciler": "^0.32.2", "its-fine": "^2.0.0", - "react-reconciler": "0.32.0", - "scheduler": "0.26.0" + "react-reconciler": "0.33.0", + "scheduler": "0.27.0" }, "peerDependencies": { "konva": "^8.0.1 || ^7.2.5 || ^9.0.0 || ^10.0.0", - "react": "^18.3.1 || ^19.0.0", - "react-dom": "^18.3.1 || ^19.0.0" + "react": "^19.2.0", + "react-dom": "^19.2.0" } }, "node_modules/react-konva/node_modules/its-fine": { @@ -24777,6 +24897,7 @@ "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", "license": "MIT", + "peer": true, "dependencies": { "@types/react-reconciler": "^0.28.9" }, @@ -24789,30 +24910,31 @@ "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "*" } }, "node_modules/react-konva/node_modules/react-reconciler": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.32.0.tgz", - "integrity": "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ==", - "license": "MIT", + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz", + "integrity": "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA==", + "peer": true, "dependencies": { - "scheduler": "^0.26.0" + "scheduler": "^0.27.0" }, "engines": { "node": ">=0.10.0" }, "peerDependencies": { - "react": "^19.1.0" + "react": "^19.2.0" } }, "node_modules/react-konva/node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT" + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "peer": true }, "node_modules/react-lifecycles-compat": { "version": "3.0.4", @@ -25004,10 +25126,10 @@ } }, "node_modules/react-spring/node_modules/@react-three/fiber": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.3.0.tgz", - "integrity": "sha512-myPe3YL/C8+Eq939/4qIVEPBW/uxV0iiUbmjfwrs9sGKYDG8ib8Dz3Okq7BQt8P+0k4igedONbjXMQy84aDFmQ==", - "license": "MIT", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.0.tgz", + "integrity": "sha512-k4iu1R6e5D54918V4sqmISUkI5OgTw3v7/sDRKEC632Wd5g2WBtUS5gyG63X0GJO/HZUj1tsjSXfyzwrUHZl1g==", + "peer": true, "dependencies": { "@babel/runtime": "^7.17.8", "@types/react-reconciler": "^0.32.0", @@ -25058,6 +25180,7 @@ "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", "license": "MIT", + "peer": true, "dependencies": { "@types/react-reconciler": "^0.28.9" }, @@ -25070,6 +25193,7 @@ "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "*" } @@ -25079,6 +25203,7 @@ "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.31.0.tgz", "integrity": "sha512-7Ob7Z+URmesIsIVRjnLoDGwBEG/tVitidU0nMsqX/eeJaLY89RISO/10ERe0MqmzuKUUB1rmY+h1itMbUHg9BQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.25.0" }, @@ -25090,13 +25215,12 @@ } }, "node_modules/react-spring/node_modules/@types/react": { - "version": "19.2.2", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", - "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", - "license": "MIT", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz", + "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==", "peer": true, "dependencies": { - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/react-spring/node_modules/brace-expansion": { @@ -25104,6 +25228,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -25128,6 +25253,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" @@ -25138,16 +25264,24 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "license": "MIT", + "peer": true, "engines": { "node": ">=18" } }, + "node_modules/react-spring/node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "peer": true + }, "node_modules/react-spring/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", "license": "ISC", + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -25168,6 +25302,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -25176,20 +25311,19 @@ } }, "node_modules/react-spring/node_modules/react-native": { - "version": "0.82.0", - "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.82.0.tgz", - "integrity": "sha512-E+sBFDgpwzoZzPn86gSGRBGLnS9Q6r4y6Xk5I57/QbkqkDOxmQb/bzQq/oCdUCdHImKiow2ldC3WJfnvAKIfzg==", - "license": "MIT", + "version": "0.82.1", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.82.1.tgz", + "integrity": "sha512-tFAqcU7Z4g49xf/KnyCEzI4nRTu1Opcx05Ov2helr8ZTg1z7AJR/3sr2rZ+AAVlAs2IXk+B0WOxXGmdD3+4czA==", "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.7.0", - "@react-native/assets-registry": "0.82.0", - "@react-native/codegen": "0.82.0", - "@react-native/community-cli-plugin": "0.82.0", - "@react-native/gradle-plugin": "0.82.0", - "@react-native/js-polyfills": "0.82.0", - "@react-native/normalize-colors": "0.82.0", - "@react-native/virtualized-lists": "0.82.0", + "@react-native/assets-registry": "0.82.1", + "@react-native/codegen": "0.82.1", + "@react-native/community-cli-plugin": "0.82.1", + "@react-native/gradle-plugin": "0.82.1", + "@react-native/js-polyfills": "0.82.1", + "@react-native/normalize-colors": "0.82.1", + "@react-native/virtualized-lists": "0.82.1", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", @@ -25235,10 +25369,10 @@ } }, "node_modules/react-spring/node_modules/react-native/node_modules/@react-native/virtualized-lists": { - "version": "0.82.0", - "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.82.0.tgz", - "integrity": "sha512-fReDITtqwWdN29doPHhmeQEqa12ATJ4M2Y1MrT8Q1Hoy5a0H3oEn9S7fknGr7Pj+/I77yHyJajUbCupnJ8vkFA==", - "license": "MIT", + "version": "0.82.1", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.82.1.tgz", + "integrity": "sha512-f5zpJg9gzh7JtCbsIwV+4kP3eI0QBuA93JGmwFRd4onQ3DnCjV2J5pYqdWtM95sjSKK1dyik59Gj01lLeKqs1Q==", + "peer": true, "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" @@ -25262,24 +25396,28 @@ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", "license": "MIT" + "peer": true }, "node_modules/react-spring/node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", "license": "MIT" + "peer": true }, "node_modules/react-spring/node_modules/scheduler": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", "license": "MIT" + "peer": true }, "node_modules/react-spring/node_modules/ws": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", "license": "MIT", + "peer": true, "dependencies": { "async-limiter": "~1.0.0" } @@ -25310,6 +25448,7 @@ "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", "license": "MIT", + "peer": true, "peerDependencies": { "react": ">=16.13", "react-dom": ">=16.13" @@ -25342,7 +25481,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/react-zdog/-/react-zdog-1.2.2.tgz", "integrity": "sha512-Ix7ALha91aOEwiHuxumCeYbARS5XNpc/w0v145oGkM6poF/CvhKJwzLhM5sEZbtrghMA+psAhOJkCTzJoseicA==", - "license": "MIT", "peer": true, "dependencies": { "react": "^18.2.0", @@ -25686,6 +25824,7 @@ "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", "license": "MIT" + "peer": true }, "node_modules/resolve": { "version": "1.22.8", @@ -26174,6 +26313,7 @@ "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", "integrity": "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -26294,6 +26434,7 @@ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.4" }, @@ -26606,12 +26747,14 @@ "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", "license": "MIT" + "peer": true }, "node_modules/stacktrace-parser": { "version": "0.1.11", "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", "license": "MIT", + "peer": true, "dependencies": { "type-fest": "^0.7.1" }, @@ -27101,6 +27244,7 @@ "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", "license": "MIT", + "peer": true, "peerDependencies": { "react": ">=17.0" } @@ -27477,6 +27621,7 @@ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "license": "ISC", + "peer": true, "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", @@ -27491,6 +27636,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -27502,6 +27648,7 @@ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", "license": "ISC", + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -27522,6 +27669,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -27571,6 +27719,7 @@ "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", "license": "MIT" + "peer": true }, "node_modules/through2": { "version": "2.0.5", @@ -27691,6 +27840,7 @@ "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "license": "BSD-3-Clause" + "peer": true }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -27958,6 +28108,7 @@ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "license": "MIT", + "peer": true, "engines": { "node": ">=4" } @@ -27967,6 +28118,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", "license": "(MIT OR CC0-1.0)", + "peer": true, "engines": { "node": ">=8" } @@ -29640,6 +29792,7 @@ "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", "license": "MIT" + "peer": true }, "node_modules/vscode-uri": { "version": "3.1.0", @@ -29666,6 +29819,7 @@ "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "makeerror": "1.0.12" } @@ -29849,6 +30003,7 @@ "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", "license": "MIT" + "peer": true }, "node_modules/whatwg-mimetype": { "version": "3.0.0", @@ -30051,6 +30206,7 @@ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", "license": "ISC", + "peer": true, "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" @@ -30190,7 +30346,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/zdog/-/zdog-1.1.3.tgz", "integrity": "sha512-raRj6r0gPzopFm5XWBJZr/NuV4EEnT4iE+U3dp5FV5pCb588Gmm3zLIp/j9yqqcMiHH8VNQlerLTgOqL7krh6w==", - "license": "MIT", "peer": true }, "node_modules/zustand": { diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 7d757bc973..059ceaf4cd 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -173,6 +173,9 @@ }, { "path": "./libs/api-enums" + }, + { + "path": "./libs/components/viewer-3d" } ] }