diff --git a/Cargo.lock b/Cargo.lock index 9c8483c4..ead15269 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -272,7 +272,6 @@ dependencies = [ "uuid_utils", "web_base64", "workspace-build-acceleration", - "worldlabs_consumer_client", ] [[package]] @@ -5682,7 +5681,6 @@ dependencies = [ "url_utils", "uuid 1.22.0", "workspace-build-acceleration", - "worldlabs_api_client", "wreq", ] @@ -9239,64 +9237,6 @@ dependencies = [ name = "workspace-build-acceleration" version = "0.1.0" -[[package]] -name = "worldlabs_api_client" -version = "0.0.1" -dependencies = [ - "anyhow", - "chrono", - "env", - "env_logger", - "errors", - "filesys", - "log", - "serde", - "serde_json", - "strum", - "test_data", - "tokio", - "uuid 1.22.0", - "workspace-build-acceleration", - "wreq", -] - -[[package]] -name = "worldlabs_consumer_client" -version = "0.0.1" -dependencies = [ - "anyhow", - "base64 0.22.1", - "bstr", - "byteorder", - "chrono", - "cloudflare_errors", - "cloudflare_mitigation", - "env", - "env_logger", - "errors", - "filesys", - "futures", - "http_headers", - "indexmap 2.13.0", - "jwt_light", - "log", - "log4rs", - "once_cell", - "regex", - "scraper", - "serde", - "serde_json", - "sha2 0.11.0-rc.5", - "test_data", - "test_utils", - "tokio", - "url", - "uuid 1.22.0", - "workspace-build-acceleration", - "wreq", - "wreq-util", -] - [[package]] name = "wreq" version = "6.0.0-rc.28" diff --git a/Cargo.toml b/Cargo.toml index 6266636d..5b9c72b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,6 @@ members = [ "crates/api_clients/midjourney_client", "crates/router", "crates/api_clients/seedance2pro_client", - "crates/api_clients/worldlabs_api_client", - "crates/api_clients/worldlabs_consumer_client", # Desktop apps "crates/artcraftx", @@ -130,8 +128,6 @@ test_utils = { path = "crates/testing/test_utils" } url_utils = { path = "crates/lib/url_utils" } uuid_utils = { path = "crates/lib/uuid_utils" } web_base64 = { path = "crates/lib/data/web_base64" } -worldlabs_api_client = { path = "crates/api_clients/worldlabs_api_client" } -worldlabs_consumer_client = { path = "crates/api_clients/worldlabs_consumer_client" } # External libs # Centrally pinned versions so that we can uniformly upgrade/downgrade versions across the workspace diff --git a/crates/api_clients/worldlabs_api_client/Cargo.toml b/crates/api_clients/worldlabs_api_client/Cargo.toml deleted file mode 100644 index 7340fdb1..00000000 --- a/crates/api_clients/worldlabs_api_client/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -[package] -name = "worldlabs_api_client" -edition = "2024" -version = "0.0.1" -publish = false - -[package.metadata.cargo-udeps.ignore] -normal = ["workspace-build-acceleration"] - -[lib] -name = "worldlabs_api_client" -path = "src/lib.rs" - -[dependencies] -# Workspace hack (faster compile times) -workspace-build-acceleration.workspace = true - -# Internal -errors.workspace = true -filesys.workspace = true - -# External -anyhow.workspace = true -log.workspace = true -serde.workspace = true -serde_json.workspace = true -tokio = { version = "1", features = ["full"] } -uuid.workspace = true -wreq.workspace = true - -[dev-dependencies] -filesys.workspace = true -env_logger.workspace = true -env.workspace = true -chrono.workspace = true -strum.workspace = true -test_data.workspace = true diff --git a/crates/api_clients/worldlabs_api_client/src/api/api_types/media_asset_id.rs b/crates/api_clients/worldlabs_api_client/src/api/api_types/media_asset_id.rs deleted file mode 100644 index 604aa51f..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/api_types/media_asset_id.rs +++ /dev/null @@ -1,9 +0,0 @@ -/// ID for a media asset in the World Labs API. -#[derive(Clone, Debug)] -pub struct MediaAssetId(pub String); - -impl MediaAssetId { - pub fn as_str(&self) -> &str { - &self.0 - } -} diff --git a/crates/api_clients/worldlabs_api_client/src/api/api_types/mod.rs b/crates/api_clients/worldlabs_api_client/src/api/api_types/mod.rs deleted file mode 100644 index 6b2a76cb..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/api_types/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod media_asset_id; -pub mod operation_id; -pub mod world_id; -pub mod world_labs_model; diff --git a/crates/api_clients/worldlabs_api_client/src/api/api_types/operation_id.rs b/crates/api_clients/worldlabs_api_client/src/api/api_types/operation_id.rs deleted file mode 100644 index 83504ee6..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/api_types/operation_id.rs +++ /dev/null @@ -1,9 +0,0 @@ -/// ID for an async operation returned by the World Labs API. -#[derive(Clone, Debug)] -pub struct OperationId(pub String); - -impl OperationId { - pub fn as_str(&self) -> &str { - &self.0 - } -} diff --git a/crates/api_clients/worldlabs_api_client/src/api/api_types/world_id.rs b/crates/api_clients/worldlabs_api_client/src/api/api_types/world_id.rs deleted file mode 100644 index 2e298585..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/api_types/world_id.rs +++ /dev/null @@ -1,13 +0,0 @@ -/// Strongly-typed World ID -#[derive(Clone, Debug)] -pub struct WorldId(pub String); - -impl WorldId { - pub fn from_str(s: &str) -> Self { - Self(s.to_string()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } -} diff --git a/crates/api_clients/worldlabs_api_client/src/api/api_types/world_labs_model.rs b/crates/api_clients/worldlabs_api_client/src/api/api_types/world_labs_model.rs deleted file mode 100644 index c73fe75e..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/api_types/world_labs_model.rs +++ /dev/null @@ -1,190 +0,0 @@ -/// The available World Labs generation models. -#[derive(Clone, Copy, Debug)] -pub enum WorldLabsModel { - /// `Marble 0.1-mini` is good for quick drafts - /// Generation time: 30-45 seconds - /// Cost: 150-330 credits - #[deprecated(note="Marble 0.1-mini is deprecated. Use `marble-1.0-draft` instead.")] - Marble0p1Mini, - - /// `Marble 0.1-plus` is best for final renders - /// Generation time: ~5 minutes, - /// Cost: 1500-1600 credits - #[deprecated(note="Marble 0.1-plus is deprecated. Use `marble-1.0` instead.")] - Marble0p1Plus, - - /// marble-1.0 - Marble1p0, - - /// marble-1.0-draft - Marble1p0Draft, - - /// marble-1.1 - Marble1p1, - - /// marble-1.1-plus - Marble1p1Plus, -} - -impl WorldLabsModel { - /// Returns the official API name string used in HTTP requests. - pub fn get_model_api_name_str(&self) -> &'static str { - match self { - Self::Marble0p1Mini => "Marble 0.1-mini", - Self::Marble0p1Plus => "Marble 0.1-plus", - Self::Marble1p0 => "marble-1.0", - Self::Marble1p0Draft => "marble-1.0-draft", - Self::Marble1p1 => "marble-1.1", - Self::Marble1p1Plus => "marble-1.1-plus", - } - } - - pub fn is_deprecated(&self) -> bool { - match self { - Self::Marble0p1Mini | Self::Marble0p1Plus => true, - _ => false, - } - } - - pub fn to_new_value(self) -> Self { - match self { - Self::Marble0p1Mini => Self::Marble1p0Draft, - Self::Marble0p1Plus => Self::Marble1p0, - _ => self, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - mod api_name { - use super::*; - - #[allow(deprecated)] - #[test] - fn marble_0p1_mini() { - assert_eq!(WorldLabsModel::Marble0p1Mini.get_model_api_name_str(), "Marble 0.1-mini"); - } - - #[allow(deprecated)] - #[test] - fn marble_0p1_plus() { - assert_eq!(WorldLabsModel::Marble0p1Plus.get_model_api_name_str(), "Marble 0.1-plus"); - } - - #[test] - fn marble_1p0() { - assert_eq!(WorldLabsModel::Marble1p0.get_model_api_name_str(), "marble-1.0"); - } - - #[test] - fn marble_1p0_draft() { - assert_eq!(WorldLabsModel::Marble1p0Draft.get_model_api_name_str(), "marble-1.0-draft"); - } - - #[test] - fn marble_1p1() { - assert_eq!(WorldLabsModel::Marble1p1.get_model_api_name_str(), "marble-1.1"); - } - - #[test] - fn marble_1p1_plus() { - assert_eq!(WorldLabsModel::Marble1p1Plus.get_model_api_name_str(), "marble-1.1-plus"); - } - } - - mod is_deprecated { - use super::*; - - #[allow(deprecated)] - #[test] - fn marble_0p1_mini_is_deprecated() { - assert!(WorldLabsModel::Marble0p1Mini.is_deprecated()); - } - - #[allow(deprecated)] - #[test] - fn marble_0p1_plus_is_deprecated() { - assert!(WorldLabsModel::Marble0p1Plus.is_deprecated()); - } - - #[test] - fn marble_1p0_is_not_deprecated() { - assert!(!WorldLabsModel::Marble1p0.is_deprecated()); - } - - #[test] - fn marble_1p0_draft_is_not_deprecated() { - assert!(!WorldLabsModel::Marble1p0Draft.is_deprecated()); - } - - #[test] - fn marble_1p1_is_not_deprecated() { - assert!(!WorldLabsModel::Marble1p1.is_deprecated()); - } - - #[test] - fn marble_1p1_plus_is_not_deprecated() { - assert!(!WorldLabsModel::Marble1p1Plus.is_deprecated()); - } - } - - mod to_new_value { - use super::*; - - #[allow(deprecated)] - #[test] - fn marble_0p1_mini_maps_to_1p0_draft() { - let new = WorldLabsModel::Marble0p1Mini.to_new_value(); - assert_eq!(new.get_model_api_name_str(), "marble-1.0-draft"); - assert!(!new.is_deprecated()); - } - - #[allow(deprecated)] - #[test] - fn marble_0p1_plus_maps_to_1p0() { - let new = WorldLabsModel::Marble0p1Plus.to_new_value(); - assert_eq!(new.get_model_api_name_str(), "marble-1.0"); - assert!(!new.is_deprecated()); - } - - #[test] - fn marble_1p0_stays_as_1p0() { - let new = WorldLabsModel::Marble1p0.to_new_value(); - assert_eq!(new.get_model_api_name_str(), "marble-1.0"); - } - - #[test] - fn marble_1p0_draft_stays_as_1p0_draft() { - let new = WorldLabsModel::Marble1p0Draft.to_new_value(); - assert_eq!(new.get_model_api_name_str(), "marble-1.0-draft"); - } - - #[test] - fn marble_1p1_stays_as_1p1() { - let new = WorldLabsModel::Marble1p1.to_new_value(); - assert_eq!(new.get_model_api_name_str(), "marble-1.1"); - } - - #[test] - fn marble_1p1_plus_stays_as_1p1_plus() { - let new = WorldLabsModel::Marble1p1Plus.to_new_value(); - assert_eq!(new.get_model_api_name_str(), "marble-1.1-plus"); - } - - #[test] - fn new_models_are_not_deprecated_after_conversion() { - for model in [ - WorldLabsModel::Marble1p0, - WorldLabsModel::Marble1p0Draft, - WorldLabsModel::Marble1p1, - WorldLabsModel::Marble1p1Plus, - ] { - let new = model.to_new_value(); - assert!(!new.is_deprecated(), "{:?} should not be deprecated after to_new_value", model); - } - } - } -} diff --git a/crates/api_clients/worldlabs_api_client/src/api/mod.rs b/crates/api_clients/worldlabs_api_client/src/api/mod.rs deleted file mode 100644 index 3a0804a3..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod api_types; -pub mod requests; diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/delete_world/delete_world.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/delete_world/delete_world.rs deleted file mode 100644 index c481ce81..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/delete_world/delete_world.rs +++ /dev/null @@ -1,102 +0,0 @@ -use crate::api::api_types::world_id::WorldId; -use crate::api::requests::delete_world::http_request::RawResponse; -use crate::credentials::world_labs_api_creds::WorldLabsApiCreds; -use crate::error::filter_world_labs_http_error::filter_world_labs_http_error; -use crate::error::world_labs_error::WorldLabsError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use log::{debug, error}; -use std::time::Duration; -use wreq::Client; - -const BASE_URL: &str = "https://api.worldlabs.ai/marble/v1/worlds"; - -pub struct DeleteWorldArgs<'a> { - pub creds: &'a WorldLabsApiCreds, - pub world_id: &'a WorldId, - pub request_timeout: Option, -} - -pub struct DeleteWorldResponse { - pub world_id: WorldId, - pub deleted: bool, -} - -/// DELETE /marble/v1/worlds/{world_id} -/// -/// Permanently delete a world and its assets. Only the owner can delete. -pub async fn delete_world(args: DeleteWorldArgs<'_>) -> Result { - let client = Client::new(); - - let url = format!("{}/{}", BASE_URL, args.world_id.as_str()); - - debug!("Requesting URL: {}", url); - - let mut request_builder = client.delete(&url) - .header("WLT-Api-Key", args.creds.api_key()); - - if let Some(timeout) = args.request_timeout { - request_builder = request_builder.timeout(timeout); - } - - let response = request_builder.send() - .await - .map_err(|err| { - error!("Error during delete_world request: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - let status = response.status(); - - let response_body = response.text() - .await - .map_err(|err| { - error!("Error reading response body: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - if !status.is_success() { - error!("delete_world returned error (code {}): {:?}", status.as_u16(), response_body); - } - - filter_world_labs_http_error(status, Some(&response_body))?; - - debug!("Response body (200): {}", response_body); - - let raw: RawResponse = serde_json::from_str(&response_body) - .map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?; - - Ok(DeleteWorldResponse { - world_id: WorldId(raw.world_id), - deleted: raw.deleted, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_utils::get_test_api_key::get_test_api_key; - use crate::test_utils::setup_test_logging::setup_test_logging; - use log::LevelFilter; - - #[tokio::test] - #[ignore] - async fn test_delete_world() { - setup_test_logging(LevelFilter::Debug); - - let creds = get_test_api_key().unwrap(); - - // Use a known world_id to delete - let world_id = WorldId::from_str("REPLACE_WITH_REAL_ID"); - - let response = delete_world(DeleteWorldArgs { - creds: &creds, - world_id: &world_id, - request_timeout: None, - }).await.unwrap(); - - println!("World ID: {}", response.world_id.as_str()); - println!("Deleted: {}", response.deleted); - - assert_eq!(1, 2); - } -} diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/delete_world/http_request.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/delete_world/http_request.rs deleted file mode 100644 index 954d02f5..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/delete_world/http_request.rs +++ /dev/null @@ -1,7 +0,0 @@ -use serde::Deserialize; - -#[derive(Deserialize)] -pub(crate) struct RawResponse { - pub world_id: String, - pub deleted: bool, -} diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/delete_world/mod.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/delete_world/mod.rs deleted file mode 100644 index 247dfa70..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/delete_world/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod delete_world; -pub(crate) mod http_request; diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/generate_world/generate_world.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/generate_world/generate_world.rs deleted file mode 100644 index 7c12eaf7..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/generate_world/generate_world.rs +++ /dev/null @@ -1,449 +0,0 @@ -use crate::api::api_types::operation_id::OperationId; -use crate::api::api_types::world_labs_model::WorldLabsModel; -use crate::api::requests::generate_world::http_request::{RawRequest, RawResponse, WorldPrompt, Permission}; -use crate::credentials::world_labs_api_creds::WorldLabsApiCreds; -use crate::error::filter_world_labs_http_error::filter_world_labs_http_error; -use crate::error::world_labs_error::WorldLabsError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use log::{debug, error}; -use std::time::Duration; -use wreq::Client; - -const URL: &str = "https://api.worldlabs.ai/marble/v1/worlds:generate"; - -pub struct GenerateWorldArgs<'a> { - pub creds: &'a WorldLabsApiCreds, - pub world_prompt: WorldPrompt, - pub display_name: Option, - pub model: WorldLabsModel, - pub seed: Option, - pub tags: Option>, - pub permission: Option, - pub request_timeout: Option, -} - -pub struct GenerateWorldResponse { - pub operation_id: OperationId, - pub done: bool, -} - -/// POST /marble/v1/worlds:generate -/// -/// Start world generation. Returns an operation_id to poll with get_operation. -pub async fn generate_world(args: GenerateWorldArgs<'_>) -> Result { - let client = Client::new(); - - let model_name = args.model - .to_new_value() - .get_model_api_name_str() - .to_string(); - - let payload = RawRequest { - world_prompt: args.world_prompt, - display_name: args.display_name, - model: Some(model_name), - seed: args.seed, - tags: args.tags, - permission: args.permission, - }; - - debug!("Requesting URL: {}", URL); - - let mut request_builder = client.post(URL) - .header("WLT-Api-Key", args.creds.api_key()) - .header("Content-Type", "application/json") - .json(&payload); - - if let Some(timeout) = args.request_timeout { - request_builder = request_builder.timeout(timeout); - } - - let response = request_builder.send() - .await - .map_err(|err| { - error!("Error during generate_world request: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - let status = response.status(); - - let response_body = response.text() - .await - .map_err(|err| { - error!("Error reading response body: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - if !status.is_success() { - error!("generate_world returned error (code {}): {:?}", status.as_u16(), response_body); - } - - filter_world_labs_http_error(status, Some(&response_body))?; - - debug!("Response body (200): {}", response_body); - - let raw: RawResponse = serde_json::from_str(&response_body) - .map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?; - - Ok(GenerateWorldResponse { - operation_id: OperationId(raw.operation_id), - done: raw.done.unwrap_or(false), - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::api::requests::generate_world::http_request::{ContentReference, WorldPrompt}; - use crate::api::requests::prepare_upload::prepare_upload::{prepare_upload, PrepareUploadArgs, MediaAssetKind}; - use crate::api::requests::upload_to_signed_url::upload_to_signed_url::{upload_to_signed_url, UploadToSignedUrlArgs}; - use crate::test_utils::get_test_api_key::get_test_api_key; - use crate::test_utils::setup_test_logging::setup_test_logging; - use filesys::file_read_bytes::file_read_bytes; - use log::LevelFilter; - - #[tokio::test] - #[ignore] - async fn test_generate_world_text_prompt() { - setup_test_logging(LevelFilter::Debug); - - let creds = get_test_api_key().unwrap(); - - let response = generate_world(GenerateWorldArgs { - creds: &creds, - world_prompt: WorldPrompt::Text { - text_prompt: Some("A cozy cabin in the snowy mountains".to_string()), - disable_recaption: None, - }, - display_name: Some("Test World".to_string()), - model: WorldLabsModel::Marble0p1Mini, - seed: None, - tags: None, - permission: None, - request_timeout: None, - }).await.unwrap(); - - println!("Operation ID: {}", response.operation_id.as_str()); - println!("Done: {}", response.done); - - assert_eq!(1, 2); - } - - #[tokio::test] - #[ignore] - async fn test_generate_world_image_uri() { - setup_test_logging(LevelFilter::Debug); - - let creds = get_test_api_key().unwrap(); - - let response = generate_world(GenerateWorldArgs { - creds: &creds, - world_prompt: WorldPrompt::Image { - image_prompt: ContentReference::Uri { - uri: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/280px-PNG_transparency_demonstration_1.png".to_string(), - }, - text_prompt: None, - is_pano: None, - disable_recaption: None, - }, - display_name: None, - model: WorldLabsModel::Marble0p1Mini, - seed: None, - tags: None, - permission: None, - request_timeout: None, - }).await.unwrap(); - - println!("Operation ID: {}", response.operation_id.as_str()); - println!("Done: {}", response.done); - - assert_eq!(1, 2); - } - - /// End-to-end: upload a safe image step by step (prepare → upload → - /// generate_world), printing each step's result for debugging. - /// - /// Uses a synthetic gradient PNG from test_data/. - #[tokio::test] - #[ignore] - async fn test_generate_world_for_safe_image() { - setup_test_logging(LevelFilter::Debug); - - let creds = get_test_api_key().unwrap(); - - // --- Read file --- - let file_bytes = file_read_bytes("/Users/bt/dev/storyteller/artcraft/test_data/image/mochi.jpg") - .expect("failed to read safe test image"); - println!("Read {} bytes from safe test image", file_bytes.len()); - - // --- Step 1: prepare_upload --- - println!("\n=== Step 1: prepare_upload ==="); - let prepare_result = prepare_upload(PrepareUploadArgs { - creds: &creds, - file_name: "mochi.jpg", - kind: MediaAssetKind::Image, - request_timeout: None, - }).await; - - println!("prepare_upload result: {:?}", prepare_result.as_ref().map(|r| &r.media_asset_id)); - - let prepare_response = match prepare_result { - Ok(r) => { - println!(" Media Asset ID: {}", r.media_asset_id.as_str()); - println!(" Upload URL: {}", r.upload_url); - println!(" Required headers: {:?}", r.required_headers); - r - } - Err(err) => { - println!(" ERROR at prepare_upload: {:?}", err); - panic!("prepare_upload failed: {:?}", err); - } - }; - - // --- Step 2: upload_to_signed_url --- - println!("\n=== Step 2: upload_to_signed_url ==="); - let upload_result = upload_to_signed_url(UploadToSignedUrlArgs { - upload_url: &prepare_response.upload_url, - file_bytes, - required_headers: &prepare_response.required_headers, - content_type: "image/jpeg", - request_timeout: None, - }).await; - - match &upload_result { - Ok(()) => println!(" Upload succeeded."), - Err(err) => { - println!(" ERROR at upload_to_signed_url: {:?}", err); - panic!("upload_to_signed_url failed: {:?}", err); - } - } - - // --- Step 3: generate_world --- - println!("\n=== Step 3: generate_world ==="); - let generate_result = generate_world(GenerateWorldArgs { - creds: &creds, - world_prompt: WorldPrompt::Image { - image_prompt: ContentReference::MediaAsset { - media_asset_id: prepare_response.media_asset_id.as_str().to_string(), - }, - text_prompt: Some("A colorful gradient landscape".to_string()), - is_pano: None, - disable_recaption: None, - }, - display_name: None, - model: WorldLabsModel::Marble0p1Mini, - seed: None, - tags: None, - permission: None, - request_timeout: None, - }).await; - - match &generate_result { - Ok(r) => { - println!(" SUCCESS — safe image"); - println!(" Operation ID: {}", r.operation_id.as_str()); - println!(" Done: {}", r.done); - } - Err(err) => { - println!(" ERROR at generate_world: {:?}", err); - } - } - - assert_eq!(1, 2, "Inspect output above"); - } - - /// End-to-end: upload an NSFW image step by step (prepare → upload → - /// generate_world), printing each step's result for debugging. - /// - /// We expect the NSFW rejection to happen at one of these steps. - /// This test helps us identify *which* step returns the moderation error - /// and what the error response body looks like. - /// - /// NB: Place the NSFW test image at `test_data/images/nsfw_test_image.png`. - /// This file is NOT checked into git. - #[tokio::test] - #[ignore] - async fn test_generate_world_for_nsfw_image() { - setup_test_logging(LevelFilter::Debug); - - let creds = get_test_api_key().unwrap(); - - // --- Read file --- - let file_bytes = file_read_bytes("/Users/bt/Pictures/nsfw_test/nsfw_test_1.jpg") - .expect("failed to read NSFW test image"); - println!("Read {} bytes from NSFW test image", file_bytes.len()); - - // --- Step 1: prepare_upload --- - println!("\n=== Step 1: prepare_upload ==="); - let prepare_result = prepare_upload(PrepareUploadArgs { - creds: &creds, - file_name: "nsfw_test_1.jpg", - kind: MediaAssetKind::Image, - request_timeout: None, - }).await; - - let prepare_response = match prepare_result { - Ok(r) => { - println!(" OK — Media Asset ID: {}", r.media_asset_id.as_str()); - println!(" Upload URL: {}", r.upload_url); - r - } - Err(err) => { - println!(" NSFW CAUGHT AT STEP 1 (prepare_upload)!"); - println!(" Error (Debug): {:?}", err); - println!(" Error (Display): {}", err); - assert_eq!(1, 2, "NSFW caught at prepare_upload — inspect output above"); - unreachable!(); - } - }; - - // --- Step 2: upload_to_signed_url --- - println!("\n=== Step 2: upload_to_signed_url ==="); - let upload_result = upload_to_signed_url(UploadToSignedUrlArgs { - upload_url: &prepare_response.upload_url, - file_bytes, - required_headers: &prepare_response.required_headers, - content_type: "image/jpeg", - request_timeout: None, - }).await; - - match &upload_result { - Ok(()) => println!(" OK — upload succeeded."), - Err(err) => { - println!(" NSFW CAUGHT AT STEP 2 (upload_to_signed_url)!"); - println!(" Error (Debug): {:?}", err); - println!(" Error (Display): {}", err); - assert_eq!(1, 2, "NSFW caught at upload — inspect output above"); - } - } - - // --- Step 3: generate_world --- - println!("\n=== Step 3: generate_world ==="); - let generate_result = generate_world(GenerateWorldArgs { - creds: &creds, - world_prompt: WorldPrompt::Image { - image_prompt: ContentReference::MediaAsset { - media_asset_id: prepare_response.media_asset_id.as_str().to_string(), - }, - text_prompt: None, - is_pano: None, - disable_recaption: None, - }, - display_name: None, - model: WorldLabsModel::Marble0p1Mini, - seed: None, - tags: None, - permission: None, - request_timeout: None, - }).await; - - match &generate_result { - Ok(r) => { - println!(" UNEXPECTED SUCCESS — NSFW image was NOT rejected at generate_world!"); - println!(" Operation ID: {}", r.operation_id.as_str()); - println!(" Done: {}", r.done); - } - Err(err) => { - println!(" NSFW CAUGHT AT STEP 3 (generate_world)!"); - println!(" Error (Debug): {:?}", err); - println!(" Error (Display): {}", err); - } - } - - assert_eq!(1, 2, "Inspect output above"); - } - - /// Marble 1.1: generate world from a mountain/tree image via URI. - #[tokio::test] - #[ignore] - async fn test_generate_world_marble_1p1_image_uri() { - setup_test_logging(LevelFilter::Debug); - - let creds = get_test_api_key().unwrap(); - - let image_url = test_data::web::image_urls::MOUNTAIN_TREE_IMAGE_URL; - - println!("=== generate_world with Marble 1.1 ==="); - println!("Image URL: {}", image_url); - - let result = generate_world(GenerateWorldArgs { - creds: &creds, - world_prompt: WorldPrompt::Image { - image_prompt: ContentReference::Uri { - uri: image_url.to_string(), - }, - text_prompt: Some("Mountain landscape with a tree".to_string()), - is_pano: None, - disable_recaption: None, - }, - display_name: None, - model: WorldLabsModel::Marble1p1, - seed: None, - tags: None, - permission: None, - request_timeout: None, - }).await; - - match &result { - Ok(r) => { - println!("SUCCESS — Marble 1.1"); - println!(" Operation ID: {}", r.operation_id.as_str()); - println!(" Done: {}", r.done); - } - Err(err) => { - println!("ERROR — Marble 1.1"); - println!(" Error (Debug): {:?}", err); - println!(" Error (Display): {}", err); - } - } - - assert_eq!(1, 2, "Inspect output above"); - } - - /// Marble 1.1-plus: generate world from a wide fall mountains image via URI. - #[tokio::test] - #[ignore] - async fn test_generate_world_marble_1p1_plus_image_uri() { - setup_test_logging(LevelFilter::Debug); - - let creds = get_test_api_key().unwrap(); - - let image_url = test_data::web::image_urls::SUPER_WIDE_FALL_MOUNTAINS_IMAGE_URL; - - println!("=== generate_world with Marble 1.1-plus ==="); - println!("Image URL: {}", image_url); - - let result = generate_world(GenerateWorldArgs { - creds: &creds, - world_prompt: WorldPrompt::Image { - image_prompt: ContentReference::Uri { - uri: image_url.to_string(), - }, - text_prompt: Some("Autumn mountain panorama".to_string()), - is_pano: None, - disable_recaption: None, - }, - display_name: None, - model: WorldLabsModel::Marble1p1Plus, - seed: None, - tags: None, - permission: None, - request_timeout: None, - }).await; - - match &result { - Ok(r) => { - println!("SUCCESS — Marble 1.1-plus"); - println!(" Operation ID: {}", r.operation_id.as_str()); - println!(" Done: {}", r.done); - } - Err(err) => { - println!("ERROR — Marble 1.1-plus"); - println!(" Error (Debug): {:?}", err); - println!(" Error (Display): {}", err); - } - } - - assert_eq!(1, 2, "Inspect output above"); - } -} diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/generate_world/http_request.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/generate_world/http_request.rs deleted file mode 100644 index fb26365e..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/generate_world/http_request.rs +++ /dev/null @@ -1,114 +0,0 @@ -use serde::{Deserialize, Serialize}; - -// ── Request types ── - -#[derive(Serialize)] -pub(crate) struct RawRequest { - pub world_prompt: WorldPrompt, - #[serde(skip_serializing_if = "Option::is_none")] - pub display_name: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub model: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub seed: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub tags: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub permission: Option, -} - -#[derive(Clone, Debug, Serialize)] -pub struct Permission { - #[serde(skip_serializing_if = "Option::is_none")] - pub public: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub allow_id_access: Option, -} - -/// The world prompt — a tagged union describing the input. -/// -/// Discriminated on the `type` field: -/// - `"text"` — text-only prompt -/// - `"image"` — single image (+ optional text) -/// - `"multi-image"` — multiple images with optional azimuth -/// - `"video"` — video input (+ optional text) -#[derive(Clone, Debug, Serialize)] -#[serde(tag = "type", rename_all = "kebab-case")] -pub enum WorldPrompt { - Text { - #[serde(skip_serializing_if = "Option::is_none")] - text_prompt: Option, - #[serde(skip_serializing_if = "Option::is_none")] - disable_recaption: Option, - }, - Image { - image_prompt: ContentReference, - #[serde(skip_serializing_if = "Option::is_none")] - text_prompt: Option, - #[serde(skip_serializing_if = "Option::is_none")] - is_pano: Option, - #[serde(skip_serializing_if = "Option::is_none")] - disable_recaption: Option, - }, - MultiImage { - multi_image_prompt: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - text_prompt: Option, - #[serde(skip_serializing_if = "Option::is_none")] - reconstruct_images: Option, - #[serde(skip_serializing_if = "Option::is_none")] - disable_recaption: Option, - }, - Video { - video_prompt: ContentReference, - #[serde(skip_serializing_if = "Option::is_none")] - text_prompt: Option, - #[serde(skip_serializing_if = "Option::is_none")] - disable_recaption: Option, - }, -} - -/// A content reference — tagged union on `source`. -#[derive(Clone, Debug, Serialize)] -#[serde(tag = "source", rename_all = "snake_case")] -pub enum ContentReference { - Uri { - uri: String, - }, - MediaAsset { - media_asset_id: String, - }, - DataBase64 { - data_base64: String, - #[serde(skip_serializing_if = "Option::is_none")] - extension: Option, - }, -} - -/// A spherically-located content item for multi-image prompts. -#[derive(Clone, Debug, Serialize)] -pub struct SphericallyLocatedContent { - pub content: ContentReference, - #[serde(skip_serializing_if = "Option::is_none")] - pub azimuth: Option, -} - -// ── Response types ── - -#[derive(Deserialize)] -#[allow(dead_code)] -pub(crate) struct RawResponse { - pub operation_id: String, - pub done: Option, - pub created_at: Option, - pub updated_at: Option, - pub expires_at: Option, - pub error: Option, -} - -#[derive(Debug, Clone, Deserialize)] -#[allow(dead_code)] -pub(crate) struct RawOperationError { - pub code: Option, - pub message: Option, -} diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/generate_world/mod.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/generate_world/mod.rs deleted file mode 100644 index c2644696..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/generate_world/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod generate_world; -pub mod http_request; diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/get_media_asset/get_media_asset.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/get_media_asset/get_media_asset.rs deleted file mode 100644 index 938736b8..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/get_media_asset/get_media_asset.rs +++ /dev/null @@ -1,112 +0,0 @@ -use crate::api::requests::get_media_asset::http_request::RawResponse; -use crate::credentials::world_labs_api_creds::WorldLabsApiCreds; -use crate::error::filter_world_labs_http_error::filter_world_labs_http_error; -use crate::error::world_labs_error::WorldLabsError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use log::{debug, error}; -use std::time::Duration; -use wreq::Client; - -const BASE_URL: &str = "https://api.worldlabs.ai/marble/v1/media-assets"; - -pub struct GetMediaAssetArgs<'a> { - pub creds: &'a WorldLabsApiCreds, - pub media_asset_id: &'a str, - pub request_timeout: Option, -} - -pub struct GetMediaAssetResponse { - pub media_asset_id: String, - pub file_name: String, - pub kind: String, - pub extension: Option, - pub created_at: Option, - pub updated_at: Option, -} - -/// GET /marble/v1/media-assets/{media_asset_id} -/// -/// Get info about a previously uploaded media asset. -pub async fn get_media_asset(args: GetMediaAssetArgs<'_>) -> Result { - let client = Client::new(); - - let url = format!("{}/{}", BASE_URL, args.media_asset_id); - - debug!("Requesting URL: {}", url); - - let mut request_builder = client.get(&url) - .header("WLT-Api-Key", args.creds.api_key()); - - if let Some(timeout) = args.request_timeout { - request_builder = request_builder.timeout(timeout); - } - - let response = request_builder.send() - .await - .map_err(|err| { - error!("Error during get_media_asset request: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - let status = response.status(); - - let response_body = response.text() - .await - .map_err(|err| { - error!("Error reading response body: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - if !status.is_success() { - error!("get_media_asset returned error (code {}): {:?}", status.as_u16(), response_body); - } - - filter_world_labs_http_error(status, Some(&response_body))?; - - debug!("Response body (200): {}", response_body); - - let raw: RawResponse = serde_json::from_str(&response_body) - .map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?; - - Ok(GetMediaAssetResponse { - media_asset_id: raw.media_asset_id, - file_name: raw.file_name, - kind: raw.kind, - extension: raw.extension, - created_at: raw.created_at, - updated_at: raw.updated_at, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_utils::get_test_api_key::get_test_api_key; - use crate::test_utils::setup_test_logging::setup_test_logging; - use log::LevelFilter; - - #[tokio::test] - #[ignore] - async fn test_get_media_asset() { - setup_test_logging(LevelFilter::Debug); - - let creds = get_test_api_key().unwrap(); - - // Use a known media_asset_id from a previous prepare_upload call - let media_asset_id = "REPLACE_WITH_REAL_ID"; - - let response = get_media_asset(GetMediaAssetArgs { - creds: &creds, - media_asset_id, - request_timeout: None, - }).await.unwrap(); - - println!("Media asset ID: {}", response.media_asset_id); - println!("File name: {}", response.file_name); - println!("Kind: {}", response.kind); - println!("Extension: {:?}", response.extension); - println!("Created at: {:?}", response.created_at); - - assert_eq!(1, 2); - } -} diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/get_media_asset/http_request.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/get_media_asset/http_request.rs deleted file mode 100644 index 49b0bab5..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/get_media_asset/http_request.rs +++ /dev/null @@ -1,14 +0,0 @@ -use serde::Deserialize; -use std::collections::HashMap; - -#[derive(Deserialize)] -#[allow(dead_code)] -pub(crate) struct RawResponse { - pub media_asset_id: String, - pub file_name: String, - pub kind: String, - pub extension: Option, - pub metadata: Option>, - pub created_at: Option, - pub updated_at: Option, -} diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/get_media_asset/mod.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/get_media_asset/mod.rs deleted file mode 100644 index b999d42a..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/get_media_asset/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod get_media_asset; -pub(crate) mod http_request; diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/get_operation/get_operation.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/get_operation/get_operation.rs deleted file mode 100644 index 6833b1a3..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/get_operation/get_operation.rs +++ /dev/null @@ -1,253 +0,0 @@ -use crate::api::api_types::operation_id::OperationId; -use crate::api::api_types::world_id::WorldId; -use crate::api::requests::get_operation::http_request::{RawResponse, RawWorld}; -use crate::credentials::world_labs_api_creds::WorldLabsApiCreds; -use crate::error::filter_world_labs_http_error::filter_world_labs_http_error; -use crate::error::world_labs_error::WorldLabsError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use log::{debug, error, warn}; -use std::time::Duration; -use wreq::Client; - -const BASE_URL: &str = "https://api.worldlabs.ai/marble/v1/operations"; - -pub struct GetOperationArgs<'a> { - pub creds: &'a WorldLabsApiCreds, - pub operation_id: &'a OperationId, - pub request_timeout: Option, -} - -pub struct GetOperationResponse { - pub operation_id: String, - pub done: bool, - pub created_at: Option, - pub updated_at: Option, - pub expires_at: Option, - pub error: Option, - pub metadata: Option, - /// Structured World object, present when done=true and no error. - pub world: Option, -} - -#[derive(Debug, Clone)] -pub struct OperationError { - pub code: Option, - pub message: Option, -} - -/// A completed World returned from an operation. -#[derive(Debug, Clone)] -pub struct World { - pub world_id: WorldId, - pub display_name: Option, - pub world_marble_url: Option, - pub created_at: Option, - pub updated_at: Option, - pub model: Option, - pub status: Option, - pub tags: Option>, - pub assets: Option, -} - -/// Assets associated with a completed World. -#[derive(Debug, Clone)] -pub struct WorldAssets { - pub caption: Option, - pub thumbnail_url: Option, - pub pano_url: Option, - pub collider_mesh_url: Option, - pub splats: Option, -} - -/// Splat (3D Gaussian) assets for a World. -#[derive(Debug, Clone)] -pub struct SplatAssets { - /// Low-res 100k splat URL - pub spz_url_100k: Option, - /// Medium-res 500k splat URL - pub spz_url_500k: Option, - /// Full resolution splat URL - pub spz_url_full_res: Option, - pub metric_scale_factor: Option, - pub ground_plane_offset: Option, -} - -/// GET /marble/v1/operations/{operation_id} -/// -/// Poll the status of an async operation. -/// When `done` is true, `world` contains the structured World object. -pub async fn get_operation(args: GetOperationArgs<'_>) -> Result { - let client = Client::new(); - - let url = format!("{}/{}", BASE_URL, args.operation_id.as_str()); - - debug!("Requesting URL: {}", url); - - let mut request_builder = client.get(&url) - .header("WLT-Api-Key", args.creds.api_key()); - - if let Some(timeout) = args.request_timeout { - request_builder = request_builder.timeout(timeout); - } - - let response = request_builder.send() - .await - .map_err(|err| { - error!("Error during get_operation request: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - let status = response.status(); - - let response_body = response.text() - .await - .map_err(|err| { - error!("Error reading response body: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - if !status.is_success() { - error!("get_operation returned error (code {}): {:?}", status.as_u16(), response_body); - } - - filter_world_labs_http_error(status, Some(&response_body))?; - - debug!("Response body (200): {}", response_body); - - let raw: RawResponse = serde_json::from_str(&response_body) - .map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?; - - let world = raw.response.and_then(|raw_world| { - match try_parse_world(raw_world) { - Some(w) => Some(w), - None => { - warn!("Operation response present but missing world_id — skipping world parse"); - None - } - } - }); - - Ok(GetOperationResponse { - operation_id: raw.operation_id, - done: raw.done.unwrap_or(false), - created_at: raw.created_at, - updated_at: raw.updated_at, - expires_at: raw.expires_at, - error: raw.error.map(|e| OperationError { - code: e.code, - message: e.message, - }), - metadata: raw.metadata, - world, - }) -} - -/// Try to parse a RawWorld into a strongly-typed World. -/// Returns None if world_id is missing (not a valid World). -fn try_parse_world(raw: RawWorld) -> Option { - let world_id = raw.world_id?; - - let assets = raw.assets.map(|a| { - let splats = a.splats.map(|s| { - let semantics = s.semantics_metadata; - SplatAssets { - spz_url_100k: s.spz_urls.as_ref().and_then(|u| u.low.clone()), - spz_url_500k: s.spz_urls.as_ref().and_then(|u| u.medium.clone()), - spz_url_full_res: s.spz_urls.as_ref().and_then(|u| u.full_res.clone()), - metric_scale_factor: semantics.as_ref().and_then(|m| m.metric_scale_factor), - ground_plane_offset: semantics.as_ref().and_then(|m| m.ground_plane_offset), - } - }); - - WorldAssets { - caption: a.caption, - thumbnail_url: a.thumbnail_url, - pano_url: a.imagery.and_then(|i| i.pano_url), - collider_mesh_url: a.mesh.and_then(|m| m.collider_mesh_url), - splats, - } - }); - - Some(World { - world_id: WorldId(world_id), - display_name: raw.display_name, - world_marble_url: raw.world_marble_url, - created_at: raw.created_at, - updated_at: raw.updated_at, - model: raw.model, - status: raw.status, - tags: raw.tags, - assets, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_utils::get_test_api_key::get_test_api_key; - use crate::test_utils::setup_test_logging::setup_test_logging; - use log::LevelFilter; - - #[tokio::test] - #[ignore] - async fn test_get_operation() { - setup_test_logging(LevelFilter::Debug); - - let creds = get_test_api_key().unwrap(); - - //let operation_id = OperationId("1fab3bf1-05a1-4929-907e-c6df07c539e2".to_string()); - let operation_id = OperationId("a0894fa6-594d-4680-8f3a-5109f22664b1".to_string()); - - let response = get_operation(GetOperationArgs { - creds: &creds, - operation_id: &operation_id, - request_timeout: None, - }).await.unwrap(); - - println!("Operation ID: {}", response.operation_id); - println!("Done: {}", response.done); - println!("Created at: {:?}", response.created_at); - println!("Updated at: {:?}", response.updated_at); - println!("Expires at: {:?}", response.expires_at); - println!("Error: {:?}", response.error); - println!("Metadata: {:?}", response.metadata); - - match &response.world { - None => println!("World: None (not yet complete)"), - Some(world) => { - println!("World:"); - println!(" World ID: {}", world.world_id.as_str()); - println!(" Display name: {:?}", world.display_name); - println!(" Marble URL: {:?}", world.world_marble_url); - println!(" Created at: {:?}", world.created_at); - println!(" Updated at: {:?}", world.updated_at); - println!(" Model: {:?}", world.model); - println!(" Status: {:?}", world.status); - println!(" Tags: {:?}", world.tags); - match &world.assets { - None => println!(" Assets: None"), - Some(assets) => { - println!(" Assets:"); - println!(" Caption: {:?}", assets.caption); - println!(" Thumbnail URL: {:?}", assets.thumbnail_url); - println!(" Pano URL: {:?}", assets.pano_url); - println!(" Collider mesh URL: {:?}", assets.collider_mesh_url); - match &assets.splats { - None => println!(" Splats: None"), - Some(splats) => { - println!(" Splats:"); - println!(" SPZ 100k: {:?}", splats.spz_url_100k); - println!(" SPZ 500k: {:?}", splats.spz_url_500k); - println!(" SPZ full res: {:?}", splats.spz_url_full_res); - println!(" Metric scale factor: {:?}", splats.metric_scale_factor); - println!(" Ground plane offset: {:?}", splats.ground_plane_offset); - } - } - } - } - } - } - - assert_eq!(1, 2); - } -} diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/get_operation/http_request.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/get_operation/http_request.rs deleted file mode 100644 index 97a71093..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/get_operation/http_request.rs +++ /dev/null @@ -1,82 +0,0 @@ -use serde::Deserialize; - -#[derive(Deserialize)] -pub(crate) struct RawResponse { - pub operation_id: String, - pub done: Option, - pub created_at: Option, - pub updated_at: Option, - pub expires_at: Option, - pub error: Option, - pub metadata: Option, - pub response: Option, -} - -#[derive(Debug, Clone, Deserialize)] -pub(crate) struct RawOperationError { - pub code: Option, - pub message: Option, -} - -/// The World object embedded in `response` when the operation completes. -/// All fields are optional to tolerate partial/unexpected shapes. -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -pub(crate) struct RawWorld { - pub world_id: Option, - pub display_name: Option, - pub world_marble_url: Option, - pub created_at: Option, - pub updated_at: Option, - pub model: Option, - pub status: Option, - pub tags: Option>, - pub assets: Option, - pub world_prompt: Option, -} - -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -pub(crate) struct RawWorldAssets { - pub caption: Option, - pub thumbnail_url: Option, - pub imagery: Option, - pub mesh: Option, - pub splats: Option, -} - -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -pub(crate) struct RawImagery { - pub pano_url: Option, -} - -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -pub(crate) struct RawMesh { - pub collider_mesh_url: Option, -} - -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -pub(crate) struct RawSplats { - pub spz_urls: Option, - pub semantics_metadata: Option, -} - -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -pub(crate) struct RawSemanticsMetadata { - pub metric_scale_factor: Option, - pub ground_plane_offset: Option, -} - -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -pub(crate) struct RawSpzUrls { - #[serde(rename = "100k")] - pub low: Option, - #[serde(rename = "500k")] - pub medium: Option, - pub full_res: Option, -} diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/get_operation/mod.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/get_operation/mod.rs deleted file mode 100644 index e337b282..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/get_operation/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod get_operation; -pub(crate) mod http_request; diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/get_world/get_world.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/get_world/get_world.rs deleted file mode 100644 index 1bccc592..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/get_world/get_world.rs +++ /dev/null @@ -1,163 +0,0 @@ -use crate::api::api_types::world_id::WorldId; -use crate::api::requests::get_world::http_request::RawResponse; -use crate::credentials::world_labs_api_creds::WorldLabsApiCreds; -use crate::error::filter_world_labs_http_error::filter_world_labs_http_error; -use crate::error::world_labs_error::WorldLabsError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use log::{debug, error}; -use std::time::Duration; -use wreq::Client; - -const BASE_URL: &str = "https://api.worldlabs.ai/marble/v1/worlds"; - -pub struct GetWorldArgs<'a> { - pub creds: &'a WorldLabsApiCreds, - pub world_id: &'a WorldId, - pub request_timeout: Option, -} - -pub struct GetWorldResponse { - pub world_id: WorldId, - pub display_name: Option, - pub world_marble_url: Option, - pub created_at: Option, - pub updated_at: Option, - pub model: Option, - /// e.g. "SUCCEEDED", "PENDING", "FAILED", "RUNNING" - pub status: Option, - pub tags: Option>, - pub is_public: Option, - pub assets: Option, -} - -pub struct WorldAssets { - pub caption: Option, - pub thumbnail_url: Option, - pub pano_url: Option, - pub collider_mesh_url: Option, - pub spz_url_100k: Option, - pub spz_url_500k: Option, - pub spz_url_full_res: Option, -} - -/// GET /marble/v1/worlds/{world_id} -/// -/// Retrieve a world by its ID. -pub async fn get_world(args: GetWorldArgs<'_>) -> Result { - let client = Client::new(); - - let url = format!("{}/{}", BASE_URL, args.world_id.as_str()); - - debug!("Requesting URL: {}", url); - - let mut request_builder = client.get(&url) - .header("WLT-Api-Key", args.creds.api_key()); - - if let Some(timeout) = args.request_timeout { - request_builder = request_builder.timeout(timeout); - } - - let response = request_builder.send() - .await - .map_err(|err| { - error!("Error during get_world request: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - let status = response.status(); - - let response_body = response.text() - .await - .map_err(|err| { - error!("Error reading response body: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - if !status.is_success() { - error!("get_world returned error (code {}): {:?}", status.as_u16(), response_body); - } - - filter_world_labs_http_error(status, Some(&response_body))?; - - debug!("Response body (200): {}", response_body); - - let raw: RawResponse = serde_json::from_str(&response_body) - .map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?; - - let assets = raw.assets.map(|a| { - let spz_urls = a.splats.and_then(|s| s.spz_urls); - WorldAssets { - caption: a.caption, - thumbnail_url: a.thumbnail_url, - pano_url: a.imagery.and_then(|i| i.pano_url), - collider_mesh_url: a.mesh.and_then(|m| m.collider_mesh_url), - spz_url_100k: spz_urls.as_ref().and_then(|u| u.low.clone()), - spz_url_500k: spz_urls.as_ref().and_then(|u| u.medium.clone()), - spz_url_full_res: spz_urls.as_ref().and_then(|u| u.full_res.clone()), - } - }); - - Ok(GetWorldResponse { - world_id: WorldId(raw.world_id), - display_name: raw.display_name, - world_marble_url: raw.world_marble_url, - created_at: raw.created_at, - updated_at: raw.updated_at, - model: raw.model, - status: raw.status, - tags: raw.tags, - is_public: raw.permission.and_then(|p| p.public), - assets, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_utils::get_test_api_key::get_test_api_key; - use crate::test_utils::setup_test_logging::setup_test_logging; - use log::LevelFilter; - - #[tokio::test] - #[ignore] - async fn test_get_world() { - setup_test_logging(LevelFilter::Debug); - - let creds = get_test_api_key().unwrap(); - - // Use a known world_id from a previous generation - let world_id = WorldId::from_str("0048d009-1c7a-4e13-9881-07e9b6ff32e1"); - //let world_id = WorldId::from_str("c8a89fb1-f8b4-44c6-ae50-525f7205c65f"); - - let response = get_world(GetWorldArgs { - creds: &creds, - world_id: &world_id, - request_timeout: None, - }).await.unwrap(); - - println!("World ID: {}", response.world_id.as_str()); - println!("Display name: {:?}", response.display_name); - println!("Marble URL: {:?}", response.world_marble_url); - println!("Created at: {:?}", response.created_at); - println!("Updated at: {:?}", response.updated_at); - println!("Model: {:?}", response.model); - println!("Status: {:?}", response.status); - println!("Tags: {:?}", response.tags); - println!("Is public: {:?}", response.is_public); - match &response.assets { - None => println!("Assets: None"), - Some(assets) => { - println!("Assets:"); - println!(" Caption: {:?}", assets.caption); - println!(" Thumbnail URL: {:?}", assets.thumbnail_url); - println!(" Pano URL: {:?}", assets.pano_url); - println!(" Collider mesh URL: {:?}", assets.collider_mesh_url); - println!(" SPZ 100k: {:?}", assets.spz_url_100k); - println!(" SPZ 500k: {:?}", assets.spz_url_500k); - println!(" SPZ full res: {:?}", assets.spz_url_full_res); - } - } - - assert_eq!(1, 2); - } -} diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/get_world/http_request.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/get_world/http_request.rs deleted file mode 100644 index 7b2d3352..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/get_world/http_request.rs +++ /dev/null @@ -1,64 +0,0 @@ -use serde::Deserialize; - -#[derive(Debug, Deserialize)] -pub(crate) struct RawResponse { - pub world_id: String, - pub display_name: Option, - pub world_marble_url: Option, - pub created_at: Option, - pub updated_at: Option, - pub model: Option, - pub status: Option, - pub tags: Option>, - pub permission: Option, - pub assets: Option, -} - -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -pub(crate) struct RawPermission { - pub public: Option, - pub allow_id_access: Option, -} - -#[derive(Debug, Deserialize)] -pub(crate) struct RawWorldAssets { - pub caption: Option, - pub thumbnail_url: Option, - pub imagery: Option, - pub mesh: Option, - pub splats: Option, -} - -#[derive(Debug, Deserialize)] -pub(crate) struct RawImagery { - pub pano_url: Option, -} - -#[derive(Debug, Deserialize)] -pub(crate) struct RawMesh { - pub collider_mesh_url: Option, -} - -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -pub(crate) struct RawSplats { - pub spz_urls: Option, - pub semantics_metadata: Option, -} - -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -pub(crate) struct RawSemanticsMetadata { - pub metric_scale_factor: Option, - pub ground_plane_offset: Option, -} - -#[derive(Debug, Deserialize)] -pub(crate) struct RawSpzUrls { - #[serde(rename = "100k")] - pub low: Option, - #[serde(rename = "500k")] - pub medium: Option, - pub full_res: Option, -} diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/get_world/mod.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/get_world/mod.rs deleted file mode 100644 index 50dd8ec6..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/get_world/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod get_world; -pub(crate) mod http_request; diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/list_worlds/http_request.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/list_worlds/http_request.rs deleted file mode 100644 index 748f2035..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/list_worlds/http_request.rs +++ /dev/null @@ -1,41 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Serialize)] -pub(crate) struct RawRequest { - #[serde(skip_serializing_if = "Option::is_none")] - pub page_size: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub page_token: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub status: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub model: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub tags: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub is_public: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub created_after: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub created_before: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub sort_by: Option, -} - -#[derive(Deserialize)] -pub(crate) struct RawResponse { - pub worlds: Vec, - pub next_page_token: Option, -} - -#[derive(Deserialize)] -pub(crate) struct RawWorldSummary { - pub world_id: String, - pub display_name: Option, - pub world_marble_url: Option, - pub created_at: Option, - pub updated_at: Option, - pub model: Option, - pub status: Option, - pub tags: Option>, -} diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/list_worlds/list_worlds.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/list_worlds/list_worlds.rs deleted file mode 100644 index cc0f9010..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/list_worlds/list_worlds.rs +++ /dev/null @@ -1,170 +0,0 @@ -use crate::api::api_types::world_id::WorldId; -use crate::api::requests::list_worlds::http_request::{RawRequest, RawResponse}; -use crate::credentials::world_labs_api_creds::WorldLabsApiCreds; -use crate::error::filter_world_labs_http_error::filter_world_labs_http_error; -use crate::error::world_labs_error::WorldLabsError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use log::{debug, error}; -use std::time::Duration; -use wreq::Client; - -const URL: &str = "https://api.worldlabs.ai/marble/v1/worlds:list"; - -pub struct ListWorldsArgs<'a> { - pub creds: &'a WorldLabsApiCreds, - /// 1-100, default 20 - pub page_size: Option, - /// Opaque cursor from a previous response - pub page_token: Option, - /// "SUCCEEDED", "PENDING", "FAILED", "RUNNING" - pub status: Option, - /// "Marble 0.1-mini" or "Marble 0.1-plus" - pub model: Option, - pub tags: Option>, - pub is_public: Option, - /// ISO 8601 datetime - pub created_after: Option, - /// ISO 8601 datetime - pub created_before: Option, - /// "created_at" or "updated_at" - pub sort_by: Option, - pub request_timeout: Option, -} - -pub struct ListWorldsResponse { - pub worlds: Vec, - pub next_page_token: Option, -} - -pub struct WorldSummary { - pub world_id: WorldId, - pub display_name: Option, - pub world_marble_url: Option, - pub created_at: Option, - pub updated_at: Option, - pub model: Option, - /// e.g. "SUCCEEDED", "PENDING", "FAILED", "RUNNING" - pub status: Option, - pub tags: Option>, -} - -/// POST /marble/v1/worlds:list -/// -/// List worlds with optional filtering and pagination. -pub async fn list_worlds(args: ListWorldsArgs<'_>) -> Result { - let client = Client::new(); - - let payload = RawRequest { - page_size: args.page_size, - page_token: args.page_token, - status: args.status, - model: args.model, - tags: args.tags, - is_public: args.is_public, - created_after: args.created_after, - created_before: args.created_before, - sort_by: args.sort_by, - }; - - debug!("Requesting URL: {}", URL); - - let mut request_builder = client.post(URL) - .header("WLT-Api-Key", args.creds.api_key()) - .header("Content-Type", "application/json") - .json(&payload); - - if let Some(timeout) = args.request_timeout { - request_builder = request_builder.timeout(timeout); - } - - let response = request_builder.send() - .await - .map_err(|err| { - error!("Error during list_worlds request: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - let status = response.status(); - - let response_body = response.text() - .await - .map_err(|err| { - error!("Error reading response body: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - if !status.is_success() { - error!("list_worlds returned error (code {}): {:?}", status.as_u16(), response_body); - } - - filter_world_labs_http_error(status, Some(&response_body))?; - - debug!("Response body (200): {}", response_body); - - let raw: RawResponse = serde_json::from_str(&response_body) - .map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?; - - let worlds = raw.worlds.into_iter().map(|w| WorldSummary { - world_id: WorldId(w.world_id), - display_name: w.display_name, - world_marble_url: w.world_marble_url, - created_at: w.created_at, - updated_at: w.updated_at, - model: w.model, - status: w.status, - tags: w.tags, - }).collect(); - - Ok(ListWorldsResponse { - worlds, - next_page_token: raw.next_page_token, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_utils::get_test_api_key::get_test_api_key; - use crate::test_utils::setup_test_logging::setup_test_logging; - use log::LevelFilter; - - #[tokio::test] - #[ignore] - async fn test_list_worlds() { - setup_test_logging(LevelFilter::Debug); - - let creds = get_test_api_key().unwrap(); - - let response = list_worlds(ListWorldsArgs { - creds: &creds, - page_size: Some(5), - page_token: None, - status: None, - model: None, - tags: None, - is_public: None, - created_after: None, - created_before: None, - sort_by: None, - request_timeout: None, - }).await.unwrap(); - - println!("Worlds count: {}", response.worlds.len()); - println!("Next page token: {:?}", response.next_page_token); - println!(); - - for (i, world) in response.worlds.iter().enumerate() { - println!("--- World {} ---", i + 1); - println!(" World ID: {}", world.world_id.as_str()); - println!(" Display name: {:?}", world.display_name); - println!(" Marble URL: {:?}", world.world_marble_url); - println!(" Created at: {:?}", world.created_at); - println!(" Updated at: {:?}", world.updated_at); - println!(" Model: {:?}", world.model); - println!(" Status: {:?}", world.status); - println!(" Tags: {:?}", world.tags); - } - - assert_eq!(1, 2); - } -} diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/list_worlds/mod.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/list_worlds/mod.rs deleted file mode 100644 index 48a82cd1..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/list_worlds/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod list_worlds; -pub(crate) mod http_request; diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/mod.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/mod.rs deleted file mode 100644 index 82ed574f..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod delete_world; -pub mod generate_world; -pub mod get_media_asset; -pub mod get_operation; -pub mod get_world; -pub mod list_worlds; -pub mod prepare_upload; -pub mod upload_to_signed_url; diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/prepare_upload/http_request.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/prepare_upload/http_request.rs deleted file mode 100644 index 6e43d7da..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/prepare_upload/http_request.rs +++ /dev/null @@ -1,34 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -#[derive(Serialize)] -pub(crate) struct RawRequest { - pub file_name: String, - pub kind: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub extension: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub metadata: Option>, -} - -#[derive(Deserialize)] -pub(crate) struct RawResponse { - pub media_asset: RawMediaAsset, - pub upload_info: RawUploadInfo, -} - -#[derive(Deserialize)] -#[allow(dead_code)] -pub(crate) struct RawMediaAsset { - pub media_asset_id: String, - pub file_name: String, - pub kind: String, - pub extension: Option, -} - -#[derive(Deserialize)] -pub(crate) struct RawUploadInfo { - pub upload_url: String, - pub upload_method: String, - pub required_headers: Option>, -} diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/prepare_upload/mod.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/prepare_upload/mod.rs deleted file mode 100644 index b79a7514..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/prepare_upload/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod prepare_upload; -pub(crate) mod http_request; diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/prepare_upload/prepare_upload.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/prepare_upload/prepare_upload.rs deleted file mode 100644 index 002c9311..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/prepare_upload/prepare_upload.rs +++ /dev/null @@ -1,151 +0,0 @@ -use crate::api::api_types::media_asset_id::MediaAssetId; -use crate::api::requests::prepare_upload::http_request::{RawRequest, RawResponse}; -use crate::credentials::world_labs_api_creds::WorldLabsApiCreds; -use crate::error::filter_world_labs_http_error::filter_world_labs_http_error; -use crate::error::world_labs_error::WorldLabsError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use log::{debug, error}; -use std::collections::HashMap; -use std::time::Duration; -use wreq::Client; - -const URL: &str = "https://api.worldlabs.ai/marble/v1/media-assets:prepare_upload"; - -/// The kind of media asset to upload. -#[derive(Clone, Copy, Debug)] -pub enum MediaAssetKind { - Image, - Video, -} - -impl MediaAssetKind { - fn as_str(&self) -> &'static str { - match self { - Self::Image => "image", - Self::Video => "video", - } - } -} - -pub struct PrepareUploadArgs<'a> { - pub creds: &'a WorldLabsApiCreds, - pub file_name: &'a str, - pub kind: MediaAssetKind, - pub request_timeout: Option, -} - -pub struct PrepareUploadResponse { - pub media_asset_id: MediaAssetId, - pub upload_url: String, - pub upload_method: String, - pub required_headers: HashMap, -} - -/// Parse the file extension from a file name, defaulting to "png". -fn extension_from_file_name(file_name: &str) -> String { - file_name - .rsplit_once('.') - .map(|(_, ext)| ext.to_lowercase()) - .unwrap_or_else(|| "png".to_string()) -} - -/// POST /marble/v1/media-assets:prepare_upload -/// -/// Get a signed upload URL and media_asset_id for uploading a file. -/// The extension is parsed from `file_name`; defaults to "png" if none found. -pub async fn prepare_upload(args: PrepareUploadArgs<'_>) -> Result { - let client = Client::new(); - - let extension = extension_from_file_name(args.file_name); - - let payload = RawRequest { - file_name: args.file_name.to_string(), - kind: args.kind.as_str().to_string(), - extension: Some(extension), - metadata: None, - }; - - debug!("Requesting URL: {}", URL); - - let mut request_builder = client.post(URL) - .header("WLT-Api-Key", args.creds.api_key()) - .header("Content-Type", "application/json") - .json(&payload); - - if let Some(timeout) = args.request_timeout { - request_builder = request_builder.timeout(timeout); - } - - let response = request_builder.send() - .await - .map_err(|err| { - error!("Error during prepare_upload request: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - let status = response.status(); - - let response_body = response.text() - .await - .map_err(|err| { - error!("Error reading response body: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - if !status.is_success() { - error!("prepare_upload returned error (code {}): {:?}", status.as_u16(), response_body); - } - - filter_world_labs_http_error(status, Some(&response_body))?; - - debug!("Response body (200): {}", response_body); - - let raw: RawResponse = serde_json::from_str(&response_body) - .map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?; - - Ok(PrepareUploadResponse { - media_asset_id: MediaAssetId(raw.media_asset.media_asset_id), - upload_url: raw.upload_info.upload_url, - upload_method: raw.upload_info.upload_method, - required_headers: raw.upload_info.required_headers.unwrap_or_default(), - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_utils::get_test_api_key::get_test_api_key; - use crate::test_utils::setup_test_logging::setup_test_logging; - use log::LevelFilter; - - #[tokio::test] - #[ignore] - async fn test_prepare_upload() { - setup_test_logging(LevelFilter::Debug); - - let creds = get_test_api_key().unwrap(); - - let response = prepare_upload(PrepareUploadArgs { - creds: &creds, - file_name: "test_image.jpg", - kind: MediaAssetKind::Image, - request_timeout: None, - }).await.unwrap(); - - println!("Media asset ID: {}", response.media_asset_id.as_str()); - println!("Upload URL: {}", response.upload_url); - println!("Upload method: {}", response.upload_method); - println!("Required headers: {:?}", response.required_headers); - - assert_eq!(1, 2); - } - - #[test] - fn test_extension_parsing() { - assert_eq!(extension_from_file_name("photo.jpg"), "jpg"); - assert_eq!(extension_from_file_name("image.PNG"), "png"); - assert_eq!(extension_from_file_name("video.mp4"), "mp4"); - assert_eq!(extension_from_file_name("no_extension"), "png"); - assert_eq!(extension_from_file_name("multi.dots.webp"), "webp"); - } -} diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/upload_to_signed_url/mod.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/upload_to_signed_url/mod.rs deleted file mode 100644 index 64ed0895..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/upload_to_signed_url/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod upload_to_signed_url; diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/upload_to_signed_url/upload_to_signed_url.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/upload_to_signed_url/upload_to_signed_url.rs deleted file mode 100644 index 7d6a5b79..00000000 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/upload_to_signed_url/upload_to_signed_url.rs +++ /dev/null @@ -1,66 +0,0 @@ -use crate::error::world_labs_error::WorldLabsError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use log::{debug, error}; -use std::collections::HashMap; -use std::time::Duration; -use wreq::Client; - -pub struct UploadToSignedUrlArgs<'a> { - pub upload_url: &'a str, - pub file_bytes: Vec, - pub required_headers: &'a HashMap, - pub content_type: &'a str, - pub request_timeout: Option, -} - -/// PUT {upload_url} -/// -/// Upload raw file bytes to the signed URL returned by prepare_upload. -/// Include any required_headers from the prepare_upload response. -pub async fn upload_to_signed_url(args: UploadToSignedUrlArgs<'_>) -> Result<(), WorldLabsError> { - let client = Client::new(); - - debug!("Uploading to signed URL: {}", args.upload_url); - - let mut request_builder = client.put(args.upload_url) - .header("Content-Type", args.content_type) - .body(args.file_bytes); - - for (key, value) in args.required_headers { - request_builder = request_builder.header(key.as_str(), value.as_str()); - } - - if let Some(timeout) = args.request_timeout { - request_builder = request_builder.timeout(timeout); - } - - let response = request_builder.send() - .await - .map_err(|err| { - error!("Error during signed URL upload: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - let status = response.status(); - - if !status.is_success() { - let body = response.text() - .await - .unwrap_or_default(); - error!("Signed URL upload failed (code {}): {:?}", status.as_u16(), body); - return Err(WorldLabsGenericApiError::GoogleUploadFailed { - status_code: status, - body, - }.into()); - } - - debug!("Upload to signed URL succeeded"); - - Ok(()) -} - -#[cfg(test)] -mod tests { - // Upload tests are covered by the end-to-end recipe test, - // since this endpoint requires a fresh signed URL from prepare_upload. -} diff --git a/crates/api_clients/worldlabs_api_client/src/credentials/mod.rs b/crates/api_clients/worldlabs_api_client/src/credentials/mod.rs deleted file mode 100644 index d576a60e..00000000 --- a/crates/api_clients/worldlabs_api_client/src/credentials/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod world_labs_api_creds; diff --git a/crates/api_clients/worldlabs_api_client/src/credentials/world_labs_api_creds.rs b/crates/api_clients/worldlabs_api_client/src/credentials/world_labs_api_creds.rs deleted file mode 100644 index b16442bc..00000000 --- a/crates/api_clients/worldlabs_api_client/src/credentials/world_labs_api_creds.rs +++ /dev/null @@ -1,16 +0,0 @@ -/// Credentials for the official World Labs API. -/// Uses a simple API key for authentication via the `WLT-Api-Key` header. -#[derive(Clone)] -pub struct WorldLabsApiCreds { - api_key: String, -} - -impl WorldLabsApiCreds { - pub fn new(api_key: String) -> Self { - Self { api_key } - } - - pub fn api_key(&self) -> &str { - &self.api_key - } -} diff --git a/crates/api_clients/worldlabs_api_client/src/error/filter_world_labs_http_error.rs b/crates/api_clients/worldlabs_api_client/src/error/filter_world_labs_http_error.rs deleted file mode 100644 index d31a695f..00000000 --- a/crates/api_clients/worldlabs_api_client/src/error/filter_world_labs_http_error.rs +++ /dev/null @@ -1,195 +0,0 @@ -use crate::error::world_labs_error::WorldLabsError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use crate::error::world_labs_specific_api_error::WorldLabsSpecificApiError; -use wreq::StatusCode; - -/// Detect error HTTP responses and coerce the response as a Rust Error. -pub fn filter_world_labs_http_error(status_code: StatusCode, maybe_body: Option<&str>) -> Result<(), WorldLabsError> { - if status_code.is_success() { - return Ok(()); - } - - match status_code { - StatusCode::PAYMENT_REQUIRED => { - return Err(WorldLabsSpecificApiError::InsufficientCredits.into()); - }, - _ => {}, - } - - // Check for NSFW content policy rejection (403 with nsfw/sexuality keywords). - if let Some(nsfw_error) = check_nsfw_content_rejection(status_code, maybe_body) { - return Err(nsfw_error.into()); - } - - if let Some(body) = maybe_body { - return Err(WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { - status_code, - body: body.to_string(), - }.into()); - } - - Err(WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { - status_code, - body: String::new(), - }.into()) -} - -/// Check if a 403 response is an NSFW content policy rejection. -/// Returns `Some(WorldLabsSpecificApiError)` if it matches, `None` otherwise. -fn check_nsfw_content_rejection( - status_code: StatusCode, - maybe_body: Option<&str>, -) -> Option { - if status_code != StatusCode::FORBIDDEN { - return None; - } - - let body = maybe_body?; - let body_lower = body.to_lowercase(); - - if body_lower.contains("nsfw") || body_lower.contains("sexuality") { - Some(WorldLabsSpecificApiError::NsfwContentPolicyRejected { - message: Some(body.to_string()), - }) - } else { - None - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn success_200_returns_ok() { - let result = filter_world_labs_http_error(StatusCode::OK, None); - assert!(result.is_ok()); - } - - #[test] - fn success_200_with_body_returns_ok() { - let result = filter_world_labs_http_error(StatusCode::OK, Some("{}")); - assert!(result.is_ok()); - } - - // --- NSFW 403 --- - - #[test] - fn nsfw_403_real_response() { - let body = r#"{"detail":"NSFW Content is not permitted by policy. Sexuality"}"#; - let result = filter_world_labs_http_error(StatusCode::FORBIDDEN, Some(body)); - let err = result.unwrap_err(); - - assert!(err.is_403_forbidden()); - assert!(matches!(err, WorldLabsError::ApiSpecific( - WorldLabsSpecificApiError::NsfwContentPolicyRejected { .. } - ))); - } - - #[test] - fn nsfw_403_preserves_message() { - let body = r#"{"detail":"NSFW Content is not permitted by policy. Sexuality"}"#; - let result = filter_world_labs_http_error(StatusCode::FORBIDDEN, Some(body)); - let err = result.unwrap_err(); - - if let WorldLabsError::ApiSpecific(WorldLabsSpecificApiError::NsfwContentPolicyRejected { message }) = err { - assert_eq!(message.as_deref(), Some(body)); - } else { - panic!("expected NsfwContentPolicyRejected, got: {:?}", err); - } - } - - #[test] - fn nsfw_403_case_insensitive_nsfw() { - let body = r#"{"detail":"nsfw content detected"}"#; - let result = filter_world_labs_http_error(StatusCode::FORBIDDEN, Some(body)); - assert!(matches!(result.unwrap_err(), WorldLabsError::ApiSpecific( - WorldLabsSpecificApiError::NsfwContentPolicyRejected { .. } - ))); - } - - #[test] - fn nsfw_403_case_insensitive_sexuality() { - let body = r#"{"detail":"Rejected due to Sexuality content"}"#; - let result = filter_world_labs_http_error(StatusCode::FORBIDDEN, Some(body)); - assert!(matches!(result.unwrap_err(), WorldLabsError::ApiSpecific( - WorldLabsSpecificApiError::NsfwContentPolicyRejected { .. } - ))); - } - - // --- Non-NSFW 403 --- - - #[test] - fn non_nsfw_403_is_generic_error() { - let body = r#"{"detail":"Forbidden: invalid API key"}"#; - let result = filter_world_labs_http_error(StatusCode::FORBIDDEN, Some(body)); - let err = result.unwrap_err(); - - assert!(err.is_403_forbidden()); - assert!(matches!(err, WorldLabsError::ApiGeneric( - WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. } - ))); - } - - #[test] - fn non_nsfw_403_no_body_is_generic_error() { - let result = filter_world_labs_http_error(StatusCode::FORBIDDEN, None); - let err = result.unwrap_err(); - assert!(err.is_403_forbidden()); - assert!(matches!(err, WorldLabsError::ApiGeneric( - WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. } - ))); - } - - // --- 402 Payment Required --- - - #[test] - fn payment_required_402() { - let result = filter_world_labs_http_error(StatusCode::PAYMENT_REQUIRED, Some("{}")); - let err = result.unwrap_err(); - assert!(matches!(err, WorldLabsError::ApiSpecific( - WorldLabsSpecificApiError::InsufficientCredits - ))); - assert!(!err.is_403_forbidden()); - } - - // --- Other errors --- - - #[test] - fn server_error_500_is_generic() { - let body = r#"{"detail":"Internal server error"}"#; - let result = filter_world_labs_http_error(StatusCode::INTERNAL_SERVER_ERROR, Some(body)); - let err = result.unwrap_err(); - assert!(!err.is_403_forbidden()); - assert!(matches!(err, WorldLabsError::ApiGeneric( - WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. } - ))); - } - - #[test] - fn bad_request_400_is_generic() { - let body = r#"{"detail":"Bad request"}"#; - let result = filter_world_labs_http_error(StatusCode::BAD_REQUEST, Some(body)); - assert!(matches!(result.unwrap_err(), WorldLabsError::ApiGeneric( - WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. } - ))); - } - - #[test] - fn nsfw_keyword_in_non_403_is_not_nsfw_error() { - // Even if the body mentions "nsfw", a 400 should NOT be classified as NSFW rejection. - let body = r#"{"detail":"nsfw check failed"}"#; - let result = filter_world_labs_http_error(StatusCode::BAD_REQUEST, Some(body)); - assert!(matches!(result.unwrap_err(), WorldLabsError::ApiGeneric( - WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { .. } - ))); - } - - #[test] - fn error_500_no_body() { - let result = filter_world_labs_http_error(StatusCode::INTERNAL_SERVER_ERROR, None); - assert!(matches!(result.unwrap_err(), WorldLabsError::ApiGeneric( - WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { body, .. } - ) if body.is_empty())); - } -} diff --git a/crates/api_clients/worldlabs_api_client/src/error/mod.rs b/crates/api_clients/worldlabs_api_client/src/error/mod.rs deleted file mode 100644 index 015cdc81..00000000 --- a/crates/api_clients/worldlabs_api_client/src/error/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod world_labs_client_error; -pub mod world_labs_error; -pub mod world_labs_generic_api_error; -pub mod world_labs_specific_api_error; -pub (crate) mod filter_world_labs_http_error; \ No newline at end of file diff --git a/crates/api_clients/worldlabs_api_client/src/error/world_labs_client_error.rs b/crates/api_clients/worldlabs_api_client/src/error/world_labs_client_error.rs deleted file mode 100644 index 5c2073cb..00000000 --- a/crates/api_clients/worldlabs_api_client/src/error/world_labs_client_error.rs +++ /dev/null @@ -1,23 +0,0 @@ -use std::error::Error; -use std::fmt::{Display, Formatter}; - -#[derive(Debug)] -pub enum WorldLabsClientError { - - /// An error was encountered in building the Wreq client - WreqClientError(wreq::Error), - - /// Can't read a local file for uploading. - CannotReadLocalFileForUpload(std::io::Error), -} - -impl Error for WorldLabsClientError {} - -impl Display for WorldLabsClientError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::WreqClientError(err) => write!(f, "Wreq client error (during client creation): {}", err), - Self::CannotReadLocalFileForUpload(err) => write!(f, "Cannot read local file for upload: {}", err), - } - } -} diff --git a/crates/api_clients/worldlabs_api_client/src/error/world_labs_error.rs b/crates/api_clients/worldlabs_api_client/src/error/world_labs_error.rs deleted file mode 100644 index 611e835f..00000000 --- a/crates/api_clients/worldlabs_api_client/src/error/world_labs_error.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::error::world_labs_client_error::WorldLabsClientError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use crate::error::world_labs_specific_api_error::WorldLabsSpecificApiError; -use std::error::Error; - -#[derive(Debug)] -pub enum WorldLabsError { - Client(WorldLabsClientError), - ApiSpecific(WorldLabsSpecificApiError), - ApiGeneric(WorldLabsGenericApiError), -} - -impl WorldLabsError { - pub fn is_403_forbidden(&self) -> bool { - match self { - Self::ApiSpecific(e) => e.is_403_forbidden(), - Self::ApiGeneric(e) => e.is_403_forbidden(), - Self::Client(_) => false, - } - } -} - -impl Error for WorldLabsError {} - -impl std::fmt::Display for WorldLabsError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Client(e) => write!(f, "WorldLabsClientError: {:?}", e), - Self::ApiSpecific(e) => write!(f, "WorldLabsSpecificApiError: {:?}", e), - Self::ApiGeneric(e) => write!(f, "WorldLabsGenericApiError: {:?}", e), - } - } -} - -impl From for WorldLabsError { - fn from(error: WorldLabsClientError) -> Self { - Self::Client(error) - } -} - -impl From for WorldLabsError { - fn from(error: WorldLabsSpecificApiError) -> Self { - Self::ApiSpecific(error) - } -} - -impl From for WorldLabsError { - fn from(error: WorldLabsGenericApiError) -> Self { - Self::ApiGeneric(error) - } -} diff --git a/crates/api_clients/worldlabs_api_client/src/error/world_labs_generic_api_error.rs b/crates/api_clients/worldlabs_api_client/src/error/world_labs_generic_api_error.rs deleted file mode 100644 index 2eb42ab7..00000000 --- a/crates/api_clients/worldlabs_api_client/src/error/world_labs_generic_api_error.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::error::Error; -use std::fmt::{Display, Formatter}; -use wreq::StatusCode; - -#[derive(Debug)] -pub enum WorldLabsGenericApiError { - /// Unknown error occurred when uploading to signed URL - GoogleUploadFailed { - status_code: StatusCode, - body: String, - }, - - /// serde_json::Error, likely from JSON deserialization schema mismatch. - /// Includes the original body. - SerdeResponseParseErrorWithBody(serde_json::Error, String), - - /// An uncategorized bad HTTP response. - UncategorizedBadResponseWithStatusAndBody { - status_code: StatusCode, - body: String, - }, - - /// An uncaught error from the API client. - WreqError(wreq::Error), -} - -impl WorldLabsGenericApiError { - pub fn is_403_forbidden(&self) -> bool { - match self { - Self::UncategorizedBadResponseWithStatusAndBody { status_code, .. } - | Self::GoogleUploadFailed { status_code, .. } => { - *status_code == StatusCode::FORBIDDEN - } - _ => false, - } - } -} - -impl Error for WorldLabsGenericApiError {} - -impl Display for WorldLabsGenericApiError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::GoogleUploadFailed { status_code, body } => write!(f, "Signed URL upload failed with HTTP {}: {:?}", status_code, body), - Self::SerdeResponseParseErrorWithBody(err, body) => write!(f, "Failed to parse response body: {:?}. Body: {}", err, body), - Self::UncategorizedBadResponseWithStatusAndBody { status_code, body } => write!(f, "Uncategorized bad response: status code {}, body: {}", status_code, body), - Self::WreqError(err) => write!(f, "Wreq client error: {}", err), - } - } -} diff --git a/crates/api_clients/worldlabs_api_client/src/error/world_labs_specific_api_error.rs b/crates/api_clients/worldlabs_api_client/src/error/world_labs_specific_api_error.rs deleted file mode 100644 index 7990f68d..00000000 --- a/crates/api_clients/worldlabs_api_client/src/error/world_labs_specific_api_error.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::error::Error; -use std::fmt::{Display, Formatter}; - -#[derive(Debug)] -pub enum WorldLabsSpecificApiError { - /// There aren't sufficient funds for generation. - InsufficientCredits, - - /// The content was rejected by WorldLabs' NSFW / content policy filter. - NsfwContentPolicyRejected { - message: Option, - }, -} - -impl WorldLabsSpecificApiError { - pub fn is_403_forbidden(&self) -> bool { - match self { - Self::NsfwContentPolicyRejected { .. } => true, - _ => false, - } - } -} - -impl Error for WorldLabsSpecificApiError {} - -impl Display for WorldLabsSpecificApiError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::InsufficientCredits => write!(f, "Insufficient credits"), - Self::NsfwContentPolicyRejected { message } => { - write!(f, "NSFW content policy rejection")?; - if let Some(msg) = message { - write!(f, ": {}", msg)?; - } - Ok(()) - } - } - } -} diff --git a/crates/api_clients/worldlabs_api_client/src/lib.rs b/crates/api_clients/worldlabs_api_client/src/lib.rs deleted file mode 100644 index 304ccae7..00000000 --- a/crates/api_clients/worldlabs_api_client/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ - -#[cfg(test)] -pub (crate) mod test_utils; - -pub mod api; -pub mod credentials; -pub mod error; -pub mod pricing; -pub mod recipes; diff --git a/crates/api_clients/worldlabs_api_client/src/pricing/check_pricing.rs b/crates/api_clients/worldlabs_api_client/src/pricing/check_pricing.rs deleted file mode 100644 index 8bb334b3..00000000 --- a/crates/api_clients/worldlabs_api_client/src/pricing/check_pricing.rs +++ /dev/null @@ -1,226 +0,0 @@ -use crate::api::api_types::world_labs_model::WorldLabsModel; - -/// The type of input used to generate a world. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[cfg_attr(test, derive(strum::EnumIter, strum::Display))] -pub enum InputType { - Text, - ImageNonPanorama, - ImagePanorama, - MultiImage, - Video, -} - -/// Cost breakdown for a World Labs generation request. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct WorldLabsCost { - pub worldlabs_credits: u32, - pub us_dollar_cents: u32, -} - -/// Calculate the cost of a World Labs generation request. -pub fn calculate_cost(model: WorldLabsModel, input_type: InputType) -> WorldLabsCost { - let worldlabs_credits = credits_for(model, input_type); - let us_dollar_cents = credits_to_us_dollar_cents(worldlabs_credits); - WorldLabsCost { worldlabs_credits, us_dollar_cents } -} - -/// Returns the credit cost for a given model and input type, -/// based on the official pricing table at https://docs.worldlabs.ai/api/pricing -fn credits_for(model: WorldLabsModel, input_type: InputType) -> u32 { - match (model, input_type) { - // Marble 0.1-mini - (WorldLabsModel::Marble0p1Mini, InputType::ImagePanorama) => 150, - (WorldLabsModel::Marble0p1Mini, InputType::Text) => 230, - (WorldLabsModel::Marble0p1Mini, InputType::ImageNonPanorama) => 230, - (WorldLabsModel::Marble0p1Mini, InputType::MultiImage) => 250, - (WorldLabsModel::Marble0p1Mini, InputType::Video) => 250, - - // Marble 0.1-plus - (WorldLabsModel::Marble0p1Plus, InputType::ImagePanorama) => 1500, - (WorldLabsModel::Marble0p1Plus, InputType::Text) => 1580, - (WorldLabsModel::Marble0p1Plus, InputType::ImageNonPanorama) => 1580, - (WorldLabsModel::Marble0p1Plus, InputType::MultiImage) => 1600, - (WorldLabsModel::Marble0p1Plus, InputType::Video) => 1600, - - // Marble 1.0 (successor to 0.1-plus) - (WorldLabsModel::Marble1p0, InputType::ImagePanorama) => 1500, - (WorldLabsModel::Marble1p0, InputType::Text) => 1580, - (WorldLabsModel::Marble1p0, InputType::ImageNonPanorama) => 1580, - (WorldLabsModel::Marble1p0, InputType::MultiImage) => 1600, - (WorldLabsModel::Marble1p0, InputType::Video) => 1600, - - // Marble 1.0-draft (successor to 0.1-mini) - (WorldLabsModel::Marble1p0Draft, InputType::ImagePanorama) => 150, - (WorldLabsModel::Marble1p0Draft, InputType::Text) => 230, - (WorldLabsModel::Marble1p0Draft, InputType::ImageNonPanorama) => 230, - (WorldLabsModel::Marble1p0Draft, InputType::MultiImage) => 250, - (WorldLabsModel::Marble1p0Draft, InputType::Video) => 250, - - // Marble 1.1 - (WorldLabsModel::Marble1p1, InputType::ImagePanorama) => 1500, - (WorldLabsModel::Marble1p1, InputType::Text) => 1580, - (WorldLabsModel::Marble1p1, InputType::ImageNonPanorama) => 1580, - (WorldLabsModel::Marble1p1, InputType::MultiImage) => 1600, - (WorldLabsModel::Marble1p1, InputType::Video) => 1600, - - // TODO: Looks like there's no way for us to know what the price will be - // for us to bill this accurately, we'll have to bill this after the fact. - // also - they don't have an API for us to ascertain this??! Wat. - // Marble 1.1-plus (range pricing — we use the max to charge enough upfront) - // Actual range: 1500–3000 / 1580–3080 / 1600–3100 - (WorldLabsModel::Marble1p1Plus, InputType::ImagePanorama) => 3000, - (WorldLabsModel::Marble1p1Plus, InputType::Text) => 3080, - (WorldLabsModel::Marble1p1Plus, InputType::ImageNonPanorama) => 3080, - (WorldLabsModel::Marble1p1Plus, InputType::MultiImage) => 3100, - (WorldLabsModel::Marble1p1Plus, InputType::Video) => 3100, - } -} - -/// Converts credits to US dollar cents. -/// Rate: $1.00 USD per 1,250 credits → 100 cents per 1,250 credits. -fn credits_to_us_dollar_cents(credits: u32) -> u32 { - // (credits * 100) / 1250 = (credits * 4) / 50 = (credits * 2) / 25 - // Use u64 to avoid overflow on large values. - ((credits as u64 * 100) / 1250) as u32 -} - - -#[cfg(test)] -mod tests { - use super::*; - use strum::IntoEnumIterator; - - #[test] - fn test_pricing_table() { - let models = [ - WorldLabsModel::Marble0p1Mini, - WorldLabsModel::Marble0p1Plus, - WorldLabsModel::Marble1p0, - WorldLabsModel::Marble1p0Draft, - WorldLabsModel::Marble1p1, - WorldLabsModel::Marble1p1Plus, - ]; - - println!("\n{:<20} {:<20} {:>10} {:>12}", "Model", "Input Type", "Credits", "USD Cents"); - println!("{}", "-".repeat(65)); - - for model in &models { - for input_type in InputType::iter() { - let cost = calculate_cost(*model, input_type); - println!( - "{:<20} {:<20} {:>10} {:>12}", - model.get_model_api_name_str(), - input_type, - cost.worldlabs_credits, - cost.us_dollar_cents, - ); - } - } - } - - #[test] - fn test_mini_credits() { - assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Mini, InputType::Text).worldlabs_credits, 230); - assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Mini, InputType::ImageNonPanorama).worldlabs_credits, 230); - assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Mini, InputType::ImagePanorama).worldlabs_credits, 150); - assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Mini, InputType::MultiImage).worldlabs_credits, 250); - assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Mini, InputType::Video).worldlabs_credits, 250); - } - - #[test] - fn test_plus_credits() { - assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Plus, InputType::Text).worldlabs_credits, 1580); - assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Plus, InputType::ImageNonPanorama).worldlabs_credits, 1580); - assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Plus, InputType::ImagePanorama).worldlabs_credits, 1500); - assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Plus, InputType::MultiImage).worldlabs_credits, 1600); - assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Plus, InputType::Video).worldlabs_credits, 1600); - } - - #[test] - fn test_marble_1p0_draft_credits() { - // Same as Marble 0.1-mini (its successor) - assert_eq!(calculate_cost(WorldLabsModel::Marble1p0Draft, InputType::Text).worldlabs_credits, 230); - assert_eq!(calculate_cost(WorldLabsModel::Marble1p0Draft, InputType::ImageNonPanorama).worldlabs_credits, 230); - assert_eq!(calculate_cost(WorldLabsModel::Marble1p0Draft, InputType::ImagePanorama).worldlabs_credits, 150); - assert_eq!(calculate_cost(WorldLabsModel::Marble1p0Draft, InputType::MultiImage).worldlabs_credits, 250); - assert_eq!(calculate_cost(WorldLabsModel::Marble1p0Draft, InputType::Video).worldlabs_credits, 250); - } - - #[test] - fn test_marble_1p0_credits() { - // Standard model, same pricing as Marble 0.1-plus - assert_eq!(calculate_cost(WorldLabsModel::Marble1p0, InputType::Text).worldlabs_credits, 1580); - assert_eq!(calculate_cost(WorldLabsModel::Marble1p0, InputType::ImageNonPanorama).worldlabs_credits, 1580); - assert_eq!(calculate_cost(WorldLabsModel::Marble1p0, InputType::ImagePanorama).worldlabs_credits, 1500); - assert_eq!(calculate_cost(WorldLabsModel::Marble1p0, InputType::MultiImage).worldlabs_credits, 1600); - assert_eq!(calculate_cost(WorldLabsModel::Marble1p0, InputType::Video).worldlabs_credits, 1600); - } - - #[test] - fn test_marble_1p1_credits() { - // Standard model, same pricing tier as 1.0 - assert_eq!(calculate_cost(WorldLabsModel::Marble1p1, InputType::Text).worldlabs_credits, 1580); - assert_eq!(calculate_cost(WorldLabsModel::Marble1p1, InputType::ImageNonPanorama).worldlabs_credits, 1580); - assert_eq!(calculate_cost(WorldLabsModel::Marble1p1, InputType::ImagePanorama).worldlabs_credits, 1500); - assert_eq!(calculate_cost(WorldLabsModel::Marble1p1, InputType::MultiImage).worldlabs_credits, 1600); - assert_eq!(calculate_cost(WorldLabsModel::Marble1p1, InputType::Video).worldlabs_credits, 1600); - } - - #[test] - fn test_marble_1p1_plus_credits() { - // Plus model uses max of the range pricing (1500–3000, 1580–3080, 1600–3100) - assert_eq!(calculate_cost(WorldLabsModel::Marble1p1Plus, InputType::Text).worldlabs_credits, 3080); - assert_eq!(calculate_cost(WorldLabsModel::Marble1p1Plus, InputType::ImageNonPanorama).worldlabs_credits, 3080); - assert_eq!(calculate_cost(WorldLabsModel::Marble1p1Plus, InputType::ImagePanorama).worldlabs_credits, 3000); - assert_eq!(calculate_cost(WorldLabsModel::Marble1p1Plus, InputType::MultiImage).worldlabs_credits, 3100); - assert_eq!(calculate_cost(WorldLabsModel::Marble1p1Plus, InputType::Video).worldlabs_credits, 3100); - } - - #[test] - fn test_marble_1p1_plus_is_more_expensive_than_standard() { - for input_type in InputType::iter() { - let standard = calculate_cost(WorldLabsModel::Marble1p1, input_type); - let plus = calculate_cost(WorldLabsModel::Marble1p1Plus, input_type); - assert!( - plus.worldlabs_credits > standard.worldlabs_credits, - "1.1-plus should cost more than 1.1 for {:?}: {} vs {}", - input_type, plus.worldlabs_credits, standard.worldlabs_credits, - ); - } - } - - #[test] - fn test_dollar_conversion() { - // 1250 credits = $1.00 = 100 cents - assert_eq!(credits_to_us_dollar_cents(1250), 100); - // 150 credits = $0.12 = 12 cents - assert_eq!(credits_to_us_dollar_cents(150), 12); - // 230 credits = $0.184 = 18 cents (truncated) - assert_eq!(credits_to_us_dollar_cents(230), 18); - // 1580 credits = $1.264 = 126 cents (truncated) - assert_eq!(credits_to_us_dollar_cents(1580), 126); - // 1600 credits = $1.28 = 128 cents - assert_eq!(credits_to_us_dollar_cents(1600), 128); - } - - #[test] - fn test_all_combinations_have_costs() { - let models = [ - WorldLabsModel::Marble0p1Mini, - WorldLabsModel::Marble0p1Plus, - WorldLabsModel::Marble1p0, - WorldLabsModel::Marble1p0Draft, - WorldLabsModel::Marble1p1, - WorldLabsModel::Marble1p1Plus, - ]; - - for model in &models { - for input_type in InputType::iter() { - let cost = calculate_cost(*model, input_type); - assert!(cost.worldlabs_credits > 0, "credits should be > 0 for {:?} {:?}", model, input_type); - assert!(cost.us_dollar_cents > 0, "dollar cents should be > 0 for {:?} {:?}", model, input_type); - } - } - } -} diff --git a/crates/api_clients/worldlabs_api_client/src/pricing/mod.rs b/crates/api_clients/worldlabs_api_client/src/pricing/mod.rs deleted file mode 100644 index b53ea403..00000000 --- a/crates/api_clients/worldlabs_api_client/src/pricing/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod check_pricing; diff --git a/crates/api_clients/worldlabs_api_client/src/recipes/mod.rs b/crates/api_clients/worldlabs_api_client/src/recipes/mod.rs deleted file mode 100644 index d17ac4bb..00000000 --- a/crates/api_clients/worldlabs_api_client/src/recipes/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod upload_image_and_create_world_with_retry; \ No newline at end of file diff --git a/crates/api_clients/worldlabs_api_client/src/recipes/upload_image_and_create_world_with_retry.rs b/crates/api_clients/worldlabs_api_client/src/recipes/upload_image_and_create_world_with_retry.rs deleted file mode 100644 index 62e25936..00000000 --- a/crates/api_clients/worldlabs_api_client/src/recipes/upload_image_and_create_world_with_retry.rs +++ /dev/null @@ -1,166 +0,0 @@ -use crate::api::api_types::media_asset_id::MediaAssetId; -use crate::api::api_types::operation_id::OperationId; -use crate::api::api_types::world_labs_model::WorldLabsModel; -use crate::api::requests::generate_world::generate_world::{generate_world, GenerateWorldArgs}; -use crate::api::requests::generate_world::http_request::{ContentReference, WorldPrompt}; -use crate::api::requests::prepare_upload::prepare_upload::{prepare_upload, PrepareUploadArgs, MediaAssetKind}; -use crate::api::requests::upload_to_signed_url::upload_to_signed_url::{upload_to_signed_url, UploadToSignedUrlArgs}; -use crate::credentials::world_labs_api_creds::WorldLabsApiCreds; -use crate::error::world_labs_client_error::WorldLabsClientError; -use crate::error::world_labs_error::WorldLabsError; -use filesys::file_read_bytes::file_read_bytes; -use log::{error, info}; -use std::path::PathBuf; -use std::time::Duration; - -pub struct UploadImageAndCreateWorldArgs<'a> { - pub creds: &'a WorldLabsApiCreds, - pub file: FileBytesOrPath, - pub text_prompt: Option, - pub model: WorldLabsModel, - pub individual_request_timeout: Option, -} - -pub enum FileBytesOrPath { - Bytes(Vec), - Path(PathBuf), -} - -pub struct UploadImageAndCreateWorldResponse { - pub operation_id: OperationId, - pub media_asset_id: MediaAssetId, -} - -/// Official World Labs API: Upload an image and generate a world. -/// -/// Flow (3 steps): -/// 1. prepare_upload — get media_asset_id + signed upload URL -/// 2. upload_to_signed_url — PUT file bytes to signed URL -/// 3. generate_world — start world generation with media_asset source -pub async fn upload_image_and_create_world(args: UploadImageAndCreateWorldArgs<'_>) -> Result { - - info!("Checking file input..."); - - let file_bytes = match args.file { - FileBytesOrPath::Bytes(bytes) => { - info!("File bytes provided ({} bytes)", bytes.len()); - bytes - } - FileBytesOrPath::Path(path) => { - info!("File path provided: {:?}", path); - match file_read_bytes(&path) { - Ok(bytes) => bytes, - Err(err) => { - error!("Error reading file bytes from path: {:?} - error: {:?}", path, err); - return Err(WorldLabsClientError::CannotReadLocalFileForUpload(err).into()); - } - } - } - }; - - // Step 1: Prepare upload - info!("Step 1 of 3: prepare_upload ..."); - - let prepare_response = prepare_upload(PrepareUploadArgs { - creds: args.creds, - file_name: "upload.jpg", - kind: MediaAssetKind::Image, - request_timeout: args.individual_request_timeout, - }).await?; - - let media_asset_id = prepare_response.media_asset_id; - let upload_url = prepare_response.upload_url; - let required_headers = prepare_response.required_headers; - - info!("Media asset ID: {}", media_asset_id.as_str()); - info!("Upload URL: {}", upload_url); - - // Step 2: Upload to signed URL - info!("Step 2 of 3: upload_to_signed_url ..."); - - upload_to_signed_url(UploadToSignedUrlArgs { - upload_url: &upload_url, - file_bytes, - required_headers: &required_headers, - content_type: "image/jpeg", - request_timeout: args.individual_request_timeout, - }).await?; - - info!("Upload complete."); - - // Step 3: Generate world - info!("Step 3 of 3: generate_world ..."); - - let world_prompt = WorldPrompt::Image { - image_prompt: ContentReference::MediaAsset { - media_asset_id: media_asset_id.as_str().to_string(), - }, - text_prompt: args.text_prompt, - is_pano: None, - disable_recaption: None, - }; - - let generate_response = generate_world(GenerateWorldArgs { - creds: args.creds, - world_prompt, - display_name: None, - model: args.model, - seed: None, - tags: None, - permission: None, - request_timeout: args.individual_request_timeout, - }).await?; - - info!("Operation ID: {}", generate_response.operation_id.as_str()); - info!("Done: {}", generate_response.done); - - Ok(UploadImageAndCreateWorldResponse { - operation_id: generate_response.operation_id, - media_asset_id, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_utils::get_test_api_key::get_test_api_key; - use crate::test_utils::setup_test_logging::setup_test_logging; - use filesys::file_read_bytes::file_read_bytes; - use log::LevelFilter; - - #[tokio::test] - #[ignore] - async fn test_upload_and_generate() { - setup_test_logging(LevelFilter::Debug); - - let creds = get_test_api_key().unwrap(); - - //let file_path = "/Users/bt/Pictures/Midjourney/moon_door.png"; - let file_path = "/Users/bt/Pictures/Midjourney/meadow_mirror.png"; - let file_bytes = file_read_bytes(file_path).unwrap(); - - println!("File bytes len: {}", file_bytes.len()); - - let results = upload_image_and_create_world(UploadImageAndCreateWorldArgs { - creds: &creds, - individual_request_timeout: None, - file: FileBytesOrPath::Bytes(file_bytes), - text_prompt: None, - model: WorldLabsModel::Marble0p1Plus, - }).await.unwrap(); - - println!("Operation ID: {}", results.operation_id.as_str()); - println!("Media Asset ID: {}", results.media_asset_id.as_str()); - - /* -00:55:36.237287 [DEBUG] - Requesting URL: https://api.worldlabs.ai/marble/v1/worlds:generate -00:55:45.047221 [DEBUG] - Response body (200): {"operation_id":"1fab3bf1-05a1-4929-907e-c6df07c539e2","created_at":"2026-03-09T04:55:37Z","updated_at":"2026-03-09T04:55:37Z","expires_at":"2026-03-09T05:55:37Z","done":false,"error":null,"metadata":null,"response":null} -00:55:45.047756 [INFO] - Operation ID: 1fab3bf1-05a1-4929-907e-c6df07c539e2 -00:55:45.047839 [INFO] - Done: false -Operation ID: 1fab3bf1-05a1-4929-907e-c6df07c539e2 -Media Asset ID: bc57b2bc-542e-416d-8089-924294152f0d - */ - - assert_eq!(1, 2); - } -} diff --git a/crates/api_clients/worldlabs_api_client/src/test_utils/get_test_api_key.rs b/crates/api_clients/worldlabs_api_client/src/test_utils/get_test_api_key.rs deleted file mode 100644 index b445a654..00000000 --- a/crates/api_clients/worldlabs_api_client/src/test_utils/get_test_api_key.rs +++ /dev/null @@ -1,10 +0,0 @@ -use crate::credentials::world_labs_api_creds::WorldLabsApiCreds; -use anyhow::Result as AnyhowResult; - -const API_KEY_FILE: &str = "/Users/bt/Artcraft/credentials/world_labs_api_key.txt"; - -#[cfg(test)] -pub(crate) fn get_test_api_key() -> AnyhowResult { - let api_key = std::fs::read_to_string(API_KEY_FILE)?.trim().to_string(); - Ok(WorldLabsApiCreds::new(api_key)) -} diff --git a/crates/api_clients/worldlabs_api_client/src/test_utils/mod.rs b/crates/api_clients/worldlabs_api_client/src/test_utils/mod.rs deleted file mode 100644 index 35f004a1..00000000 --- a/crates/api_clients/worldlabs_api_client/src/test_utils/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod get_test_api_key; -pub mod setup_test_logging; diff --git a/crates/api_clients/worldlabs_api_client/src/test_utils/setup_test_logging.rs b/crates/api_clients/worldlabs_api_client/src/test_utils/setup_test_logging.rs deleted file mode 100644 index 9438da10..00000000 --- a/crates/api_clients/worldlabs_api_client/src/test_utils/setup_test_logging.rs +++ /dev/null @@ -1,52 +0,0 @@ -use chrono::Local; -use env_logger::Builder; -use log::{debug, error, info, trace, warn, LevelFilter}; -use std::io::Write; - -const RUST_LOG : &str = "RUST_LOG"; - -pub fn setup_test_logging(level: LevelFilter) { - let level_str = get_level_str(level); - - println!("Previous log level: {:?}", env::var(RUST_LOG)); - - let is_unsafe = env::set_var(RUST_LOG, level_str); - - if is_unsafe.is_none() { - println!("Unsafely setting Rust Log Level..."); - unsafe { std::env::set_var(RUST_LOG, level_str) } - } - - println!("Log level: {:?}", env::var(RUST_LOG)); - - Builder::new() - .is_test(true) - .format(|buf, record| { - writeln!(buf, - "{} [{}] - {}", - Local::now().format("%H:%M:%S%.6f"), - record.level(), - record.args() - ) - }) - .filter(None, level) - .filter_level(level) - .init(); - - trace!("Test trace log"); - debug!("Test debug log"); - info!("Test info log"); - warn!("Test warn log"); - error!("Test error log"); -} - -fn get_level_str(level: LevelFilter) -> &'static str { - match level { - LevelFilter::Error => "error", - LevelFilter::Warn => "warn", - LevelFilter::Info => "info", - LevelFilter::Debug => "debug", - LevelFilter::Trace => "trace", - LevelFilter::Off => "off", - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/Cargo.toml b/crates/api_clients/worldlabs_consumer_client/Cargo.toml deleted file mode 100644 index d9d2f052..00000000 --- a/crates/api_clients/worldlabs_consumer_client/Cargo.toml +++ /dev/null @@ -1,54 +0,0 @@ -[package] -name = "worldlabs_consumer_client" -edition = "2024" -version = "0.0.1" -publish = false - -[package.metadata.cargo-udeps.ignore] -normal = ["workspace-build-acceleration"] - -[lib] -name = "worldlabs_consumer_client" -path = "src/lib.rs" - -[dependencies] -# Workspace hack (faster compile times) -workspace-build-acceleration.workspace = true - -# Internal -cloudflare_errors.workspace = true -cloudflare_mitigation.workspace = true -errors.workspace = true -filesys.workspace = true -http_headers.workspace = true -jwt_light.workspace = true - -# External -anyhow.workspace = true -base64.workspace = true -bstr.workspace = true -byteorder.workspace = true -chrono.workspace = true -futures.workspace = true -indexmap.workspace = true -log.workspace = true -once_cell.workspace = true -regex.workspace = true -scraper = "0.24.0" -serde.workspace = true -serde_json.workspace = true -sha2.workspace = true -tokio = { version = "1", features = ["full"] } -url.workspace = true -uuid.workspace = true -wreq-util.workspace = true -wreq.workspace = true - -[dev-dependencies] -filesys.workspace = true -test_data.workspace = true -test_utils.workspace = true -log4rs = "1.4.0" -env_logger.workspace = true -env.workspace = true -chrono.workspace = true diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/api_types/image_input_object_id.rs b/crates/api_clients/worldlabs_consumer_client/src/api/api_types/image_input_object_id.rs deleted file mode 100644 index 207d6a5e..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/api_types/image_input_object_id.rs +++ /dev/null @@ -1,26 +0,0 @@ -use uuid::Uuid; - -/// Type for Image Input Object Ids -/// These are used to refer to worlds -/// These appear to be bare UUIDs. -#[derive(Clone, Debug)] -pub struct ImageInputObjectId(pub String); - -impl ImageInputObjectId { - pub fn generate_new() -> Self { - let uuid = Uuid::new_v4().to_string(); - Self(uuid) - } - - pub fn from_str(s: &str) -> Self { - Self(s.to_string()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn to_string(&self) -> String { - self.0.clone() - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/api_types/meta_world_object_id.rs b/crates/api_clients/worldlabs_consumer_client/src/api/api_types/meta_world_object_id.rs deleted file mode 100644 index 54cd53a1..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/api_types/meta_world_object_id.rs +++ /dev/null @@ -1,26 +0,0 @@ -use uuid::Uuid; - -/// Type for "meta" World IDs (not sure what to term this). -/// These are in the first run patch request -/// These appear to be bare UUIDs. -#[derive(Clone, Debug)] -pub struct MetaWorldObjectId(pub String); - -impl MetaWorldObjectId { - pub fn generate_new() -> Self { - let uuid = Uuid::new_v4().to_string(); - Self(uuid) - } - - pub fn from_str(s: &str) -> Self { - Self(s.to_string()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn to_string(&self) -> String { - self.0.clone() - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/api_types/mod.rs b/crates/api_clients/worldlabs_consumer_client/src/api/api_types/mod.rs deleted file mode 100644 index ed4b37eb..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/api_types/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod image_input_object_id; -pub mod meta_world_object_id; -pub mod pano_object_id; -pub mod run_object; -pub mod run_object_id; -pub mod upload_mime_type; -pub mod upload_object_id; -pub mod world_id; \ No newline at end of file diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/api_types/pano_object_id.rs b/crates/api_clients/worldlabs_consumer_client/src/api/api_types/pano_object_id.rs deleted file mode 100644 index 96c359f4..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/api_types/pano_object_id.rs +++ /dev/null @@ -1,26 +0,0 @@ -use uuid::Uuid; - -/// Type for Pano Object Ids -/// These are used to refer to worlds -/// These appear to be bare UUIDs. -#[derive(Clone, Debug)] -pub struct PanoObjectId(pub String); - -impl PanoObjectId { - pub fn generate_new() -> Self { - let uuid = Uuid::new_v4().to_string(); - Self(uuid) - } - - pub fn from_str(s: &str) -> Self { - Self(s.to_string()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn to_string(&self) -> String { - self.0.clone() - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/api_types/run_object.rs b/crates/api_clients/worldlabs_consumer_client/src/api/api_types/run_object.rs deleted file mode 100644 index e923607c..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/api_types/run_object.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::collections::HashMap; -use chrono::Utc; -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -const RUN_MIME_TYPE: &str = "application/run+json"; - -const RUN_VERSION : &str = "0.0.1"; - -/// World Labs' API is very frontend driven -/// They send whole objects back to the frontend that the frontend is in charge of mutatingbjects or PATCH them back to the server (yuck): -/// - request #1 - create run -/// - request #6 - patch run after image upload -/// Gotta go fast? -#[derive(Serialize, Deserialize, Clone)] -pub struct RunObject { - /// The primary ID of the run. - /// This won't exist until we post it to the server. - #[serde(skip_serializing_if = "Option::is_none")] - pub id: Option, - - pub metadata: RunObjectMetadata, - pub mime_type: String, -} - -#[derive(Serialize, Deserialize, Clone)] -pub struct RunObjectMetadata { - pub version: String, - - #[serde(rename = "createdAt")] - pub created_at: u64, - - #[serde(rename = "updatedAt")] - pub updated_at: u64, - - #[serde(rename = "usesAdvancedEditing")] - pub uses_advanced_editing: bool, - - #[serde(rename = "draftMode")] - pub draft_mode: bool, - - /// Polymorphic set of ID-to-object mappings. - pub nodes: HashMap, -} - -impl Default for RunObject { - fn default() -> Self { - let now = Utc::now(); - let now = now.timestamp().unsigned_abs() * 1000; // Millisecond resolution - Self { - id: None, - metadata: RunObjectMetadata{ - version: RUN_VERSION.to_string(), - created_at: now, - updated_at: now, - uses_advanced_editing: false, - draft_mode: false, - nodes: HashMap::new(), - }, - mime_type: RUN_MIME_TYPE.to_string(), - } - } -} - -#[cfg(test)] -mod tests { - use crate::api::api_types::run_object::RunObject; - - #[test] - fn default_runobject() { - let object = RunObject::default(); - assert!(object.id.is_none()); - assert_eq!(&object.metadata.version, "0.0.1"); - assert!(object.metadata.created_at > 1705433415549); - assert!(object.metadata.updated_at > 1705433415549); - assert_eq!(&object.mime_type, "application/run+json"); - } -} - diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/api_types/run_object_id.rs b/crates/api_clients/worldlabs_consumer_client/src/api/api_types/run_object_id.rs deleted file mode 100644 index 73a50ae2..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/api_types/run_object_id.rs +++ /dev/null @@ -1,15 +0,0 @@ -/// Type for Object IDs. -/// These are used to create inference runs and refer to the result -/// These appear to be bare UUIDs. -#[derive(Clone, Debug)] -pub struct RunObjectId(pub String); - -impl RunObjectId { - pub fn from_str(s: &str) -> Self { - Self(s.to_string()) - } - - pub fn to_string(&self) -> String { - self.0.clone() - } -} \ No newline at end of file diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/api_types/upload_mime_type.rs b/crates/api_clients/worldlabs_consumer_client/src/api/api_types/upload_mime_type.rs deleted file mode 100644 index 72c60853..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/api_types/upload_mime_type.rs +++ /dev/null @@ -1,21 +0,0 @@ -use serde::Serialize; - -#[derive(Clone, Copy, Debug, Serialize)] -pub enum UploadMimeType { - #[serde(rename = "image/jpeg")] - ImageJpeg, -} - -impl UploadMimeType { - pub fn content_type(&self) -> &'static str { - match self { - Self::ImageJpeg => "image/jpeg", - } - } - - pub fn extension_without_period(&self) -> &'static str { - match self { - Self::ImageJpeg => "jpeg", - } - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/api_types/upload_object_id.rs b/crates/api_clients/worldlabs_consumer_client/src/api/api_types/upload_object_id.rs deleted file mode 100644 index 26eeb401..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/api_types/upload_object_id.rs +++ /dev/null @@ -1,15 +0,0 @@ -/// Type for Upload IDs. -/// These are used to upload images. -/// These appear to be bare UUIDs. -#[derive(Clone, Debug)] -pub struct UploadObjectId(pub String); - -impl UploadObjectId { - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn to_string(&self) -> String { - self.0.clone() - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/api_types/world_id.rs b/crates/api_clients/worldlabs_consumer_client/src/api/api_types/world_id.rs deleted file mode 100644 index 5d6b32b1..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/api_types/world_id.rs +++ /dev/null @@ -1,26 +0,0 @@ -use uuid::Uuid; - -/// Type for World IDs. -/// These are used to refer to worlds -/// These appear to be bare UUIDs. -#[derive(Clone, Debug)] -pub struct WorldObjectId(pub String); - -impl WorldObjectId { - pub fn new() -> Self { - let uuid = Uuid::new_v4().to_string(); - Self(uuid) - } - - pub fn from_str(s: &str) -> Self { - Self(s.to_string()) - } - - pub fn as_str(&self) -> &str { - &self.0 - } - - pub fn to_string(&self) -> String { - self.0.clone() - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/common/common_header_values.rs b/crates/api_clients/worldlabs_consumer_client/src/api/common/common_header_values.rs deleted file mode 100644 index 2df742be..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/common/common_header_values.rs +++ /dev/null @@ -1,6 +0,0 @@ - -/// Referer header value -pub(crate) const REFERER_VALUE: &str = "https://marble.worldlabs.ai/"; - -/// Origin header value -pub(crate) const ORIGIN_VALUE: &str = "https://marble.worldlabs.ai/"; diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/common/mod.rs b/crates/api_clients/worldlabs_consumer_client/src/api/common/mod.rs deleted file mode 100644 index 6356e946..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/common/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub (crate) mod common_header_values; \ No newline at end of file diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/mod.rs b/crates/api_clients/worldlabs_consumer_client/src/api/mod.rs deleted file mode 100644 index 9729e3e7..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod api_types; -pub mod requests; - -pub (crate) mod common; -pub mod utils; diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/requests/google_refresh_token/google_refresh_token.rs b/crates/api_clients/worldlabs_consumer_client/src/api/requests/google_refresh_token/google_refresh_token.rs deleted file mode 100644 index 7db5e6b9..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/requests/google_refresh_token/google_refresh_token.rs +++ /dev/null @@ -1,154 +0,0 @@ -use crate::api::api_types::upload_mime_type::UploadMimeType; -use crate::api::common::common_header_values::{ORIGIN_VALUE, REFERER_VALUE}; -use crate::credentials::world_labs_bearer_token::WorldLabsBearerToken; -use crate::credentials::world_labs_cookies::WorldLabsCookies; -use crate::credentials::worldlabs_refresh_token::WorldLabsRefreshToken; -use crate::error::world_labs_client_error::WorldLabsClientError; -use crate::error::world_labs_error::WorldLabsError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use crate::error::world_labs_specific_api_error::WorldLabsSpecificApiError; -use http_headers::names::{PRIORITY, SEC_FETCH_DEST, SEC_FETCH_MODE, SEC_FETCH_SITE, SEC_GPC}; -use http_headers::values::accept::ACCEPT_ALL; -use http_headers::values::accept_encoding::ACCEPT_ENCODING_GZIP_ETC; -use http_headers::values::accept_language::ACCEPT_LANGUAGE_EN_US; -use http_headers::values::cache_control::CACHE_CONTROL_NO_CACHE; -use http_headers::values::connection::CONNECTION_KEEP_ALIVE; -use http_headers::values::pragma::PRAGMA_NO_CACHE; -use http_headers::values::priority::{PRIORITY_4, PRIORITY_6}; -use http_headers::values::sec::{SEC_FETCH_DEST_EMPTY, SEC_FETCH_MODE_CORS, SEC_FETCH_SITE_CROSS_SITE}; -use http_headers::values::te::TE_TRAILERS; -use log::{debug, error}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::time::Duration; -use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_LENGTH, CONTENT_TYPE, ORIGIN, PRAGMA, REFERER, TE}; -use wreq::multipart::Form; -use wreq::Client; -use wreq_util::Emulation; - -/// NB: key is the public API key for WorldLabs -/// https://docs.cloud.google.com/identity-platform/docs/use-rest-api -const URL : &str = "https://securetoken.googleapis.com/v1/token?key=AIzaSyAA88yObUN-0pnXAxcgOrT2exZxFWLW1BI"; -pub struct GoogleRefreshTokenArgs<'a> { - pub refresh_token: &'a WorldLabsRefreshToken, - pub request_timeout: Option, -} - -pub struct GoogleRefreshTokenResponse { - pub bearer_token: WorldLabsBearerToken, - pub refresh_token: WorldLabsRefreshToken, -} - -/// Refresh Access Token for WorldLabs using Google. -pub async fn google_refresh_token(args: GoogleRefreshTokenArgs<'_>) -> Result { - let client = Client::builder() - .emulation(Emulation::Firefox143) - .build() - .map_err(|err| WorldLabsClientError::WreqClientError(err))?; - - - debug!("Requesting URL: {}", URL); - - let mut request_builder = client.post(URL) - .header(ACCEPT, ACCEPT_ALL) - .header(ACCEPT_LANGUAGE, ACCEPT_LANGUAGE_EN_US) - .header(ACCEPT_ENCODING, ACCEPT_ENCODING_GZIP_ETC) - .header(REFERER, REFERER_VALUE) - .header(CONNECTION, CONNECTION_KEEP_ALIVE) - .header(SEC_FETCH_DEST, SEC_FETCH_DEST_EMPTY) - .header(SEC_FETCH_MODE, SEC_FETCH_MODE_CORS) - .header(SEC_FETCH_SITE, SEC_FETCH_SITE_CROSS_SITE) - .header(PRIORITY, PRIORITY_4) - .header(TE, TE_TRAILERS); - - let mut form = HashMap::new(); - form.insert("grant_type", "refresh_token".to_string()); - form.insert("refresh_token", args.refresh_token.to_raw_string()); - - if let Some(timeout) = args.request_timeout { - request_builder = request_builder.timeout(timeout); - } - - let http_request = request_builder - .form(&form) - .build() - .map_err(|err| { - error!("Error building request: {:?}", err); - WorldLabsClientError::WreqClientError(err) - })?; - - let response = client.execute(http_request) - .await - .map_err(|err| { - error!("Error during request execution: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - let status = response.status(); - - let response_body = response.text() - .await - .map_err(|err| { - error!("Error reading response body: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - if !status.is_success() { - error!("Request returned an error (code {}) : {:?}", status.as_u16(), response_body); - - if response_body.contains("SignatureDoesNotMatch") || response_body.contains("does not match the signature") { - return Err(WorldLabsSpecificApiError::GoogleUploadSignatureDoesNotMatch.into()); - } else { - return Err(WorldLabsGenericApiError::GoogleUploadFailed { status_code: status, body: response_body }.into()); - } - } - - debug!("Response body (200): {}", response_body); - - let response : RawResponse = serde_json::from_str(&response_body) - .map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?; - - let bearer_token = WorldLabsBearerToken::new(response.access_token); - let refresh_token = WorldLabsRefreshToken::new(response.refresh_token); - - Ok(GoogleRefreshTokenResponse { - bearer_token, - refresh_token, - }) -} - -#[derive(Deserialize)] -struct RawResponse { - pub access_token: String, - pub refresh_token: String, - - // eg. "3600" - pub expires_in: Option, - pub id_token: Option, - pub user_id: Option, -} - -#[cfg(test)] -mod tests { - use crate::api::requests::google_refresh_token::google_refresh_token::{google_refresh_token, GoogleRefreshTokenArgs}; - use crate::credentials::worldlabs_refresh_token::WorldLabsRefreshToken; - use crate::test_utils::setup_test_logging::setup_test_logging; - use errors::AnyhowResult; - use log::LevelFilter; - - #[tokio::test] - #[ignore] // Client side tests only - async fn test_requests() -> AnyhowResult<()> { - setup_test_logging(LevelFilter::Debug); - - let refresh_token = "AMf-vByUgA0J9S93vULECe-sD50cbR85AFVYF60gKAmLFF1MBYwXlKRJCsv7z3oSpLtP0ApyOU5fAl52qHWM2U0yKgEu5gQ5UCUDt5SEG4UBRHWvHEmLLcu-Zl04Fq4ljocSEO3qJzuX1wyshmptZxlDPRkjdLYgyn-Kp03woI2yRPKLuQF5hQtABHabsNUh9Zhs129sgxuUlOuw4zsFI4L8UdE3bEuF3k6Mic7KlE3440YwyQ8Qtmk9zHtXIx2ob56N-1WTGSqwMUuQ1mX4OOvLve9bw2zDW3UzchomTWkh732886RPrP-DNRkPbO3FejUEMxuYQFWYX3EEeIEdfNGXFNGPL1dNniSXlSaKadpX699Ishjviv-x9o-pPQa3zoAj3NzWLyd8mn5rDBh9qAO_iDETaeo01Sjhb4mGYfAWZ-g4S6nW778"; - let refresh_token = WorldLabsRefreshToken::new(refresh_token.to_string()); - - let result = google_refresh_token(GoogleRefreshTokenArgs { - refresh_token: &refresh_token, - request_timeout: None, - }).await?; - - Ok(()) - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/requests/google_refresh_token/mod.rs b/crates/api_clients/worldlabs_consumer_client/src/api/requests/google_refresh_token/mod.rs deleted file mode 100644 index cfade37f..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/requests/google_refresh_token/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod google_refresh_token; \ No newline at end of file diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/requests/google_upload/google_upload_image.rs b/crates/api_clients/worldlabs_consumer_client/src/api/requests/google_upload/google_upload_image.rs deleted file mode 100644 index 7d2e0dc9..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/requests/google_upload/google_upload_image.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::api::api_types::upload_mime_type::UploadMimeType; -use crate::api::common::common_header_values::{ORIGIN_VALUE, REFERER_VALUE}; -use crate::credentials::world_labs_bearer_token::WorldLabsBearerToken; -use crate::credentials::world_labs_cookies::WorldLabsCookies; -use crate::error::world_labs_client_error::WorldLabsClientError; -use crate::error::world_labs_error::WorldLabsError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use crate::error::world_labs_specific_api_error::WorldLabsSpecificApiError; -use http_headers::names::{PRIORITY, SEC_FETCH_DEST, SEC_FETCH_MODE, SEC_FETCH_SITE, SEC_GPC}; -use http_headers::values::accept::ACCEPT_ALL; -use http_headers::values::cache_control::CACHE_CONTROL_NO_CACHE; -use http_headers::values::connection::CONNECTION_KEEP_ALIVE; -use http_headers::values::pragma::PRAGMA_NO_CACHE; -use http_headers::values::priority::PRIORITY_6; -use http_headers::values::sec::{SEC_FETCH_DEST_EMPTY, SEC_FETCH_MODE_CORS, SEC_FETCH_SITE_CROSS_SITE}; -use http_headers::values::te::TE_TRAILERS; -use log::{debug, error}; -use serde::Serialize; -use std::time::Duration; -use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_LENGTH, CONTENT_TYPE, ORIGIN, PRAGMA, REFERER, TE}; -use wreq::Client; -use wreq_util::Emulation; - -pub struct GoogleUploadImageArgs<'a> { - pub upload_url: &'a str, - pub upload_mime_type: UploadMimeType, - pub file_bytes: Vec, - pub request_timeout: Option, -} - -/// Marble Image-to-World -/// Upload the binary to GCP using the signed URL -/// Request #4 (of ~10) -pub async fn google_upload_image(args: GoogleUploadImageArgs<'_>) -> Result<(), WorldLabsError> { - let client = Client::builder() - .emulation(Emulation::Firefox143) - .build() - .map_err(|err| WorldLabsClientError::WreqClientError(err))?; - - let content_type = args.upload_mime_type.content_type(); - - debug!("Requesting URL: {}", args.upload_url); - - let mut request_builder = client.put(args.upload_url) - .header(CONTENT_TYPE, content_type) - .header("x-goog-content-length-range", "0,1048576000"); - - if let Some(timeout) = args.request_timeout { - request_builder = request_builder.timeout(timeout); - } - - let http_request = request_builder - .body(args.file_bytes) - .build() - .map_err(|err| { - error!("Error building request: {:?}", err); - WorldLabsClientError::WreqClientError(err) - })?; - - let response = client.execute(http_request) - .await - .map_err(|err| { - error!("Error during request execution: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - let status = response.status(); - - let response_body = response.text() - .await - .map_err(|err| { - error!("Error reading response body: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - if !status.is_success() { - error!("Request returned an error (code {}) : {:?}", status.as_u16(), response_body); - - if response_body.contains("SignatureDoesNotMatch") || response_body.contains("does not match the signature") { - return Err(WorldLabsSpecificApiError::GoogleUploadSignatureDoesNotMatch.into()); - } else { - return Err(WorldLabsGenericApiError::GoogleUploadFailed { status_code: status, body: response_body }.into()); - } - } - - Ok(()) -} - -#[cfg(test)] -mod tests { -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/requests/google_upload/mod.rs b/crates/api_clients/worldlabs_consumer_client/src/api/requests/google_upload/mod.rs deleted file mode 100644 index 66f712bf..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/requests/google_upload/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod google_upload_image; \ No newline at end of file diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/requests/mod.rs b/crates/api_clients/worldlabs_consumer_client/src/api/requests/mod.rs deleted file mode 100644 index 618aa293..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/requests/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod google_upload; -pub mod objects; -pub mod recaption; -pub mod worlds; -pub mod google_refresh_token; \ No newline at end of file diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/requests/objects/begin_object_image_upload.rs b/crates/api_clients/worldlabs_consumer_client/src/api/requests/objects/begin_object_image_upload.rs deleted file mode 100644 index 26d55d55..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/requests/objects/begin_object_image_upload.rs +++ /dev/null @@ -1,147 +0,0 @@ -use crate::api::api_types::run_object_id::RunObjectId; -use crate::api::api_types::upload_mime_type::UploadMimeType; -use crate::api::api_types::upload_object_id::UploadObjectId; -use crate::api::common::common_header_values::{ORIGIN_VALUE, REFERER_VALUE}; -use crate::credentials::world_labs_bearer_token::WorldLabsBearerToken; -use crate::credentials::world_labs_cookies::WorldLabsCookies; -use crate::error::world_labs_client_error::WorldLabsClientError; -use crate::error::world_labs_error::WorldLabsError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use chrono::Utc; -use http_headers::names::{PRIORITY, SEC_FETCH_DEST, SEC_FETCH_MODE, SEC_FETCH_SITE, SEC_GPC}; -use http_headers::values::accept::ACCEPT_ALL; -use http_headers::values::cache_control::CACHE_CONTROL_NO_CACHE; -use http_headers::values::connection::CONNECTION_KEEP_ALIVE; -use http_headers::values::content_type::CONTENT_TYPE_APPLICATION_JSON; -use http_headers::values::pragma::PRAGMA_NO_CACHE; -use http_headers::values::priority::PRIORITY_4; -use http_headers::values::sec::{SEC_FETCH_DEST_EMPTY, SEC_FETCH_MODE_CORS, SEC_FETCH_SITE_CROSS_SITE}; -use http_headers::values::te::TE_TRAILERS; -use log::{debug, error, info}; -use serde::{Deserialize, Serialize}; -use std::time::Duration; -use uuid::Uuid; -use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_LENGTH, CONTENT_TYPE, ORIGIN, PRAGMA, REFERER, TE}; -use wreq::Client; -use wreq_util::Emulation; - -const BASE_URL : &str = "https://api.worldlabs.ai/api/v1/objects"; - -// Note: WorldLabs is phasing out the old URL scheme: -// const BASE_URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects"; - -fn get_url(upload_id: &UploadObjectId) -> String { - format!("{}/{}:upload", BASE_URL, upload_id.0) -} - -pub struct BeginObjectImageUploadArgs<'a> { - pub cookies: &'a WorldLabsCookies, - pub bearer_token: &'a WorldLabsBearerToken, - pub upload_mime_type: UploadMimeType, - pub upload_id: &'a UploadObjectId, - pub request_timeout: Option, -} - -pub struct BeginObjectImageUploadResponse { - pub upload_url: String, -} - -/// Marble Image-to-World -/// This request follows object attachment and signals upload. It returns the upload URL. -/// Request #3 (of ~10) -pub async fn begin_object_image_upload(args: BeginObjectImageUploadArgs<'_>) -> Result { - let client = Client::builder() - .emulation(Emulation::Firefox143) - .build() - .map_err(|err| WorldLabsClientError::WreqClientError(err))?; - - let url = get_url(args.upload_id); - - debug!("Requesting URL: {}", url); - - let mut request_builder = client.post(url) - .header(ACCEPT, ACCEPT_ALL) - .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5") - .header(ACCEPT_ENCODING, "gzip, deflate, br, zstd") - .header(REFERER, REFERER_VALUE) - .header(CONTENT_TYPE, CONTENT_TYPE_APPLICATION_JSON) - .header(AUTHORIZATION, args.bearer_token.to_bearer_token_header_string()) - .header(ORIGIN, ORIGIN_VALUE) - .header(SEC_GPC, "1") - .header(CONNECTION, CONNECTION_KEEP_ALIVE) - .header(SEC_FETCH_DEST, SEC_FETCH_DEST_EMPTY) - .header(SEC_FETCH_MODE, SEC_FETCH_MODE_CORS) - .header(SEC_FETCH_SITE, SEC_FETCH_SITE_CROSS_SITE) - .header(PRIORITY, PRIORITY_4) - .header(PRAGMA, PRAGMA_NO_CACHE) - .header(CACHE_CONTROL, CACHE_CONTROL_NO_CACHE) - .header(CONTENT_LENGTH, 0) - .header(TE, TE_TRAILERS); - - if let Some(timeout) = args.request_timeout { - request_builder = request_builder.timeout(timeout); - } - - let http_request = request_builder - .build() - .map_err(|err| { - error!("Error building request: {:?}", err); - WorldLabsClientError::WreqClientError(err) - })?; - - let response = client.execute(http_request) - .await - .map_err(|err| { - error!("Error during request execution: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - let status = response.status(); - - let response_body = response.text() - .await - .map_err(|err| { - error!("Error reading response body: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - // TODO: Handle errors (Cloudflare, Grok, etc.) - if !status.is_success() { - error!("Request returned an error (code {}) : {:?}", status.as_u16(), response_body); - //return Err(classify_general_http_status_code_and_body(status, response_body)); - return Err(WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { status_code: status, body: response_body}.into()) - } - - debug!("Response body (200) (ready for Google upload): {}", response_body); - - let response : RawResponse = serde_json::from_str(&response_body) - .map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?; - - info!("Curl command: {}", response.curl_example); - - Ok(BeginObjectImageUploadResponse { - upload_url: response.upload_url, - }) -} - -#[derive(Deserialize)] -struct RawResponse { - /// eg. "https://storage.googleapis.com/wlt-training-gsc-marble-assets-prod/temp/objects/{...}", - pub upload_url: String, - - /// Just for debugging, the server reports a test cURL command - pub curl_example: String, -} - -#[cfg(test)] -mod tests { - use crate::api::api_types::upload_object_id::UploadObjectId; - use crate::api::requests::objects::begin_object_image_upload::{get_url, BASE_URL}; - - #[test] - fn test_get_url() { - let upload_id = UploadObjectId("foo-bar-baz-bin".to_string()); - let expected = format!("{}/foo-bar-baz-bin:upload", BASE_URL); - assert_eq!(get_url(&upload_id), expected); - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/requests/objects/create_run_object.rs b/crates/api_clients/worldlabs_consumer_client/src/api/requests/objects/create_run_object.rs deleted file mode 100644 index 341cc321..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/requests/objects/create_run_object.rs +++ /dev/null @@ -1,124 +0,0 @@ -use crate::api::api_types::run_object::RunObject; -use crate::api::api_types::run_object_id::RunObjectId; -use crate::api::common::common_header_values::{ORIGIN_VALUE, REFERER_VALUE}; -use crate::credentials::world_labs_bearer_token::WorldLabsBearerToken; -use crate::credentials::world_labs_cookies::WorldLabsCookies; -use crate::error::world_labs_client_error::WorldLabsClientError; -use crate::error::world_labs_error::WorldLabsError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use crate::error::world_labs_specific_api_error::WorldLabsSpecificApiError; -use chrono::Utc; -use http_headers::names::{PRIORITY, SEC_FETCH_DEST, SEC_FETCH_MODE, SEC_FETCH_SITE, SEC_GPC}; -use http_headers::values::accept::ACCEPT_ALL; -use http_headers::values::cache_control::CACHE_CONTROL_NO_CACHE; -use http_headers::values::connection::CONNECTION_KEEP_ALIVE; -use http_headers::values::content_type::CONTENT_TYPE_APPLICATION_JSON; -use http_headers::values::pragma::PRAGMA_NO_CACHE; -use http_headers::values::priority::PRIORITY_HIGHEST; -use http_headers::values::sec::{SEC_FETCH_DEST_EMPTY, SEC_FETCH_MODE_CORS, SEC_FETCH_SITE_CROSS_SITE}; -use http_headers::values::te::TE_TRAILERS; -use log::{debug, error}; -use serde::{Deserialize, Serialize}; -use std::time::Duration; -use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_TYPE, ORIGIN, PRAGMA, REFERER, TE}; -use wreq::Client; -use wreq_util::Emulation; - -const URL : &str = "https://api.worldlabs.ai/api/v1/objects"; - -// Note: WorldLabs is phasing out the old URL scheme: -// const URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects"; - -pub struct CreateRunObjectArgs<'a> { - pub cookies: &'a WorldLabsCookies, - pub bearer_token: &'a WorldLabsBearerToken, - pub request_timeout: Option, -} - -pub struct CreateRunObjectResponse { - pub id: RunObjectId, - pub run_object: RunObject, -} - -/// Marble Image-to-World -/// This request starts the process: it initializes a new world object inference and returns the object id. -/// Request #1 (of ~10) -pub async fn create_run_object(args: CreateRunObjectArgs<'_>) -> Result { - let client = Client::builder() - .emulation(Emulation::Firefox143) - .build() - .map_err(|err| WorldLabsClientError::WreqClientError(err))?; - - debug!("Requesting URL: {}", URL); - - let mut request_builder = client.post(URL) - .header(ACCEPT, ACCEPT_ALL) - .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5") - .header(ACCEPT_ENCODING, "gzip, deflate, br, zstd") - .header(REFERER, REFERER_VALUE) - .header(CONTENT_TYPE, CONTENT_TYPE_APPLICATION_JSON) - .header(AUTHORIZATION, args.bearer_token.to_bearer_token_header_string()) - .header(ORIGIN, ORIGIN_VALUE) - .header(SEC_GPC, "1") - .header(CONNECTION, CONNECTION_KEEP_ALIVE) - .header(SEC_FETCH_DEST, SEC_FETCH_DEST_EMPTY) - .header(SEC_FETCH_MODE, SEC_FETCH_MODE_CORS) - .header(SEC_FETCH_SITE, SEC_FETCH_SITE_CROSS_SITE) - .header(PRIORITY, PRIORITY_HIGHEST) - .header(PRAGMA, PRAGMA_NO_CACHE) - .header(CACHE_CONTROL, CACHE_CONTROL_NO_CACHE) - .header(TE, TE_TRAILERS); - - if let Some(timeout) = args.request_timeout { - request_builder = request_builder.timeout(timeout); - } - - let new_run_object = RunObject::default(); - - let http_request = request_builder.json(&new_run_object) - .build() - .map_err(|err| { - error!("Error building request: {:?}", err); - WorldLabsClientError::WreqClientError(err) - })?; - - let response = client.execute(http_request) - .await - .map_err(|err| { - error!("Error during request execution: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - let status = response.status(); - - let response_body = response.text() - .await - .map_err(|err| { - error!("Error reading response body: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - // TODO: Handle errors (Cloudflare, Grok, etc.) - if !status.is_success() { - error!("Request returned an error (code {}) : {:?}", status.as_u16(), response_body); - //return Err(classify_general_http_status_code_and_body(status, response_body)); - return Err(WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { status_code: status, body: response_body}.into()) - } - - debug!("Response body (200): {}", response_body); - - let response : RunObject = serde_json::from_str(&response_body) - .map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?; - - let run_id = match &response.id { - Some(id) => RunObjectId(id.to_string()), - None => { - return Err(WorldLabsSpecificApiError::NoRunIdInResponse { response_body }.into()) - } - }; - - Ok(CreateRunObjectResponse { - id: run_id, - run_object: response, - }) -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/requests/objects/create_upload_object.rs b/crates/api_clients/worldlabs_consumer_client/src/api/requests/objects/create_upload_object.rs deleted file mode 100644 index ce329a84..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/requests/objects/create_upload_object.rs +++ /dev/null @@ -1,155 +0,0 @@ -use crate::api::api_types::run_object_id::RunObjectId; -use crate::api::api_types::upload_mime_type::UploadMimeType; -use crate::api::api_types::upload_object_id::UploadObjectId; -use crate::api::common::common_header_values::{ORIGIN_VALUE, REFERER_VALUE}; -use crate::credentials::world_labs_bearer_token::WorldLabsBearerToken; -use crate::credentials::world_labs_cookies::WorldLabsCookies; -use crate::error::world_labs_client_error::WorldLabsClientError; -use crate::error::world_labs_error::WorldLabsError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use chrono::Utc; -use http_headers::names::{PRIORITY, SEC_FETCH_DEST, SEC_FETCH_MODE, SEC_FETCH_SITE, SEC_GPC}; -use http_headers::values::accept::ACCEPT_ALL; -use http_headers::values::cache_control::CACHE_CONTROL_NO_CACHE; -use http_headers::values::connection::CONNECTION_KEEP_ALIVE; -use http_headers::values::content_type::CONTENT_TYPE_APPLICATION_JSON; -use http_headers::values::pragma::PRAGMA_NO_CACHE; -use http_headers::values::priority::PRIORITY_4; -use http_headers::values::sec::{SEC_FETCH_DEST_EMPTY, SEC_FETCH_MODE_CORS, SEC_FETCH_SITE_CROSS_SITE}; -use http_headers::values::te::TE_TRAILERS; -use log::{debug, error, info}; -use serde::{Deserialize, Serialize}; -use std::time::Duration; -use uuid::Uuid; -use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_TYPE, ORIGIN, PRAGMA, REFERER, TE}; -use wreq::Client; -use wreq_util::Emulation; - -const URL : &str = "https://api.worldlabs.ai/api/v1/objects"; - -// Note: WorldLabs is phasing out the old URL scheme: -// const URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects"; - -pub struct CreateUploadObjectArgs<'a> { - pub cookies: &'a WorldLabsCookies, - pub bearer_token: &'a WorldLabsBearerToken, - pub upload_mime_type: UploadMimeType, - pub request_timeout: Option, -} - -pub struct CreateUploadObjectResponse { - pub id: UploadObjectId, -} - -/// Marble Image-to-World -/// This request follows object initialization and prepares for image upload. -/// Request #2 (of ~10) -pub async fn create_upload_object(args: CreateUploadObjectArgs<'_>) -> Result { - let client = Client::builder() - .emulation(Emulation::Firefox143) - .build() - .map_err(|err| WorldLabsClientError::WreqClientError(err))?; - - debug!("Requesting URL: {}", URL); - - let mut request_builder = client.post(URL) - .header(ACCEPT, ACCEPT_ALL) - .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5") - .header(ACCEPT_ENCODING, "gzip, deflate, br, zstd") - .header(REFERER, REFERER_VALUE) - .header(CONTENT_TYPE, CONTENT_TYPE_APPLICATION_JSON) - .header(AUTHORIZATION, args.bearer_token.to_bearer_token_header_string()) - .header(ORIGIN, ORIGIN_VALUE) - .header(SEC_GPC, "1") - .header(CONNECTION, CONNECTION_KEEP_ALIVE) - .header(SEC_FETCH_DEST, SEC_FETCH_DEST_EMPTY) - .header(SEC_FETCH_MODE, SEC_FETCH_MODE_CORS) - .header(SEC_FETCH_SITE, SEC_FETCH_SITE_CROSS_SITE) - .header(PRIORITY, PRIORITY_4) - .header(PRAGMA, PRAGMA_NO_CACHE) - .header(CACHE_CONTROL, CACHE_CONTROL_NO_CACHE) - .header(TE, TE_TRAILERS); - - if let Some(timeout) = args.request_timeout { - request_builder = request_builder.timeout(timeout); - } - - let request_payload = RawRequest::for_mime_type(args.upload_mime_type); - - let http_request = request_builder.json(&request_payload) - .build() - .map_err(|err| { - error!("Error building request: {:?}", err); - WorldLabsClientError::WreqClientError(err) - })?; - - let response = client.execute(http_request) - .await - .map_err(|err| { - error!("Error during request execution: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - let status = response.status(); - - let response_body = response.text() - .await - .map_err(|err| { - error!("Error reading response body: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - // TODO: Handle errors (Cloudflare, Grok, etc.) - if !status.is_success() { - error!("Request returned an error (code {}) : {:?}", status.as_u16(), response_body); - //return Err(classify_general_http_status_code_and_body(status, response_body)); - return Err(WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { status_code: status, body: response_body}.into()) - } - - debug!("Response body (200): {}", response_body); - - let response : RawResponse = serde_json::from_str(&response_body) - .map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?; - - Ok(CreateUploadObjectResponse { - id: UploadObjectId(response.id), - }) -} - -#[derive(Serialize)] -struct RawRequest { - pub display_name: String, - pub mime_type: String, -} - -impl RawRequest { - pub fn for_mime_type(mime_type: UploadMimeType) -> Self { - let uuid = Uuid::new_v4().to_string(); - let extension = mime_type.extension_without_period(); - let display_name = format!("uploaded_image_{uuid}.{extension}"); - Self { - display_name, - mime_type: mime_type.content_type().to_string(), - } - } -} - -#[derive(Deserialize)] -struct RawResponse { - pub id: String, -} - -#[cfg(test)] -mod tests { - use crate::api::api_types::upload_mime_type::UploadMimeType; - use crate::api::requests::objects::create_upload_object::RawRequest; - - #[test] - fn test_request() { - let request = RawRequest::for_mime_type(UploadMimeType::ImageJpeg); - // The display name embeds a random uuid, eg. "uploaded_image_{uuid}.jpeg" - assert!(request.display_name.starts_with("uploaded_image_")); - assert!(request.display_name.ends_with(".jpeg")); - assert_eq!(request.mime_type, "image/jpeg"); - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/requests/objects/finalize_object_image_upload.rs b/crates/api_clients/worldlabs_consumer_client/src/api/requests/objects/finalize_object_image_upload.rs deleted file mode 100644 index de10d2d5..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/requests/objects/finalize_object_image_upload.rs +++ /dev/null @@ -1,172 +0,0 @@ -use crate::api::api_types::run_object_id::RunObjectId; -use crate::api::api_types::upload_mime_type::UploadMimeType; -use crate::api::api_types::upload_object_id::UploadObjectId; -use crate::api::common::common_header_values::{ORIGIN_VALUE, REFERER_VALUE}; -use crate::credentials::world_labs_bearer_token::WorldLabsBearerToken; -use crate::credentials::world_labs_cookies::WorldLabsCookies; -use crate::error::world_labs_client_error::WorldLabsClientError; -use crate::error::world_labs_error::WorldLabsError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use chrono::Utc; -use http_headers::names::{PRIORITY, SEC_FETCH_DEST, SEC_FETCH_MODE, SEC_FETCH_SITE, SEC_GPC}; -use http_headers::values::accept::ACCEPT_ALL; -use http_headers::values::cache_control::CACHE_CONTROL_NO_CACHE; -use http_headers::values::connection::CONNECTION_KEEP_ALIVE; -use http_headers::values::content_type::CONTENT_TYPE_APPLICATION_JSON; -use http_headers::values::pragma::PRAGMA_NO_CACHE; -use http_headers::values::priority::PRIORITY_4; -use http_headers::values::sec::{SEC_FETCH_DEST_EMPTY, SEC_FETCH_MODE_CORS, SEC_FETCH_SITE_CROSS_SITE}; -use http_headers::values::te::TE_TRAILERS; -use log::{debug, error}; -use serde::{Deserialize, Serialize}; -use std::time::Duration; -use uuid::Uuid; -use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_LENGTH, CONTENT_TYPE, ORIGIN, PRAGMA, REFERER, TE}; -use wreq::Client; -use wreq_util::Emulation; - -const BASE_URL : &str = "https://api.worldlabs.ai/api/v1/objects"; - -// Note: WorldLabs is phasing out the old URL scheme: -// const BASE_URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects"; - -fn get_url(upload_id: &UploadObjectId) -> String { - format!("{}/{}:complete", BASE_URL, upload_id.0) -} - -pub struct FinalizeObjectImageUploadArgs<'a> { - pub cookies: &'a WorldLabsCookies, - pub bearer_token: &'a WorldLabsBearerToken, - pub upload_id: &'a UploadObjectId, - pub request_timeout: Option, -} - -pub struct FinalizeObjectImageUploadResponse { - pub object_url: String, - pub updated_at: u64, -} - -/// Marble Image-to-World -/// Mark the upload as complete. -/// Request #5 (of ~10) -pub async fn finalize_object_image_upload(args: FinalizeObjectImageUploadArgs<'_>) -> Result { - let client = Client::builder() - .emulation(Emulation::Firefox143) - .build() - .map_err(|err| WorldLabsClientError::WreqClientError(err))?; - - let url = get_url(args.upload_id); - - debug!("Requesting URL: {}", url); - - let mut request_builder = client.post(url) - .header(ACCEPT, ACCEPT_ALL) - .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5") - .header(ACCEPT_ENCODING, "gzip, deflate, br, zstd") - .header(REFERER, REFERER_VALUE) - .header(AUTHORIZATION, args.bearer_token.to_bearer_token_header_string()) - .header(ORIGIN, ORIGIN_VALUE) - .header(SEC_GPC, "1") - .header(CONNECTION, CONNECTION_KEEP_ALIVE) - .header(SEC_FETCH_DEST, SEC_FETCH_DEST_EMPTY) - .header(SEC_FETCH_MODE, SEC_FETCH_MODE_CORS) - .header(SEC_FETCH_SITE, SEC_FETCH_SITE_CROSS_SITE) - .header(PRIORITY, PRIORITY_4) - .header(PRAGMA, PRAGMA_NO_CACHE) - .header(CACHE_CONTROL, CACHE_CONTROL_NO_CACHE) - .header(CONTENT_LENGTH, 0) - .header(TE, TE_TRAILERS); - - if let Some(timeout) = args.request_timeout { - request_builder = request_builder.timeout(timeout); - } - - let http_request = request_builder - .build() - .map_err(|err| { - error!("Error building request: {:?}", err); - WorldLabsClientError::WreqClientError(err) - })?; - - let response = client.execute(http_request) - .await - .map_err(|err| { - error!("Error during request execution: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - let status = response.status(); - - let response_body = response.text() - .await - .map_err(|err| { - error!("Error reading response body: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - // TODO: Handle errors (Cloudflare, Grok, etc.) - if !status.is_success() { - error!("Request returned an error (code {}) : {:?}", status.as_u16(), response_body); - //return Err(classify_general_http_status_code_and_body(status, response_body)); - return Err(WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { status_code: status, body: response_body}.into()) - } - - debug!("Response body (200): {}", response_body); - - let response : RawResponse = serde_json::from_str(&response_body) - .map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?; - - Ok(FinalizeObjectImageUploadResponse { - object_url: response.object_uri, - updated_at: response.updated_at, - }) -} - -#[derive(Deserialize)] -struct RawResponse { - /// eg. "https://cdn.marble.worldlabs.ai/object/foo-bar-baz-bin/asset.jpg" - pub object_uri: String, - - /// Seconds-resolution timestamp. - /// eg. `1765745771` - pub updated_at: u64, -} - -#[cfg(test)] -mod tests { - use crate::api::api_types::upload_object_id::UploadObjectId; - use crate::api::requests::objects::finalize_object_image_upload::{finalize_object_image_upload, get_url, FinalizeObjectImageUploadArgs, BASE_URL}; - use crate::test_utils::get_test_bearer_token::get_test_bearer_token; - use crate::test_utils::get_test_cookies::get_typed_test_cookies; - use crate::test_utils::setup_test_logging::setup_test_logging; - use log::LevelFilter; - - #[test] - fn test_get_url() { - let upload_id = UploadObjectId("foo-bar-baz-bin".to_string()); - let expected = format!("{}/foo-bar-baz-bin:complete", BASE_URL); - assert_eq!(get_url(&upload_id), expected); - } - - #[tokio::test] - #[ignore] // Client side tests only - async fn test_requests() { - setup_test_logging(LevelFilter::Debug); - - let cookies = get_typed_test_cookies().unwrap(); - let bearer_token = get_test_bearer_token().unwrap(); - - let upload_id = UploadObjectId("09d86ef8-edcd-42d9-9b95-8efd28be0bee".to_string()); - - let response = finalize_object_image_upload(FinalizeObjectImageUploadArgs { - cookies: &cookies, - bearer_token: &bearer_token, - upload_id: &upload_id, - request_timeout: None, - }).await.unwrap(); - - println!("Object URL: {}", response.object_url); - - assert_eq!(1, 2); - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/requests/objects/mod.rs b/crates/api_clients/worldlabs_consumer_client/src/api/requests/objects/mod.rs deleted file mode 100644 index 3c27307f..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/requests/objects/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod create_upload_object; -pub mod begin_object_image_upload; -pub mod create_run_object; -pub mod finalize_object_image_upload; -pub mod update_run_object_with_upload; -pub mod update_run_object_with_world; \ No newline at end of file diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/requests/objects/update_run_object_with_upload.rs b/crates/api_clients/worldlabs_consumer_client/src/api/requests/objects/update_run_object_with_upload.rs deleted file mode 100644 index 4414b7c4..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/requests/objects/update_run_object_with_upload.rs +++ /dev/null @@ -1,523 +0,0 @@ -use crate::api::api_types::image_input_object_id::ImageInputObjectId; -use crate::api::api_types::meta_world_object_id::MetaWorldObjectId; -use crate::api::api_types::pano_object_id::PanoObjectId; -use crate::api::api_types::run_object_id::RunObjectId; -use crate::api::api_types::upload_mime_type::UploadMimeType; -use crate::api::api_types::upload_object_id::UploadObjectId; -use crate::api::common::common_header_values::{ORIGIN_VALUE, REFERER_VALUE}; -use crate::api::utils::upload_id_to_image_url::upload_id_to_image_url; -use crate::credentials::world_labs_bearer_token::WorldLabsBearerToken; -use crate::credentials::world_labs_cookies::WorldLabsCookies; -use crate::error::world_labs_client_error::WorldLabsClientError; -use crate::error::world_labs_error::WorldLabsError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use chrono::Utc; -use http_headers::names::{PRIORITY, SEC_FETCH_DEST, SEC_FETCH_MODE, SEC_FETCH_SITE, SEC_GPC}; -use http_headers::values::accept::ACCEPT_ALL; -use http_headers::values::cache_control::CACHE_CONTROL_NO_CACHE; -use http_headers::values::connection::CONNECTION_KEEP_ALIVE; -use http_headers::values::content_type::CONTENT_TYPE_APPLICATION_JSON; -use http_headers::values::pragma::PRAGMA_NO_CACHE; -use http_headers::values::priority::PRIORITY_4; -use http_headers::values::sec::{SEC_FETCH_DEST_EMPTY, SEC_FETCH_MODE_CORS, SEC_FETCH_SITE_CROSS_SITE}; -use http_headers::values::te::TE_TRAILERS; -use indexmap::IndexMap; -use log::{debug, error}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::collections::HashMap; -use std::time::Duration; -use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_TYPE, ORIGIN, PRAGMA, REFERER, TE}; -use wreq::Client; -use wreq_util::Emulation; - -const BASE_URL : &str = "https://api.worldlabs.ai/api/v1/objects"; - -// Note: WorldLabs is phasing out the old URL scheme: -// const BASE_URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects"; - -fn get_url(run_id: &RunObjectId) -> String { - format!("{}/{}", BASE_URL, run_id.0) -} - -pub struct UpdateRunObjectWithUploadArgs<'a> { - pub cookies: &'a WorldLabsCookies, - pub bearer_token: &'a WorldLabsBearerToken, - - pub payload_args: UpdateRunObjectWithUploadPayloadArgs<'a>, - - pub request_timeout: Option, -} - -pub struct UpdateRunObjectWithUploadPayloadArgs<'a> { - pub run_id: &'a RunObjectId, - pub run_created_at_timestamp: u64, - - // eg. https://cdn.marble.worldlabs.ai/object/13ca760c-8ef9-4bf5-acc3-5a507fad3abf/asset.jpg - pub image_upload_url: &'a str, - - pub image_input_id: &'a ImageInputObjectId, - pub pano_id: &'a PanoObjectId, - pub meta_world_id: &'a MetaWorldObjectId, -} - -pub struct UpdateRunObjectWithUploadResponse { - pub image_input_id: ImageInputObjectId, - pub pano_id: PanoObjectId, - pub meta_world_id: MetaWorldObjectId, - pub run_updated_timestamp: u64, -} - -/// Marble Image-to-World -/// This request patches an object. Seems to update it with the attached files. -/// Request #6 (of ~10) -pub async fn update_run_object_with_upload(args: UpdateRunObjectWithUploadArgs<'_>) -> Result { - let client = Client::builder() - .emulation(Emulation::Firefox143) - .build() - .map_err(|err| WorldLabsClientError::WreqClientError(err))?; - - let url = get_url(args.payload_args.run_id); - - debug!("Requesting URL: {}", url); - - let mut request_builder = client.patch(url) - .header(ACCEPT, ACCEPT_ALL) - .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5") - .header(ACCEPT_ENCODING, "gzip, deflate, br, zstd") - .header(REFERER, REFERER_VALUE) - .header(CONTENT_TYPE, CONTENT_TYPE_APPLICATION_JSON) - .header(AUTHORIZATION, args.bearer_token.to_bearer_token_header_string()) - .header(ORIGIN, ORIGIN_VALUE) - .header(SEC_GPC, "1") - .header(CONNECTION, CONNECTION_KEEP_ALIVE) - .header(SEC_FETCH_DEST, SEC_FETCH_DEST_EMPTY) - .header(SEC_FETCH_MODE, SEC_FETCH_MODE_CORS) - .header(SEC_FETCH_SITE, SEC_FETCH_SITE_CROSS_SITE) - .header(PRIORITY, PRIORITY_4) - .header(PRAGMA, PRAGMA_NO_CACHE) - .header(CACHE_CONTROL, CACHE_CONTROL_NO_CACHE) - .header(TE, TE_TRAILERS); - - if let Some(timeout) = args.request_timeout { - request_builder = request_builder.timeout(timeout); - } - - let payload = RawRequest::from_args(&args.payload_args); - let updated_at = payload.object.metadata.updated_at; - - let http_request = request_builder.json(&payload) - .build() - .map_err(|err| { - error!("Error building request: {:?}", err); - WorldLabsClientError::WreqClientError(err) - })?; - - let response = client.execute(http_request) - .await - .map_err(|err| { - error!("Error during request execution: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - let status = response.status(); - - let response_body = response.text() - .await - .map_err(|err| { - error!("Error reading response body: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - // TODO: Handle errors (Cloudflare, Grok, etc.) - if !status.is_success() { - error!("Request returned an error (code {}) : {:?}", status.as_u16(), response_body); - //return Err(classify_general_http_status_code_and_body(status, response_body)); - return Err(WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { status_code: status, body: response_body}.into()) - } - - debug!("Response body (200): {}", response_body); - - let _response : RawResponse = serde_json::from_str(&response_body) - .map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?; - - Ok(UpdateRunObjectWithUploadResponse { - image_input_id: args.payload_args.image_input_id.clone(), - pano_id: args.payload_args.pano_id.clone(), - meta_world_id: args.payload_args.meta_world_id.clone(), - run_updated_timestamp: updated_at, - }) -} - -#[derive(Serialize)] -struct RawRequest { - pub object: ObjectDef, - pub update_mask: Vec, -} - -impl Default for RawRequest { - fn default() -> Self { - let now = Utc::now(); - let now = now.timestamp_millis().unsigned_abs(); - Self { - object: ObjectDef { - metadata: ObjectMetadata { - version: "0.0.1".to_string(), - created_at: now, - updated_at: now, - uses_advanced_editing: false, - draft_mode: false, - nodes: IndexMap::new(), - } - }, - update_mask: vec!["metadata".to_string()] - } - } -} - -impl RawRequest { - pub fn from_args(args: &UpdateRunObjectWithUploadPayloadArgs) -> Self { - let mut request = RawRequest::default(); - request.add_image_input_node({ - let mut node = ImageInputNode::default(); - node.id = args.image_input_id.to_string(); - node.source.image_url = args.image_upload_url.to_string(); - node - }); - request.add_pano_node({ - let mut node = PanoNode::default(); - node.id = args.pano_id.to_string(); - node.parent_id= Some(args.image_input_id.to_string()); - node - }); - request.add_world_node({ - let mut node = WorldNode::default(); - node.id = args.meta_world_id.to_string(); - node.parent_id= Some(args.pano_id.to_string()); - node - }); - request.set_updated_now(); - request - } - - pub fn add_image_input_node(&mut self, mut node: ImageInputNode) { - node.created_at = self.object.metadata.updated_at; - self.object.metadata.nodes.insert(node.id.clone(), NodeValue::ImageInput(node)); - } - - pub fn add_pano_node(&mut self, mut node: PanoNode) { - node.created_at = self.object.metadata.updated_at; - self.object.metadata.nodes.insert(node.id.clone(), NodeValue::Pano(node)); - } - - pub fn add_world_node(&mut self, mut node: WorldNode) { - node.created_at = self.object.metadata.updated_at; - self.object.metadata.nodes.insert(node.id.clone(), NodeValue::World(node)); - } - - pub fn set_updated_now(&mut self) { - let now = Utc::now(); - let now = now.timestamp_millis().unsigned_abs(); - self.set_updated_at(now); - } - - pub fn set_updated_at(&mut self, updated_at: u64) { - self.object.metadata.updated_at = updated_at; - for (_key, node) in self.object.metadata.nodes.iter_mut() { - match node { - NodeValue::ImageInput(value) => value.created_at = updated_at, - NodeValue::Pano(value) => value.created_at = updated_at, - NodeValue::World(value) => value.created_at = updated_at, - } - } - } -} - -#[derive(Serialize)] -struct ObjectDef { - pub metadata: ObjectMetadata, -} - -#[derive(Serialize)] -struct ObjectMetadata { - pub version: String, - - #[serde(rename = "createdAt")] - pub created_at: u64, - - #[serde(rename = "updatedAt")] - pub updated_at: u64, - - #[serde(rename = "usesAdvancedEditing")] - pub uses_advanced_editing: bool, - - #[serde(rename = "draftMode")] - pub draft_mode: bool, - - /// Polymorphic set of ID-to-object mappings. - /// NB: IndexMap maintains insertion order. - pub nodes: IndexMap, -} - -#[derive(Serialize)] -#[serde(untagged)] -//#[serde(tag = "type", content = "value")] -enum NodeValue { - ImageInput(ImageInputNode), - Pano(PanoNode), - World(WorldNode), -} - -#[derive(Serialize, Default)] -struct ImageInputNode { - pub id: String, - pub r#type: TypeInput, - #[serde(rename = "parentId")] - pub parent_id: Option, - pub source: ImageInputNodeSource, - #[serde(rename = "createdAt")] - pub created_at: u64, -} - -#[derive(Serialize, Default)] -struct ImageInputNodeSource { - pub r#type: TypeImage, - pub image_url: String, -} - -#[derive(Serialize, Default)] -enum TypeInput { - #[serde(rename = "input")] - #[default] - Input -} - -#[derive(Serialize, Default)] -enum TypeImage { - #[serde(rename = "image")] - #[default] - Image -} - -#[derive(Serialize, Default)] -enum StatusPending { - #[serde(rename = "pending")] - #[default] - Pending -} - -impl ImageInputNode { - pub fn with_id(id: &str) -> Self{ - Self { - id: id.to_string(), - ..Default::default() - } - } -} - -#[derive(Serialize, Default)] -struct PanoNode { - pub id: String, - pub r#type: TypePano, - #[serde(rename = "parentId")] - pub parent_id: Option, - pub source: PanoNodeSource, - #[serde(rename = "createdAt")] - pub created_at: u64, -} - -#[derive(Serialize, Default)] -struct PanoNodeSource { - pub r#type: TypeGenerateWorld, - pub world_id: StatusPending, - pub status: StatusPending, -} - -#[derive(Serialize, Default)] -enum TypePano { - #[serde(rename = "pano")] - #[default] - Pano -} - -#[derive(Serialize, Default)] -enum TypeGenerateWorld { - #[serde(rename = "generate_world")] - #[default] - GenerateWorld -} - -impl PanoNode { - pub fn with_id(id: &str) -> Self{ - Self { - id: id.to_string(), - ..Default::default() - } - } -} - -#[derive(Serialize, Default)] -struct WorldNode { - pub id: String, - pub r#type: TypeWorld, - #[serde(rename = "parentId")] - pub parent_id: Option, - pub source: WorldNodeSource, - #[serde(rename = "createdAt")] - pub created_at: u64, -} - -#[derive(Serialize, Default)] -struct WorldNodeSource { - pub r#type: TypeGenerateWorld, - pub world_id: StatusPending, - pub posed_cubemaps_url: String, - pub minimap_url: String, - pub minimap_metadata: String, - pub status: StatusPending, -} - -#[derive(Serialize, Default)] -enum TypeWorld { - #[serde(rename = "world")] - #[default] - World -} - -impl WorldNode { - pub fn with_id(id: &str) -> Self{ - Self { - id: id.to_string(), - ..Default::default() - } - } -} - -#[derive(Deserialize)] -struct RawResponse { - pub id: String, -} - -#[cfg(test)] -mod tests { - use crate::api::api_types::image_input_object_id::ImageInputObjectId; - use crate::api::api_types::meta_world_object_id::MetaWorldObjectId; - use crate::api::api_types::pano_object_id::PanoObjectId; - use crate::api::api_types::run_object_id::RunObjectId; - use crate::api::requests::objects::update_run_object_with_upload::{RawRequest, UpdateRunObjectWithUploadPayloadArgs}; - use serde_json::{json, Value}; - - /// Any timestamp from `Utc::now()` should exceed this (2023-11-14 in epoch millis). - const MIN_PLAUSIBLE_EPOCH_MILLIS: u64 = 1_700_000_000_000; - - #[test] - fn request_default() { - let request = RawRequest::default(); - assert_eq!(request.object.metadata.version, "0.0.1"); - assert!(request.object.metadata.created_at > 10000); - assert!(request.object.metadata.updated_at > 10000); - } - - #[test] - fn json() { - let run_id = RunObjectId::from_str("79795b32-e44d-4333-bac1-05b2c9a3ea12"); - let image_input_id = ImageInputObjectId::from_str("bc17252b-811e-49d7-be3e-b7c538df9d30"); - let pano_id = PanoObjectId::from_str("82f6b488-8afe-4311-9022-80ad5789ad92"); - let world_id = MetaWorldObjectId::from_str("f08ec501-601c-47c6-a104-402d1820b8f0"); - - let args = UpdateRunObjectWithUploadPayloadArgs { - run_created_at_timestamp: 1234, - image_upload_url: "https://todo.com", - run_id: &run_id, - image_input_id: &image_input_id, - pano_id: &pano_id, - meta_world_id: &world_id, - }; - - let request = RawRequest::from_args(&args); - - // The `createdAt` / `updatedAt` timestamps come from `Utc::now()` at build time, - // so they're nondeterministic. Sanity-check them, then zero them out before - // comparing the rest of the document. - assert!(request.object.metadata.created_at > MIN_PLAUSIBLE_EPOCH_MILLIS); - assert!(request.object.metadata.updated_at >= request.object.metadata.created_at); - - let mut actual = serde_json::to_value(&request).unwrap(); - zero_out_timestamps(&mut actual); - - let expected = json!({ - "object": { - "metadata": { - "version": "0.0.1", - "createdAt": 0, - "updatedAt": 0, - "usesAdvancedEditing": false, - "draftMode": false, - "nodes": { - "bc17252b-811e-49d7-be3e-b7c538df9d30": { - "id": "bc17252b-811e-49d7-be3e-b7c538df9d30", - "type": "input", - "parentId": null, - "source": { - "type": "image", - "image_url": "https://todo.com" - }, - "createdAt": 0 - }, - "82f6b488-8afe-4311-9022-80ad5789ad92": { - "id": "82f6b488-8afe-4311-9022-80ad5789ad92", - "type": "pano", - "parentId": "bc17252b-811e-49d7-be3e-b7c538df9d30", - "source": { - "type": "generate_world", - "world_id": "pending", - "status": "pending" - }, - "createdAt": 0 - }, - "f08ec501-601c-47c6-a104-402d1820b8f0": { - "id": "f08ec501-601c-47c6-a104-402d1820b8f0", - "type": "world", - "parentId": "82f6b488-8afe-4311-9022-80ad5789ad92", - "source": { - "type": "generate_world", - "world_id": "pending", - "posed_cubemaps_url": "", - "minimap_url": "", - "minimap_metadata": "", - "status": "pending" - }, - "createdAt": 0 - } - } - } - }, - "update_mask": [ - "metadata" - ] - }); - - assert_eq!(actual, expected); - } - - /// Recursively replaces nondeterministic `createdAt` / `updatedAt` values with 0. - fn zero_out_timestamps(value: &mut Value) { - match value { - Value::Object(map) => { - for (key, child) in map.iter_mut() { - if key == "createdAt" || key == "updatedAt" { - *child = json!(0); - } else { - zero_out_timestamps(child); - } - } - }, - Value::Array(items) => { - for item in items.iter_mut() { - zero_out_timestamps(item); - } - }, - _ => {}, - } - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/requests/objects/update_run_object_with_world.rs b/crates/api_clients/worldlabs_consumer_client/src/api/requests/objects/update_run_object_with_world.rs deleted file mode 100644 index f04f49ff..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/requests/objects/update_run_object_with_world.rs +++ /dev/null @@ -1,540 +0,0 @@ -use crate::api::api_types::image_input_object_id::ImageInputObjectId; -use crate::api::api_types::pano_object_id::PanoObjectId; -use crate::api::api_types::run_object_id::RunObjectId; -use crate::api::api_types::upload_mime_type::UploadMimeType; -use crate::api::api_types::upload_object_id::UploadObjectId; -use crate::api::api_types::meta_world_object_id::MetaWorldObjectId; -use crate::api::common::common_header_values::{ORIGIN_VALUE, REFERER_VALUE}; -use crate::api::utils::upload_id_to_image_url::upload_id_to_image_url; -use crate::credentials::world_labs_bearer_token::WorldLabsBearerToken; -use crate::credentials::world_labs_cookies::WorldLabsCookies; -use crate::error::world_labs_client_error::WorldLabsClientError; -use crate::error::world_labs_error::WorldLabsError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use chrono::Utc; -use http_headers::names::{PRIORITY, SEC_FETCH_DEST, SEC_FETCH_MODE, SEC_FETCH_SITE, SEC_GPC}; -use http_headers::values::accept::ACCEPT_ALL; -use http_headers::values::cache_control::CACHE_CONTROL_NO_CACHE; -use http_headers::values::connection::CONNECTION_KEEP_ALIVE; -use http_headers::values::content_type::CONTENT_TYPE_APPLICATION_JSON; -use http_headers::values::pragma::PRAGMA_NO_CACHE; -use http_headers::values::priority::PRIORITY_4; -use http_headers::values::sec::{SEC_FETCH_DEST_EMPTY, SEC_FETCH_MODE_CORS, SEC_FETCH_SITE_CROSS_SITE}; -use http_headers::values::te::TE_TRAILERS; -use indexmap::IndexMap; -use log::{debug, error}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::collections::HashMap; -use std::time::Duration; -use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_TYPE, ORIGIN, PRAGMA, REFERER, TE}; -use wreq::Client; -use wreq_util::Emulation; -use crate::api::api_types::world_id::WorldObjectId; -const BASE_URL : &str = "https://api.worldlabs.ai/api/v1/objects"; - -// Note: WorldLabs is phasing out the old URL scheme: -// const BASE_URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects"; - -fn get_url(run_id: &RunObjectId) -> String { - format!("{}/{}", BASE_URL, run_id.0) -} - -pub struct UpdateRunObjectWithWorldArgs<'a> { - pub cookies: &'a WorldLabsCookies, - pub bearer_token: &'a WorldLabsBearerToken, - - pub payload_args: UpdateRunObjectWithWorldPayloadArgs<'a>, - - pub request_timeout: Option, -} - -pub struct UpdateRunObjectWithWorldPayloadArgs<'a> { - pub run_id: &'a RunObjectId, - pub run_created_at_timestamp: u64, - pub first_patch_timestamp: u64, - - // eg. https://cdn.marble.worldlabs.ai/object/13ca760c-8ef9-4bf5-acc3-5a507fad3abf/asset.jpg - pub image_upload_url: &'a str, - - pub image_input_id: &'a ImageInputObjectId, - pub pano_id: &'a PanoObjectId, - pub meta_world_id: &'a MetaWorldObjectId, - pub world_id: &'a WorldObjectId, - - pub title: &'a str, - pub text_prompt: &'a str, -} - -pub struct UpdateRunObjectWithWorldResponse { - //pub image_input_id: ImageInputObjectId, - //pub pano_id: PanoObjectId, - //pub meta_world_id: MetaWorldObjectId, - //pub world_id: WorldObjectId, -} - -/// Marble Image-to-World -/// This request patches the run object a second time. It updates -/// more fields - prompt, world_id, etc. -/// Request #9 (of ~10) -pub async fn update_run_object_with_world(args: UpdateRunObjectWithWorldArgs<'_>) -> Result { - let client = Client::builder() - .emulation(Emulation::Firefox143) - .build() - .map_err(|err| WorldLabsClientError::WreqClientError(err))?; - - let url = get_url(args.payload_args.run_id); - - debug!("Requesting URL: {}", url); - - let mut request_builder = client.patch(url) - .header(ACCEPT, ACCEPT_ALL) - .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5") - .header(ACCEPT_ENCODING, "gzip, deflate, br, zstd") - .header(REFERER, REFERER_VALUE) - .header(CONTENT_TYPE, CONTENT_TYPE_APPLICATION_JSON) - .header(AUTHORIZATION, args.bearer_token.to_bearer_token_header_string()) - .header(ORIGIN, ORIGIN_VALUE) - .header(SEC_GPC, "1") - .header(CONNECTION, CONNECTION_KEEP_ALIVE) - .header(SEC_FETCH_DEST, SEC_FETCH_DEST_EMPTY) - .header(SEC_FETCH_MODE, SEC_FETCH_MODE_CORS) - .header(SEC_FETCH_SITE, SEC_FETCH_SITE_CROSS_SITE) - .header(PRIORITY, PRIORITY_4) - .header(PRAGMA, PRAGMA_NO_CACHE) - .header(CACHE_CONTROL, CACHE_CONTROL_NO_CACHE) - .header(TE, TE_TRAILERS); - - if let Some(timeout) = args.request_timeout { - request_builder = request_builder.timeout(timeout); - } - - let payload = RawRequest::from_args(&args.payload_args); - - let http_request = request_builder.json(&payload) - .build() - .map_err(|err| { - error!("Error building request: {:?}", err); - WorldLabsClientError::WreqClientError(err) - })?; - - let response = client.execute(http_request) - .await - .map_err(|err| { - error!("Error during request execution: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - let status = response.status(); - - let response_body = response.text() - .await - .map_err(|err| { - error!("Error reading response body: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - // TODO: Handle errors (Cloudflare, Grok, etc.) - if !status.is_success() { - error!("Request returned an error (code {}) : {:?}", status.as_u16(), response_body); - //return Err(classify_general_http_status_code_and_body(status, response_body)); - return Err(WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { status_code: status, body: response_body}.into()) - } - - debug!("Response body (200): {}", response_body); - - let _response : RawResponse = serde_json::from_str(&response_body) - .map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?; - - Ok(UpdateRunObjectWithWorldResponse { - //image_input_id: args.payload_args.image_input_id.clone(), - //pano_id: args.payload_args.pano_id.clone(), - //meta_world_id: args.payload_args.meta_world_id.clone(), - }) -} - -#[derive(Serialize)] -struct RawRequest { - pub object: ObjectDef, - pub update_mask: Vec, -} - -impl Default for RawRequest { - fn default() -> Self { - let now = Utc::now(); - let now = now.timestamp_millis().unsigned_abs(); - Self { - object: ObjectDef { - metadata: ObjectMetadata { - version: "0.0.1".to_string(), - name: None, - created_at: now, - updated_at: now, - uses_advanced_editing: false, - draft_mode: false, - nodes: IndexMap::new(), - } - }, - update_mask: vec!["metadata".to_string()] - } - } -} - -impl RawRequest { - pub fn from_args(args: &UpdateRunObjectWithWorldPayloadArgs) -> Self { - let mut request = RawRequest::default(); - request.add_image_input_node({ - let mut node = ImageInputNode::default(); - node.id = args.image_input_id.to_string(); - node.source.image_url = args.image_upload_url.to_string(); - node.title = args.title.to_string(); - node.caption = args.text_prompt.to_string(); - node.created_at = args.first_patch_timestamp; - node - }); - request.add_pano_node({ - let mut node = PanoNode::default(); - node.id = args.pano_id.to_string(); - node.parent_id = Some(args.image_input_id.to_string()); - node.source.world_id = Some(args.world_id.to_string()); - node.created_at = args.first_patch_timestamp; - node - }); - request.add_world_node({ - let mut node = WorldNode::default(); - node.id = args.meta_world_id.to_string(); - node.parent_id= Some(args.pano_id.to_string()); - node.source.world_id = Some(args.world_id.to_string()); - node.created_at = args.first_patch_timestamp; - node - }); - let now = Utc::now(); - let now = now.timestamp_millis().unsigned_abs(); - request.object.metadata.updated_at = now; - request.object.metadata.name = Some(args.title.to_string()); - request - } - - pub fn add_image_input_node(&mut self, mut node: ImageInputNode) { - node.created_at = self.object.metadata.updated_at; - self.object.metadata.nodes.insert(node.id.clone(), NodeValue::ImageInput(node)); - } - - pub fn add_pano_node(&mut self, mut node: PanoNode) { - node.created_at = self.object.metadata.updated_at; - self.object.metadata.nodes.insert(node.id.clone(), NodeValue::Pano(node)); - } - - pub fn add_world_node(&mut self, mut node: WorldNode) { - node.created_at = self.object.metadata.updated_at; - self.object.metadata.nodes.insert(node.id.clone(), NodeValue::World(node)); - } -} - -#[derive(Serialize)] -struct ObjectDef { - pub metadata: ObjectMetadata, -} - -#[derive(Serialize)] -struct ObjectMetadata { - pub version: String, - - // NB: Not null in request, just for builder - pub name: Option, - - #[serde(rename = "createdAt")] - pub created_at: u64, - - #[serde(rename = "updatedAt")] - pub updated_at: u64, - - #[serde(rename = "usesAdvancedEditing")] - pub uses_advanced_editing: bool, - - #[serde(rename = "draftMode")] - pub draft_mode: bool, - - /// Polymorphic set of ID-to-object mappings. - /// NB: IndexMap maintains insertion order. - pub nodes: IndexMap, -} - -#[derive(Serialize)] -#[serde(untagged)] -//#[serde(tag = "type", content = "value")] -enum NodeValue { - ImageInput(ImageInputNode), - Pano(PanoNode), - World(WorldNode), -} - -#[derive(Serialize, Default)] -struct ImageInputNode { - pub id: String, - pub r#type: TypeInput, - #[serde(rename = "parentId")] - pub parent_id: Option, - pub source: ImageInputNodeSource, - #[serde(rename = "createdAt")] - pub created_at: u64, - pub title: String, - pub caption: String, -} - -#[derive(Serialize, Default)] -struct ImageInputNodeSource { - pub r#type: TypeImage, - pub image_url: String, -} - -#[derive(Serialize, Default)] -enum TypeInput { - #[serde(rename = "input")] - #[default] - Input -} - -#[derive(Serialize, Default)] -enum TypeImage { - #[serde(rename = "image")] - #[default] - Image -} - -#[derive(Serialize, Default)] -enum StatusPending { - #[serde(rename = "pending")] - #[default] - Pending -} - -impl ImageInputNode { - pub fn with_id(id: &str) -> Self{ - Self { - id: id.to_string(), - ..Default::default() - } - } -} - -#[derive(Serialize, Default)] -struct PanoNode { - pub id: String, - pub r#type: TypePano, - #[serde(rename = "parentId")] - pub parent_id: Option, - pub source: PanoNodeSource, - #[serde(rename = "createdAt")] - pub created_at: u64, -} - -#[derive(Serialize, Default)] -struct PanoNodeSource { - pub r#type: TypeGenerateWorld, - // NB: Not null in request, just for builder - pub world_id: Option, - pub status: StatusPending, -} - -#[derive(Serialize, Default)] -enum TypePano { - #[serde(rename = "pano")] - #[default] - Pano -} - -#[derive(Serialize, Default)] -enum TypeGenerateWorld { - #[serde(rename = "generate_world")] - #[default] - GenerateWorld -} - -impl PanoNode { - pub fn with_id(id: &str) -> Self{ - Self { - id: id.to_string(), - ..Default::default() - } - } -} - -#[derive(Serialize, Default)] -struct WorldNode { - pub id: String, - pub r#type: TypeWorld, - #[serde(rename = "parentId")] - pub parent_id: Option, - pub source: WorldNodeSource, - #[serde(rename = "createdAt")] - pub created_at: u64, -} - -#[derive(Serialize, Default)] -struct WorldNodeSource { - pub r#type: TypeGenerateWorld, - // NB: Not null in request, just for - pub world_id: Option, - pub posed_cubemaps_url: String, - pub minimap_url: String, - pub minimap_metadata: String, - pub status: StatusPending, -} - -#[derive(Serialize, Default)] -enum TypeWorld { - #[serde(rename = "world")] - #[default] - World -} - -impl WorldNode { - pub fn with_id(id: &str) -> Self{ - Self { - id: id.to_string(), - ..Default::default() - } - } -} - -#[derive(Deserialize)] -struct RawResponse { - pub id: String, -} - -#[cfg(test)] -mod tests { - use crate::api::api_types::image_input_object_id::ImageInputObjectId; - use crate::api::api_types::pano_object_id::PanoObjectId; - use crate::api::api_types::run_object_id::RunObjectId; - use crate::api::api_types::meta_world_object_id::MetaWorldObjectId; - use crate::api::api_types::world_id::WorldObjectId; - use crate::api::requests::objects::update_run_object_with_world::{RawRequest, UpdateRunObjectWithWorldPayloadArgs}; - use serde_json::{json, Value}; - - /// Any timestamp from `Utc::now()` should exceed this (2023-11-14 in epoch millis). - const MIN_PLAUSIBLE_EPOCH_MILLIS: u64 = 1_700_000_000_000; - - #[test] - fn request_default() { - let request = RawRequest::default(); - assert_eq!(request.object.metadata.version, "0.0.1"); - assert!(request.object.metadata.created_at > 10000); - assert!(request.object.metadata.updated_at > 10000); - } - - #[test] - fn json() { - let run_id = RunObjectId::from_str("79795b32-e44d-4333-bac1-05b2c9a3ea12"); - let image_input_id = ImageInputObjectId::from_str("bc17252b-811e-49d7-be3e-b7c538df9d30"); - let pano_id = PanoObjectId::from_str("82f6b488-8afe-4311-9022-80ad5789ad92"); - let meta_world_id = MetaWorldObjectId::from_str("f08ec501-601c-47c6-a104-402d1820b8f0"); - let world_id = WorldObjectId::from_str("d60cfa21-5506-43de-9a9c-0707fc17a5ec"); - - let title = "Cows Grazing in Indoor Pasture"; - let caption = "The scene is a surreal indoor agricultural environment, depicted with a realistic and slightly eerie tone, as rows of bright lights illuminate a vast, artificial pasture. The overall mood is quiet and contemplative, emphasizing the unusual juxtaposition of natural elements within a manufactured setting. Large herds of cattle are distributed across the lush, green grassy floor, peacefully grazing on the abundant vegetation. The entire space is encased within a massive, warehouse-like structure, supported by countless concrete columns that extend from the floor to the ceiling in an orderly grid. Above the columns, a network of metal beams forms the infrastructure of the roof, from which an extensive array of industrial lights is suspended. These lights are arranged in long, parallel lines, creating a striking visual perspective that converges towards a distant vanishing point at the center of the structure. The dense, vibrant green grass covers every inch of the ground, appearing soft and uniform, without any discernible paths or bare patches. Each cow is distinct, varying in color from black to brown and white, and each is engaged in individual grazing or resting. The concrete columns maintain a consistent light gray color and texture, creating a rhythmic architectural presence throughout the vast indoor landscape. To the right, another cluster of columns continues the architectural pattern, while the grassy expanse remains uninterrupted, extending toward the edges of the structure. "; - - let args = UpdateRunObjectWithWorldPayloadArgs { - run_created_at_timestamp: 1765756772176, - first_patch_timestamp: 1765756773647, - image_upload_url: "https://cdn.marble.worldlabs.ai/object/13ca760c-8ef9-4bf5-acc3-5a507fad3abf/asset.jpg", - run_id: &run_id, - image_input_id: &image_input_id, - pano_id: &pano_id, - meta_world_id: &meta_world_id, - world_id: &world_id, - title, - text_prompt: caption, - }; - - let request = RawRequest::from_args(&args); - - // The `createdAt` / `updatedAt` timestamps come from `Utc::now()` at build time, - // so they're nondeterministic. Sanity-check them, then zero them out before - // comparing the rest of the document. - assert!(request.object.metadata.created_at > MIN_PLAUSIBLE_EPOCH_MILLIS); - assert!(request.object.metadata.updated_at >= request.object.metadata.created_at); - - let mut actual = serde_json::to_value(&request).unwrap(); - zero_out_timestamps(&mut actual); - - let expected = json!({ - "object": { - "metadata": { - "version": "0.0.1", - "name": title, - "createdAt": 0, - "updatedAt": 0, - "usesAdvancedEditing": false, - "draftMode": false, - "nodes": { - "bc17252b-811e-49d7-be3e-b7c538df9d30": { - "id": "bc17252b-811e-49d7-be3e-b7c538df9d30", - "type": "input", - "parentId": null, - "source": { - "type": "image", - "image_url": "https://cdn.marble.worldlabs.ai/object/13ca760c-8ef9-4bf5-acc3-5a507fad3abf/asset.jpg" - }, - "createdAt": 0, - "title": title, - "caption": caption - }, - "82f6b488-8afe-4311-9022-80ad5789ad92": { - "id": "82f6b488-8afe-4311-9022-80ad5789ad92", - "type": "pano", - "parentId": "bc17252b-811e-49d7-be3e-b7c538df9d30", - "source": { - "type": "generate_world", - "world_id": "d60cfa21-5506-43de-9a9c-0707fc17a5ec", - "status": "pending" - }, - "createdAt": 0 - }, - "f08ec501-601c-47c6-a104-402d1820b8f0": { - "id": "f08ec501-601c-47c6-a104-402d1820b8f0", - "type": "world", - "parentId": "82f6b488-8afe-4311-9022-80ad5789ad92", - "source": { - "type": "generate_world", - "world_id": "d60cfa21-5506-43de-9a9c-0707fc17a5ec", - "posed_cubemaps_url": "", - "minimap_url": "", - "minimap_metadata": "", - "status": "pending" - }, - "createdAt": 0 - } - } - } - }, - "update_mask": [ - "metadata" - ] - }); - - assert_eq!(actual, expected); - } - - /// Recursively replaces nondeterministic `createdAt` / `updatedAt` values with 0. - fn zero_out_timestamps(value: &mut Value) { - match value { - Value::Object(map) => { - for (key, child) in map.iter_mut() { - if key == "createdAt" || key == "updatedAt" { - *child = json!(0); - } else { - zero_out_timestamps(child); - } - } - }, - Value::Array(items) => { - for item in items.iter_mut() { - zero_out_timestamps(item); - } - }, - _ => {}, - } - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/requests/recaption/mod.rs b/crates/api_clients/worldlabs_consumer_client/src/api/requests/recaption/mod.rs deleted file mode 100644 index 58671219..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/requests/recaption/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod recaption_image; \ No newline at end of file diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/requests/recaption/recaption_image.rs b/crates/api_clients/worldlabs_consumer_client/src/api/requests/recaption/recaption_image.rs deleted file mode 100644 index 29c97327..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/requests/recaption/recaption_image.rs +++ /dev/null @@ -1,174 +0,0 @@ -use crate::api::api_types::run_object_id::RunObjectId; -use crate::api::api_types::upload_mime_type::UploadMimeType; -use crate::api::api_types::upload_object_id::UploadObjectId; -use crate::api::common::common_header_values::{ORIGIN_VALUE, REFERER_VALUE}; -use crate::api::utils::upload_id_to_image_url::upload_id_to_image_url; -use crate::credentials::world_labs_bearer_token::WorldLabsBearerToken; -use crate::credentials::world_labs_cookies::WorldLabsCookies; -use crate::error::world_labs_client_error::WorldLabsClientError; -use crate::error::world_labs_error::WorldLabsError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use http_headers::names::{PRIORITY, SEC_FETCH_DEST, SEC_FETCH_MODE, SEC_FETCH_SITE, SEC_GPC}; -use http_headers::values::accept::ACCEPT_ALL; -use http_headers::values::cache_control::CACHE_CONTROL_NO_CACHE; -use http_headers::values::connection::CONNECTION_KEEP_ALIVE; -use http_headers::values::content_type::CONTENT_TYPE_APPLICATION_JSON; -use http_headers::values::pragma::PRAGMA_NO_CACHE; -use http_headers::values::priority::PRIORITY_4; -use http_headers::values::sec::{SEC_FETCH_DEST_EMPTY, SEC_FETCH_MODE_CORS, SEC_FETCH_SITE_CROSS_SITE}; -use http_headers::values::te::TE_TRAILERS; -use log::{debug, error}; -use serde::{Deserialize, Serialize}; -use std::time::Duration; -use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_TYPE, ORIGIN, PRAGMA, REFERER, TE}; -use wreq::Client; -use wreq_util::Emulation; - -const URL : &str = "https://api.worldlabs.ai/api/v1/recaption2"; - -// Note: WorldLabs is phasing out the old URL scheme: -// const URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/recaption2"; - -pub struct RecaptionImageArgs<'a> { - pub cookies: &'a WorldLabsCookies, - pub bearer_token: &'a WorldLabsBearerToken, - pub upload_id: &'a UploadObjectId, - pub upload_mime_type: UploadMimeType, - pub run_id: &'a RunObjectId, - pub request_timeout: Option, -} - -pub struct RecaptionImageResponse { - pub title: String, - pub caption: String, -} - -/// Marble Image-to-World -/// This request generates captions and titles for images. It's a VLM. -/// The prompts are then used downstream to create the scenes. -/// Request #6 (of ~10) -pub async fn recaption_image(args: RecaptionImageArgs<'_>) -> Result { - let client = Client::builder() - .emulation(Emulation::Firefox143) - .build() - .map_err(|err| WorldLabsClientError::WreqClientError(err))?; - - debug!("Requesting URL: {}", URL); - - let mut request_builder = client.post(URL) - .header(ACCEPT, ACCEPT_ALL) - .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5") - .header(ACCEPT_ENCODING, "gzip, deflate, br, zstd") - .header(REFERER, REFERER_VALUE) - .header(CONTENT_TYPE, CONTENT_TYPE_APPLICATION_JSON) - .header(AUTHORIZATION, args.bearer_token.to_bearer_token_header_string()) - .header(ORIGIN, ORIGIN_VALUE) - .header(SEC_GPC, "1") - .header(CONNECTION, CONNECTION_KEEP_ALIVE) - .header(SEC_FETCH_DEST, SEC_FETCH_DEST_EMPTY) - .header(SEC_FETCH_MODE, SEC_FETCH_MODE_CORS) - .header(SEC_FETCH_SITE, SEC_FETCH_SITE_CROSS_SITE) - .header(PRIORITY, PRIORITY_4) - .header(PRAGMA, PRAGMA_NO_CACHE) - .header(CACHE_CONTROL, CACHE_CONTROL_NO_CACHE) - .header(TE, TE_TRAILERS); - - if let Some(timeout) = args.request_timeout { - request_builder = request_builder.timeout(timeout); - } - - let request_payload = RawRequest::for_image_and_run(args.upload_id, args.upload_mime_type, args.run_id); - - let http_request = request_builder.json(&request_payload) - .build() - .map_err(|err| { - error!("Error building request: {:?}", err); - WorldLabsClientError::WreqClientError(err) - })?; - - let response = client.execute(http_request) - .await - .map_err(|err| { - error!("Error during request execution: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - let status = response.status(); - - let response_body = response.text() - .await - .map_err(|err| { - error!("Error reading response body: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - // TODO: Handle errors (Cloudflare, Grok, etc.) - if !status.is_success() { - error!("Request returned an error (code {}) : {:?}", status.as_u16(), response_body); - //return Err(classify_general_http_status_code_and_body(status, response_body)); - return Err(WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { status_code: status, body: response_body}.into()) - } - - debug!("Response body (200): {}", response_body); - - let response : RawResponse = serde_json::from_str(&response_body) - .map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?; - - Ok(RecaptionImageResponse { - title: response.title, - caption: response.caption, - }) -} - -#[derive(Serialize)] -struct RawRequest { - pub world_prompt: WorldPrompt, - pub user_caption: String, - pub context: Context, -} - -#[derive(Serialize)] -struct WorldPrompt { - pub r#type: String, - pub image_prompt: ImagePrompt, - pub text_prompt: String, -} - -#[derive(Serialize)] -struct ImagePrompt { - pub uri: String, -} - -#[derive(Serialize)] -struct Context { - pub run_id: String, -} - -impl RawRequest { - pub fn for_image_and_run(image_upload_id: &UploadObjectId, image_mime_type: UploadMimeType, run_id: &RunObjectId) -> Self { - let image_url = upload_id_to_image_url(image_upload_id, image_mime_type); - Self { - world_prompt: WorldPrompt { - r#type: "image".to_string(), - image_prompt: ImagePrompt { - uri: image_url, - }, - text_prompt: "".to_string(), // NB: empty - }, - user_caption: "".to_string(), // NB: empty - context: Context { - run_id: run_id.0.clone(), - } - } - } -} - -#[derive(Deserialize)] -struct RawResponse { - pub caption: String, - pub title: String, -} - -#[cfg(test)] -mod tests { -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/requests/worlds/create_world.rs b/crates/api_clients/worldlabs_consumer_client/src/api/requests/worlds/create_world.rs deleted file mode 100644 index 5ebd5702..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/requests/worlds/create_world.rs +++ /dev/null @@ -1,186 +0,0 @@ -use crate::api::api_types::meta_world_object_id::MetaWorldObjectId; -use crate::api::api_types::run_object_id::RunObjectId; -use crate::api::api_types::upload_mime_type::UploadMimeType; -use crate::api::api_types::upload_object_id::UploadObjectId; -use crate::api::api_types::world_id::WorldObjectId; -use crate::api::common::common_header_values::{ORIGIN_VALUE, REFERER_VALUE}; -use crate::api::utils::upload_id_to_image_url::upload_id_to_image_url; -use crate::credentials::world_labs_bearer_token::WorldLabsBearerToken; -use crate::credentials::world_labs_cookies::WorldLabsCookies; -use crate::error::filter_world_labs_http_error::filter_world_labs_http_error; -use crate::error::world_labs_client_error::WorldLabsClientError; -use crate::error::world_labs_error::WorldLabsError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use http_headers::names::{PRIORITY, SEC_FETCH_DEST, SEC_FETCH_MODE, SEC_FETCH_SITE, SEC_GPC}; -use http_headers::values::accept::ACCEPT_ALL; -use http_headers::values::cache_control::CACHE_CONTROL_NO_CACHE; -use http_headers::values::connection::CONNECTION_KEEP_ALIVE; -use http_headers::values::content_type::CONTENT_TYPE_APPLICATION_JSON; -use http_headers::values::pragma::PRAGMA_NO_CACHE; -use http_headers::values::priority::PRIORITY_4; -use http_headers::values::sec::{SEC_FETCH_DEST_EMPTY, SEC_FETCH_MODE_CORS, SEC_FETCH_SITE_CROSS_SITE}; -use http_headers::values::te::TE_TRAILERS; -use log::{debug, error}; -use serde::{Deserialize, Serialize}; -use std::time::Duration; -use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_TYPE, ORIGIN, PRAGMA, REFERER, TE}; -use wreq::Client; -use wreq_util::Emulation; - -const URL : &str = "https://api.worldlabs.ai/api/v1/worlds"; - -// Note: WorldLabs is phasing out the old URL scheme: -//const URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/worlds"; - -pub struct CreateWorldArgs<'a> { - pub cookies: &'a WorldLabsCookies, - pub bearer_token: &'a WorldLabsBearerToken, - - // eg. https://cdn.marble.worldlabs.ai/object/13ca760c-8ef9-4bf5-acc3-5a507fad3abf/asset.jpg - pub image_upload_url: &'a str, - pub text_prompt: &'a str, - - pub request_timeout: Option, -} - -pub struct CreateWorldResponse { - pub world_id: WorldObjectId, -} - -/// Marble Image-to-World -/// This request starts world generation -/// Request #7 (of ~10) -pub async fn create_world(args: CreateWorldArgs<'_>) -> Result { - let client = Client::builder() - .emulation(Emulation::Firefox143) - .build() - .map_err(|err| WorldLabsClientError::WreqClientError(err))?; - - debug!("Requesting URL: {}", URL); - - let mut request_builder = client.post(URL) - .header(ACCEPT, ACCEPT_ALL) - .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5") - .header(ACCEPT_ENCODING, "gzip, deflate, br, zstd") - .header(REFERER, REFERER_VALUE) - .header(CONTENT_TYPE, CONTENT_TYPE_APPLICATION_JSON) - .header(AUTHORIZATION, args.bearer_token.to_bearer_token_header_string()) - .header(ORIGIN, ORIGIN_VALUE) - .header(SEC_GPC, "1") - .header(CONNECTION, CONNECTION_KEEP_ALIVE) - .header(SEC_FETCH_DEST, SEC_FETCH_DEST_EMPTY) - .header(SEC_FETCH_MODE, SEC_FETCH_MODE_CORS) - .header(SEC_FETCH_SITE, SEC_FETCH_SITE_CROSS_SITE) - .header(PRIORITY, PRIORITY_4) - .header(PRAGMA, PRAGMA_NO_CACHE) - .header(CACHE_CONTROL, CACHE_CONTROL_NO_CACHE) - .header(TE, TE_TRAILERS); - - if let Some(timeout) = args.request_timeout { - request_builder = request_builder.timeout(timeout); - } - - let mut payload = RawRequest::default(); - payload.generation_input.prompt.image_prompt.uri = Some(args.image_upload_url.to_string()); - payload.generation_input.prompt.text_prompt = Some(args.text_prompt.to_string()); - - let http_request = request_builder.json(&payload) - .build() - .map_err(|err| { - error!("Error building request: {:?}", err); - WorldLabsClientError::WreqClientError(err) - })?; - - let response = client.execute(http_request) - .await - .map_err(|err| { - error!("Error during request execution: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - let status = response.status(); - - let response_body = response.text() - .await - .map_err(|err| { - error!("Error reading response body: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - if !status.is_success() { - error!("Request returned an error (code {}) : {:?}", status.as_u16(), response_body); - } - - filter_world_labs_http_error(status, Some(&response_body))?; - - debug!("Response body (200): {}", response_body); - - let response : RawResponse = serde_json::from_str(&response_body) - .map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?; - - Ok(CreateWorldResponse { - world_id: WorldObjectId(response.id), - }) -} - -#[derive(Serialize)] -struct RawRequest { - pub permission: Permission, - pub generation_input: GenerationInput, -} - -#[derive(Serialize)] -struct Permission { - pub public: bool, -} - -#[derive(Serialize)] -struct GenerationInput { - pub model: String, - pub prompt: Prompt, -} - -#[derive(Serialize)] -struct Prompt { - pub r#type: String, - pub image_prompt: ImagePrompt, - - // NB: Not null in request, just for building - pub text_prompt: Option, -} - -#[derive(Serialize)] -struct ImagePrompt { - // NB: Not null in request, just for building - pub uri: Option, -} - -impl Default for RawRequest { - fn default() -> Self { - Self { - permission: Permission { - public: true, - }, - generation_input: GenerationInput { - model: "Marble 0.1-plus".to_string(), - prompt: Prompt { - r#type: "image".to_string(), - image_prompt: ImagePrompt { - uri: None, - }, - text_prompt: None, - }, - }, - } - } -} - - -#[derive(Deserialize)] -struct RawResponse { - pub id: String, -} - -#[cfg(test)] -mod tests { -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/requests/worlds/mod.rs b/crates/api_clients/worldlabs_consumer_client/src/api/requests/worlds/mod.rs deleted file mode 100644 index 58522dea..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/requests/worlds/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod create_world; -pub mod poll_world_status; \ No newline at end of file diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/requests/worlds/poll_world_status.rs b/crates/api_clients/worldlabs_consumer_client/src/api/requests/worlds/poll_world_status.rs deleted file mode 100644 index 55b7c09c..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/requests/worlds/poll_world_status.rs +++ /dev/null @@ -1,212 +0,0 @@ -use crate::api::api_types::run_object_id::RunObjectId; -use crate::api::api_types::upload_mime_type::UploadMimeType; -use crate::api::api_types::upload_object_id::UploadObjectId; -use crate::api::api_types::world_id::WorldObjectId; -use crate::api::common::common_header_values::{ORIGIN_VALUE, REFERER_VALUE}; -use crate::credentials::world_labs_bearer_token::WorldLabsBearerToken; -use crate::credentials::world_labs_cookies::WorldLabsCookies; -use crate::error::world_labs_client_error::WorldLabsClientError; -use crate::error::world_labs_error::WorldLabsError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use chrono::Utc; -use http_headers::names::{PRIORITY, SEC_FETCH_DEST, SEC_FETCH_MODE, SEC_FETCH_SITE, SEC_GPC}; -use http_headers::values::accept::ACCEPT_ALL; -use http_headers::values::cache_control::CACHE_CONTROL_NO_CACHE; -use http_headers::values::connection::CONNECTION_KEEP_ALIVE; -use http_headers::values::content_type::CONTENT_TYPE_APPLICATION_JSON; -use http_headers::values::pragma::PRAGMA_NO_CACHE; -use http_headers::values::priority::PRIORITY_4; -use http_headers::values::sec::{SEC_FETCH_DEST_EMPTY, SEC_FETCH_MODE_CORS, SEC_FETCH_SITE_CROSS_SITE}; -use http_headers::values::te::TE_TRAILERS; -use log::{debug, error, info}; -use serde::{Deserialize, Serialize}; -use std::time::Duration; -use uuid::Uuid; -use wreq::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_LENGTH, CONTENT_TYPE, ORIGIN, PRAGMA, REFERER, TE}; -use wreq::Client; -use wreq_util::Emulation; - -const BASE_URL : &str = "https://api.worldlabs.ai/api/v1/worlds"; - -// Note: WorldLabs is phasing out the old URL scheme: -// const BASE_URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/worlds"; - -fn get_url(world_id: &WorldObjectId) -> String { - format!("{}/{}", BASE_URL, world_id.0) -} - -pub struct PollWorldStatusArgs<'a> { - pub cookies: &'a WorldLabsCookies, - pub bearer_token: &'a WorldLabsBearerToken, - pub world_id: &'a WorldObjectId, - pub request_timeout: Option, -} - -pub struct PollWorldStatusResponse { - pub is_complete: bool, - pub spz_splat_url: Option, -} - -/// Marble Image-to-World -/// Poll the world generation status -/// Request #10 (of ~10) -pub async fn poll_world_status(args: PollWorldStatusArgs<'_>) -> Result { - let client = Client::builder() - .emulation(Emulation::Firefox143) - .build() - .map_err(|err| WorldLabsClientError::WreqClientError(err))?; - - let url = get_url(args.world_id); - - info!("Polling World: {}", url); - - let mut request_builder = client.get(url) - .header(ACCEPT, ACCEPT_ALL) - .header(ACCEPT_LANGUAGE, "en-US,en;q=0.5") - .header(ACCEPT_ENCODING, "gzip, deflate, br, zstd") - .header(REFERER, REFERER_VALUE) - .header(AUTHORIZATION, args.bearer_token.to_bearer_token_header_string()) - .header(ORIGIN, ORIGIN_VALUE) - .header(SEC_GPC, "1") - .header(CONNECTION, CONNECTION_KEEP_ALIVE) - .header(SEC_FETCH_DEST, SEC_FETCH_DEST_EMPTY) - .header(SEC_FETCH_MODE, SEC_FETCH_MODE_CORS) - .header(SEC_FETCH_SITE, SEC_FETCH_SITE_CROSS_SITE) - .header(PRIORITY, PRIORITY_4) - .header(PRAGMA, PRAGMA_NO_CACHE) - .header(CACHE_CONTROL, CACHE_CONTROL_NO_CACHE) - .header(CONTENT_LENGTH, 0) - .header(TE, TE_TRAILERS); - - if let Some(timeout) = args.request_timeout { - request_builder = request_builder.timeout(timeout); - } - - let http_request = request_builder - .build() - .map_err(|err| { - error!("Error building request: {:?}", err); - WorldLabsClientError::WreqClientError(err) - })?; - - let response = client.execute(http_request) - .await - .map_err(|err| { - error!("Error during request execution: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - let status = response.status(); - - let response_body = response.text() - .await - .map_err(|err| { - error!("Error reading response body: {:?}", err); - WorldLabsGenericApiError::WreqError(err) - })?; - - // TODO: Handle errors (Cloudflare, Grok, etc.) - if !status.is_success() { - error!("Request returned an error (code {}) : {:?}", status.as_u16(), response_body); - //return Err(classify_general_http_status_code_and_body(status, response_body)); - return Err(WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { status_code: status, body: response_body}.into()) - } - - debug!("Response body (200): {}", response_body); - - let response : RawResponse = serde_json::from_str(&response_body) - .map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?; - - debug!("World status: {}", response.status); - - let maybe_spz_url = response.generation_output - .map(|out| out.spz_urls.full_res); - - Ok(PollWorldStatusResponse { - is_complete: maybe_spz_url.is_some(), - spz_splat_url: maybe_spz_url, - }) -} - -#[derive(Deserialize)] -struct RawResponse { - /// eg. "INITIALIZING", "SUCCEEDED", etc. - pub status: String, - - pub generation_output: Option, -} - -#[derive(Deserialize)] -struct GenerationOutput { - pub spz_urls: SpzUrls, - -} - -#[derive(Deserialize)] -struct SpzUrls { - // eg. https://cdn.marble.worldlabs.ai/d60cfa21-5506-43de-9a9c-0707fc17a5ec/1002dd35-fcad-4eeb-ba15-0d113c1778d8_ceramic.spz - pub full_res: String, -} - -#[cfg(test)] -mod tests { - use crate::api::api_types::world_id::WorldObjectId; - use crate::api::requests::worlds::poll_world_status::{get_url, poll_world_status, PollWorldStatusArgs, BASE_URL}; - use crate::test_utils::get_test_bearer_token::get_test_bearer_token; - use crate::test_utils::get_test_cookies::get_typed_test_cookies; - use crate::test_utils::setup_test_logging::setup_test_logging; - use log::LevelFilter; - - #[test] - fn test_get_url() { - let world_id = WorldObjectId("d60cfa21-5506-43de-9a9c-0707fc17a5ec".to_string()); - let expected = format!("{}/d60cfa21-5506-43de-9a9c-0707fc17a5ec", BASE_URL); - assert_eq!(get_url(&world_id), expected); - } - - #[tokio::test] - #[ignore] // Client side tests only - async fn test_requests() { - setup_test_logging(LevelFilter::Debug); - - let cookies = get_typed_test_cookies().unwrap(); - let bearer_token = get_test_bearer_token().unwrap(); - - let world_id = WorldObjectId("403c379b-b7d2-4449-8f1e-ebdb56142604".to_string()); - - let response = poll_world_status(PollWorldStatusArgs { - cookies: &cookies, - bearer_token: &bearer_token, - world_id: &world_id, - request_timeout: None, - }).await.unwrap(); - - println!("Is complete: {}", response.is_complete); - println!("Spz Url: {}", response.spz_splat_url.unwrap_or_else(|| "incomplete".to_string())); - - assert_eq!(1, 2); - } - - #[tokio::test] - #[ignore] // Client side tests only - async fn test_request_2() { - setup_test_logging(LevelFilter::Debug); - - let cookies = get_typed_test_cookies().unwrap(); - let bearer_token = get_test_bearer_token().unwrap(); - - let world_id = WorldObjectId("e739b11e-6039-49c7-b73d-b79e1b28e295".to_string()); - - let response = poll_world_status(PollWorldStatusArgs { - cookies: &cookies, - bearer_token: &bearer_token, - world_id: &world_id, - request_timeout: None, - }).await.unwrap(); - - println!("Is complete: {}", response.is_complete); - println!("Spz Url: {}", response.spz_splat_url.unwrap_or_else(|| "incomplete".to_string())); - - assert_eq!(1, 2); - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/utils/mod.rs b/crates/api_clients/worldlabs_consumer_client/src/api/utils/mod.rs deleted file mode 100644 index ff1db7c1..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/utils/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod upload_id_to_image_url; \ No newline at end of file diff --git a/crates/api_clients/worldlabs_consumer_client/src/api/utils/upload_id_to_image_url.rs b/crates/api_clients/worldlabs_consumer_client/src/api/utils/upload_id_to_image_url.rs deleted file mode 100644 index 3dc266bb..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/api/utils/upload_id_to_image_url.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::api::api_types::upload_object_id::UploadObjectId; -use crate::api::api_types::upload_mime_type::UploadMimeType; - -pub fn upload_id_to_image_url(upload_id: &UploadObjectId, upload_mime_type: UploadMimeType) -> String { - let extension_without_period = match upload_mime_type { - UploadMimeType::ImageJpeg => "jpg", - }; - let upload_id_str = upload_id.as_str(); - format!("https://cdn.marble.worldlabs.ai/object/{upload_id_str}/asset.{extension_without_period}") -} - -#[cfg(test)] -mod tests { - use super::upload_id_to_image_url; - use crate::api::api_types::upload_object_id::UploadObjectId; - use crate::api::api_types::upload_mime_type::UploadMimeType; - - // llm tests... sigh - #[test] - fn builds_jpeg_url_with_jpg_extension() { - let id = UploadObjectId("abc123".to_string()); - let url = upload_id_to_image_url(&id, UploadMimeType::ImageJpeg); - assert_eq!( - url, - "https://cdn.marble.worldlabs.ai/object/abc123/asset.jpg" - ); - } - - // llm tests... sigh - #[test] - fn preserves_exact_upload_id_in_url() { - let raw_id = "550e8400-e29b-41d4-a716-446655440000"; - let id = UploadObjectId(raw_id.to_string()); - let url = upload_id_to_image_url(&id, UploadMimeType::ImageJpeg); - assert!(url.contains(raw_id)); - assert!(url.ends_with("/asset.jpg")); - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/client/browser_user_agents.rs b/crates/api_clients/worldlabs_consumer_client/src/client/browser_user_agents.rs deleted file mode 100644 index 94f3923b..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/client/browser_user_agents.rs +++ /dev/null @@ -1,10 +0,0 @@ - -/// Firefox 139 (2025) on macOS, which matches wreq's Emulation::Firefox139 -pub const FIREFOX_139_MAC_USER_AGENT : &str = - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0"; - -pub const FIREFOX_143_MAC_USER_AGENT : &str = - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:143.0) Gecko/20100101 Firefox/143.0"; - -pub const FIREFOX_144_MAC_USER_AGENT_UNSUPPORTED_TLS_FINGERPRINTING : &str = - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:144.0) Gecko/20100101 Firefox/144.0"; diff --git a/crates/api_clients/worldlabs_consumer_client/src/client/mod.rs b/crates/api_clients/worldlabs_consumer_client/src/client/mod.rs deleted file mode 100644 index 82c176fa..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/client/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod browser_user_agents; \ No newline at end of file diff --git a/crates/api_clients/worldlabs_consumer_client/src/credentials/jwt_claims.rs b/crates/api_clients/worldlabs_consumer_client/src/credentials/jwt_claims.rs deleted file mode 100644 index aa15ba35..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/credentials/jwt_claims.rs +++ /dev/null @@ -1,101 +0,0 @@ -use base64::Engine; -use chrono::{DateTime, Utc}; -use jwt_light::common_claims::CommonClaims; -use jwt_light::error::JwtError; -use jwt_light::parse_jwt_claims_trait::ParseJwtClaims; -use serde_json::{Map, Value}; - - -#[derive(Clone, Debug)] -pub struct JwtClaims { - /// From the "iat" field - pub created: DateTime, - - /// From the "exp" field - pub expiration: DateTime, - - /// From the "user_id" field - pub user_id: Option, - - /// From the "email" field - pub email: Option, -} - -impl ParseJwtClaims for JwtClaims { - fn extract_claims(common_claims: CommonClaims, extra_claims: Map) -> Result { - let user_id = extra_claims.get("user_id") - .map(|val| val.as_str()) - .flatten() - .map(|val| val.to_string()); - - let mut email = extra_claims.get("email") - .map(|val| val.as_str()) - .flatten() - .map(|val| val.to_string()); - - if email.is_none() { - // Parse out email from this field: - // "firebase": { - // "identities": { - // "email": [ - // "sabalid234@crsay.com" - // ] - // }, - // }, - email = extra_claims.get("firebase") - .and_then(|val| val.as_object()) - .and_then(|fb| fb.get("identities")) - .and_then(|val| val.as_object()) - .and_then(|id| id.get("email")) - .and_then(|emails| emails.as_array()) - .and_then(|emails| emails.get(0)) - .and_then(|email| email.as_str()) - .map(|email| email.to_string()); - } - - Ok(Self { - created: common_claims.created, - expiration: common_claims.expiration, - user_id, - email, - }) - } -} - -#[cfg(test)] -mod tests { - use crate::credentials::jwt_claims::JwtClaims; - use jwt_light::parse_jwt_claims_trait::ParseJwtClaims; - - #[test] - fn test_token() { - /* - { - "iss": "https://securetoken.google.com/wlt-training-gsc", - "aud": "wlt-training-gsc", - "auth_time": 1766002923, - "user_id": "CArJJ2PQFFd8l2Rf8uptZhl86d33", - "sub": "CArJJ2PQFFd8l2Rf8uptZhl86d33", - "iat": 1766099113, - "exp": 1766102713, - "email": "sabalid234@crsay.com", - "email_verified": true, - "firebase": { - "identities": { - "email": [ - "sabalid234@crsay.com" - ] - }, - "sign_in_provider": "password" - } - } - */ - let jwt = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjM4MTFiMDdmMjhiODQxZjRiNDllNDgyNTg1ZmQ2NmQ1NWUzOGRiNWQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vd2x0LXRyYWluaW5nLWdzYyIsImF1ZCI6IndsdC10cmFpbmluZy1nc2MiLCJhdXRoX3RpbWUiOjE3NjYwMDI5MjMsInVzZXJfaWQiOiJDQXJKSjJQUUZGZDhsMlJmOHVwdFpobDg2ZDMzIiwic3ViIjoiQ0FySkoyUFFGRmQ4bDJSZjh1cHRaaGw4NmQzMyIsImlhdCI6MTc2NjA5OTExMywiZXhwIjoxNzY2MTAyNzEzLCJlbWFpbCI6InNhYmFsaWQyMzRAY3JzYXkuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsic2FiYWxpZDIzNEBjcnNheS5jb20iXX0sInNpZ25faW5fcHJvdmlkZXIiOiJwYXNzd29yZCJ9fQ.byDW2QiRfajxDgQMD89YNdlRxO5rsHves3nwK_Ry5ZLFThARbdU9-wTgbCz1mzz9P3amUkM_XM9E4rEAnj2u-kCv5iW_FIfRDeJ0Z8FlwqxCt6ekvJgwYE80BUIedEvTiNpP_AilThz1ILqAu3gVQ2GXUcolquvS9i12iGTl-thJH6gnZgv3UdP2Vrr5ZP9VhKgMi5gQER5umFQ4Ud64kD9P9SLttRI2RJdolavJengQBAfec8hOEk6uikw20rEBeT_xzl8TpLNG6i4SIGYqJQHfRALGNniYoU9RlIoFR9VGeyk_6BFFGRd8I_EgL-fxnfmpV8w-riXHlOUg6uNRgw"; - let claims = JwtClaims::parse_claims(jwt).unwrap(); - - assert_eq!(claims.user_id, Some("CArJJ2PQFFd8l2Rf8uptZhl86d33".to_string())); - assert_eq!(claims.email, Some("sabalid234@crsay.com".to_string())); - assert_eq!(claims.created.timestamp(), 1766099113); - assert_eq!(claims.expiration.timestamp(), 1766102713); - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/credentials/mod.rs b/crates/api_clients/worldlabs_consumer_client/src/credentials/mod.rs deleted file mode 100644 index f752f37e..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/credentials/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod world_labs_bearer_token; -pub mod world_labs_cookies; -pub mod worldlabs_refresh_token; -pub mod jwt_claims; diff --git a/crates/api_clients/worldlabs_consumer_client/src/credentials/world_labs_bearer_token.rs b/crates/api_clients/worldlabs_consumer_client/src/credentials/world_labs_bearer_token.rs deleted file mode 100644 index 779b3968..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/credentials/world_labs_bearer_token.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::credentials::jwt_claims::JwtClaims; -use crate::error::world_labs_client_error::WorldLabsClientError; -use crate::error::world_labs_error::WorldLabsError; -use jwt_light::parse_jwt_claims_trait::ParseJwtClaims; -use log::error; - -/* - -This is issued by Google, even for email+password login: - - https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={ENTROPY} - -Value: - -{ - "kind": "identitytoolkit#VerifyPasswordResponse", - "localId": "{{WorldLabsUserId}}", - "email": "{{email@whatever.com}}", - "displayName": "", - "idToken": "{{BEARER_TOKEN}}", - "registered": true, - "refreshToken": "{{REFRESH_TOKEN}}", - "expiresIn": "3600" -} - -*/ - -#[derive(Clone)] -pub struct WorldLabsBearerToken { - /// The bearer token - /// Without "Authorization:" header name and without "Bearer(space)" prefix. - bearer_token: String, -} - -impl WorldLabsBearerToken { - pub fn new(mut bearer_token: String) -> Self { - if bearer_token.starts_with("Bearer") { - bearer_token = bearer_token - .trim_start_matches("Bearer") - .trim() - .to_string(); - } - Self { bearer_token } - } - - pub fn as_raw_str(&self) -> &str { - &self.bearer_token - } - - pub fn as_raw_bytes(&self) -> &[u8] { - self.bearer_token.as_bytes() - } - - pub fn to_raw_string(&self) -> String { - self.bearer_token.clone() - } - - pub fn to_bearer_token_header_string(&self) -> String { - format!("Bearer {}", self.bearer_token) - } - - pub fn parse_jwt_claims(&self) -> Result { - JwtClaims::parse_claims(&self.bearer_token) - .map_err(|err| { - error!("Failed to parse bearer token into JWT claims: {}", err); - WorldLabsError::Client(WorldLabsClientError::FailedToParseJwtClaims(err)) - }) - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/credentials/world_labs_cookies.rs b/crates/api_clients/worldlabs_consumer_client/src/credentials/world_labs_cookies.rs deleted file mode 100644 index f953c47d..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/credentials/world_labs_cookies.rs +++ /dev/null @@ -1,24 +0,0 @@ - -#[derive(Clone)] -pub struct WorldLabsCookies { - /// The entire cookies header (for now) - cookies: String, -} - -impl WorldLabsCookies { - pub fn new(cookies: String) -> Self { - Self { cookies } - } - - pub fn as_str(&self) -> &str { - &self.cookies - } - - pub fn as_bytes(&self) -> &[u8] { - self.cookies.as_bytes() - } - - pub fn to_string(&self) -> String { - self.cookies.clone() - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/credentials/worldlabs_refresh_token.rs b/crates/api_clients/worldlabs_consumer_client/src/credentials/worldlabs_refresh_token.rs deleted file mode 100644 index 5e305ec4..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/credentials/worldlabs_refresh_token.rs +++ /dev/null @@ -1,25 +0,0 @@ - -#[derive(Clone)] -pub struct WorldLabsRefreshToken { - /// The refresh token - /// Used to get a new bearer token - refresh_token: String, -} - -impl WorldLabsRefreshToken { - pub fn new(refresh_token: String) -> Self { - Self { refresh_token } - } - - pub fn as_raw_str(&self) -> &str { - &self.refresh_token - } - - pub fn as_raw_bytes(&self) -> &[u8] { - self.refresh_token.as_bytes() - } - - pub fn to_raw_string(&self) -> String { - self.refresh_token.clone() - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/error/filter_world_labs_http_error.rs b/crates/api_clients/worldlabs_consumer_client/src/error/filter_world_labs_http_error.rs deleted file mode 100644 index 46cc9547..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/error/filter_world_labs_http_error.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::error::world_labs_error::WorldLabsError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use crate::error::world_labs_specific_api_error::WorldLabsSpecificApiError; -use cloudflare_errors::filter_cloudflare_errors::filter_cloudflare_errors; -use wreq::StatusCode; - -/// Detect error HTTP responses and coerce the response as a Rust Error. -pub fn filter_world_labs_http_error(status_code: StatusCode, maybe_body: Option<&str>) -> Result<(), WorldLabsError> { - if status_code.is_success() { - return Ok(()); - } - - /// Early and easy to detect errors. - match status_code { - StatusCode::PAYMENT_REQUIRED => { - // {"detail":"Insufficient credits for product 'Marble 0.1-plus'. Please add credits to your account."} - return Err(WorldLabsSpecificApiError::InsufficientCredits.into()); - }, - _ => {}, // Fall Through. - } - - if let Some(body) = maybe_body { - filter_cloudflare_errors(status_code.as_u16(), body)?; - - return Err(WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { - status_code, - body: body.to_string(), - }.into()); - } - - Err(WorldLabsGenericApiError::UncategorizedBadResponseWithStatus(status_code).into()) -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/error/mod.rs b/crates/api_clients/worldlabs_consumer_client/src/error/mod.rs deleted file mode 100644 index 015cdc81..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/error/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod world_labs_client_error; -pub mod world_labs_error; -pub mod world_labs_generic_api_error; -pub mod world_labs_specific_api_error; -pub (crate) mod filter_world_labs_http_error; \ No newline at end of file diff --git a/crates/api_clients/worldlabs_consumer_client/src/error/world_labs_client_error.rs b/crates/api_clients/worldlabs_consumer_client/src/error/world_labs_client_error.rs deleted file mode 100644 index abcfb7f7..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/error/world_labs_client_error.rs +++ /dev/null @@ -1,158 +0,0 @@ -use base64::DecodeError; -use std::error::Error; -use std::fmt::{Display, Formatter}; - -#[derive(Debug)] -pub enum WorldLabsClientError { - - /// An error was encountered in building the Wreq client - WreqClientError(wreq::Error), - - /// Failed to parse JWT claims. - FailedToParseJwtClaims(jwt_light::error::JwtError), - - /// Error serializing a message to send the websocket - WebsocketRequestSerializationError(serde_json::Error), - - /// Error locking the websocket for sending/receiving - WebsocketLockError, - - /// Error reading from a websocket. - WebsocketReadError(wreq::Error), - - /// Error sending to an open websocket. - WebsocketSendError(wreq::Error), - - /// Can't open a local file for uploading. - CannotOpenLocalFileForUpload(std::io::Error), - - /// Can't read a local file for uploading. - CannotReadLocalFileForUpload(std::io::Error), - - /// Couldn't write to the file - CannotOpenLocalFileForWriting(std::io::Error), - - /// Couldn't write to the file - CannotWriteLocalFile(std::io::Error), - - /// File for upload has an invalid path. - FileForUploadHasInvalidPath, - - /// Error parsing HTML - HtmlParsingError, - - /// Error decoding verification token - FailedToDecodeVerificationToken(DecodeError), - - /// Verification token bytes are invalid - InvalidVerificationTokenBytes, - - /// 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, - - /// Something is broken with timeout math - TimeoutMathBroken, - - /// 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), -// -// /// The file path provided for upload is invalid. -// FileForUploadHasInvalidPath, -// -// /// Something is wrong with the JWT bearer token. -// /// This error originates on our end as we try to parse the JWT. -// LocalJwtClaimsParseError(String), -// -// /// There was an error constructing the form-multipart request. -// MultipartFormError(wreq::Error), -// -// /// We haven't received a bearer token yet -// /// This is our own internal application state error, not something Sora returns. -// /// We know our client can't make the request, so we preemptively fail it. -// NoBearerTokenForRequest, -// -// /// A sentinel token is not present in the client, which is required for some requests. -// NoSentinelTokenForRequest, -// -// /// Issue with using the SoraCredentialBuilder. -// SoraCredentialBuilderError(&'static str), -// -// /// Error parsing a request URL. -// UrlParseError(url::ParseError), -// -// /// Error serializing the sentinel store token to JSON (typically for persistent storage). -// CouldNotSerializeSentinelTokenStore(serde_json::Error), -// -// /// Error deserializing the sentinel token from JSON (typically for persistent storage). -// CouldNotDeserializeSentinelTokenStore { -// error: serde_json::Error, -// raw_json: String -// }, -} - -impl Error for WorldLabsClientError {} - -impl Display for WorldLabsClientError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::WreqClientError(err) => write!(f, "Wreq client error (during client creation): {}", err), - Self::FailedToParseJwtClaims(err) => write!(f, "Failed to parse JWT claims: {}", err), - Self::WebsocketRequestSerializationError(err) => write!(f, "Websocket request serialization error: {}", err), - Self::WebsocketLockError => write!(f, "Websocket lock error"), - Self::WebsocketReadError(err) => write!(f, "Websocket read error: {}", err), - Self::WebsocketSendError(err) => write!(f, "Websocket send error: {}", err), - Self::CannotOpenLocalFileForUpload(err) => write!(f, "Cannot open local file for upload: {}", err), - Self::CannotReadLocalFileForUpload(err) => write!(f, "Cannot read local file for upload: {}", err), - Self::CannotOpenLocalFileForWriting(err) => write!(f, "Cannot open local file for writing: {}", err), - Self::CannotWriteLocalFile(err) => write!(f, "Cannot write local file: {}", err), - Self::FileForUploadHasInvalidPath => write!(f, "File for upload has invalid path"), - Self::HtmlParsingError => write!(f, "Html parsing error"), - 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."), - //Self::LocalJwtClaimsParseError(msg) => write!(f, "Local JWT claims parse error: {}", msg), - //Self::MultipartFormError(err) => write!(f, "Multipart form error: {}", err), - //Self::NoBearerTokenForRequest => write!(f, "No bearer token available. The client needs a bearer token to make the request."), - //Self::NoSentinelTokenForRequest => write!(f, "No sentinel token available. The client needs a sentinel token to make the request."), - //Self::SoraCredentialBuilderError(msg) => write!(f, "Sora Credential Builder error: {}", msg), - //Self::UrlParseError(err) => write!(f, "URL parse error: {}", err), - //Self::CouldNotSerializeSentinelToken(err) => write!(f, "Could not serialize sentinel token to JSON: {}", err), - //Self::CouldNotSerializeSentinelTokenStore(err) => write!(f, "Could not serialize sentinel token store to JSON: {}", err), - //Self::CouldNotDeserializeSentinelTokenStore { error, raw_json} => { - // write!(f, "Could not deserialize sentinel token store from JSON: {:?}, raw JSON: {}", error, raw_json) - //}, - } - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/error/world_labs_error.rs b/crates/api_clients/worldlabs_consumer_client/src/error/world_labs_error.rs deleted file mode 100644 index 813240b0..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/error/world_labs_error.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::error::world_labs_client_error::WorldLabsClientError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use crate::error::world_labs_specific_api_error::WorldLabsSpecificApiError; -use cloudflare_errors::cloudflare_error::CloudflareError; -use std::error::Error; - -#[derive(Debug)] -pub enum WorldLabsError { - Client(WorldLabsClientError), - ApiSpecific(WorldLabsSpecificApiError), - ApiGeneric(WorldLabsGenericApiError), -} - -impl WorldLabsError { - pub fn is_world_labs_having_downtime_issues(&self) -> bool { - match self { - Self::ApiGeneric(WorldLabsGenericApiError::CloudflareError(CloudflareError::BadGateway502)) => true, - Self::ApiGeneric(WorldLabsGenericApiError::CloudflareError(CloudflareError::GatewayTimeout504)) => true, - Self::ApiGeneric(WorldLabsGenericApiError::CloudflareError(CloudflareError::TimeoutOccurred524)) => true, - _ => false, - } - } -} - -impl Error for WorldLabsError {} - -impl std::fmt::Display for WorldLabsError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Client(e) => write!(f, "WorldLabsClientError: {:?}", e), - Self::ApiSpecific(e) => write!(f, "WorldLabsSpecificApiError: {:?}", e), - Self::ApiGeneric(e) => write!(f, "WorldLabsGenericApiError: {:?}", e), - } - } -} - -impl From for WorldLabsError { - fn from(error: WorldLabsClientError) -> Self { - Self::Client(error) - } -} - -impl From for WorldLabsError { - fn from(error: WorldLabsSpecificApiError) -> Self { - Self::ApiSpecific(error) - } -} - -impl From for WorldLabsError { - fn from(error: WorldLabsGenericApiError) -> Self { - Self::ApiGeneric(error) - } -} - -impl From for WorldLabsError { - fn from(error: CloudflareError) -> Self { - Self::ApiGeneric(WorldLabsGenericApiError::CloudflareError(error)) - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/error/world_labs_generic_api_error.rs b/crates/api_clients/worldlabs_consumer_client/src/error/world_labs_generic_api_error.rs deleted file mode 100644 index b3c6f2f8..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/error/world_labs_generic_api_error.rs +++ /dev/null @@ -1,89 +0,0 @@ -use cloudflare_errors::cloudflare_error::CloudflareError; -use std::error::Error; -use std::fmt::{Display, Formatter}; -use wreq::StatusCode; - -#[derive(Debug)] -pub enum WorldLabsGenericApiError { - /// Unknown error occurred when uploading to Google GCP - GoogleUploadFailed { - status_code: StatusCode, - body: String, - }, - - /// Specific Cloudflare errors. - CloudflareError(CloudflareError), - - /// The downloaded index.html did not include the expected data. - /// This is necessary for handshakes between client and server. - IndexHtmlDidNotIncludeExpectedData { - message: String, - }, - - /// The downloaded javascript did not include the expected data. - /// This is necessary for handshakes between client and server. - ScriptBodyDidNotIncludeExpectedData { - message: String, - }, - - /// Error creating the websocket due to 401 Unauthorized, likely an authentication error with Grok. - /// Unfortunately we can't read the response body text to diagnose more because wreq has some API design issues. - LikelyWebsocketAuthentication401, - - /// We received a 403 on websocket upgrade, likely from Cloudflare. - /// Unfortunately we can't read the response body text to diagnose more because wreq has some API design issues. - LikelyWebsocketCloudflare403, - - /// Unknown status code on websocket upgrade. - UnexpectedWebsocketUpgradeStatusCode(StatusCode), - - /// serde_json::Error, likely from JSON deserialization schema mismatch. - /// Includes the original body. - SerdeResponseParseErrorWithBody(serde_json::Error, String), - - /// serde_json::Error, likely from JSON deserialization schema mismatch. - /// Includes the original body. - /// Specifically, this is for non-200 (error) responses. - SerdeParseErrorWithBodyOnNon200(serde_json::Error, String), - - /// An uncategorized bad HTTP response from Grok. - UncategorizedBadResponseWithStatus(StatusCode), - - /// An uncategorized bad HTTP response from Grok. - UncategorizedBadResponseWithStatusAndBody { - status_code: StatusCode, - body: String, - }, - - /// Our previous upload failed - UploadFailed, - - /// An error upgrading the connection to a websocket. - WreqWebsocketUpgradeError(wreq::Error), - - /// An uncaught error from the API client. - WreqError(wreq::Error), -} - -impl Error for WorldLabsGenericApiError {} - -impl Display for WorldLabsGenericApiError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::GoogleUploadFailed { status_code, body } => write!(f, "Google world upload failed with HTTP {}: {:?}", status_code, body), - Self::CloudflareError(err) => write!(f, "Cloudflare error: {}", err), - Self::IndexHtmlDidNotIncludeExpectedData { message } => write!(f, "IndexHtmlDidNotIncludeExpectedData ( message: {} )", message), - Self::ScriptBodyDidNotIncludeExpectedData { message } => write!(f, "ScriptBodyDidNotIncludeExpectedData ( message: {} )", message), - Self::LikelyWebsocketAuthentication401 => write!(f, "Likely websocket authentication 401 error (probably Grok authentication issue with cookies)"), - Self::LikelyWebsocketCloudflare403 => write!(f, "Likely Cloudflare 403 error on websocket upgrade"), - Self::UnexpectedWebsocketUpgradeStatusCode(status) => write!(f, "Unexpected websocket upgrade status code: {}", status), - Self::SerdeResponseParseErrorWithBody(err, body) => write!(f, "Failed to parse response body: {:?}. Body: {}", err, body), - Self::SerdeParseErrorWithBodyOnNon200(err, body) => write!(f, "Failed to parse non-200 response body: {:?}. Body: {}", err, body), - Self::UncategorizedBadResponseWithStatus(status) => write!(f, "Uncategorized with status code: {}", status), - Self::UncategorizedBadResponseWithStatusAndBody { status_code, body } => write!(f, "Uncategorized bad response: status code {}, body: {}", status_code, body), - Self::UploadFailed => write!(f, "Upload failed"), - Self::WreqWebsocketUpgradeError(err) => write!(f, "Websocket upgrade error: {}", err), - Self::WreqError(err) => write!(f, "Wreq client error: {}", err), - } - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/error/world_labs_specific_api_error.rs b/crates/api_clients/worldlabs_consumer_client/src/error/world_labs_specific_api_error.rs deleted file mode 100644 index aba50870..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/error/world_labs_specific_api_error.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::error::Error; -use std::fmt::{Display, Formatter}; - -#[derive(Debug)] -pub enum WorldLabsSpecificApiError { - /// If the API drops run ids (unlikely) - NoRunIdInResponse { response_body: String }, - - /// The Google GCP upload failed because the signature does not match - GoogleUploadSignatureDoesNotMatch, - - /// Grok is detecting and blocking our bot, likely due to new secrets use. - AutomationBlocked, - - /// Too many videos are being generated. - TooManyVideos, - - /// There aren't sufficient funds for generation. - InsufficientCredits, - -// /// Failed to generate a bearer token despite the `200 OK` response. -// /// This has been generated by sending old/expired session cookies or empty cookies. -// FailedToGenerateBearerToken, -// -// /// The endpoint told us we used an invalid JWT. -// InvalidJwt, -// -// /// Sora has responded with "sentinel block". -// /// This usually means we need to refresh our sentinel token. -// SentinelBlockError, -// -// /// Sora requires users to set up an account username before they can generate images. -// /// This requires the user to set up that username. -// SoraUsernameNotYetCreated, -// -// /// Sora has responded with "token expired". -// /// This (probably?) means we need to refresh our JWT bearer token. -// TokenExpiredError, -// -// /// We're sending too many tasks to Sora. -// /// Sora is telling us to cool it. -// TooManyConcurrentTasks, -// -// // TODO: Try to make concrete error types for these distinct cases. -// /// Unauthorized, cookie and/or bearer token expired. We'll need to ask for a refreshed login. -// /// -// /// Example message, e.g. from the upload endpoint: -// /// { -// /// "error": { -// /// "message": "Your authentication token has expired. Please try signing in again.", -// /// "type": "invalid_request_error", -// /// "param": null, -// /// "code": "token_expired" -// /// } -// /// } -// UnauthorizedCookieOrBearerExpired, -// -// /// The Sora sentinel token response is missing fields -// SentinelResponseIsMissingFields { -// missing_token: bool, -// missing_turnstile: bool, -// raw_response: String, -// }, -} - -impl Error for WorldLabsSpecificApiError {} - -impl Display for WorldLabsSpecificApiError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::NoRunIdInResponse { response_body } => write!(f, "no run.id found in response: {}", response_body), - Self::GoogleUploadSignatureDoesNotMatch => write!(f, "Google upload signature does not match"), - Self::AutomationBlocked => write!(f, "Automation blocked"), - Self::TooManyVideos => write!(f, "Too many videos are being generated"), - Self::InsufficientCredits => write!(f, "Insufficient credits"), - - //Self::FailedToGenerateBearerToken => write!(f, "Failed to generate bearer token. Probably invalid or expired session cookies."), - //Self::InvalidJwt => write!(f, "Invalid JWT token. Please refresh the token."), - //Self::SentinelBlockError => write!(f, "Request blocked by Sentinel Token Block. Please refresh the sentinel token."), - //Self::SoraUsernameNotYetCreated => write!(f, "Sora username not yet created. Please create a username in the Sora app or website."), - //Self::TokenExpiredError => write!(f, "JWT token has expired. Please refresh the token."), - //Self::TooManyConcurrentTasks => write!(f, "Too many concurrent tasks. Please wait."), - //Self::UnauthorizedCookieOrBearerExpired => write!(f, "Unauthorized: cookie and/or bearer token expired"), - //Self::SentinelResponseIsMissingFields { missing_token: token_missing, missing_turnstile: turnstile_missing, raw_response } => { - // write!(f, "Sentinel response is missing fields. token_missing: {}, turnstile_missing: {}. Raw response: {}", token_missing, turnstile_missing, raw_response) - //}, - } - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/lib.rs b/crates/api_clients/worldlabs_consumer_client/src/lib.rs deleted file mode 100644 index dda4bd67..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/lib.rs +++ /dev/null @@ -1,9 +0,0 @@ - -#[cfg(test)] -pub (crate) mod test_utils; - -pub mod api; -pub mod client; -pub mod credentials; -pub mod error; -pub mod recipes; diff --git a/crates/api_clients/worldlabs_consumer_client/src/recipes/mod.rs b/crates/api_clients/worldlabs_consumer_client/src/recipes/mod.rs deleted file mode 100644 index d17ac4bb..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/recipes/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod upload_image_and_create_world_with_retry; \ No newline at end of file diff --git a/crates/api_clients/worldlabs_consumer_client/src/recipes/upload_image_and_create_world_with_retry.rs b/crates/api_clients/worldlabs_consumer_client/src/recipes/upload_image_and_create_world_with_retry.rs deleted file mode 100644 index b841da4f..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/recipes/upload_image_and_create_world_with_retry.rs +++ /dev/null @@ -1,346 +0,0 @@ -use crate::api::api_types::image_input_object_id::ImageInputObjectId; -use crate::api::api_types::meta_world_object_id::MetaWorldObjectId; -use crate::api::api_types::pano_object_id::PanoObjectId; -use crate::api::api_types::run_object_id::RunObjectId; -use crate::api::api_types::upload_mime_type::UploadMimeType; -use crate::api::api_types::world_id::WorldObjectId; -use crate::api::requests::google_refresh_token::google_refresh_token::{google_refresh_token, GoogleRefreshTokenArgs}; -use crate::api::requests::google_upload::google_upload_image::{google_upload_image, GoogleUploadImageArgs}; -use crate::api::requests::objects::begin_object_image_upload::{begin_object_image_upload, BeginObjectImageUploadArgs}; -use crate::api::requests::objects::create_run_object::{create_run_object, CreateRunObjectArgs}; -use crate::api::requests::objects::create_upload_object::{create_upload_object, CreateUploadObjectArgs}; -use crate::api::requests::objects::finalize_object_image_upload::{finalize_object_image_upload, FinalizeObjectImageUploadArgs}; -use crate::api::requests::objects::update_run_object_with_upload::{update_run_object_with_upload, UpdateRunObjectWithUploadArgs, UpdateRunObjectWithUploadPayloadArgs}; -use crate::api::requests::objects::update_run_object_with_world::{update_run_object_with_world, UpdateRunObjectWithWorldArgs, UpdateRunObjectWithWorldPayloadArgs}; -use crate::api::requests::recaption::recaption_image::{recaption_image, RecaptionImageArgs}; -use crate::api::requests::worlds::create_world::{create_world, CreateWorldArgs}; -use crate::credentials::world_labs_bearer_token::WorldLabsBearerToken; -use crate::credentials::world_labs_cookies::WorldLabsCookies; -use crate::credentials::worldlabs_refresh_token::WorldLabsRefreshToken; -use crate::error::world_labs_client_error::WorldLabsClientError; -use crate::error::world_labs_error::WorldLabsError; -use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError; -use anyhow::bail; -use chrono::{TimeDelta, Utc}; -use filesys::file_read_bytes::file_read_bytes; -use log::{error, info, warn}; -use serde::Deserialize; -use std::path::PathBuf; -use std::time::Duration; -use uuid::Uuid; - -pub struct UploadImageAndCreateWorldWithRetryArgs<'a> { - pub file: FileBytesOrPath, - pub cookies: &'a WorldLabsCookies, - pub bearer_token: &'a WorldLabsBearerToken, - pub refresh_token: &'a WorldLabsRefreshToken, - pub individual_request_timeout: Option, -} - -pub enum FileBytesOrPath { - Bytes(Vec), - Path(PathBuf), -} - -pub struct UploadImageAndCreateWorldWithRetryResponse { - pub run_id: RunObjectId, - pub world_id: WorldObjectId, - pub image_upload_url: String, - - /// If the tokens were renewed, this is them. - pub maybe_new_access_tokens: Option, -} - -pub struct NewAccessTokens { - pub bearer_token: WorldLabsBearerToken, - pub refresh_token: WorldLabsRefreshToken, -} - -/// Marble Image-to-World -/// Mark the upload as complete. -/// Request #5 (of ~10) -pub async fn upload_image_and_create_world_with_retry(args: UploadImageAndCreateWorldWithRetryArgs<'_>) -> Result { - - info!("Checking to see if bearer token needs refresh (fails open) ..."); - - let mut maybe_refresh_bearer = false; - - match args.bearer_token.parse_jwt_claims() { - Err(err) => { - warn!("Failed to parse bearer_token jwt claims (failing open) : {}", err); - } - Ok(jwt) => { - let now = Utc::now(); - if now > jwt.expiration { - info!("Bearer is expired."); - maybe_refresh_bearer = true; - } - - let sooner_expiry = jwt.expiration - .checked_sub_signed(TimeDelta::minutes(30)) - .unwrap_or(jwt.expiration); - - if now > sooner_expiry { - info!("Bearer will expire soon, so we're renewing it in advance."); - maybe_refresh_bearer = true; - } - } - }; - - let mut maybe_new_access_tokens = None; - - if maybe_refresh_bearer { - info!("Refreshing bearer token..."); - - let updated = google_refresh_token(GoogleRefreshTokenArgs { - refresh_token: &args.refresh_token, - request_timeout: args.individual_request_timeout, - }).await?; - - info!("Bearer token refreshed!"); - - maybe_new_access_tokens = Some(NewAccessTokens { - bearer_token: updated.bearer_token, - refresh_token: updated.refresh_token, - }); - } - - let use_bearer_token = maybe_new_access_tokens - .as_ref() - .map(|tokens| &tokens.bearer_token) - .unwrap_or_else(|| args.bearer_token); - - info!("Checking file input..."); - - let file_bytes = match args.file { - FileBytesOrPath::Bytes(bytes) => { - info!("File bytes provided"); - bytes - } - FileBytesOrPath::Path(path) => { - info!("File path provided"); - match file_read_bytes(&path) { - Ok(bytes) => bytes, - Err(err) => { - error!("Error reading file bytes from path: {:?} - error: {:?}", path, err); - return Err(WorldLabsClientError::CannotReadLocalFileForUpload(err).into()); - } - } - } - }; - - info!("Request #1 of 10: create run object ..."); - - let response = create_run_object(CreateRunObjectArgs { - cookies: args.cookies, - bearer_token: use_bearer_token, - request_timeout: args.individual_request_timeout, - }).await?; - - let run_object_id = response.id; - let run_object = response.run_object; - - info!("Object id: {}", run_object_id.0); - - // TODO - multiple types. - let upload_mime_type= UploadMimeType::ImageJpeg; - - info!("Request #2 of 10: begin image attachment ..."); - - let response = create_upload_object(CreateUploadObjectArgs { - cookies: &args.cookies, - bearer_token: &use_bearer_token, - upload_mime_type, - request_timeout: args.individual_request_timeout, - }).await?; - - let upload_id = response.id; - - info!("Upload id: {}", upload_id.0); - - info!("Request #3 of 10: begin image upload ..."); - - let response = begin_object_image_upload(BeginObjectImageUploadArgs { - cookies: &args.cookies, - bearer_token: &use_bearer_token, - upload_mime_type, - request_timeout: args.individual_request_timeout, - upload_id: &upload_id, - }).await?; - - let google_upload_url = response.upload_url; - - info!("Request #4 of 10: upload to Google GCP ..."); - - info!("Google upload URL: {:?}", &google_upload_url); - - let _response = google_upload_image(GoogleUploadImageArgs { - upload_url: &google_upload_url, - upload_mime_type, - file_bytes, - request_timeout: args.individual_request_timeout, - }).await?; - - info!("Request #5 of 10: finalize object upload ..."); - - let response = finalize_object_image_upload(FinalizeObjectImageUploadArgs { - cookies: &args.cookies, - bearer_token: &use_bearer_token, - upload_id: &upload_id, - request_timeout: args.individual_request_timeout, - }).await?; - - let image_url = response.object_url; - - info!("Object/image URL: {image_url}"); - - info!("Request #6 of 10: Patch run with new info (first time) ..."); - - let image_input_id = ImageInputObjectId::generate_new(); - let pano_id = PanoObjectId::generate_new(); - let meta_world_id = MetaWorldObjectId::generate_new(); - - let response = update_run_object_with_upload(UpdateRunObjectWithUploadArgs { - cookies: &args.cookies, - bearer_token: &use_bearer_token, - payload_args: UpdateRunObjectWithUploadPayloadArgs { - run_id: &run_object_id, - run_created_at_timestamp: run_object.metadata.created_at, - image_upload_url: &image_url, - image_input_id: &image_input_id, - pano_id: &pano_id, - meta_world_id: &meta_world_id, - }, - request_timeout: args.individual_request_timeout, - }).await?; - - let run_first_updated_at = response.run_updated_timestamp; - - info!("Request #7 of 10: captioning with VLM ..."); - - let response = recaption_image(RecaptionImageArgs { - cookies: &args.cookies, - bearer_token: &use_bearer_token, - upload_id: &upload_id, - upload_mime_type, - run_id: &run_object_id, - request_timeout: args.individual_request_timeout, - }).await?; - - info!("Title: {}", response.title); - info!("Caption: {}", response.caption); - - let title = response.title; - let text_prompt = response.caption; - - info!("Request #8 of 10: create world ..."); - - let response = create_world(CreateWorldArgs { - cookies: &args.cookies, - bearer_token: &use_bearer_token, - text_prompt: &text_prompt, - image_upload_url: &image_url, - request_timeout: args.individual_request_timeout, - }).await?; - - let world_id = response.world_id; - - info!("World ID: {}", &world_id.0); - - info!("Request #9 of 10: patch run with new info (second time) ..."); - - let response = update_run_object_with_world(UpdateRunObjectWithWorldArgs { - cookies: &args.cookies, - bearer_token: &use_bearer_token, - payload_args: UpdateRunObjectWithWorldPayloadArgs { - run_id: &run_object_id, - run_created_at_timestamp: run_object.metadata.created_at, - first_patch_timestamp: run_first_updated_at, - image_upload_url: &image_url, - image_input_id: &image_input_id, - pano_id: &pano_id, - meta_world_id: &meta_world_id, - world_id: &world_id, - title: &title, - text_prompt: &text_prompt, - }, - request_timeout: args.individual_request_timeout, - }).await?; - - Ok(UploadImageAndCreateWorldWithRetryResponse { - run_id: run_object_id, - image_upload_url: image_url, - world_id, - maybe_new_access_tokens, - }) -} - -#[cfg(test)] -mod tests { - use crate::recipes::upload_image_and_create_world_with_retry::{upload_image_and_create_world_with_retry, FileBytesOrPath, UploadImageAndCreateWorldWithRetryArgs}; - use crate::test_utils::get_test_bearer_token::get_test_bearer_token; - use crate::test_utils::get_test_cookies::get_typed_test_cookies; - use crate::test_utils::get_test_refresh_token::get_typed_test_refresh_token; - use crate::test_utils::setup_test_logging::setup_test_logging; - use filesys::file_read_bytes::file_read_bytes; - use log::LevelFilter; - - #[tokio::test] - #[ignore] // Client side tests only - async fn test_requests() { - setup_test_logging(LevelFilter::Debug); - - let cookies = get_typed_test_cookies().unwrap(); - let bearer_token = get_test_bearer_token().unwrap(); - let refresh_token = get_typed_test_refresh_token().unwrap(); - - //let file_path = "/home/bt/Pictures/locations/island.jpg"; - let file_path = "/Users/bt/Pictures/Midjourney/jeep.jpeg"; - let file_bytes = file_read_bytes(file_path).unwrap(); - - println!("File bytes len: {}", file_bytes.len()); - - let results = upload_image_and_create_world_with_retry(UploadImageAndCreateWorldWithRetryArgs { - cookies: &cookies, - bearer_token: &bearer_token, - refresh_token: &refresh_token, - individual_request_timeout: None, - file: FileBytesOrPath::Bytes(file_bytes), - }).await.unwrap(); - - println!("Object ID: {}", results.run_id.0); - println!("World ID: {}", results.world_id.0); - println!("Upload URL: {}", results.image_upload_url); - println!("Has new access tokens: {}", results.maybe_new_access_tokens.is_some()); - - assert_eq!(1, 2); - } - - - #[tokio::test] - #[ignore] // Client side tests only - async fn test_requests_2() { - setup_test_logging(LevelFilter::Debug); - - let cookies = get_typed_test_cookies().unwrap(); - let bearer_token = get_test_bearer_token().unwrap(); - let refresh_token = get_typed_test_refresh_token().unwrap(); - - //let file_path = "/home/bt/Pictures/locations/island.jpg"; - let file_path = "/Users/bt/Pictures/Midjourney/train_ghibli.jpeg"; - let file_bytes = file_read_bytes(file_path).unwrap(); - - println!("File bytes len: {}", file_bytes.len()); - - let results = upload_image_and_create_world_with_retry(UploadImageAndCreateWorldWithRetryArgs { - cookies: &cookies, - bearer_token: &bearer_token, - refresh_token: &refresh_token, - individual_request_timeout: None, - file: FileBytesOrPath::Bytes(file_bytes), - }).await.unwrap(); - - println!("Upload URL: {}", results.image_upload_url); - println!("Object ID: {}", results.run_id.0); - } -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/test_utils/get_test_bearer_token.rs b/crates/api_clients/worldlabs_consumer_client/src/test_utils/get_test_bearer_token.rs deleted file mode 100644 index 93aabc64..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/test_utils/get_test_bearer_token.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::credentials::world_labs_bearer_token::WorldLabsBearerToken; -use errors::AnyhowResult; -use std::fs::read_to_string; - -#[cfg(test)] -pub (crate) fn get_test_bearer_token_raw() -> AnyhowResult { - let cookies = read_to_string("/Users/bt/Artcraft/credentials/worldlabs_bearer.txt")?; - //let cookies = read_to_string("/home/bt/Artcraft/credentials/world_labs_bearer.txt")?; - let cookies = cookies.trim().to_string(); - Ok(cookies) -} - -#[cfg(test)] -pub (crate) fn get_test_bearer_token() -> AnyhowResult { - let cookies = get_test_bearer_token_raw()?; - Ok(WorldLabsBearerToken::new(cookies)) -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/test_utils/get_test_cookies.rs b/crates/api_clients/worldlabs_consumer_client/src/test_utils/get_test_cookies.rs deleted file mode 100644 index 3fd53c1a..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/test_utils/get_test_cookies.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::credentials::world_labs_cookies::WorldLabsCookies; -use errors::AnyhowResult; -use std::fs::read_to_string; - -#[cfg(test)] -pub (crate) fn get_test_cookies() -> AnyhowResult { - //let cookies = read_to_string("/home/bt/Artcraft/credentials/worldlabs_cookies.txt")?; - let cookies = read_to_string("/Users/bt/Artcraft/credentials/worldlabs_cookies.txt")?; - let cookies = cookies.trim().to_string(); - Ok(cookies) -} - -#[cfg(test)] -pub (crate) fn get_typed_test_cookies() -> AnyhowResult { - let cookies = get_test_cookies()?; - Ok(WorldLabsCookies::new(cookies)) -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/test_utils/get_test_refresh_token.rs b/crates/api_clients/worldlabs_consumer_client/src/test_utils/get_test_refresh_token.rs deleted file mode 100644 index 385a868a..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/test_utils/get_test_refresh_token.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::credentials::worldlabs_refresh_token::WorldLabsRefreshToken; -use errors::AnyhowResult; -use std::fs::read_to_string; - -#[cfg(test)] -pub (crate) fn get_test_refresh_token() -> AnyhowResult { - //let cookies = read_to_string("/home/bt/Artcraft/credentials/worldlabs_cookies.txt")?; - let token = read_to_string("/Users/bt/Artcraft/credentials/worldlabs_refresh.txt")?; - let token = token.trim().to_string(); - Ok(token) -} - -#[cfg(test)] -pub (crate) fn get_typed_test_refresh_token() -> AnyhowResult { - let token = get_test_refresh_token()?; - Ok(WorldLabsRefreshToken::new(token)) -} diff --git a/crates/api_clients/worldlabs_consumer_client/src/test_utils/mod.rs b/crates/api_clients/worldlabs_consumer_client/src/test_utils/mod.rs deleted file mode 100644 index ef9d0b8e..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/test_utils/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod get_test_cookies; -pub mod get_test_bearer_token; -pub mod setup_test_logging; -pub mod get_test_refresh_token; \ No newline at end of file diff --git a/crates/api_clients/worldlabs_consumer_client/src/test_utils/setup_test_logging.rs b/crates/api_clients/worldlabs_consumer_client/src/test_utils/setup_test_logging.rs deleted file mode 100644 index 9438da10..00000000 --- a/crates/api_clients/worldlabs_consumer_client/src/test_utils/setup_test_logging.rs +++ /dev/null @@ -1,52 +0,0 @@ -use chrono::Local; -use env_logger::Builder; -use log::{debug, error, info, trace, warn, LevelFilter}; -use std::io::Write; - -const RUST_LOG : &str = "RUST_LOG"; - -pub fn setup_test_logging(level: LevelFilter) { - let level_str = get_level_str(level); - - println!("Previous log level: {:?}", env::var(RUST_LOG)); - - let is_unsafe = env::set_var(RUST_LOG, level_str); - - if is_unsafe.is_none() { - println!("Unsafely setting Rust Log Level..."); - unsafe { std::env::set_var(RUST_LOG, level_str) } - } - - println!("Log level: {:?}", env::var(RUST_LOG)); - - Builder::new() - .is_test(true) - .format(|buf, record| { - writeln!(buf, - "{} [{}] - {}", - Local::now().format("%H:%M:%S%.6f"), - record.level(), - record.args() - ) - }) - .filter(None, level) - .filter_level(level) - .init(); - - trace!("Test trace log"); - debug!("Test debug log"); - info!("Test info log"); - warn!("Test warn log"); - error!("Test error log"); -} - -fn get_level_str(level: LevelFilter) -> &'static str { - match level { - LevelFilter::Error => "error", - LevelFilter::Warn => "warn", - LevelFilter::Info => "info", - LevelFilter::Debug => "debug", - LevelFilter::Trace => "trace", - LevelFilter::Off => "off", - } -} diff --git a/crates/artcraftx/Cargo.toml b/crates/artcraftx/Cargo.toml index 9fb64c08..27f0d350 100644 --- a/crates/artcraftx/Cargo.toml +++ b/crates/artcraftx/Cargo.toml @@ -52,7 +52,6 @@ models.workspace = true sqlite_identifiers.workspace = true url_utils.workspace = true web_base64.workspace = true -worldlabs_consumer_client.workspace = true # External anyhow.workspace = true diff --git a/crates/artcraftx/src/commands/generate/common/router_image_request_to_artcraft_prompt.rs b/crates/artcraftx/src/commands/generate/common/router_image_request_to_artcraft_prompt.rs index 22f7c027..a4b28d5b 100644 --- a/crates/artcraftx/src/commands/generate/common/router_image_request_to_artcraft_prompt.rs +++ b/crates/artcraftx/src/commands/generate/common/router_image_request_to_artcraft_prompt.rs @@ -79,7 +79,6 @@ fn provider_to_generation_provider(provider: RouterProvider) -> GenerationSource RouterProvider::Seedance2Pro => GenerationSource::Artcraft , RouterProvider::GmiCloud => GenerationSource::Artcraft, RouterProvider::GrokApi => GenerationSource::Artcraft, - RouterProvider::WorldLabs => GenerationSource::Artcraft, } } diff --git a/crates/artcraftx/src/commands/generate/common/router_video_request_to_artcraft_prompt.rs b/crates/artcraftx/src/commands/generate/common/router_video_request_to_artcraft_prompt.rs index 7a652129..4e6a68c8 100644 --- a/crates/artcraftx/src/commands/generate/common/router_video_request_to_artcraft_prompt.rs +++ b/crates/artcraftx/src/commands/generate/common/router_video_request_to_artcraft_prompt.rs @@ -102,7 +102,6 @@ fn provider_to_generation_provider(provider: RouterProvider) -> GenerationSource RouterProvider::Seedance2Pro => GenerationSource::Artcraft , RouterProvider::GmiCloud => GenerationSource::Artcraft, RouterProvider::GrokApi => GenerationSource::Artcraft, - RouterProvider::WorldLabs => GenerationSource::Artcraft, } } diff --git a/crates/artcraftx/src/commands/generate/generate_error.rs b/crates/artcraftx/src/commands/generate/generate_error.rs index 2714f1c5..ba5b1b40 100644 --- a/crates/artcraftx/src/commands/generate/generate_error.rs +++ b/crates/artcraftx/src/commands/generate/generate_error.rs @@ -11,7 +11,6 @@ use higgsfield_client::error::higgsfield_error::HiggsfieldError; use midjourney_client::error::midjourney_error::MidjourneyError; use artcraft_client::error::storyteller_error::StorytellerError; use router::errors::download_error::DownloadError; -use worldlabs_consumer_client::error::world_labs_error::WorldLabsError; //use fal_client::error::fal_error_plus::FalErrorPlus; #[derive(Debug)] @@ -103,7 +102,6 @@ pub enum MissingCredentialsReason { NeedsMidjourneyUserId, NeedsMidjourneyUserInfo, NeedsStorytellerCredentials, - NeedsWorldLabsCredentials, } #[derive(Debug)] @@ -133,7 +131,6 @@ pub enum ProviderFailureReason { /// NB: The midjourney client doesn't categorize all errors, so we have to do so on our end. MidjourneyJobEnqueueFailed, StorytellerError(StorytellerError), - WorldLabsError(WorldLabsError), } impl GenerateError { @@ -177,10 +174,6 @@ impl GenerateError { pub fn needs_storyteller_credentials() -> Self { Self::MissingCredentials(MissingCredentialsReason::NeedsStorytellerCredentials) } - - pub fn needs_worldlabs_credentials() -> Self { - Self::MissingCredentials(MissingCredentialsReason::NeedsWorldLabsCredentials) - } } impl From for GenerateError { @@ -238,12 +231,6 @@ impl From for GenerateError { } } -impl From for GenerateError { - fn from(value: WorldLabsError) -> Self { - Self::ProviderFailure(ProviderFailureReason::WorldLabsError(value)) - } -} - impl From for GenerateError { fn from(value: ArtcraftRouterError) -> Self { match value { @@ -262,7 +249,6 @@ impl From for GenerateError { ProviderError::GrokApi(_) => BillingProvider::Artcraft, ProviderError::Grok(_) => BillingProvider::Artcraft, ProviderError::Higgsfield(_) => BillingProvider::Higgsfield, - ProviderError::WorldLabs(_) => BillingProvider::Artcraft, }; Self::BillingIssue(BillingIssueReason { provider }) }, @@ -278,9 +264,6 @@ impl From for GenerateError { ArtcraftRouterError::Provider(ProviderError::Midjourney(e)) => Self::ProviderFailure(ProviderFailureReason::MidjourneyError(e)), ArtcraftRouterError::Provider(ProviderError::MidjourneySubscriptionRequired(message)) => Self::ProviderRejected(message), ArtcraftRouterError::Provider(ProviderError::MidjourneySubmitRejected(message)) => Self::ProviderRejected(message), - // NB: the desktop app reaches World Labs through the Artcraft backend; the router's - // direct World Labs provider isn't used here. - ArtcraftRouterError::Provider(ProviderError::WorldLabs(_)) => Self::ArtcraftRouterNotYetSupportedProvider("world_labs"), ArtcraftRouterError::UnsupportedModel(model) => Self::NotYetImplemented(format!("Unsupported model: {}", model)), ArtcraftRouterError::UnsupportedProviderAndModelForNewApi(_message) => Self::ArtcraftRouterNotYetSupportedProvider("unsupported model for new router API"), ArtcraftRouterError::ProviderResponseInvalid(message) => Self::ProviderRejected(message), diff --git a/crates/artcraftx/src/commands/generate/generate_splat/handle_credential_router.rs b/crates/artcraftx/src/commands/generate/generate_splat/handle_credential_router.rs index dd600f83..ab1325e5 100644 --- a/crates/artcraftx/src/commands/generate/generate_splat/handle_credential_router.rs +++ b/crates/artcraftx/src/commands/generate/generate_splat/handle_credential_router.rs @@ -6,7 +6,6 @@ use router::client::request_mismatch_mitigation_strategy::RequestMismatchMitigat use router::client::router_artcraft_client::RouterArtcraftClient; use router::client::router_client::RouterClient; use router::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder; -use router::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest; use router::api::image_list_ref::ImageListRef; use router::api::video_ref::VideoRef; @@ -104,19 +103,11 @@ async fn handle_artcraft_credential( info!("Building Artcraft splat generation plan: model={:?}", router_model); - let generation_request = match router_request.build2() { - Ok(SplatGenerationDraftOrRequest::Request(generation_request)) => generation_request, - Ok(SplatGenerationDraftOrRequest::Draft(draft)) => { - warn!("Artcraft build unexpectedly produced a draft: {:?}", draft); - return Err(GenerateError::NotYetImplemented( - "Artcraft requests should not require a draft phase".to_string(), - )); - } - Err(err) => { - warn!("Could not build Artcraft router request: {:?}", err); - return Err(err.into()); - } - }; + let generation_request = router_request.build2() + .map_err(|err| { + warn!("Could not build Artcraft router request: {:?}", err); + GenerateError::from(err) + })?; let response = generation_request.send_request(&client).await .map_err(|err| { diff --git a/crates/artcraftx/src/lib.rs b/crates/artcraftx/src/lib.rs index 736f7a5c..d6d28a29 100644 --- a/crates/artcraftx/src/lib.rs +++ b/crates/artcraftx/src/lib.rs @@ -66,8 +66,6 @@ use crate::services::grok::state::grok_websockets::GrokWebsockets; use crate::services::midjourney::state::midjourney_credential_manager::MidjourneyCredentialManager; use crate::services::midjourney::state::midjourney_live_session::MidjourneyLiveSession; use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager; -use crate::services::worldlabs::state::worldlabs_bearer_bridge::WorldlabsBearerBridge; -use crate::services::worldlabs::state::worldlabs_credential_manager::WorldlabsCredentialManager; use log::error; use crate::state::usage_tracker::artcraft_usage_tracker::ArtcraftUsageTracker; @@ -117,12 +115,6 @@ pub fn run() { let grok_websockets = GrokWebsockets::new(); let grok_websockets_2 = grok_websockets.clone(); - let worldlabs_creds_manager = WorldlabsCredentialManager::initialize_from_disk_infallible(&app_data_root); - let worldlabs_creds_manager_2 = worldlabs_creds_manager.clone(); - - let worldlabs_bearer_bridge = WorldlabsBearerBridge::empty(); - let worldlabs_bearer_bridge_2 = worldlabs_bearer_bridge.clone(); - let artcraft_usage_tracker = ArtcraftUsageTracker::new(); let artcraft_usage_tracker_2 = artcraft_usage_tracker.clone(); @@ -162,8 +154,6 @@ pub fn run() { midjourney_live_session_2, grok_creds_manager_2, grok_websockets_2, - worldlabs_bearer_bridge_2, - worldlabs_creds_manager_2, ).await; if let Err(err) = result { @@ -183,9 +173,7 @@ pub fn run() { .manage(grok_websockets) .manage(midjourney_creds_manager) .manage(midjourney_live_session) - .manage(storyteller_creds_manager_3) - .manage(worldlabs_bearer_bridge) - .manage(worldlabs_creds_manager); + .manage(storyteller_creds_manager_3); // TODO: Break this out into another module, because RustRover/IntelliJ lags with these macros. // My first attempt at naively doing this didn't work because the macros can't find their codegen'd targets. diff --git a/crates/artcraftx/src/lifecycle/startup/handle_tauri_startup.rs b/crates/artcraftx/src/lifecycle/startup/handle_tauri_startup.rs index 369b6bbb..a819268f 100644 --- a/crates/artcraftx/src/lifecycle/startup/handle_tauri_startup.rs +++ b/crates/artcraftx/src/lifecycle/startup/handle_tauri_startup.rs @@ -17,10 +17,7 @@ use crate::services::midjourney::state::midjourney_live_session::MidjourneyLiveS use crate::services::midjourney::threads::midjourney_long_polling_thread::midjourney_long_polling_thread; use crate::services::midjourney::threads::midjourney_websocket_thread::midjourney_websocket_thread; use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager; -use crate::services::worldlabs::state::worldlabs_bearer_bridge::WorldlabsBearerBridge; -use crate::services::worldlabs::state::worldlabs_credential_manager::WorldlabsCredentialManager; use crate::threads::third_party_task_polling_thread::third_party_task_polling_thread::third_party_task_polling_thread; -use crate::services::worldlabs::threads::worldlabs_marble_task_polling::worldlabs_marble_task_polling; use errors::AnyhowResult; use tauri::AppHandle; @@ -34,8 +31,6 @@ pub async fn handle_tauri_startup( midjourney_live_session: MidjourneyLiveSession, grok_creds_manager: GrokCredentialManager, grok_websockets: GrokWebsockets, - _worldlabs_bearer_bridge: WorldlabsBearerBridge, - worldlabs_creds_manager: WorldlabsCredentialManager, ) -> AnyhowResult<()> { set_app_log_level( @@ -98,15 +93,6 @@ pub async fn handle_tauri_startup( storyteller_creds_manager.clone(), )); - tauri::async_runtime::spawn(worldlabs_marble_task_polling( - app.clone(), - root.clone(), - app_preferences.clone(), - task_database.clone(), - worldlabs_creds_manager.clone(), - storyteller_creds_manager.clone(), - )); - tauri::async_runtime::spawn(third_party_task_polling_thread( app.clone(), root.clone(), diff --git a/crates/artcraftx/src/services/mod.rs b/crates/artcraftx/src/services/mod.rs index ca61b941..3a51b84d 100644 --- a/crates/artcraftx/src/services/mod.rs +++ b/crates/artcraftx/src/services/mod.rs @@ -4,4 +4,3 @@ pub mod local_files; pub mod higgsfield; pub mod midjourney; pub mod storyteller; -pub mod worldlabs; \ No newline at end of file diff --git a/crates/artcraftx/src/services/worldlabs/mod.rs b/crates/artcraftx/src/services/worldlabs/mod.rs deleted file mode 100644 index 0bfa4030..00000000 --- a/crates/artcraftx/src/services/worldlabs/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod state; -pub mod threads; -pub mod windows; \ No newline at end of file diff --git a/crates/artcraftx/src/services/worldlabs/state/legacy_credential_paths.rs b/crates/artcraftx/src/services/worldlabs/state/legacy_credential_paths.rs deleted file mode 100644 index 145ae2c5..00000000 --- a/crates/artcraftx/src/services/worldlabs/state/legacy_credential_paths.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! Legacy on-disk credential paths for World Labs. -//! -//! TODO(artcraftx): retire once World Labs moves to the unified TOML -//! credential store in `crate::credentials`. - -use crate::state::data_dir::subdirectory::app_credentials_dir::AppCredentialsDir; -use crate::state::data_dir::subdirectory::trait_data_subdir::DataSubdir; -use std::path::PathBuf; - -pub trait WorldlabsLegacyCredentialPaths { - fn get_worldlabs_state_path(&self) -> PathBuf; - fn get_worldlabs_cookies_path(&self) -> PathBuf; - fn get_worldlabs_bearer_path(&self) -> PathBuf; - fn get_worldlabs_refresh_path(&self) -> PathBuf; -} - -impl WorldlabsLegacyCredentialPaths for AppCredentialsDir { - fn get_worldlabs_state_path(&self) -> PathBuf { - self.path().join("worldlabs_state.json") - } - - fn get_worldlabs_cookies_path(&self) -> PathBuf { - self.path().join("worldlabs_cookies.txt") - } - - fn get_worldlabs_bearer_path(&self) -> PathBuf { - self.path().join("worldlabs_bearer.txt") - } - - fn get_worldlabs_refresh_path(&self) -> PathBuf { - self.path().join("worldlabs_refresh.txt") - } -} diff --git a/crates/artcraftx/src/services/worldlabs/state/mod.rs b/crates/artcraftx/src/services/worldlabs/state/mod.rs deleted file mode 100644 index 4465c4c6..00000000 --- a/crates/artcraftx/src/services/worldlabs/state/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod legacy_credential_paths; -pub mod worldlabs_credential_manager; -pub mod worldlabs_serializable_state; -pub mod worldlabs_bearer_bridge; - -// Private -mod worldlabs_credential_holder; diff --git a/crates/artcraftx/src/services/worldlabs/state/worldlabs_bearer_bridge.rs b/crates/artcraftx/src/services/worldlabs/state/worldlabs_bearer_bridge.rs deleted file mode 100644 index 5ffac67c..00000000 --- a/crates/artcraftx/src/services/worldlabs/state/worldlabs_bearer_bridge.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::sync::{Arc, RwLock}; -use log::error; -use crate::error::artcraftx_error::ArtcraftXError; - -#[derive(Clone)] -pub struct WorldlabsBearerBridge { - state: Arc>> -} - -#[derive(Clone, Debug)] -pub struct WorldlabsBearerBridgeInner { - pub bearer_token: String, - pub refresh_token: String, -} - -impl WorldlabsBearerBridge { - pub fn empty() -> Self { - Self { - state: Arc::new(RwLock::new(None)), - } - } - - pub fn clear(&self) -> Result<(), ArtcraftXError> { - match self.state.write() { - Ok(mut state) => { - *state = None; - Ok(()) - } - Err(err) => { - error!("Lock poisoned: {:?}", err); - Err(ArtcraftXError::RwLockWriteError) - } - } - } - - pub fn get(&self) -> Result, ArtcraftXError> { - match self.state.read() { - Ok(state) => { - Ok(state.clone()) - } - Err(err) => { - error!("Lock poisoned: {:?}", err); - Err(ArtcraftXError::RwLockReadError) - } - } - } - - pub fn set(&self, new_state: WorldlabsBearerBridgeInner) -> Result<(), ArtcraftXError> { - match self.state.write() { - Ok(mut state) => { - *state = Some(new_state); - Ok(()) - } - Err(err) => { - error!("Lock poisoned: {:?}", err); - Err(ArtcraftXError::RwLockReadError) - } - } - } -} diff --git a/crates/artcraftx/src/services/worldlabs/state/worldlabs_credential_holder.rs b/crates/artcraftx/src/services/worldlabs/state/worldlabs_credential_holder.rs deleted file mode 100644 index 6704617a..00000000 --- a/crates/artcraftx/src/services/worldlabs/state/worldlabs_credential_holder.rs +++ /dev/null @@ -1,35 +0,0 @@ -use cookie_store_wrapper::cookie_store::CookieStore; -use worldlabs_consumer_client::credentials::world_labs_bearer_token::WorldLabsBearerToken; -use worldlabs_consumer_client::credentials::world_labs_cookies::WorldLabsCookies; -use worldlabs_consumer_client::credentials::worldlabs_refresh_token::WorldLabsRefreshToken; - -#[derive(Clone)] -pub struct WorldlabsCredentialHolder { - /// Directly off the Tauri browser session. - /// Read once, write to disk - /// The Worldlabs client consumes a string-only form (rather than this cookie jar) - pub browser_cookies: Option, - - /// Copy of cookies, specifically for the client library. - /// NOT PERSISTED TO DISK. - pub world_labs_cookies: Option, - - /// Bearer token - /// This needs to be persisted to disk. - pub world_labs_bearer_token: Option, - - /// Refresh token - /// This needs to be persisted to disk. - pub world_labs_refresh_token: Option, -} - -impl WorldlabsCredentialHolder { - pub fn empty() -> Self { - Self { - browser_cookies: None, - world_labs_cookies: None, - world_labs_bearer_token: None, - world_labs_refresh_token: None, - } - } -} diff --git a/crates/artcraftx/src/services/worldlabs/state/worldlabs_credential_manager.rs b/crates/artcraftx/src/services/worldlabs/state/worldlabs_credential_manager.rs deleted file mode 100644 index 16756ea4..00000000 --- a/crates/artcraftx/src/services/worldlabs/state/worldlabs_credential_manager.rs +++ /dev/null @@ -1,227 +0,0 @@ -use crate::services::worldlabs::state::legacy_credential_paths::WorldlabsLegacyCredentialPaths; -use crate::state::data_dir::app_data_root::AppDataRoot; -use crate::services::worldlabs::state::worldlabs_credential_holder::WorldlabsCredentialHolder; -use crate::services::worldlabs::state::worldlabs_serializable_state::{WorldlabsSerializableState, SERIALIZABLE_WORLDLABS_STATE_VERSION}; -use cookie_store_wrapper::cookie_store::CookieStore; -use log::warn; -use std::sync::{Arc, RwLock}; -use worldlabs_consumer_client::credentials::world_labs_bearer_token::WorldLabsBearerToken; -use worldlabs_consumer_client::credentials::world_labs_cookies::WorldLabsCookies; -use worldlabs_consumer_client::credentials::worldlabs_refresh_token::WorldLabsRefreshToken; - -#[derive(Clone)] -pub struct WorldlabsCredentialManager { - // TODO: Put last write/last read timestamps on these. - credential_data: Arc>, - - //user_info: Arc>>, - app_data_root: AppDataRoot, -} - -impl WorldlabsCredentialManager { - pub fn initialize_empty(app_data_root: &AppDataRoot) -> Self { - Self { - credential_data: Arc::new(RwLock::new(WorldlabsCredentialHolder::empty())), - app_data_root: app_data_root.clone(), - } - } - - pub fn initialize_from_disk_infallible(app_data_root: &AppDataRoot) -> Self { - let credential_data; - - let result = WorldlabsSerializableState::read_from_disk(app_data_root); - match result { - Err(err) => { - warn!("Failed to read worldlabs state from disk: {:?}", err); - credential_data = Arc::new(RwLock::new(WorldlabsCredentialHolder::empty())); - }, - Ok(None) => { - credential_data = Arc::new(RwLock::new(WorldlabsCredentialHolder::empty())); - } - Ok(Some(state)) => { - let maybe_browser_cookies = state.user_cookies; - - let maybe_cookies = maybe_browser_cookies - .as_ref() - .map(|cookies| cookies.to_cookie_string()) - .map(|cookies| WorldLabsCookies::new(cookies)); - - let maybe_bearer = state.bearer_token - .map(|bearer| WorldLabsBearerToken::new(bearer)); - - let maybe_refresh = state.refresh_token - .map(|bearer| WorldLabsRefreshToken::new(bearer)); - - credential_data = Arc::new(RwLock::new(WorldlabsCredentialHolder { - browser_cookies: maybe_browser_cookies, - world_labs_cookies: maybe_cookies, - world_labs_bearer_token: maybe_bearer, - world_labs_refresh_token: maybe_refresh, - })); - } - }; - - Self { - credential_data, - app_data_root: app_data_root.clone(), - } - } - - pub fn maybe_copy_cookie_store(&self) -> anyhow::Result> { - match self.credential_data.read() { - Err(err) => Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err)), - Ok(holder) => Ok(holder.browser_cookies.clone()), - } - } - - pub fn maybe_copy_cookie_header_string(&self) -> anyhow::Result> { - let maybe_cookies = self.maybe_copy_cookie_store()?; - let maybe_cookies = maybe_cookies.map(|cookies| { - cookies.to_cookie_string() - }); - Ok(maybe_cookies) - } - - pub fn maybe_copy_typed_cookies(&self) -> anyhow::Result> { - let maybe_cookies = self.maybe_copy_cookie_header_string()?; - let maybe_cookies = maybe_cookies.map(|cookies| { - WorldLabsCookies::new(cookies) - }); - Ok(maybe_cookies) - } - - pub fn maybe_copy_bearer_token(&self) -> anyhow::Result> { - match self.credential_data.read() { - Err(err) => Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err)), - Ok(holder) => Ok(holder.world_labs_bearer_token.clone()), - } - } - - pub fn maybe_copy_refresh_token(&self) -> anyhow::Result> { - match self.credential_data.read() { - Err(err) => Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err)), - Ok(holder) => Ok(holder.world_labs_refresh_token.clone()), - } - } - - pub fn replace_cookie_store(&self, store: CookieStore) -> anyhow::Result<()> { - match self.credential_data.write() { - Err(err) => Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err)), - Ok(mut holder) => { - holder.browser_cookies = Some(store); - Ok(()) - } - } - } - - pub fn replace_bearer_token(&self, token: WorldLabsBearerToken) -> anyhow::Result<()> { - match self.credential_data.write() { - Err(err) => Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err)), - Ok(mut holder) => { - holder.world_labs_bearer_token = Some(token); - Ok(()) - } - } - } - - pub fn replace_refresh_token(&self, token: WorldLabsRefreshToken) -> anyhow::Result<()> { - match self.credential_data.write() { - Err(err) => Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err)), - Ok(mut holder) => { - holder.world_labs_refresh_token = Some(token); - Ok(()) - } - } - } - - pub fn replace_bearer_and_refresh_token(&self, bearer_token: WorldLabsBearerToken, refresh_token: WorldLabsRefreshToken) -> anyhow::Result<()> { - match self.credential_data.write() { - Err(err) => Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err)), - Ok(mut holder) => { - holder.world_labs_bearer_token = Some(bearer_token); - holder.world_labs_refresh_token = Some(refresh_token); - 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() - } - - // NB: This is just a heuristic. We'll add better checks later. - pub fn session_appears_active(&self) -> anyhow::Result { - let holder = match self.credential_data.read() { - Err(err) => return Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err)), - Ok(store) => store.clone(), - }; - - if holder.world_labs_bearer_token.is_some() { - return Ok(true); - } - - if holder.browser_cookies.is_none() { - return Ok(false); - } - - Ok(false) - } - - pub fn clear_credentials(&self) -> anyhow::Result<()> { - match self.credential_data.write() { - Err(err) => return Err(anyhow::anyhow!("Failed to acquire write lock: {:?}", err)), - Ok(mut store) => *store = WorldlabsCredentialHolder::empty(), - } - Ok(()) - } - - pub fn persist_to_disk(&self) -> anyhow::Result<()> { - let creds = match self.credential_data.read() { - Err(err) => return Err(anyhow::anyhow!("Failed to acquire read lock: {:?}", err)), - Ok(store) => store.clone(), - }; - - let state = WorldlabsSerializableState { - version: SERIALIZABLE_WORLDLABS_STATE_VERSION, - user_cookies: creds.browser_cookies.clone(), - user_id: None, // TODO - user_email: None, // TODO - bearer_token: creds.world_labs_bearer_token - .as_ref() - .map(|token| token.to_raw_string()), - refresh_token: creds.world_labs_refresh_token - .as_ref() - .map(|token| token.to_raw_string()), - }; - - let path = self.app_data_root.credentials_dir().get_worldlabs_state_path(); - let serialized = serde_json::to_string(&state)?; - - std::fs::write(path, serialized)?; - - // TODO: This is just for building the client and testing. - { - if let Some(cookies) = creds.browser_cookies.as_ref() { - let cookies_header = cookies.to_cookie_string(); - let path = self.app_data_root.credentials_dir().get_worldlabs_cookies_path(); - std::fs::write(path, cookies_header)?; - } - - // TODO: This is just for building the client and testing. - if let Some(bearer) = creds.world_labs_bearer_token.as_ref() { - let value = bearer.to_raw_string(); - let path = self.app_data_root.credentials_dir().get_worldlabs_bearer_path(); - std::fs::write(path, value)?; - } - - if let Some(refresh) = creds.world_labs_refresh_token.as_ref() { - let value = refresh.to_raw_string(); - let path = self.app_data_root.credentials_dir().get_worldlabs_refresh_path(); - std::fs::write(path, value)?; - } - } - - Ok(()) - } -} diff --git a/crates/artcraftx/src/services/worldlabs/state/worldlabs_serializable_state.rs b/crates/artcraftx/src/services/worldlabs/state/worldlabs_serializable_state.rs deleted file mode 100644 index eb12a1c4..00000000 --- a/crates/artcraftx/src/services/worldlabs/state/worldlabs_serializable_state.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::services::worldlabs::state::legacy_credential_paths::WorldlabsLegacyCredentialPaths; -use crate::state::data_dir::app_data_root::AppDataRoot; -use cookie_store_wrapper::cookie_store::CookieStore; -use serde_derive::{Deserialize, Serialize}; -use std::fs::read_to_string; - -/// History -/// v1 - initial -pub (super) const SERIALIZABLE_WORLDLABS_STATE_VERSION: u32 = 1; - -/// We only need to serialize Grok cookies. -/// Most of the other state (verification_token, etc.) can be pulled at runtime. -/// We will cache the `user_id` and `user_email`. -#[derive(Serialize, Deserialize)] -pub (super) struct WorldlabsSerializableState { - pub (super) version: u32, - - #[serde(skip_serializing_if = "Option::is_none")] - pub (super) user_cookies: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub (super) bearer_token: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub (super) refresh_token: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub (super) user_id: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub (super) user_email: Option, -} - -impl WorldlabsSerializableState { - pub fn read_from_disk(root: &AppDataRoot) -> anyhow::Result> { - let state_path = root.credentials_dir().get_worldlabs_state_path(); - - if !state_path.exists() { - return Ok(None); - } - - let contents = read_to_string(&state_path)? - .trim() - .to_string(); - - let state: WorldlabsSerializableState = serde_json::from_str(&contents)?; - - Ok(Some(state)) - } -} diff --git a/crates/artcraftx/src/services/worldlabs/threads/mod.rs b/crates/artcraftx/src/services/worldlabs/threads/mod.rs deleted file mode 100644 index e03acdf8..00000000 --- a/crates/artcraftx/src/services/worldlabs/threads/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod worldlabs_marble_task_polling; \ No newline at end of file diff --git a/crates/artcraftx/src/services/worldlabs/threads/worldlabs_marble_task_polling.rs b/crates/artcraftx/src/services/worldlabs/threads/worldlabs_marble_task_polling.rs deleted file mode 100644 index 611b0a63..00000000 --- a/crates/artcraftx/src/services/worldlabs/threads/worldlabs_marble_task_polling.rs +++ /dev/null @@ -1,249 +0,0 @@ -use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager; -use crate::state::data_dir::app_data_root::AppDataRoot; -use crate::state::data_dir::subdirectory::trait_data_subdir::DataSubdir; -use crate::state::database::task_database::TaskDatabase; -use crate::database::task_database_pending_statuses::TASK_DATABASE_PENDING_STATUSES; -use crate::services::storyteller::state::storyteller_credential_manager::StorytellerCredentialManager; -use crate::services::worldlabs::state::worldlabs_credential_manager::WorldlabsCredentialManager; -use core_types::enums::generation_source::GenerationSource; -use sqlite_identifiers::enums::task_media_file_class::TaskMediaFileClass; -use errors::AnyhowResult; -use uuid_utils::uuid::generate_random_uuid; -use log::{error, info}; -use sqlite_database::queries::read::list_tasks_by_provider_and_status::{list_tasks_by_provider_and_status, ListTasksByProviderAndStatusArgs, TaskList}; -use sqlite_database::queries::task::Task; -use std::collections::HashMap; -use std::fs::File; -use std::io::Write; -use std::path::PathBuf; -use std::time::Duration; -use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet; -use crate::threads::task_completion::complete_task_with_local_files::{complete_task_with_local_files, CompleteTaskArgs}; -use crate::threads::task_completion::upload_results_to_artcraft::CompletionPrompt; -use tauri::AppHandle; -use worldlabs_consumer_client::api::api_types::world_id::WorldObjectId; -use worldlabs_consumer_client::api::requests::worlds::poll_world_status::{poll_world_status, PollWorldStatusArgs}; -use worldlabs_consumer_client::credentials::world_labs_bearer_token::WorldLabsBearerToken; -use worldlabs_consumer_client::credentials::world_labs_cookies::WorldLabsCookies; -use worldlabs_consumer_client::credentials::worldlabs_refresh_token::WorldLabsRefreshToken; - -/// Download filename slug when the task has no model type recorded. -const MARBLE_FALLBACK_MODEL_SLUG: &str = "marble"; - -pub async fn worldlabs_marble_task_polling( - app_handle: AppHandle, - app_data_root: AppDataRoot, - app_preferences: AppPreferencesManager, - task_database: TaskDatabase, - creds: WorldlabsCredentialManager, - storyteller_creds_manager: StorytellerCredentialManager, -) -> ! { - loop { - let res = polling_loop( - &app_handle, - &app_data_root, - &app_preferences, - &task_database, - &creds, - &storyteller_creds_manager, - ).await; - if let Err(err) = res { - error!("An error occurred: {:?}", err); - } - // NB: Only sleep if an error occurs. - tokio::time::sleep(Duration::from_millis(30_000)).await; - } -} - -async fn polling_loop( - app_handle: &AppHandle, - app_data_root: &AppDataRoot, - app_preferences: &AppPreferencesManager, - task_database: &TaskDatabase, - worldlabs_creds: &WorldlabsCredentialManager, - storyteller_creds_manager: &StorytellerCredentialManager, -) -> AnyhowResult<()> { - loop { - if !worldlabs_creds.do_task_polling()? { - tokio::time::sleep(Duration::from_millis(10_000)).await; - continue; - } - - // Optional: without an ArtCraft session, results are still saved locally. - let maybe_storyteller_creds = storyteller_creds_manager.get_credentials()?; - - let world_labs_cookies = match worldlabs_creds.maybe_copy_typed_cookies()? { - Some(cookies) => cookies, - None => { - info!("No full WorldLabs cookies"); - tokio::time::sleep(Duration::from_millis(30_000)).await; - continue; - } - }; - - let world_labs_bearer = match worldlabs_creds.maybe_copy_bearer_token()? { - Some(bearer) => bearer, - None => { - info!("No full WorldLabs bearer"); - tokio::time::sleep(Duration::from_millis(30_000)).await; - continue; - } - }; - - let world_labs_refresh = match worldlabs_creds.maybe_copy_refresh_token()? { - Some(bearer) => bearer, - None => { - info!("No full WorldLabs refresh"); - tokio::time::sleep(Duration::from_millis(30_000)).await; - continue; - } - }; - - let local_tasks = list_tasks_by_provider_and_status(ListTasksByProviderAndStatusArgs { - db: task_database.get_connection(), - provider: GenerationSource::WorldLabs, - task_statuses: &TASK_DATABASE_PENDING_STATUSES, - }).await?; - - poll_worldlabs_tasks( - app_handle, - app_data_root, - app_preferences, - task_database, - &world_labs_cookies, - &world_labs_bearer, - &world_labs_refresh, - maybe_storyteller_creds.as_ref(), - local_tasks, - ).await?; - - tokio::time::sleep(Duration::from_millis(2_000)).await; - } -} - -async fn poll_worldlabs_tasks( - app_handle: &AppHandle, - app_data_root: &AppDataRoot, - app_preferences: &AppPreferencesManager, - task_database: &TaskDatabase, - world_labs_cookies: &WorldLabsCookies, - world_labs_bearer: &WorldLabsBearerToken, - _world_labs_refresh: &WorldLabsRefreshToken, - maybe_storyteller_creds: Option<&StorytellerCredentialSet>, - local_tasks: TaskList, -) -> AnyhowResult<()> { - let local_tasks = local_tasks.tasks; - - if local_tasks.is_empty() { - return Ok(()) - } - - info!("WorldLabs tasks waiting: {:?}", local_tasks.len()); - - // Map of WorldLabs World ID to Local Task. - let local_tasks_by_world_labs_world_id = local_tasks.iter() - .filter_map(|task| { - if let Some(provider_job_id) = &task.provider_job_id { - Some((provider_job_id.clone(), task.clone())) - } else { - None - } - }) - .collect::>(); - - for (world_id, local_task) in local_tasks_by_world_labs_world_id.iter() { - let world_id = WorldObjectId(world_id.to_string()); - - let poll_world_response = poll_world_status(PollWorldStatusArgs { - cookies: &world_labs_cookies, - bearer_token: &world_labs_bearer, - world_id: &world_id, - request_timeout: None, - }).await?; - - if !poll_world_response.is_complete { - tokio::time::sleep(Duration::from_millis(2_000)).await; - continue; - } - - let spz_url = match poll_world_response.spz_splat_url { - Some(url) => url, - None => { - error!("No spz splat URL despite being marked complete"); - continue; - } - }; - - let result = complete_spz_splat( - app_handle, - app_data_root, - app_preferences, - task_database, - maybe_storyteller_creds, - local_task, - &spz_url, - ).await; - - if let Err(err) = result { - error!("Failed to complete WorldLabs task {}: {:?}", local_task.id.as_str(), err); - } - } - - tokio::time::sleep(Duration::from_millis(5_000)).await; - - Ok(()) -} - -/// Download the finished splat to the temp dir, then hand it to the shared -/// completion routine. -async fn complete_spz_splat( - app_handle: &AppHandle, - app_data_root: &AppDataRoot, - app_preferences: &AppPreferencesManager, - task_database: &TaskDatabase, - maybe_storyteller_creds: Option<&StorytellerCredentialSet>, - local_task: &Task, - spz_url: &str, -) -> AnyhowResult { - info!("Downloading generated spz splat ..."); - - let download_path = download_spz(spz_url, app_data_root).await?; - - complete_task_with_local_files(CompleteTaskArgs { - app_handle, - app_data_root, - app_preferences, - task_database, - maybe_storyteller_creds, - task: local_task, - generation_provider: GenerationSource::WorldLabs, - media_class: TaskMediaFileClass::Splat, - prompt: CompletionPrompt::None, - fallback_model_slug: MARBLE_FALLBACK_MODEL_SLUG, - local_files: &[download_path], - }).await -} - -async fn download_spz( - spz_url: &str, - app_data_root: &AppDataRoot, -) -> AnyhowResult { - info!("Downloading splat from URL: {}", spz_url); - - let response = reqwest::get(spz_url).await?; - let image_bytes = response.bytes().await?; - - let uuid = generate_random_uuid(); - let extension_without_period = "ceramic.spz"; - - let tempdir = app_data_root.temp_dir().path(); - let download_filename = format!("{}.{}", uuid, extension_without_period); - let download_path = tempdir.join(download_filename); - - info!("Writing to path: {:?}", download_path); - - let mut file = File::create(&download_path)?; - file.write_all(&image_bytes)?; - - Ok(download_path) -} diff --git a/crates/artcraftx/src/services/worldlabs/windows/mod.rs b/crates/artcraftx/src/services/worldlabs/windows/mod.rs deleted file mode 100644 index b4a07130..00000000 --- a/crates/artcraftx/src/services/worldlabs/windows/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod worldlabs_login_window; \ No newline at end of file diff --git a/crates/artcraftx/src/services/worldlabs/windows/worldlabs_login_window/mod.rs b/crates/artcraftx/src/services/worldlabs/windows/worldlabs_login_window/mod.rs deleted file mode 100644 index b1a1c8bb..00000000 --- a/crates/artcraftx/src/services/worldlabs/windows/worldlabs_login_window/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod worldlabs_login_window_open; -pub (super) mod worldlabs_login_window_thread; -pub (super) mod worldlabs_login_webview_extract_cookies; -pub (super) mod worldlabs_javascript; diff --git a/crates/artcraftx/src/services/worldlabs/windows/worldlabs_login_window/worldlabs_javascript.rs b/crates/artcraftx/src/services/worldlabs/windows/worldlabs_login_window/worldlabs_javascript.rs deleted file mode 100644 index 097520e0..00000000 --- a/crates/artcraftx/src/services/worldlabs/windows/worldlabs_login_window/worldlabs_javascript.rs +++ /dev/null @@ -1,91 +0,0 @@ - - - -pub (super) const WORLDLABS_JAVASCRIPT_EXPORT_BEARER_TOKENS : &str = r#" - (async () => { - if (window.tokensExported) { - return; - } - - const request = indexedDB.open("firebaseLocalStorageDb"); - - request.onerror = (event) => {}; - - request.onsuccess = (event) => { - const db = event.target.result; - - const tx = db.transaction("firebaseLocalStorage", "readonly"); - const store = tx.objectStore("firebaseLocalStorage"); - - const keysRequest = store.getAllKeys(); - - keysRequest.onerror = (event) => {}; - - keysRequest.onsuccess = () => { - const keys = keysRequest.result; // array of keys - if (keys.length < 1) { - return; - } - - const key = keys[0]; - const getKeyRequest = store.getKey(key); - - getKeyRequest.onsuccess = () => {}; - - store.openCursor().onsuccess = async (event) => { - const cursor = event.target.result; - - if (cursor) { - let tokens = cursor.value?.value?.stsTokenManager; - - if (tokens?.accessToken && tokens?.refreshToken) { - // Send to Tauri - let result = await window.__TAURI__.core.invoke("worldlabs_receive_bearer_command", { - request: { - bearer_token: tokens.accessToken, - refresh_token: tokens.refreshToken, - } - }); - window.tokensExported = true; - } - cursor.continue(); - } - }; // end cursor - - }; // end keyRequest - }; // end DB - })(); - "#; - - -/// Take the Bearer tokens and send them to Tauri -pub (super) const WORLDLABS_JAVASCRIPT_SEND_BEARER_TO_TAURI : &str = r#" - (async () => { - if (!(window.tokens?.accessToken && window.tokens?.refreshToken)) { - return; - } - - let result = await window.__TAURI__.core.invoke("worldlabs_receive_bearer_command", { - request: { - bearer_token: window.tokens.accessToken, - refresh_token: window.tokens.refreshToken, - } - }); - - error.log('>>> result', result); - - })(); - "#; - - -/// See if the pricing button exists. -/// This is only for the logged-in state. -/// We can use redirection to detect completion. -pub (super) const WORLDLABS_JAVASCRIPT_SEE_IF_PRICING_EXISTS : &str = r#" - (() => { - let pricing = document.querySelectorAll("a[href='/pricing']"); - if (pricing.length > 0) { - window.location.href = "https://www.worldlabs.ai/about"; - } - })(); - "#; diff --git a/crates/artcraftx/src/services/worldlabs/windows/worldlabs_login_window/worldlabs_login_webview_extract_cookies.rs b/crates/artcraftx/src/services/worldlabs/windows/worldlabs_login_window/worldlabs_login_webview_extract_cookies.rs deleted file mode 100644 index 5bea1eb7..00000000 --- a/crates/artcraftx/src/services/worldlabs/windows/worldlabs_login_window/worldlabs_login_webview_extract_cookies.rs +++ /dev/null @@ -1,44 +0,0 @@ -use cookie_store_wrapper::cookie_store::{CapturedCookie, CookieStore}; -use errors::AnyhowResult; -use once_cell::sync::Lazy; -use reqwest::Url; -use tauri::webview::Cookie; -use tauri::WebviewWindow; - -static ROOT_COOKIE_URL: Lazy = Lazy::new(|| { - Url::parse("https://worldlabs.ai").expect("URL should parse") -}); - -pub fn worldlabs_login_webview_extract_cookies(webview: &WebviewWindow) -> AnyhowResult { - let mut cookie_store = CookieStore::empty(); - let cookies = get_all_worldlabs_cookies(webview)?; - for cookie in cookies.iter() { - cookie_store.insert_captured( - CapturedCookie { - name: cookie.name().to_string(), - value: cookie.value().to_string(), - maybe_domain: cookie.domain().map(str::to_string), - maybe_path: cookie.path().map(str::to_string), - }, - &ROOT_COOKIE_URL, - ); - } - Ok(cookie_store) -} - -fn get_all_worldlabs_cookies(webview: &WebviewWindow) -> AnyhowResult>> { - //let www_cookies = webview.cookies_for_url(WWW_COOKIE_URL.clone())?; - let root_cookies = webview.cookies_for_url(ROOT_COOKIE_URL.clone())?; - - let all_cookies = root_cookies; - //let mut cookie_names = HashSet::new(); - - //for cookie in root_cookies.iter() { - // if !cookie_names.contains(cookie.name()) { - // cookie_names.insert(cookie.name().to_string()); - // all_cookies.push(cookie.clone()); - // } - //} - - Ok(all_cookies) -} diff --git a/crates/artcraftx/src/services/worldlabs/windows/worldlabs_login_window/worldlabs_login_window_open.rs b/crates/artcraftx/src/services/worldlabs/windows/worldlabs_login_window/worldlabs_login_window_open.rs deleted file mode 100644 index 24915e22..00000000 --- a/crates/artcraftx/src/services/worldlabs/windows/worldlabs_login_window/worldlabs_login_window_open.rs +++ /dev/null @@ -1,107 +0,0 @@ -use crate::state::data_dir::app_data_root::AppDataRoot; -use crate::utils::window::clear_all_webview_cookies::clear_all_webview_cookies; -use crate::services::worldlabs::state::worldlabs_bearer_bridge::WorldlabsBearerBridge; -use crate::services::worldlabs::state::worldlabs_credential_manager::WorldlabsCredentialManager; -use crate::services::worldlabs::windows::worldlabs_login_window::worldlabs_login_window_thread::worldlabs_login_window_thread; -use anyhow::anyhow; -use errors::AnyhowResult; -use log::info; -use once_cell::sync::Lazy; -use reqwest::Url; -use std::time::Duration; -use tauri::webview::NewWindowResponse; -use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder}; - -/// Name of the window -pub (super) const WORLDLABS_LOGIN_WINDOW_NAME: &str = "worldlabs_login_window"; - -pub (super) static START_URL: Lazy = Lazy::new(|| { - Url::parse("https://google.com").expect("URL should parse") -}); - -pub (super) static WORLDLABS_HOMEPAGE_URL: Lazy = Lazy::new(|| { - Url::parse("https://marble.worldlabs.ai/").expect("URL should parse") -}); - -pub (super) static WORLDLABS_LOGIN_URL: Lazy = Lazy::new(|| { - Url::parse("https://marble.worldlabs.ai/").expect("URL should parse") -}); - -pub async fn worldlabs_login_window_open( - app: &AppHandle, - app_data_root: &AppDataRoot, - worldlabs_bearer_bridge: &WorldlabsBearerBridge, - worldlabs_creds_manager: &WorldlabsCredentialManager, -) -> AnyhowResult<()> { - - if app.get_window(WORLDLABS_LOGIN_WINDOW_NAME).is_some() { - return Err(anyhow!("Login window already open")); - } - - info!("Clearing bearer bridge..."); - - worldlabs_bearer_bridge.clear()?; - - let url = WebviewUrl::External(START_URL.clone()); - - let window = WebviewWindowBuilder::new(app, WORLDLABS_LOGIN_WINDOW_NAME, url) - .on_new_window(move |_url, _features| { - // WorldLabs needs popups as they (1) don't have a distinct login page - // (it's embedded) and (2) they don't redirect with OAuth, but rather - // open a popup to Google. It still works, but it's annoying. - NewWindowResponse::Allow - }) - .always_on_top(false) - .title("Login to WorldLabs") - .center() - .resizable(true) - .visible(true) - .closable(true) - .min_inner_size(800.0, 800.0) - .focused(true) - .devtools(true) - .build()?; - - let webview = window.get_webview(WORLDLABS_LOGIN_WINDOW_NAME) - .ok_or_else(|| anyhow!("no webview found"))?; - - clear_all_webview_cookies(&webview)?; - - webview.navigate(WORLDLABS_HOMEPAGE_URL.clone())?; - - tokio::time::sleep(Duration::from_millis(200)).await; - - info!("Running script..."); - - for _ in 0 .. 10 { - tokio::time::sleep(Duration::from_millis(100)).await; - - // Close the "welcome" modal - webview.eval(r#"document.querySelectorAll("div:has(h2) ~ button[data-slot=dialog-close]").forEach((el) => el.click())"#)?; - } - - for _ in 0 .. 10 { - tokio::time::sleep(Duration::from_millis(100)).await; - - // Open the "login" modal - webview.eval(r#"document.querySelectorAll("button:has(span[data-slot=avatar])").forEach((el) => el.click())"#)?; - } - - info!("Script done..."); - - let app_handle = app.clone(); - let app_data_root = app_data_root.clone(); - let worldlabs_creds_manager = worldlabs_creds_manager.clone(); - let worldlabs_bearer_bridge = worldlabs_bearer_bridge.clone(); - - let _ = tauri::async_runtime::spawn(async move { - worldlabs_login_window_thread( - app_handle, - app_data_root, - worldlabs_bearer_bridge, - worldlabs_creds_manager - ).await; - }); - - Ok(()) -} diff --git a/crates/artcraftx/src/services/worldlabs/windows/worldlabs_login_window/worldlabs_login_window_thread.rs b/crates/artcraftx/src/services/worldlabs/windows/worldlabs_login_window/worldlabs_login_window_thread.rs deleted file mode 100644 index 163be996..00000000 --- a/crates/artcraftx/src/services/worldlabs/windows/worldlabs_login_window/worldlabs_login_window_thread.rs +++ /dev/null @@ -1,116 +0,0 @@ -use crate::events::basic_sendable_event_trait::BasicSendableEvent; -use crate::events::functional_events::refresh_account_state_event::RefreshAccountStateEvent; -use crate::events::sendable_event_trait::SendableEvent; -use crate::state::data_dir::app_data_root::AppDataRoot; -use crate::services::worldlabs::state::worldlabs_bearer_bridge::WorldlabsBearerBridge; -use crate::services::worldlabs::state::worldlabs_credential_manager::WorldlabsCredentialManager; -use crate::services::worldlabs::windows::worldlabs_login_window::worldlabs_javascript::WORLDLABS_JAVASCRIPT_EXPORT_BEARER_TOKENS; -use crate::services::worldlabs::windows::worldlabs_login_window::worldlabs_login_webview_extract_cookies::worldlabs_login_webview_extract_cookies; -use crate::services::worldlabs::windows::worldlabs_login_window::worldlabs_login_window_open::WORLDLABS_LOGIN_WINDOW_NAME; -use core_types::enums::generation_source::GenerationSource; -use errors::AnyhowResult; -use log::{error, info}; -use tauri::{AppHandle, Manager, WebviewWindow}; -use worldlabs_consumer_client::credentials::world_labs_bearer_token::WorldLabsBearerToken; -use worldlabs_consumer_client::credentials::worldlabs_refresh_token::WorldLabsRefreshToken; - -pub async fn worldlabs_login_window_thread( - app: AppHandle, - app_data_root: AppDataRoot, - worldlabs_bearer_bridge: WorldlabsBearerBridge, - worldlabs_creds_manager: WorldlabsCredentialManager, -) { - let mut visited_login = false; - - loop { - let login_webview_window = match app.get_webview_window(WORLDLABS_LOGIN_WINDOW_NAME) { - Some(webview) => webview, - None => { - info!("Exit WorldLabs login thread."); - return; // NB: Only exit if we don't have the webview. - } - }; - - let result = check_login_window( - &app, - &login_webview_window, - &app_data_root, - &worldlabs_bearer_bridge, - &worldlabs_creds_manager, - &mut visited_login, - ).await; - - match result { - Err(err) => { - error!("Error checking Grok login window: {:?}", err); - } - Ok(false) => {} // Continue iteration and try again... - Ok(true) => { - info!("Successfully saved cookies from login window. Closing."); - if let Err(err) = login_webview_window.close() { - error!("Error closing login window: {:?}", err); - } - return; - } - } - - tokio::time::sleep(std::time::Duration::from_millis(2_000)).await; - } -} - -/// NOTE: THERE IS THIS - https://marble.worldlabs.ai/sign-in?redirect=%2Fworlds - -/// Returns true if we can exit. -async fn check_login_window( - app_handle: &AppHandle, - webview_window: &WebviewWindow, - _app_data_root: &AppDataRoot, - worldlabs_bearer_bridge: &WorldlabsBearerBridge, - worldlabs_creds_manager: &WorldlabsCredentialManager, - _visited_login: &mut bool, -) -> AnyhowResult { - - // World labs has no distinct login page. Everything is in a single SPA. - - webview_window.eval(WORLDLABS_JAVASCRIPT_EXPORT_BEARER_TOKENS)?; - - let maybe_bearer = worldlabs_bearer_bridge.get()?; - - let bearer_info = match maybe_bearer { - Some(bearer) => bearer, - None => { - return Ok(false); // Not logged in. - } - }; - - let cookie_store = worldlabs_login_webview_extract_cookies(webview_window)?; - - info!("Current cookies (len {}): {:?}", cookie_store.len(), cookie_store.to_cookie_string()); - - worldlabs_creds_manager.replace_cookie_store(cookie_store)?; - - let bearer_token = WorldLabsBearerToken::new(bearer_info.bearer_token); - - worldlabs_creds_manager.replace_bearer_token(bearer_token)?; - - let refresh_token = WorldLabsRefreshToken::new(bearer_info.refresh_token); - - worldlabs_creds_manager.replace_refresh_token(refresh_token)?; - - let result = worldlabs_creds_manager.persist_to_disk(); - - if let Err(err) = result { - error!("Error persisting WorldLabs credentials to disk: {:?}", err); - return Ok(false); - } - - let event = RefreshAccountStateEvent { - provider: Some(GenerationSource::WorldLabs), - }; - - if let Err(err) = event.send(&app_handle) { - error!("Failed to send RefreshAccountStateEvent: {:?}", err); // Fail open - } - - Ok(true) -} diff --git a/crates/models/src/enums/generation_provider.rs b/crates/models/src/enums/generation_provider.rs index ebd96390..6e4adc64 100644 --- a/crates/models/src/enums/generation_provider.rs +++ b/crates/models/src/enums/generation_provider.rs @@ -16,8 +16,6 @@ pub enum GenerationProvider { Higgsfield, /// First-party (cookie-session) Midjourney. Midjourney, - /// First-party (cookie-session) World Labs. - WorldLabs, } #[cfg(test)] @@ -27,7 +25,6 @@ mod tests { #[test] fn serializes_to_frontend_values() { assert_eq!(serde_json::to_string(&GenerationProvider::Artcraft).unwrap(), "\"artcraft\""); - assert_eq!(serde_json::to_string(&GenerationProvider::WorldLabs).unwrap(), "\"world_labs\""); assert_eq!(serde_json::to_string(&GenerationProvider::Higgsfield).unwrap(), "\"higgsfield\""); assert_eq!(serde_json::from_str::("\"higgsfield\"").unwrap(), GenerationProvider::Higgsfield); } diff --git a/crates/models/src/providers/splat_providers.rs b/crates/models/src/providers/splat_providers.rs index cba4863a..b4161d0c 100644 --- a/crates/models/src/providers/splat_providers.rs +++ b/crates/models/src/providers/splat_providers.rs @@ -27,13 +27,6 @@ fn splat_providers() -> Vec { SplatModel::Marble1p0Draft, SplatModel::TripoSplat, ]), - // First-party (cookie-session) World Labs: the Marble models. - SplatProviderOffering::of(GenerationProvider::WorldLabs, &[ - SplatModel::Marble1p1, - SplatModel::Marble1p1Plus, - SplatModel::Marble1p0, - SplatModel::Marble1p0Draft, - ]), ] } diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 6cc28cac..aae4b035 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -28,7 +28,6 @@ midjourney_client.workspace = true seedance2pro_client.workspace = true sqlite_identifiers.workspace = true url_utils.workspace = true -worldlabs_api_client.workspace = true # External anyhow.workspace = true diff --git a/crates/router/src/api/router_provider.rs b/crates/router/src/api/router_provider.rs index 50a87e48..55fb16de 100644 --- a/crates/router/src/api/router_provider.rs +++ b/crates/router/src/api/router_provider.rs @@ -15,5 +15,4 @@ pub enum RouterProvider { /// First-party (cookie-session) Midjourney. Midjourney, Seedance2Pro, - WorldLabs, } diff --git a/crates/router/src/client/mod.rs b/crates/router/src/client/mod.rs index 08ea8842..680be1a1 100644 --- a/crates/router/src/client/mod.rs +++ b/crates/router/src/client/mod.rs @@ -11,4 +11,3 @@ pub mod router_grok_client; pub mod router_higgsfield_client; pub mod router_midjourney_client; pub mod router_seedance2pro_client; -pub mod router_worldlabs_client; diff --git a/crates/router/src/client/multi_router_client.rs b/crates/router/src/client/multi_router_client.rs index f9857a5e..6edf7b6b 100644 --- a/crates/router/src/client/multi_router_client.rs +++ b/crates/router/src/client/multi_router_client.rs @@ -6,7 +6,6 @@ use crate::client::router_grok_client::RouterGrokClient; use crate::client::router_higgsfield_client::RouterHiggsfieldClient; use crate::client::router_midjourney_client::RouterMidjourneyClient; use crate::client::router_seedance2pro_client::RouterSeedance2ProClient; -use crate::client::router_worldlabs_client::RouterWorldLabsClient; use crate::errors::client_error::{ClientError, ClientType}; pub struct MultiRouterClient { @@ -18,7 +17,6 @@ pub struct MultiRouterClient { pub(crate) higgsfield_client: Option, pub(crate) midjourney_client: Option, pub(crate) seedance2pro_client: Option, - pub(crate) worldlabs_client: Option, } impl MultiRouterClient { @@ -62,8 +60,4 @@ impl MultiRouterClient { .ok_or(ClientError::ClientNotConfigured(ClientType::Seedance2Pro)) } - pub fn get_worldlabs_client_ref(&self) -> Result<&RouterWorldLabsClient, ClientError> { - self.worldlabs_client.as_ref() - .ok_or(ClientError::ClientNotConfigured(ClientType::WorldLabs)) - } } diff --git a/crates/router/src/client/multi_router_client_builder.rs b/crates/router/src/client/multi_router_client_builder.rs index 6bf25e98..cd8b1a4c 100644 --- a/crates/router/src/client/multi_router_client_builder.rs +++ b/crates/router/src/client/multi_router_client_builder.rs @@ -7,7 +7,6 @@ use crate::client::router_grok_client::RouterGrokClient; use crate::client::router_higgsfield_client::RouterHiggsfieldClient; use crate::client::router_midjourney_client::RouterMidjourneyClient; use crate::client::router_seedance2pro_client::RouterSeedance2ProClient; -use crate::client::router_worldlabs_client::RouterWorldLabsClient; pub struct MultiRouterClientBuilder { artcraft_client: Option, @@ -18,7 +17,6 @@ pub struct MultiRouterClientBuilder { higgsfield_client: Option, midjourney_client: Option, seedance2pro_client: Option, - worldlabs_client: Option, } impl MultiRouterClientBuilder { @@ -32,7 +30,6 @@ impl MultiRouterClientBuilder { higgsfield_client: None, midjourney_client: None, seedance2pro_client: None, - worldlabs_client: None, } } @@ -76,10 +73,6 @@ impl MultiRouterClientBuilder { self } - pub fn set_worldlabs_client(mut self, client: RouterWorldLabsClient) -> Self { - self.worldlabs_client = Some(client); - self - } pub fn build(self) -> MultiRouterClient { MultiRouterClient { @@ -91,7 +84,6 @@ impl MultiRouterClientBuilder { higgsfield_client: self.higgsfield_client, midjourney_client: self.midjourney_client, seedance2pro_client: self.seedance2pro_client, - worldlabs_client: self.worldlabs_client, } } } diff --git a/crates/router/src/client/router_client.rs b/crates/router/src/client/router_client.rs index ab182640..b4011442 100644 --- a/crates/router/src/client/router_client.rs +++ b/crates/router/src/client/router_client.rs @@ -7,7 +7,6 @@ use crate::client::router_grok_client::RouterGrokClient; use crate::client::router_higgsfield_client::RouterHiggsfieldClient; use crate::client::router_midjourney_client::RouterMidjourneyClient; use crate::client::router_seedance2pro_client::RouterSeedance2ProClient; -use crate::client::router_worldlabs_client::RouterWorldLabsClient; use crate::errors::client_error::{ClientError, ClientType}; pub enum RouterClient { @@ -20,7 +19,6 @@ pub enum RouterClient { Higgsfield(RouterHiggsfieldClient), Midjourney(RouterMidjourneyClient), Seedance2Pro(RouterSeedance2ProClient), - WorldLabs(RouterWorldLabsClient), } impl RouterClient { @@ -88,11 +86,4 @@ impl RouterClient { } } - pub fn get_worldlabs_client_ref(&self) -> Result<&RouterWorldLabsClient, ClientError> { - match self { - RouterClient::WorldLabs(client) => Ok(client), - RouterClient::Multi(multi) => multi.get_worldlabs_client_ref(), - _ => Err(ClientError::ClientNotConfigured(ClientType::WorldLabs)), - } - } } diff --git a/crates/router/src/client/router_worldlabs_client.rs b/crates/router/src/client/router_worldlabs_client.rs deleted file mode 100644 index 1967085f..00000000 --- a/crates/router/src/client/router_worldlabs_client.rs +++ /dev/null @@ -1,16 +0,0 @@ -use worldlabs_api_client::credentials::world_labs_api_creds::WorldLabsApiCreds; - -/// A client for the official World Labs (Marble) API. -pub struct RouterWorldLabsClient { - pub(crate) creds: WorldLabsApiCreds, -} - -impl RouterWorldLabsClient { - pub fn new(creds: WorldLabsApiCreds) -> Self { - Self { creds } - } - - pub fn new_from_raw_key(api_key: &str) -> Self { - Self::new(WorldLabsApiCreds::new(api_key.to_string())) - } -} diff --git a/crates/router/src/errors/client_error.rs b/crates/router/src/errors/client_error.rs index 3dc08a4b..f6c1e26e 100644 --- a/crates/router/src/errors/client_error.rs +++ b/crates/router/src/errors/client_error.rs @@ -14,7 +14,6 @@ pub enum ClientType { Higgsfield, Midjourney, Seedance2Pro, - WorldLabs, } impl Display for ClientType { @@ -28,7 +27,6 @@ impl Display for ClientType { Self::Higgsfield => write!(f, "Higgsfield"), Self::Midjourney => write!(f, "Midjourney"), Self::Seedance2Pro => write!(f, "Seedance2Pro"), - Self::WorldLabs => write!(f, "WorldLabs"), } } } diff --git a/crates/router/src/errors/provider_error.rs b/crates/router/src/errors/provider_error.rs index 2b80369e..a53bc184 100644 --- a/crates/router/src/errors/provider_error.rs +++ b/crates/router/src/errors/provider_error.rs @@ -7,7 +7,6 @@ use grok_api_client::error::grok_error::GrokError; use higgsfield_client::error::higgsfield_error::HiggsfieldError; use midjourney_client::error::midjourney_error::MidjourneyError; use seedance2pro_client::error::seedance2pro_error::Seedance2ProError; -use worldlabs_api_client::error::world_labs_error::WorldLabsError; #[derive(Debug)] pub enum ProviderError { @@ -30,7 +29,6 @@ pub enum ProviderError { /// (e.g. softban, filtered prompt). The string carries the raw detail. MidjourneySubmitRejected(String), Seedance2Pro(Seedance2ProError), - WorldLabs(WorldLabsError), } impl Error for ProviderError {} @@ -48,7 +46,6 @@ impl Display for ProviderError { Self::MidjourneySubscriptionRequired(detail) => write!(f, "Midjourney subscription required: {}", detail), Self::MidjourneySubmitRejected(detail) => write!(f, "Midjourney submit rejected: {}", detail), Self::Seedance2Pro(e) => write!(f, "Seedance2Pro provider error: {}", e), - Self::WorldLabs(e) => write!(f, "WorldLabs provider error: {}", e), } } } @@ -94,9 +91,3 @@ impl From for ProviderError { Self::Seedance2Pro(error) } } - -impl From for ProviderError { - fn from(error: WorldLabsError) -> Self { - Self::WorldLabs(error) - } -} diff --git a/crates/router/src/generate/generate_mesh/generate_mesh_request_builder.rs b/crates/router/src/generate/generate_mesh/generate_mesh_request_builder.rs index 4f760c4b..045a9752 100644 --- a/crates/router/src/generate/generate_mesh/generate_mesh_request_builder.rs +++ b/crates/router/src/generate/generate_mesh/generate_mesh_request_builder.rs @@ -269,7 +269,6 @@ mod tests { RouterProvider::GmiCloud, RouterProvider::GrokApi, RouterProvider::Seedance2Pro, - RouterProvider::WorldLabs, ] { let result = builder_with_url_image(provider, RouterMeshModel::Hunyuan3d3).build2(); assert!( diff --git a/crates/router/src/generate/generate_splat/cost/artcraft/estimate_splat_cost_artcraft_marble_0p1_mini.rs b/crates/router/src/generate/generate_splat/cost/artcraft/estimate_splat_cost_artcraft_marble_0p1_mini.rs index 7f5ba026..5202aef7 100644 --- a/crates/router/src/generate/generate_splat/cost/artcraft/estimate_splat_cost_artcraft_marble_0p1_mini.rs +++ b/crates/router/src/generate/generate_splat/cost/artcraft/estimate_splat_cost_artcraft_marble_0p1_mini.rs @@ -1,20 +1,14 @@ use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_mini::PlanArtcraftMarble0p1Mini; use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate; -use worldlabs_api_client::api::api_types::world_labs_model::WorldLabsModel; -use worldlabs_api_client::pricing::check_pricing::{calculate_cost, InputType}; -#[allow(deprecated)] // legacy marble 0.1 pricing intentionally uses the deprecated variants +/// Marble 0.1-mini costs 230 World Labs credits for text or a single image +/// ($1 per 1,250 credits), so it is flat priced. +const COST_IN_USD_CENTS: u64 = 18; + pub(crate) fn estimate_splat_cost_artcraft_marble_0p1_mini( - plan: &PlanArtcraftMarble0p1Mini, + _plan: &PlanArtcraftMarble0p1Mini, ) -> SplatGenerationCostEstimate { - let input_type = if plan.reference_image.is_some() { - InputType::ImageNonPanorama - } else { - InputType::Text - }; - - let cost = calculate_cost(WorldLabsModel::Marble0p1Mini, input_type); - let cost_in_usd_cents = cost.us_dollar_cents as u64; + let cost_in_usd_cents = COST_IN_USD_CENTS; SplatGenerationCostEstimate { cost_in_credits: Some(cost_in_usd_cents), diff --git a/crates/router/src/generate/generate_splat/cost/artcraft/estimate_splat_cost_artcraft_marble_0p1_plus.rs b/crates/router/src/generate/generate_splat/cost/artcraft/estimate_splat_cost_artcraft_marble_0p1_plus.rs index 1b633ebb..068d8c59 100644 --- a/crates/router/src/generate/generate_splat/cost/artcraft/estimate_splat_cost_artcraft_marble_0p1_plus.rs +++ b/crates/router/src/generate/generate_splat/cost/artcraft/estimate_splat_cost_artcraft_marble_0p1_plus.rs @@ -1,20 +1,14 @@ use crate::generate::generate_splat::plan::artcraft::plan_generate_splat_artcraft_marble_0p1_plus::PlanArtcraftMarble0p1Plus; use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate; -use worldlabs_api_client::api::api_types::world_labs_model::WorldLabsModel; -use worldlabs_api_client::pricing::check_pricing::{calculate_cost, InputType}; -#[allow(deprecated)] // legacy marble 0.1 pricing intentionally uses the deprecated variants +/// Marble 0.1-plus costs 1580 World Labs credits for text or a single image +/// ($1 per 1,250 credits), so it is flat priced. +const COST_IN_USD_CENTS: u64 = 126; + pub(crate) fn estimate_splat_cost_artcraft_marble_0p1_plus( - plan: &PlanArtcraftMarble0p1Plus, + _plan: &PlanArtcraftMarble0p1Plus, ) -> SplatGenerationCostEstimate { - let input_type = if plan.reference_image.is_some() { - InputType::ImageNonPanorama - } else { - InputType::Text - }; - - let cost = calculate_cost(WorldLabsModel::Marble0p1Plus, input_type); - let cost_in_usd_cents = cost.us_dollar_cents as u64; + let cost_in_usd_cents = COST_IN_USD_CENTS; SplatGenerationCostEstimate { cost_in_credits: Some(cost_in_usd_cents), diff --git a/crates/router/src/generate/generate_splat/generate_splat_request_builder.rs b/crates/router/src/generate/generate_splat/generate_splat_request_builder.rs index 6a75d9a1..0a34406f 100644 --- a/crates/router/src/generate/generate_splat/generate_splat_request_builder.rs +++ b/crates/router/src/generate/generate_splat/generate_splat_request_builder.rs @@ -10,14 +10,10 @@ use crate::generate::generate_splat::providers::artcraft::marble_1p1::build::bui use crate::generate::generate_splat::providers::artcraft::marble_1p1_plus::build::build_artcraft_marble_1p1_plus; use crate::generate::generate_splat::providers::artcraft::triposplat::build::build_artcraft_triposplat; use crate::generate::generate_splat::providers::fal::triposplat::build::build_fal_triposplat; -use crate::generate::generate_splat::providers::worldlabs::marble_1p0::build::build_worldlabs_marble_1p0; -use crate::generate::generate_splat::providers::worldlabs::marble_1p0_draft::build::build_worldlabs_marble_1p0_draft; -use crate::generate::generate_splat::providers::worldlabs::marble_1p1::build::build_worldlabs_marble_1p1; -use crate::generate::generate_splat::providers::worldlabs::marble_1p1_plus::build::build_worldlabs_marble_1p1_plus; -use crate::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest; +use crate::generate::generate_splat::splat_generation_request::SplatGenerationRequest; /// RouterProvider-agnostic splat (gaussian world) generation request. -/// Distilled by `build2()` into a `SplatGenerationDraftOrRequest` for the +/// Distilled by `build2()` into a `SplatGenerationRequest` for the /// selected (provider, model) pair. /// /// NB: The deprecated marble 0.1 models are treated as their marble 1.x @@ -74,7 +70,7 @@ impl Default for GenerateSplatRequestBuilder { impl GenerateSplatRequestBuilder { - pub fn build2(self) -> Result { + pub fn build2(self) -> Result { match (self.provider, self.model) { // Artcraft (RouterProvider::Artcraft, RouterSplatModel::Marble1p0 | RouterSplatModel::Marble0p1Plus) => build_artcraft_marble_1p0(self), @@ -84,11 +80,6 @@ impl GenerateSplatRequestBuilder { (RouterProvider::Artcraft, RouterSplatModel::TripoSplat) => build_artcraft_triposplat(self), // Fal (RouterProvider::Fal, RouterSplatModel::TripoSplat) => build_fal_triposplat(self), - // World Labs - (RouterProvider::WorldLabs, RouterSplatModel::Marble1p0 | RouterSplatModel::Marble0p1Plus) => build_worldlabs_marble_1p0(self), - (RouterProvider::WorldLabs, RouterSplatModel::Marble1p0Draft | RouterSplatModel::Marble0p1Mini) => build_worldlabs_marble_1p0_draft(self), - (RouterProvider::WorldLabs, RouterSplatModel::Marble1p1) => build_worldlabs_marble_1p1(self), - (RouterProvider::WorldLabs, RouterSplatModel::Marble1p1Plus) => build_worldlabs_marble_1p1_plus(self), _ => self.unsupported_provider_and_model(), } } @@ -98,7 +89,7 @@ impl GenerateSplatRequestBuilder { .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()) } - fn unsupported_provider_and_model(&self) -> Result { + fn unsupported_provider_and_model(&self) -> Result { Err(ArtcraftRouterError::UnsupportedProviderAndModelForNewApi( format!("Splat generation for model `{:?}` is not supported for provider {:?}", self.model, self.provider) )) @@ -107,15 +98,8 @@ impl GenerateSplatRequestBuilder { #[cfg(test)] mod tests { - use sqlite_identifiers::ids::media_file_token::MediaFileToken; - - use crate::generate::generate_splat::splat_generation_draft::SplatGenerationDraftRequest; - use crate::generate::generate_splat::splat_generation_request::SplatGenerationRequest; - use super::*; - const IMAGE_URL: &str = "https://example.com/room.png"; - mod artcraft_dispatch_tests { use super::*; @@ -130,7 +114,7 @@ mod tests { for model in cases { let result = artcraft_builder(model).build2() .unwrap_or_else(|e| panic!("build should succeed for {model:?}: {e}")); - let request = expect_request(result); + let request = result; let matches = matches!( (model, &request), (RouterSplatModel::Marble1p0, SplatGenerationRequest::ArtcraftMarble1p0(_)) @@ -146,7 +130,7 @@ mod tests { fn legacy_marble_0p1_mini_is_treated_as_marble_1p0_draft() { let result = artcraft_builder(RouterSplatModel::Marble0p1Mini).build2().expect("build"); assert!(matches!( - expect_request(result), + result, SplatGenerationRequest::ArtcraftMarble1p0Draft(_) )); } @@ -155,94 +139,12 @@ mod tests { fn legacy_marble_0p1_plus_is_treated_as_marble_1p0() { let result = artcraft_builder(RouterSplatModel::Marble0p1Plus).build2().expect("build"); assert!(matches!( - expect_request(result), + result, SplatGenerationRequest::ArtcraftMarble1p0(_) )); } } - mod worldlabs_dispatch_tests { - use super::*; - - #[test] - fn text_prompts_dispatch_to_direct_requests() { - let cases = [ - RouterSplatModel::Marble1p0, - RouterSplatModel::Marble1p0Draft, - RouterSplatModel::Marble1p1, - RouterSplatModel::Marble1p1Plus, - ]; - for model in cases { - let result = worldlabs_text_builder(model).build2() - .unwrap_or_else(|e| panic!("build should succeed for {model:?}: {e}")); - let request = expect_request(result); - let matches = matches!( - (model, &request), - (RouterSplatModel::Marble1p0, SplatGenerationRequest::WorldLabsMarble1p0(_)) - | (RouterSplatModel::Marble1p0Draft, SplatGenerationRequest::WorldLabsMarble1p0Draft(_)) - | (RouterSplatModel::Marble1p1, SplatGenerationRequest::WorldLabsMarble1p1(_)) - | (RouterSplatModel::Marble1p1Plus, SplatGenerationRequest::WorldLabsMarble1p1Plus(_)) - ); - assert!(matches, "unexpected dispatch for {model:?}: {request:?}"); - } - } - - #[test] - fn media_inputs_dispatch_to_drafts() { - let cases = [ - RouterSplatModel::Marble1p0, - RouterSplatModel::Marble1p0Draft, - RouterSplatModel::Marble1p1, - RouterSplatModel::Marble1p1Plus, - ]; - for model in cases { - let builder = GenerateSplatRequestBuilder { - reference_images: Some(ImageListRef::Urls(vec![IMAGE_URL.to_string()])), - ..worldlabs_text_builder(model) - }; - let result = builder.build2() - .unwrap_or_else(|e| panic!("build should succeed for {model:?}: {e}")); - let draft = expect_draft(result); - let matches = matches!( - (model, &draft), - (RouterSplatModel::Marble1p0, SplatGenerationDraftRequest::WorldLabsMarble1p0(_)) - | (RouterSplatModel::Marble1p0Draft, SplatGenerationDraftRequest::WorldLabsMarble1p0Draft(_)) - | (RouterSplatModel::Marble1p1, SplatGenerationDraftRequest::WorldLabsMarble1p1(_)) - | (RouterSplatModel::Marble1p1Plus, SplatGenerationDraftRequest::WorldLabsMarble1p1Plus(_)) - ); - assert!(matches, "unexpected dispatch for {model:?}: {draft:?}"); - } - } - - #[test] - fn legacy_models_are_treated_as_their_successors() { - let mini = worldlabs_text_builder(RouterSplatModel::Marble0p1Mini).build2().expect("build"); - assert!(matches!( - expect_request(mini), - SplatGenerationRequest::WorldLabsMarble1p0Draft(_) - )); - - let plus = worldlabs_text_builder(RouterSplatModel::Marble0p1Plus).build2().expect("build"); - assert!(matches!( - expect_request(plus), - SplatGenerationRequest::WorldLabsMarble1p0(_) - )); - } - - #[test] - fn media_token_inputs_dispatch_to_drafts() { - let builder = GenerateSplatRequestBuilder { - prompt: None, - reference_images: Some(ImageListRef::MediaFileTokens(vec![ - MediaFileToken::new("mf_test123".to_string()), - ])), - ..worldlabs_text_builder(RouterSplatModel::Marble1p0) - }; - let result = builder.build2().expect("build"); - assert!(matches!(result, SplatGenerationDraftOrRequest::Draft(_))); - } - } - mod unsupported_combo_tests { use super::*; @@ -278,27 +180,4 @@ mod tests { ..Default::default() } } - - fn worldlabs_text_builder(model: RouterSplatModel) -> GenerateSplatRequestBuilder { - GenerateSplatRequestBuilder { - provider: RouterProvider::WorldLabs, - model, - prompt: Some("a cozy cabin in the snowy mountains".to_string()), - ..Default::default() - } - } - - fn expect_request(result: SplatGenerationDraftOrRequest) -> SplatGenerationRequest { - match result { - SplatGenerationDraftOrRequest::Request(request) => request, - SplatGenerationDraftOrRequest::Draft(draft) => panic!("expected Request, got Draft: {draft:?}"), - } - } - - fn expect_draft(result: SplatGenerationDraftOrRequest) -> SplatGenerationDraftRequest { - match result { - SplatGenerationDraftOrRequest::Draft(draft) => draft, - SplatGenerationDraftOrRequest::Request(request) => panic!("expected Draft, got Request: {request:?}"), - } - } } diff --git a/crates/router/src/generate/generate_splat/generate_splat_response.rs b/crates/router/src/generate/generate_splat/generate_splat_response.rs index 965dd6ad..3493861a 100644 --- a/crates/router/src/generate/generate_splat/generate_splat_response.rs +++ b/crates/router/src/generate/generate_splat/generate_splat_response.rs @@ -9,13 +9,6 @@ pub struct ArtcraftSplatResponsePayload { pub all_inference_job_tokens: Vec, } -#[derive(Clone, Debug)] -pub struct WorldLabsSplatResponsePayload { - /// The World Labs operation ID; poll it for generation status. - pub operation_id: String, - pub done: bool, -} - /// Fal's queue/webhook submission receipt for a splat generation. #[derive(Clone, Debug)] pub struct FalSplatResponsePayload { @@ -40,7 +33,6 @@ pub struct FalSplatResponsePayload { pub enum GenerateSplatResponse { Artcraft(ArtcraftSplatResponsePayload), Fal(FalSplatResponsePayload), - WorldLabs(WorldLabsSplatResponsePayload), } impl GenerateSplatResponse { @@ -57,11 +49,4 @@ impl GenerateSplatResponse { _ => None, } } - - pub fn get_worldlabs_payload(&self) -> Option { - match self { - Self::WorldLabs(p) => Some(p.clone()), - _ => None, - } - } } diff --git a/crates/router/src/generate/generate_splat/mod.rs b/crates/router/src/generate/generate_splat/mod.rs index b0acb8e9..5bca59a7 100644 --- a/crates/router/src/generate/generate_splat/mod.rs +++ b/crates/router/src/generate/generate_splat/mod.rs @@ -1,9 +1,6 @@ // New audio-style pipeline (marble 1.x; use `GenerateSplatRequestBuilder::build2()`). pub mod generate_splat_request_builder; pub mod providers; -pub mod splat_generation_draft; -pub mod splat_generation_draft_context; -pub mod splat_generation_draft_or_request; pub mod splat_generation_request; // Shared between the new and legacy pipelines. diff --git a/crates/router/src/generate/generate_splat/providers/artcraft/cost_common.rs b/crates/router/src/generate/generate_splat/providers/artcraft/cost_common.rs index d4a8db94..79fcd6bf 100644 --- a/crates/router/src/generate/generate_splat/providers/artcraft/cost_common.rs +++ b/crates/router/src/generate/generate_splat/providers/artcraft/cost_common.rs @@ -1,5 +1,6 @@ use artcraft_client::api_defs::omni_gen::cost_and_generate_requests::omni_gen_splat_cost_and_generate_request::OmniGenSplatCostAndGenerateRequest; -use worldlabs_api_client::pricing::check_pricing::InputType; + +use crate::generate::generate_splat::providers::artcraft::input_type::InputType; use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate; diff --git a/crates/router/src/generate/generate_splat/providers/artcraft/input_type.rs b/crates/router/src/generate/generate_splat/providers/artcraft/input_type.rs new file mode 100644 index 00000000..10a01679 --- /dev/null +++ b/crates/router/src/generate/generate_splat/providers/artcraft/input_type.rs @@ -0,0 +1,11 @@ +/// How a splat request is driven, for pricing. Marble prices differ by +/// input kind (World Labs' published table), and Artcraft's flat prices +/// follow the same split. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum InputType { + Text, + ImageNonPanorama, + ImagePanorama, + MultiImage, + Video, +} diff --git a/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p0/build.rs b/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p0/build.rs index b06f1f0e..4cdd392f 100644 --- a/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p0/build.rs +++ b/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p0/build.rs @@ -4,11 +4,10 @@ use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder; use crate::generate::generate_splat::providers::artcraft::build_common::build_artcraft_omni_splat_request; use crate::generate::generate_splat::providers::artcraft::marble_1p0::request::ArtcraftMarble1p0RequestState; -use crate::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest; use crate::generate::generate_splat::splat_generation_request::SplatGenerationRequest; -pub fn build_artcraft_marble_1p0(builder: GenerateSplatRequestBuilder) -> Result { +pub fn build_artcraft_marble_1p0(builder: GenerateSplatRequestBuilder) -> Result { let request = build_artcraft_omni_splat_request(builder, CommonSplatModelEnum::Marble1p0)?; let state = ArtcraftMarble1p0RequestState { request }; - Ok(SplatGenerationDraftOrRequest::Request(SplatGenerationRequest::ArtcraftMarble1p0(state))) + Ok(SplatGenerationRequest::ArtcraftMarble1p0(state)) } diff --git a/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p0/cost.rs b/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p0/cost.rs index 99ca65a7..66bdf1b9 100644 --- a/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p0/cost.rs +++ b/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p0/cost.rs @@ -1,4 +1,4 @@ -use worldlabs_api_client::pricing::check_pricing::InputType; +use crate::generate::generate_splat::providers::artcraft::input_type::InputType; use crate::generate::generate_splat::providers::artcraft::cost_common::{ artcraft_splat_cost_estimate, derive_input_type_for_pricing, ArtcraftSplatPriceTable, diff --git a/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p0_draft/build.rs b/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p0_draft/build.rs index ba2585f2..311761de 100644 --- a/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p0_draft/build.rs +++ b/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p0_draft/build.rs @@ -4,11 +4,10 @@ use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder; use crate::generate::generate_splat::providers::artcraft::build_common::build_artcraft_omni_splat_request; use crate::generate::generate_splat::providers::artcraft::marble_1p0_draft::request::ArtcraftMarble1p0DraftRequestState; -use crate::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest; use crate::generate::generate_splat::splat_generation_request::SplatGenerationRequest; -pub fn build_artcraft_marble_1p0_draft(builder: GenerateSplatRequestBuilder) -> Result { +pub fn build_artcraft_marble_1p0_draft(builder: GenerateSplatRequestBuilder) -> Result { let request = build_artcraft_omni_splat_request(builder, CommonSplatModelEnum::Marble1p0Draft)?; let state = ArtcraftMarble1p0DraftRequestState { request }; - Ok(SplatGenerationDraftOrRequest::Request(SplatGenerationRequest::ArtcraftMarble1p0Draft(state))) + Ok(SplatGenerationRequest::ArtcraftMarble1p0Draft(state)) } diff --git a/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p0_draft/cost.rs b/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p0_draft/cost.rs index d0326b84..98d09673 100644 --- a/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p0_draft/cost.rs +++ b/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p0_draft/cost.rs @@ -1,4 +1,4 @@ -use worldlabs_api_client::pricing::check_pricing::InputType; +use crate::generate::generate_splat::providers::artcraft::input_type::InputType; use crate::generate::generate_splat::providers::artcraft::cost_common::{ artcraft_splat_cost_estimate, derive_input_type_for_pricing, ArtcraftSplatPriceTable, diff --git a/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p0_draft/request.rs b/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p0_draft/request.rs index 568e5eca..8790ff6d 100644 --- a/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p0_draft/request.rs +++ b/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p0_draft/request.rs @@ -22,7 +22,6 @@ mod tests { use crate::api::router_splat_model::RouterSplatModel; use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder; use crate::generate::generate_splat::generate_splat_response::GenerateSplatResponse; - use crate::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest; use crate::test_helpers::get_artcraft_client; #[tokio::test] @@ -36,11 +35,7 @@ mod tests { }; let client = get_artcraft_client(); - let draft_or_request = builder.build2().expect("build2 should succeed"); - let request = match draft_or_request { - SplatGenerationDraftOrRequest::Request(r) => r, - _ => panic!("expected Request variant (Artcraft skips draft)"), - }; + let request = builder.build2().expect("build2 should succeed"); let response = request.send_request(&client).await.expect("send_request should succeed"); diff --git a/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p1/build.rs b/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p1/build.rs index 7eb97845..af22b518 100644 --- a/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p1/build.rs +++ b/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p1/build.rs @@ -4,11 +4,10 @@ use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder; use crate::generate::generate_splat::providers::artcraft::build_common::build_artcraft_omni_splat_request; use crate::generate::generate_splat::providers::artcraft::marble_1p1::request::ArtcraftMarble1p1RequestState; -use crate::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest; use crate::generate::generate_splat::splat_generation_request::SplatGenerationRequest; -pub fn build_artcraft_marble_1p1(builder: GenerateSplatRequestBuilder) -> Result { +pub fn build_artcraft_marble_1p1(builder: GenerateSplatRequestBuilder) -> Result { let request = build_artcraft_omni_splat_request(builder, CommonSplatModelEnum::Marble1p1)?; let state = ArtcraftMarble1p1RequestState { request }; - Ok(SplatGenerationDraftOrRequest::Request(SplatGenerationRequest::ArtcraftMarble1p1(state))) + Ok(SplatGenerationRequest::ArtcraftMarble1p1(state)) } diff --git a/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p1/cost.rs b/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p1/cost.rs index 8eb927fb..8d46f6a7 100644 --- a/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p1/cost.rs +++ b/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p1/cost.rs @@ -1,4 +1,4 @@ -use worldlabs_api_client::pricing::check_pricing::InputType; +use crate::generate::generate_splat::providers::artcraft::input_type::InputType; use crate::generate::generate_splat::providers::artcraft::cost_common::{ artcraft_splat_cost_estimate, derive_input_type_for_pricing, ArtcraftSplatPriceTable, diff --git a/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p1_plus/build.rs b/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p1_plus/build.rs index 3261622a..3c8f1c00 100644 --- a/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p1_plus/build.rs +++ b/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p1_plus/build.rs @@ -4,11 +4,10 @@ use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder; use crate::generate::generate_splat::providers::artcraft::build_common::build_artcraft_omni_splat_request; use crate::generate::generate_splat::providers::artcraft::marble_1p1_plus::request::ArtcraftMarble1p1PlusRequestState; -use crate::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest; use crate::generate::generate_splat::splat_generation_request::SplatGenerationRequest; -pub fn build_artcraft_marble_1p1_plus(builder: GenerateSplatRequestBuilder) -> Result { +pub fn build_artcraft_marble_1p1_plus(builder: GenerateSplatRequestBuilder) -> Result { let request = build_artcraft_omni_splat_request(builder, CommonSplatModelEnum::Marble1p1Plus)?; let state = ArtcraftMarble1p1PlusRequestState { request }; - Ok(SplatGenerationDraftOrRequest::Request(SplatGenerationRequest::ArtcraftMarble1p1Plus(state))) + Ok(SplatGenerationRequest::ArtcraftMarble1p1Plus(state)) } diff --git a/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p1_plus/cost.rs b/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p1_plus/cost.rs index 89faa1b3..0e850fa9 100644 --- a/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p1_plus/cost.rs +++ b/crates/router/src/generate/generate_splat/providers/artcraft/marble_1p1_plus/cost.rs @@ -1,4 +1,4 @@ -use worldlabs_api_client::pricing::check_pricing::InputType; +use crate::generate::generate_splat::providers::artcraft::input_type::InputType; use crate::generate::generate_splat::providers::artcraft::cost_common::{ artcraft_splat_cost_estimate, derive_input_type_for_pricing, ArtcraftSplatPriceTable, diff --git a/crates/router/src/generate/generate_splat/providers/artcraft/mod.rs b/crates/router/src/generate/generate_splat/providers/artcraft/mod.rs index 7db75e46..31c71496 100644 --- a/crates/router/src/generate/generate_splat/providers/artcraft/mod.rs +++ b/crates/router/src/generate/generate_splat/providers/artcraft/mod.rs @@ -1,5 +1,6 @@ pub(crate) mod build_common; pub(crate) mod cost_common; +pub mod input_type; pub(crate) mod request_common; pub(crate) mod resolve; pub mod marble_1p0; diff --git a/crates/router/src/generate/generate_splat/providers/artcraft/triposplat/build.rs b/crates/router/src/generate/generate_splat/providers/artcraft/triposplat/build.rs index e9d949aa..bdd3c3db 100644 --- a/crates/router/src/generate/generate_splat/providers/artcraft/triposplat/build.rs +++ b/crates/router/src/generate/generate_splat/providers/artcraft/triposplat/build.rs @@ -4,11 +4,10 @@ use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder; use crate::generate::generate_splat::providers::artcraft::build_common::build_artcraft_omni_splat_request; use crate::generate::generate_splat::providers::artcraft::triposplat::request::ArtcraftTripoSplatRequestState; -use crate::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest; use crate::generate::generate_splat::splat_generation_request::SplatGenerationRequest; -pub fn build_artcraft_triposplat(builder: GenerateSplatRequestBuilder) -> Result { +pub fn build_artcraft_triposplat(builder: GenerateSplatRequestBuilder) -> Result { let request = build_artcraft_omni_splat_request(builder, CommonSplatModelEnum::TripoSplat)?; let state = ArtcraftTripoSplatRequestState { request }; - Ok(SplatGenerationDraftOrRequest::Request(SplatGenerationRequest::ArtcraftTripoSplat(state))) + Ok(SplatGenerationRequest::ArtcraftTripoSplat(state)) } diff --git a/crates/router/src/generate/generate_splat/providers/fal/triposplat/build.rs b/crates/router/src/generate/generate_splat/providers/fal/triposplat/build.rs index 0231b974..22c051bb 100644 --- a/crates/router/src/generate/generate_splat/providers/fal/triposplat/build.rs +++ b/crates/router/src/generate/generate_splat/providers/fal/triposplat/build.rs @@ -10,15 +10,14 @@ use crate::errors::client_error::ClientError; use crate::generate::generate_mesh::providers::reject_unsupported::reject_unsupported_option; use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder; use crate::generate::generate_splat::providers::fal::triposplat::request::FalTripoSplatRequestState; -use crate::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest; use crate::generate::generate_splat::splat_generation_request::SplatGenerationRequest; /// TripoSplat takes exactly one input image and nothing else. Prompts, /// videos, panorama and recaption flags are unsupported: rejected under /// `ErrorOut`, dropped otherwise. -pub fn build_fal_triposplat(builder: GenerateSplatRequestBuilder) -> Result { +pub fn build_fal_triposplat(builder: GenerateSplatRequestBuilder) -> Result { let state = build_fal_triposplat_state(builder)?; - Ok(SplatGenerationDraftOrRequest::Request(SplatGenerationRequest::FalTripoSplat(state))) + Ok(SplatGenerationRequest::FalTripoSplat(state)) } pub(crate) fn build_fal_triposplat_state( diff --git a/crates/router/src/generate/generate_splat/providers/fal/triposplat/request.rs b/crates/router/src/generate/generate_splat/providers/fal/triposplat/request.rs index 3e756b1f..bd98b8dc 100644 --- a/crates/router/src/generate/generate_splat/providers/fal/triposplat/request.rs +++ b/crates/router/src/generate/generate_splat/providers/fal/triposplat/request.rs @@ -26,7 +26,6 @@ mod tests { use crate::api::router_splat_model::RouterSplatModel; use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder; use crate::generate::generate_splat::generate_splat_response::GenerateSplatResponse; - use crate::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest; use crate::test_helpers::get_fal_client; #[tokio::test] @@ -43,11 +42,7 @@ mod tests { ..Default::default() }; - let draft_or_request = builder.build2().expect("build2 should succeed"); - let request = match draft_or_request { - SplatGenerationDraftOrRequest::Request(r) => r, - _ => panic!("expected Request variant (Fal splat skips draft)"), - }; + let request = builder.build2().expect("build2 should succeed"); let response = request.send_request(&client).await.expect("send_request should succeed"); println!("response: {:?}", response); diff --git a/crates/router/src/generate/generate_splat/providers/mod.rs b/crates/router/src/generate/generate_splat/providers/mod.rs index d2473302..cded7c36 100644 --- a/crates/router/src/generate/generate_splat/providers/mod.rs +++ b/crates/router/src/generate/generate_splat/providers/mod.rs @@ -1,3 +1,2 @@ pub mod artcraft; pub mod fal; -pub mod worldlabs; diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/build_common.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/build_common.rs deleted file mode 100644 index 59fcbd0a..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/build_common.rs +++ /dev/null @@ -1,54 +0,0 @@ -use worldlabs_api_client::api::api_types::world_labs_model::WorldLabsModel; -use worldlabs_api_client::api::requests::generate_world::http_request::WorldPrompt; - -use crate::errors::artcraft_router_error::ArtcraftRouterError; -use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder; -use crate::generate::generate_splat::providers::worldlabs::draft_common::WorldLabsSplatDraft; -use crate::generate::generate_splat::providers::worldlabs::request_common::WorldLabsSplatRequest; -use crate::generate::generate_splat::providers::worldlabs::resolve::{plan_splat_input, SplatInput}; - -/// Either a ready-to-send request (text input) or a draft that must upload -/// media assets first. Wrapped into the per-model states by each model's -/// `build.rs` shim. -#[derive(Clone, Debug)] -pub(crate) enum WorldLabsSplatDraftOrRequest { - Draft(WorldLabsSplatDraft), - Request(WorldLabsSplatRequest), -} - -/// Build a World Labs splat draft-or-request from the builder. Text-only -/// prompts go DIRECT (no upload); any media input needs the draft phase to -/// re-upload the media as World Labs media assets. -pub(crate) fn build_worldlabs_splat( - mut builder: GenerateSplatRequestBuilder, - model: WorldLabsModel, -) -> Result { - let strategy = builder.request_mismatch_mitigation_strategy; - let prompt = builder.prompt.take(); - - let input = plan_splat_input( - builder.reference_images.take(), - builder.reference_video.take(), - builder.is_panoramic, - prompt.is_some(), - strategy, - )?; - - match input { - SplatInput::Text => { - let world_prompt = WorldPrompt::Text { - text_prompt: prompt, - disable_recaption: builder.disable_recaption, - }; - Ok(WorldLabsSplatDraftOrRequest::Request(WorldLabsSplatRequest { model, world_prompt })) - } - media_input => { - Ok(WorldLabsSplatDraftOrRequest::Draft(WorldLabsSplatDraft { - model, - text_prompt: prompt, - disable_recaption: builder.disable_recaption, - input: media_input, - })) - } - } -} diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/draft_common.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/draft_common.rs deleted file mode 100644 index c89d129a..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/draft_common.rs +++ /dev/null @@ -1,273 +0,0 @@ -use std::time::Duration; - -use sqlite_identifiers::ids::media_file_token::MediaFileToken; -use worldlabs_api_client::api::api_types::world_labs_model::WorldLabsModel; -use worldlabs_api_client::api::requests::generate_world::http_request::{ - ContentReference, SphericallyLocatedContent, WorldPrompt, -}; -use worldlabs_api_client::api::requests::prepare_upload::prepare_upload::{ - prepare_upload, MediaAssetKind, PrepareUploadArgs, -}; -use worldlabs_api_client::api::requests::upload_to_signed_url::upload_to_signed_url::{ - upload_to_signed_url, UploadToSignedUrlArgs, -}; -use worldlabs_api_client::pricing::check_pricing::InputType; - -use crate::api::image_ref::ImageRef; -use crate::api::video_ref::VideoRef; -use crate::client::router_worldlabs_client::RouterWorldLabsClient; -use crate::errors::artcraft_router_error::ArtcraftRouterError; -use crate::errors::client_error::{ClientError, ClientType}; -use crate::errors::provider_error::ProviderError; -use crate::generate::generate_splat::providers::worldlabs::request_common::WorldLabsSplatRequest; -use crate::generate::generate_splat::providers::worldlabs::resolve::SplatInput; -use crate::generate::generate_splat::splat_generation_draft_context::SplatGenerationDraftContext; -use crate::utils::download_file::download_file; - -/// How long to wait for each prepare/upload call during the draft phase. -const UPLOAD_REQUEST_TIMEOUT: Duration = Duration::from_secs(120); - -/// A World Labs splat draft, shared by all marble model draft states. Holds -/// unresolved media references that `to_request()` downloads from their -/// source URLs and re-uploads as World Labs media assets. -#[derive(Clone, Debug)] -pub struct WorldLabsSplatDraft { - pub model: WorldLabsModel, - pub text_prompt: Option, - pub disable_recaption: Option, - - /// Pending media input to resolve and upload. Never `SplatInput::Text` — - /// text-only prompts skip the draft phase. - pub(crate) input: SplatInput, -} - -impl WorldLabsSplatDraft { - - pub async fn to_request( - &self, - draft_context: &SplatGenerationDraftContext<'_>, - ) -> Result { - let client = draft_context.get_worldlabs_client_ref()?; - - let world_prompt = match &self.input { - // Text-only prompts never enter the draft phase, but assemble the - // prompt anyway so the state machine has no dead ends. - SplatInput::Text => WorldPrompt::Text { - text_prompt: self.text_prompt.clone(), - disable_recaption: self.disable_recaption, - }, - SplatInput::Image { image, is_panoramic } => { - let source_url = resolve_image_source_url(image, draft_context)?; - let content = upload_media_asset(client, &source_url, MediaAssetKind::Image).await?; - WorldPrompt::Image { - image_prompt: content, - text_prompt: self.text_prompt.clone(), - is_pano: Some(*is_panoramic), - disable_recaption: self.disable_recaption, - } - } - SplatInput::MultiImage { images } => { - let mut multi_image_prompt = Vec::with_capacity(images.len()); - for image in images { - let source_url = resolve_image_source_url(image, draft_context)?; - let content = upload_media_asset(client, &source_url, MediaAssetKind::Image).await?; - // Spherical coordinates aren't exposed through the router yet, so - // every image is submitted without an azimuth. - multi_image_prompt.push(SphericallyLocatedContent { content, azimuth: None }); - } - WorldPrompt::MultiImage { - multi_image_prompt, - text_prompt: self.text_prompt.clone(), - reconstruct_images: None, - disable_recaption: self.disable_recaption, - } - } - SplatInput::Video { video } => { - let source_url = resolve_video_source_url(video, draft_context)?; - let content = upload_media_asset(client, &source_url, MediaAssetKind::Video).await?; - WorldPrompt::Video { - video_prompt: content, - text_prompt: self.text_prompt.clone(), - disable_recaption: self.disable_recaption, - } - } - }; - - Ok(WorldLabsSplatRequest { - model: self.model, - world_prompt, - }) - } - - /// The pricing input type of the pending input. - pub(crate) fn input_type(&self) -> InputType { - self.input.to_input_type() - } -} - -// ── Resolve/upload helpers ── - -/// Download the media from its source URL and re-upload it as a World Labs -/// media asset, returning the content reference for the generation prompt. -async fn upload_media_asset( - client: &RouterWorldLabsClient, - source_url: &str, - kind: MediaAssetKind, -) -> Result { - let file_bytes = download_file(source_url).await?; - - let file_name = file_name_for_upload(source_url, kind); - let content_type = content_type_for_file_name(&file_name, kind); - - let prepared = prepare_upload(PrepareUploadArgs { - creds: &client.creds, - file_name: &file_name, - kind, - request_timeout: Some(UPLOAD_REQUEST_TIMEOUT), - }) - .await - .map_err(|err| ArtcraftRouterError::Provider(ProviderError::WorldLabs(err)))?; - - upload_to_signed_url(UploadToSignedUrlArgs { - upload_url: &prepared.upload_url, - file_bytes, - required_headers: &prepared.required_headers, - content_type, - request_timeout: Some(UPLOAD_REQUEST_TIMEOUT), - }) - .await - .map_err(|err| ArtcraftRouterError::Provider(ProviderError::WorldLabs(err)))?; - - Ok(ContentReference::MediaAsset { - media_asset_id: prepared.media_asset_id.as_str().to_string(), - }) -} - -fn resolve_image_source_url( - image: &ImageRef, - draft_context: &SplatGenerationDraftContext<'_>, -) -> Result { - match image { - ImageRef::Url(url) => Ok(url.clone()), - ImageRef::MediaFileToken(token) => resolve_token_source_url(token, draft_context), - ImageRef::LocalPath(_) | ImageRef::Bytes(_) => { - Err(ArtcraftRouterError::Client(ClientError::ProviderCannotUseLocalMedia { - client_type: ClientType::WorldLabs, - })) - } - } -} - -fn resolve_video_source_url( - video: &VideoRef, - draft_context: &SplatGenerationDraftContext<'_>, -) -> Result { - match video { - VideoRef::Url(url) => Ok(url.clone()), - VideoRef::MediaFileToken(token) => resolve_token_source_url(token, draft_context), - VideoRef::LocalPath(_) | VideoRef::Bytes(_) => { - Err(ArtcraftRouterError::Client(ClientError::ProviderCannotUseLocalMedia { - client_type: ClientType::WorldLabs, - })) - } - } -} - -fn resolve_token_source_url( - token: &MediaFileToken, - draft_context: &SplatGenerationDraftContext<'_>, -) -> Result { - let map = draft_context.media_file_to_artcraft_url_map - .ok_or(ArtcraftRouterError::Client(ClientError::MediaFileToUrlMapNotProvided))?; - map.get(token).cloned().ok_or_else(|| { - ArtcraftRouterError::Client(ClientError::MediaFileTokenNotFoundInMap { - token: token.clone(), - }) - }) -} - -// ── File name / content type helpers ── - -fn file_name_for_upload(source_url: &str, kind: MediaAssetKind) -> String { - let default_extension = match kind { - MediaAssetKind::Image => "png", - MediaAssetKind::Video => "mp4", - }; - let extension = extension_from_url(source_url) - .unwrap_or_else(|| default_extension.to_string()); - format!("upload.{extension}") -} - -fn extension_from_url(url: &str) -> Option { - let path = url.split(['?', '#']).next().unwrap_or(url); - let file_name = path.rsplit('/').next()?; - let (_, extension) = file_name.rsplit_once('.')?; - if extension.is_empty() || extension.len() > 5 { - return None; - } - Some(extension.to_lowercase()) -} - -fn content_type_for_file_name(file_name: &str, kind: MediaAssetKind) -> &'static str { - let extension = file_name.rsplit_once('.').map(|(_, ext)| ext).unwrap_or(""); - match extension { - "jpg" | "jpeg" => "image/jpeg", - "png" => "image/png", - "webp" => "image/webp", - "gif" => "image/gif", - "mp4" => "video/mp4", - "mov" => "video/quicktime", - "webm" => "video/webm", - _ => match kind { - MediaAssetKind::Image => "image/png", - MediaAssetKind::Video => "video/mp4", - }, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - mod extension_tests { - use super::*; - - #[test] - fn extension_is_parsed_from_url_path() { - assert_eq!(extension_from_url("https://example.com/a/b/photo.JPG").as_deref(), Some("jpg")); - assert_eq!(extension_from_url("https://example.com/clip.mp4?query=1").as_deref(), Some("mp4")); - assert_eq!(extension_from_url("https://example.com/pic.png#frag").as_deref(), Some("png")); - } - - #[test] - fn missing_extension_returns_none() { - assert_eq!(extension_from_url("https://example.com/no-extension"), None); - assert_eq!(extension_from_url("https://example.com/dir/"), None); - } - - #[test] - fn file_name_falls_back_to_kind_default() { - assert_eq!(file_name_for_upload("https://example.com/x", MediaAssetKind::Image), "upload.png"); - assert_eq!(file_name_for_upload("https://example.com/x", MediaAssetKind::Video), "upload.mp4"); - assert_eq!(file_name_for_upload("https://example.com/a.webp", MediaAssetKind::Image), "upload.webp"); - } - } - - mod content_type_tests { - use super::*; - - #[test] - fn known_extensions_map_to_content_types() { - assert_eq!(content_type_for_file_name("upload.jpg", MediaAssetKind::Image), "image/jpeg"); - assert_eq!(content_type_for_file_name("upload.png", MediaAssetKind::Image), "image/png"); - assert_eq!(content_type_for_file_name("upload.mp4", MediaAssetKind::Video), "video/mp4"); - assert_eq!(content_type_for_file_name("upload.mov", MediaAssetKind::Video), "video/quicktime"); - } - - #[test] - fn unknown_extensions_fall_back_to_kind_default() { - assert_eq!(content_type_for_file_name("upload.xyz", MediaAssetKind::Image), "image/png"); - assert_eq!(content_type_for_file_name("upload.xyz", MediaAssetKind::Video), "video/mp4"); - } - } -} diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0/build.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0/build.rs deleted file mode 100644 index 925b7c23..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0/build.rs +++ /dev/null @@ -1,25 +0,0 @@ -use worldlabs_api_client::api::api_types::world_labs_model::WorldLabsModel; - -use crate::errors::artcraft_router_error::ArtcraftRouterError; -use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder; -use crate::generate::generate_splat::providers::worldlabs::build_common::{ - build_worldlabs_splat, WorldLabsSplatDraftOrRequest, -}; -use crate::generate::generate_splat::providers::worldlabs::marble_1p0::draft::WorldLabsMarble1p0DraftState; -use crate::generate::generate_splat::providers::worldlabs::marble_1p0::request::WorldLabsMarble1p0RequestState; -use crate::generate::generate_splat::splat_generation_draft::SplatGenerationDraftRequest; -use crate::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest; -use crate::generate::generate_splat::splat_generation_request::SplatGenerationRequest; - -pub fn build_worldlabs_marble_1p0(builder: GenerateSplatRequestBuilder) -> Result { - match build_worldlabs_splat(builder, WorldLabsModel::Marble1p0)? { - WorldLabsSplatDraftOrRequest::Request(request) => { - let state = WorldLabsMarble1p0RequestState { request }; - Ok(SplatGenerationDraftOrRequest::Request(SplatGenerationRequest::WorldLabsMarble1p0(state))) - } - WorldLabsSplatDraftOrRequest::Draft(draft) => { - let state = WorldLabsMarble1p0DraftState { draft }; - Ok(SplatGenerationDraftOrRequest::Draft(SplatGenerationDraftRequest::WorldLabsMarble1p0(state))) - } - } -} diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0/cost.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0/cost.rs deleted file mode 100644 index 722afa8a..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0/cost.rs +++ /dev/null @@ -1,136 +0,0 @@ -use worldlabs_api_client::api::api_types::world_labs_model::WorldLabsModel; -use worldlabs_api_client::pricing::check_pricing::{calculate_cost, InputType}; - -use crate::generate::generate_splat::providers::worldlabs::marble_1p0::draft::WorldLabsMarble1p0DraftState; -use crate::generate::generate_splat::providers::worldlabs::marble_1p0::request::WorldLabsMarble1p0RequestState; -use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate; - -const MODEL: WorldLabsModel = WorldLabsModel::Marble1p0; - -/// Cost math is owned by worldlabs_api_client's pricing table — the router -/// just forwards the result so router cost ≡ provider cost by construction. -#[derive(Clone, Debug)] -pub struct WorldLabsMarble1p0CostState { - pub input_type: InputType, -} - -impl WorldLabsMarble1p0CostState { - pub fn from_request(request: &WorldLabsMarble1p0RequestState) -> Self { - Self { input_type: request.request.input_type() } - } - - pub fn from_draft(draft: &WorldLabsMarble1p0DraftState) -> Self { - Self { input_type: draft.draft.input_type() } - } - - pub fn estimate_cost(&self) -> SplatGenerationCostEstimate { - let cost = calculate_cost(MODEL, self.input_type); - SplatGenerationCostEstimate { - cost_in_credits: Some(cost.worldlabs_credits as u64), - cost_in_usd_cents: Some(cost.us_dollar_cents as u64), - is_free: false, - is_unlimited: false, - is_rate_limited: false, - has_watermark: false, - failures_are_refunded: None, - } - } -} - -#[cfg(test)] -mod tests { - use sqlite_identifiers::ids::media_file_token::MediaFileToken; - - use crate::api::image_list_ref::ImageListRef; - use crate::api::router_provider::RouterProvider; - use crate::api::router_splat_model::RouterSplatModel; - use crate::api::video_ref::VideoRef; - use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder; - - #[test] - fn text_is_126_cents() { - assert_eq!(estimate_usd_cents(text_builder()), 126); - } - - #[test] - fn single_image_is_126_cents() { - assert_eq!(estimate_usd_cents(single_image_builder(false)), 126); - } - - #[test] - fn panoramic_image_is_120_cents() { - assert_eq!(estimate_usd_cents(single_image_builder(true)), 120); - } - - #[test] - fn multi_image_is_128_cents() { - assert_eq!(estimate_usd_cents(multi_image_builder()), 128); - } - - #[test] - fn video_is_128_cents() { - assert_eq!(estimate_usd_cents(video_builder()), 128); - } - - #[test] - fn credits_come_from_the_provider_pricing_table() { - let estimate = text_builder().build2() - .expect("build should succeed") - .estimate_cost() - .expect("estimate should succeed"); - assert_eq!(estimate.cost_in_credits, Some(1580)); - } - - // ── Helpers ── - - fn base_builder() -> GenerateSplatRequestBuilder { - GenerateSplatRequestBuilder { - model: RouterSplatModel::Marble1p0, - provider: RouterProvider::WorldLabs, - ..Default::default() - } - } - - fn text_builder() -> GenerateSplatRequestBuilder { - GenerateSplatRequestBuilder { - prompt: Some("a cozy cabin in the snowy mountains".to_string()), - ..base_builder() - } - } - - fn single_image_builder(is_panoramic: bool) -> GenerateSplatRequestBuilder { - GenerateSplatRequestBuilder { - reference_images: Some(ImageListRef::MediaFileTokens(vec![ - MediaFileToken::new("mf_image_1".to_string()), - ])), - is_panoramic: Some(is_panoramic), - ..base_builder() - } - } - - fn multi_image_builder() -> GenerateSplatRequestBuilder { - GenerateSplatRequestBuilder { - reference_images: Some(ImageListRef::MediaFileTokens(vec![ - MediaFileToken::new("mf_image_1".to_string()), - MediaFileToken::new("mf_image_2".to_string()), - ])), - ..base_builder() - } - } - - fn video_builder() -> GenerateSplatRequestBuilder { - GenerateSplatRequestBuilder { - reference_video: Some(VideoRef::MediaFileToken(MediaFileToken::new("mf_video".to_string()))), - ..base_builder() - } - } - - fn estimate_usd_cents(builder: GenerateSplatRequestBuilder) -> u64 { - builder.build2() - .expect("build should succeed") - .estimate_cost() - .expect("estimate should succeed") - .cost_in_usd_cents - .expect("cost should be present") - } -} diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0/draft.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0/draft.rs deleted file mode 100644 index e4dae690..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0/draft.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::errors::artcraft_router_error::ArtcraftRouterError; -use crate::generate::generate_splat::providers::worldlabs::draft_common::WorldLabsSplatDraft; -use crate::generate::generate_splat::providers::worldlabs::marble_1p0::request::WorldLabsMarble1p0RequestState; -use crate::generate::generate_splat::splat_generation_draft_context::SplatGenerationDraftContext; - -#[derive(Clone, Debug)] -pub struct WorldLabsMarble1p0DraftState { - /// Shared World Labs draft; the model is baked in at build time. - pub draft: WorldLabsSplatDraft, -} - -impl WorldLabsMarble1p0DraftState { - pub async fn to_request(&self, draft_context: &SplatGenerationDraftContext<'_>) -> Result { - let request = self.draft.to_request(draft_context).await?; - Ok(WorldLabsMarble1p0RequestState { request }) - } -} diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0/mod.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0/mod.rs deleted file mode 100644 index 89225a36..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod build; -pub mod cost; -pub mod draft; -pub mod request; diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0/request.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0/request.rs deleted file mode 100644 index ea0d151c..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0/request.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::client::router_worldlabs_client::RouterWorldLabsClient; -use crate::errors::artcraft_router_error::ArtcraftRouterError; -use crate::generate::generate_splat::generate_splat_response::GenerateSplatResponse; -use crate::generate::generate_splat::providers::worldlabs::request_common::WorldLabsSplatRequest; - -#[derive(Clone, Debug)] -pub struct WorldLabsMarble1p0RequestState { - /// Final materialized request; ready to fire. Media references (if any) - /// have been uploaded as World Labs media assets by the draft phase. - pub request: WorldLabsSplatRequest, -} - -impl WorldLabsMarble1p0RequestState { - pub async fn send(&self, client: &RouterWorldLabsClient) -> Result { - self.request.send(client).await - } -} diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0_draft/build.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0_draft/build.rs deleted file mode 100644 index 1d11a654..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0_draft/build.rs +++ /dev/null @@ -1,25 +0,0 @@ -use worldlabs_api_client::api::api_types::world_labs_model::WorldLabsModel; - -use crate::errors::artcraft_router_error::ArtcraftRouterError; -use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder; -use crate::generate::generate_splat::providers::worldlabs::build_common::{ - build_worldlabs_splat, WorldLabsSplatDraftOrRequest, -}; -use crate::generate::generate_splat::providers::worldlabs::marble_1p0_draft::draft::WorldLabsMarble1p0DraftModelDraftState; -use crate::generate::generate_splat::providers::worldlabs::marble_1p0_draft::request::WorldLabsMarble1p0DraftModelRequestState; -use crate::generate::generate_splat::splat_generation_draft::SplatGenerationDraftRequest; -use crate::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest; -use crate::generate::generate_splat::splat_generation_request::SplatGenerationRequest; - -pub fn build_worldlabs_marble_1p0_draft(builder: GenerateSplatRequestBuilder) -> Result { - match build_worldlabs_splat(builder, WorldLabsModel::Marble1p0Draft)? { - WorldLabsSplatDraftOrRequest::Request(request) => { - let state = WorldLabsMarble1p0DraftModelRequestState { request }; - Ok(SplatGenerationDraftOrRequest::Request(SplatGenerationRequest::WorldLabsMarble1p0Draft(state))) - } - WorldLabsSplatDraftOrRequest::Draft(draft) => { - let state = WorldLabsMarble1p0DraftModelDraftState { draft }; - Ok(SplatGenerationDraftOrRequest::Draft(SplatGenerationDraftRequest::WorldLabsMarble1p0Draft(state))) - } - } -} diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0_draft/cost.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0_draft/cost.rs deleted file mode 100644 index 63e4009c..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0_draft/cost.rs +++ /dev/null @@ -1,136 +0,0 @@ -use worldlabs_api_client::api::api_types::world_labs_model::WorldLabsModel; -use worldlabs_api_client::pricing::check_pricing::{calculate_cost, InputType}; - -use crate::generate::generate_splat::providers::worldlabs::marble_1p0_draft::draft::WorldLabsMarble1p0DraftModelDraftState; -use crate::generate::generate_splat::providers::worldlabs::marble_1p0_draft::request::WorldLabsMarble1p0DraftModelRequestState; -use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate; - -const MODEL: WorldLabsModel = WorldLabsModel::Marble1p0Draft; - -/// Cost math is owned by worldlabs_api_client's pricing table — the router -/// just forwards the result so router cost ≡ provider cost by construction. -#[derive(Clone, Debug)] -pub struct WorldLabsMarble1p0DraftModelCostState { - pub input_type: InputType, -} - -impl WorldLabsMarble1p0DraftModelCostState { - pub fn from_request(request: &WorldLabsMarble1p0DraftModelRequestState) -> Self { - Self { input_type: request.request.input_type() } - } - - pub fn from_draft(draft: &WorldLabsMarble1p0DraftModelDraftState) -> Self { - Self { input_type: draft.draft.input_type() } - } - - pub fn estimate_cost(&self) -> SplatGenerationCostEstimate { - let cost = calculate_cost(MODEL, self.input_type); - SplatGenerationCostEstimate { - cost_in_credits: Some(cost.worldlabs_credits as u64), - cost_in_usd_cents: Some(cost.us_dollar_cents as u64), - is_free: false, - is_unlimited: false, - is_rate_limited: false, - has_watermark: false, - failures_are_refunded: None, - } - } -} - -#[cfg(test)] -mod tests { - use sqlite_identifiers::ids::media_file_token::MediaFileToken; - - use crate::api::image_list_ref::ImageListRef; - use crate::api::router_provider::RouterProvider; - use crate::api::router_splat_model::RouterSplatModel; - use crate::api::video_ref::VideoRef; - use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder; - - #[test] - fn text_is_18_cents() { - assert_eq!(estimate_usd_cents(text_builder()), 18); - } - - #[test] - fn single_image_is_18_cents() { - assert_eq!(estimate_usd_cents(single_image_builder(false)), 18); - } - - #[test] - fn panoramic_image_is_12_cents() { - assert_eq!(estimate_usd_cents(single_image_builder(true)), 12); - } - - #[test] - fn multi_image_is_20_cents() { - assert_eq!(estimate_usd_cents(multi_image_builder()), 20); - } - - #[test] - fn video_is_20_cents() { - assert_eq!(estimate_usd_cents(video_builder()), 20); - } - - #[test] - fn credits_come_from_the_provider_pricing_table() { - let estimate = text_builder().build2() - .expect("build should succeed") - .estimate_cost() - .expect("estimate should succeed"); - assert_eq!(estimate.cost_in_credits, Some(230)); - } - - // ── Helpers ── - - fn base_builder() -> GenerateSplatRequestBuilder { - GenerateSplatRequestBuilder { - model: RouterSplatModel::Marble1p0Draft, - provider: RouterProvider::WorldLabs, - ..Default::default() - } - } - - fn text_builder() -> GenerateSplatRequestBuilder { - GenerateSplatRequestBuilder { - prompt: Some("a cozy cabin in the snowy mountains".to_string()), - ..base_builder() - } - } - - fn single_image_builder(is_panoramic: bool) -> GenerateSplatRequestBuilder { - GenerateSplatRequestBuilder { - reference_images: Some(ImageListRef::MediaFileTokens(vec![ - MediaFileToken::new("mf_image_1".to_string()), - ])), - is_panoramic: Some(is_panoramic), - ..base_builder() - } - } - - fn multi_image_builder() -> GenerateSplatRequestBuilder { - GenerateSplatRequestBuilder { - reference_images: Some(ImageListRef::MediaFileTokens(vec![ - MediaFileToken::new("mf_image_1".to_string()), - MediaFileToken::new("mf_image_2".to_string()), - ])), - ..base_builder() - } - } - - fn video_builder() -> GenerateSplatRequestBuilder { - GenerateSplatRequestBuilder { - reference_video: Some(VideoRef::MediaFileToken(MediaFileToken::new("mf_video".to_string()))), - ..base_builder() - } - } - - fn estimate_usd_cents(builder: GenerateSplatRequestBuilder) -> u64 { - builder.build2() - .expect("build should succeed") - .estimate_cost() - .expect("estimate should succeed") - .cost_in_usd_cents - .expect("cost should be present") - } -} diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0_draft/draft.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0_draft/draft.rs deleted file mode 100644 index 67fa389c..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0_draft/draft.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::errors::artcraft_router_error::ArtcraftRouterError; -use crate::generate::generate_splat::providers::worldlabs::draft_common::WorldLabsSplatDraft; -use crate::generate::generate_splat::providers::worldlabs::marble_1p0_draft::request::WorldLabsMarble1p0DraftModelRequestState; -use crate::generate::generate_splat::splat_generation_draft_context::SplatGenerationDraftContext; - -#[derive(Clone, Debug)] -pub struct WorldLabsMarble1p0DraftModelDraftState { - /// Shared World Labs draft; the model is baked in at build time. - pub draft: WorldLabsSplatDraft, -} - -impl WorldLabsMarble1p0DraftModelDraftState { - pub async fn to_request(&self, draft_context: &SplatGenerationDraftContext<'_>) -> Result { - let request = self.draft.to_request(draft_context).await?; - Ok(WorldLabsMarble1p0DraftModelRequestState { request }) - } -} diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0_draft/mod.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0_draft/mod.rs deleted file mode 100644 index 89225a36..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0_draft/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod build; -pub mod cost; -pub mod draft; -pub mod request; diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0_draft/request.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0_draft/request.rs deleted file mode 100644 index 6fbfec0b..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p0_draft/request.rs +++ /dev/null @@ -1,102 +0,0 @@ -use crate::client::router_worldlabs_client::RouterWorldLabsClient; -use crate::errors::artcraft_router_error::ArtcraftRouterError; -use crate::generate::generate_splat::generate_splat_response::GenerateSplatResponse; -use crate::generate::generate_splat::providers::worldlabs::request_common::WorldLabsSplatRequest; - -#[derive(Clone, Debug)] -pub struct WorldLabsMarble1p0DraftModelRequestState { - /// Final materialized request; ready to fire. Media references (if any) - /// have been uploaded as World Labs media assets by the draft phase. - pub request: WorldLabsSplatRequest, -} - -impl WorldLabsMarble1p0DraftModelRequestState { - pub async fn send(&self, client: &RouterWorldLabsClient) -> Result { - self.request.send(client).await - } -} - -#[cfg(test)] -mod tests { - use test_data::web::image_urls::MOUNTAIN_TREE_IMAGE_URL; - - use crate::api::image_list_ref::ImageListRef; - use crate::api::router_provider::RouterProvider; - use crate::api::router_splat_model::RouterSplatModel; - use crate::client::router_client::RouterClient; - use crate::client::router_worldlabs_client::RouterWorldLabsClient; - use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder; - use crate::generate::generate_splat::generate_splat_response::GenerateSplatResponse; - use crate::generate::generate_splat::splat_generation_draft_context::SplatGenerationDraftContext; - use crate::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest; - - const WORLDLABS_API_KEY_PATH: &str = "/Users/bt/Artcraft/credentials/world_labs_api_key.txt"; - - #[tokio::test] - #[ignore] // sends a real generation to World Labs, incurs cost - async fn text_to_world() { - let response = run_pipeline(GenerateSplatRequestBuilder { - prompt: Some("A cozy cabin in the snowy mountains".to_string()), - ..base_builder() - }).await; - let payload = response.get_worldlabs_payload().expect("expected WorldLabs payload"); - assert!(!payload.operation_id.is_empty()); - assert_eq!(1, 2, "Inspect output above"); - } - - #[tokio::test] - #[ignore] // sends a real generation to World Labs, incurs cost; uploads media - async fn image_to_world() { - let response = run_pipeline(GenerateSplatRequestBuilder { - prompt: Some("Mountain landscape with a tree".to_string()), - reference_images: Some(ImageListRef::Urls(vec![MOUNTAIN_TREE_IMAGE_URL.to_string()])), - ..base_builder() - }).await; - let payload = response.get_worldlabs_payload().expect("expected WorldLabs payload"); - assert!(!payload.operation_id.is_empty()); - assert_eq!(1, 2, "Inspect output above"); - } - - // ── Helpers ── - - fn base_builder() -> GenerateSplatRequestBuilder { - GenerateSplatRequestBuilder { - model: RouterSplatModel::Marble1p0Draft, - provider: RouterProvider::WorldLabs, - ..Default::default() - } - } - - fn get_worldlabs_client() -> RouterClient { - let api_key = std::fs::read_to_string(WORLDLABS_API_KEY_PATH) - .expect("Failed to read world_labs_api_key.txt"); - RouterClient::WorldLabs(RouterWorldLabsClient::new_from_raw_key(api_key.trim())) - } - - async fn run_pipeline(builder: GenerateSplatRequestBuilder) -> GenerateSplatResponse { - let client = get_worldlabs_client(); - - let draft_or_request = builder.build2().expect("build2 should succeed"); - let request = match draft_or_request { - SplatGenerationDraftOrRequest::Request(request) => request, - SplatGenerationDraftOrRequest::Draft(draft) => { - let draft_context = SplatGenerationDraftContext { - client: Some(&client), - ..Default::default() - }; - draft.finalize(draft_context).await.expect("finalize should succeed") - } - }; - - let response = request.send_request(&client).await.expect("send_request should succeed"); - - match &response { - GenerateSplatResponse::WorldLabs(p) => { - println!("operation_id={}, done={}", p.operation_id, p.done); - } - other => println!("response: {:?}", other), - } - - response - } -} diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1/build.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1/build.rs deleted file mode 100644 index 2972c348..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1/build.rs +++ /dev/null @@ -1,25 +0,0 @@ -use worldlabs_api_client::api::api_types::world_labs_model::WorldLabsModel; - -use crate::errors::artcraft_router_error::ArtcraftRouterError; -use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder; -use crate::generate::generate_splat::providers::worldlabs::build_common::{ - build_worldlabs_splat, WorldLabsSplatDraftOrRequest, -}; -use crate::generate::generate_splat::providers::worldlabs::marble_1p1::draft::WorldLabsMarble1p1DraftState; -use crate::generate::generate_splat::providers::worldlabs::marble_1p1::request::WorldLabsMarble1p1RequestState; -use crate::generate::generate_splat::splat_generation_draft::SplatGenerationDraftRequest; -use crate::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest; -use crate::generate::generate_splat::splat_generation_request::SplatGenerationRequest; - -pub fn build_worldlabs_marble_1p1(builder: GenerateSplatRequestBuilder) -> Result { - match build_worldlabs_splat(builder, WorldLabsModel::Marble1p1)? { - WorldLabsSplatDraftOrRequest::Request(request) => { - let state = WorldLabsMarble1p1RequestState { request }; - Ok(SplatGenerationDraftOrRequest::Request(SplatGenerationRequest::WorldLabsMarble1p1(state))) - } - WorldLabsSplatDraftOrRequest::Draft(draft) => { - let state = WorldLabsMarble1p1DraftState { draft }; - Ok(SplatGenerationDraftOrRequest::Draft(SplatGenerationDraftRequest::WorldLabsMarble1p1(state))) - } - } -} diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1/cost.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1/cost.rs deleted file mode 100644 index 48cdeeeb..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1/cost.rs +++ /dev/null @@ -1,136 +0,0 @@ -use worldlabs_api_client::api::api_types::world_labs_model::WorldLabsModel; -use worldlabs_api_client::pricing::check_pricing::{calculate_cost, InputType}; - -use crate::generate::generate_splat::providers::worldlabs::marble_1p1::draft::WorldLabsMarble1p1DraftState; -use crate::generate::generate_splat::providers::worldlabs::marble_1p1::request::WorldLabsMarble1p1RequestState; -use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate; - -const MODEL: WorldLabsModel = WorldLabsModel::Marble1p1; - -/// Cost math is owned by worldlabs_api_client's pricing table — the router -/// just forwards the result so router cost ≡ provider cost by construction. -#[derive(Clone, Debug)] -pub struct WorldLabsMarble1p1CostState { - pub input_type: InputType, -} - -impl WorldLabsMarble1p1CostState { - pub fn from_request(request: &WorldLabsMarble1p1RequestState) -> Self { - Self { input_type: request.request.input_type() } - } - - pub fn from_draft(draft: &WorldLabsMarble1p1DraftState) -> Self { - Self { input_type: draft.draft.input_type() } - } - - pub fn estimate_cost(&self) -> SplatGenerationCostEstimate { - let cost = calculate_cost(MODEL, self.input_type); - SplatGenerationCostEstimate { - cost_in_credits: Some(cost.worldlabs_credits as u64), - cost_in_usd_cents: Some(cost.us_dollar_cents as u64), - is_free: false, - is_unlimited: false, - is_rate_limited: false, - has_watermark: false, - failures_are_refunded: None, - } - } -} - -#[cfg(test)] -mod tests { - use sqlite_identifiers::ids::media_file_token::MediaFileToken; - - use crate::api::image_list_ref::ImageListRef; - use crate::api::router_provider::RouterProvider; - use crate::api::router_splat_model::RouterSplatModel; - use crate::api::video_ref::VideoRef; - use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder; - - #[test] - fn text_is_126_cents() { - assert_eq!(estimate_usd_cents(text_builder()), 126); - } - - #[test] - fn single_image_is_126_cents() { - assert_eq!(estimate_usd_cents(single_image_builder(false)), 126); - } - - #[test] - fn panoramic_image_is_120_cents() { - assert_eq!(estimate_usd_cents(single_image_builder(true)), 120); - } - - #[test] - fn multi_image_is_128_cents() { - assert_eq!(estimate_usd_cents(multi_image_builder()), 128); - } - - #[test] - fn video_is_128_cents() { - assert_eq!(estimate_usd_cents(video_builder()), 128); - } - - #[test] - fn credits_come_from_the_provider_pricing_table() { - let estimate = text_builder().build2() - .expect("build should succeed") - .estimate_cost() - .expect("estimate should succeed"); - assert_eq!(estimate.cost_in_credits, Some(1580)); - } - - // ── Helpers ── - - fn base_builder() -> GenerateSplatRequestBuilder { - GenerateSplatRequestBuilder { - model: RouterSplatModel::Marble1p1, - provider: RouterProvider::WorldLabs, - ..Default::default() - } - } - - fn text_builder() -> GenerateSplatRequestBuilder { - GenerateSplatRequestBuilder { - prompt: Some("a cozy cabin in the snowy mountains".to_string()), - ..base_builder() - } - } - - fn single_image_builder(is_panoramic: bool) -> GenerateSplatRequestBuilder { - GenerateSplatRequestBuilder { - reference_images: Some(ImageListRef::MediaFileTokens(vec![ - MediaFileToken::new("mf_image_1".to_string()), - ])), - is_panoramic: Some(is_panoramic), - ..base_builder() - } - } - - fn multi_image_builder() -> GenerateSplatRequestBuilder { - GenerateSplatRequestBuilder { - reference_images: Some(ImageListRef::MediaFileTokens(vec![ - MediaFileToken::new("mf_image_1".to_string()), - MediaFileToken::new("mf_image_2".to_string()), - ])), - ..base_builder() - } - } - - fn video_builder() -> GenerateSplatRequestBuilder { - GenerateSplatRequestBuilder { - reference_video: Some(VideoRef::MediaFileToken(MediaFileToken::new("mf_video".to_string()))), - ..base_builder() - } - } - - fn estimate_usd_cents(builder: GenerateSplatRequestBuilder) -> u64 { - builder.build2() - .expect("build should succeed") - .estimate_cost() - .expect("estimate should succeed") - .cost_in_usd_cents - .expect("cost should be present") - } -} diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1/draft.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1/draft.rs deleted file mode 100644 index 457e6e41..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1/draft.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::errors::artcraft_router_error::ArtcraftRouterError; -use crate::generate::generate_splat::providers::worldlabs::draft_common::WorldLabsSplatDraft; -use crate::generate::generate_splat::providers::worldlabs::marble_1p1::request::WorldLabsMarble1p1RequestState; -use crate::generate::generate_splat::splat_generation_draft_context::SplatGenerationDraftContext; - -#[derive(Clone, Debug)] -pub struct WorldLabsMarble1p1DraftState { - /// Shared World Labs draft; the model is baked in at build time. - pub draft: WorldLabsSplatDraft, -} - -impl WorldLabsMarble1p1DraftState { - pub async fn to_request(&self, draft_context: &SplatGenerationDraftContext<'_>) -> Result { - let request = self.draft.to_request(draft_context).await?; - Ok(WorldLabsMarble1p1RequestState { request }) - } -} diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1/mod.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1/mod.rs deleted file mode 100644 index 89225a36..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod build; -pub mod cost; -pub mod draft; -pub mod request; diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1/request.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1/request.rs deleted file mode 100644 index 8556d4aa..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1/request.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::client::router_worldlabs_client::RouterWorldLabsClient; -use crate::errors::artcraft_router_error::ArtcraftRouterError; -use crate::generate::generate_splat::generate_splat_response::GenerateSplatResponse; -use crate::generate::generate_splat::providers::worldlabs::request_common::WorldLabsSplatRequest; - -#[derive(Clone, Debug)] -pub struct WorldLabsMarble1p1RequestState { - /// Final materialized request; ready to fire. Media references (if any) - /// have been uploaded as World Labs media assets by the draft phase. - pub request: WorldLabsSplatRequest, -} - -impl WorldLabsMarble1p1RequestState { - pub async fn send(&self, client: &RouterWorldLabsClient) -> Result { - self.request.send(client).await - } -} diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1_plus/build.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1_plus/build.rs deleted file mode 100644 index 899930f9..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1_plus/build.rs +++ /dev/null @@ -1,25 +0,0 @@ -use worldlabs_api_client::api::api_types::world_labs_model::WorldLabsModel; - -use crate::errors::artcraft_router_error::ArtcraftRouterError; -use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder; -use crate::generate::generate_splat::providers::worldlabs::build_common::{ - build_worldlabs_splat, WorldLabsSplatDraftOrRequest, -}; -use crate::generate::generate_splat::providers::worldlabs::marble_1p1_plus::draft::WorldLabsMarble1p1PlusDraftState; -use crate::generate::generate_splat::providers::worldlabs::marble_1p1_plus::request::WorldLabsMarble1p1PlusRequestState; -use crate::generate::generate_splat::splat_generation_draft::SplatGenerationDraftRequest; -use crate::generate::generate_splat::splat_generation_draft_or_request::SplatGenerationDraftOrRequest; -use crate::generate::generate_splat::splat_generation_request::SplatGenerationRequest; - -pub fn build_worldlabs_marble_1p1_plus(builder: GenerateSplatRequestBuilder) -> Result { - match build_worldlabs_splat(builder, WorldLabsModel::Marble1p1Plus)? { - WorldLabsSplatDraftOrRequest::Request(request) => { - let state = WorldLabsMarble1p1PlusRequestState { request }; - Ok(SplatGenerationDraftOrRequest::Request(SplatGenerationRequest::WorldLabsMarble1p1Plus(state))) - } - WorldLabsSplatDraftOrRequest::Draft(draft) => { - let state = WorldLabsMarble1p1PlusDraftState { draft }; - Ok(SplatGenerationDraftOrRequest::Draft(SplatGenerationDraftRequest::WorldLabsMarble1p1Plus(state))) - } - } -} diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1_plus/cost.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1_plus/cost.rs deleted file mode 100644 index 95183c2a..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1_plus/cost.rs +++ /dev/null @@ -1,136 +0,0 @@ -use worldlabs_api_client::api::api_types::world_labs_model::WorldLabsModel; -use worldlabs_api_client::pricing::check_pricing::{calculate_cost, InputType}; - -use crate::generate::generate_splat::providers::worldlabs::marble_1p1_plus::draft::WorldLabsMarble1p1PlusDraftState; -use crate::generate::generate_splat::providers::worldlabs::marble_1p1_plus::request::WorldLabsMarble1p1PlusRequestState; -use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate; - -const MODEL: WorldLabsModel = WorldLabsModel::Marble1p1Plus; - -/// Cost math is owned by worldlabs_api_client's pricing table — the router -/// just forwards the result so router cost ≡ provider cost by construction. -#[derive(Clone, Debug)] -pub struct WorldLabsMarble1p1PlusCostState { - pub input_type: InputType, -} - -impl WorldLabsMarble1p1PlusCostState { - pub fn from_request(request: &WorldLabsMarble1p1PlusRequestState) -> Self { - Self { input_type: request.request.input_type() } - } - - pub fn from_draft(draft: &WorldLabsMarble1p1PlusDraftState) -> Self { - Self { input_type: draft.draft.input_type() } - } - - pub fn estimate_cost(&self) -> SplatGenerationCostEstimate { - let cost = calculate_cost(MODEL, self.input_type); - SplatGenerationCostEstimate { - cost_in_credits: Some(cost.worldlabs_credits as u64), - cost_in_usd_cents: Some(cost.us_dollar_cents as u64), - is_free: false, - is_unlimited: false, - is_rate_limited: false, - has_watermark: false, - failures_are_refunded: None, - } - } -} - -#[cfg(test)] -mod tests { - use sqlite_identifiers::ids::media_file_token::MediaFileToken; - - use crate::api::image_list_ref::ImageListRef; - use crate::api::router_provider::RouterProvider; - use crate::api::router_splat_model::RouterSplatModel; - use crate::api::video_ref::VideoRef; - use crate::generate::generate_splat::generate_splat_request_builder::GenerateSplatRequestBuilder; - - #[test] - fn text_is_246_cents() { - assert_eq!(estimate_usd_cents(text_builder()), 246); - } - - #[test] - fn single_image_is_246_cents() { - assert_eq!(estimate_usd_cents(single_image_builder(false)), 246); - } - - #[test] - fn panoramic_image_is_240_cents() { - assert_eq!(estimate_usd_cents(single_image_builder(true)), 240); - } - - #[test] - fn multi_image_is_248_cents() { - assert_eq!(estimate_usd_cents(multi_image_builder()), 248); - } - - #[test] - fn video_is_248_cents() { - assert_eq!(estimate_usd_cents(video_builder()), 248); - } - - #[test] - fn credits_come_from_the_provider_pricing_table() { - let estimate = text_builder().build2() - .expect("build should succeed") - .estimate_cost() - .expect("estimate should succeed"); - assert_eq!(estimate.cost_in_credits, Some(3080)); - } - - // ── Helpers ── - - fn base_builder() -> GenerateSplatRequestBuilder { - GenerateSplatRequestBuilder { - model: RouterSplatModel::Marble1p1Plus, - provider: RouterProvider::WorldLabs, - ..Default::default() - } - } - - fn text_builder() -> GenerateSplatRequestBuilder { - GenerateSplatRequestBuilder { - prompt: Some("a cozy cabin in the snowy mountains".to_string()), - ..base_builder() - } - } - - fn single_image_builder(is_panoramic: bool) -> GenerateSplatRequestBuilder { - GenerateSplatRequestBuilder { - reference_images: Some(ImageListRef::MediaFileTokens(vec![ - MediaFileToken::new("mf_image_1".to_string()), - ])), - is_panoramic: Some(is_panoramic), - ..base_builder() - } - } - - fn multi_image_builder() -> GenerateSplatRequestBuilder { - GenerateSplatRequestBuilder { - reference_images: Some(ImageListRef::MediaFileTokens(vec![ - MediaFileToken::new("mf_image_1".to_string()), - MediaFileToken::new("mf_image_2".to_string()), - ])), - ..base_builder() - } - } - - fn video_builder() -> GenerateSplatRequestBuilder { - GenerateSplatRequestBuilder { - reference_video: Some(VideoRef::MediaFileToken(MediaFileToken::new("mf_video".to_string()))), - ..base_builder() - } - } - - fn estimate_usd_cents(builder: GenerateSplatRequestBuilder) -> u64 { - builder.build2() - .expect("build should succeed") - .estimate_cost() - .expect("estimate should succeed") - .cost_in_usd_cents - .expect("cost should be present") - } -} diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1_plus/draft.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1_plus/draft.rs deleted file mode 100644 index 986dddf1..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1_plus/draft.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::errors::artcraft_router_error::ArtcraftRouterError; -use crate::generate::generate_splat::providers::worldlabs::draft_common::WorldLabsSplatDraft; -use crate::generate::generate_splat::providers::worldlabs::marble_1p1_plus::request::WorldLabsMarble1p1PlusRequestState; -use crate::generate::generate_splat::splat_generation_draft_context::SplatGenerationDraftContext; - -#[derive(Clone, Debug)] -pub struct WorldLabsMarble1p1PlusDraftState { - /// Shared World Labs draft; the model is baked in at build time. - pub draft: WorldLabsSplatDraft, -} - -impl WorldLabsMarble1p1PlusDraftState { - pub async fn to_request(&self, draft_context: &SplatGenerationDraftContext<'_>) -> Result { - let request = self.draft.to_request(draft_context).await?; - Ok(WorldLabsMarble1p1PlusRequestState { request }) - } -} diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1_plus/mod.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1_plus/mod.rs deleted file mode 100644 index 89225a36..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1_plus/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod build; -pub mod cost; -pub mod draft; -pub mod request; diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1_plus/request.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1_plus/request.rs deleted file mode 100644 index d1bb0a2a..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/marble_1p1_plus/request.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::client::router_worldlabs_client::RouterWorldLabsClient; -use crate::errors::artcraft_router_error::ArtcraftRouterError; -use crate::generate::generate_splat::generate_splat_response::GenerateSplatResponse; -use crate::generate::generate_splat::providers::worldlabs::request_common::WorldLabsSplatRequest; - -#[derive(Clone, Debug)] -pub struct WorldLabsMarble1p1PlusRequestState { - /// Final materialized request; ready to fire. Media references (if any) - /// have been uploaded as World Labs media assets by the draft phase. - pub request: WorldLabsSplatRequest, -} - -impl WorldLabsMarble1p1PlusRequestState { - pub async fn send(&self, client: &RouterWorldLabsClient) -> Result { - self.request.send(client).await - } -} diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/mod.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/mod.rs deleted file mode 100644 index acf8c398..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub(crate) mod build_common; -pub(crate) mod draft_common; -pub(crate) mod request_common; -pub(crate) mod resolve; -pub mod marble_1p0; -pub mod marble_1p0_draft; -pub mod marble_1p1; -pub mod marble_1p1_plus; diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/request_common.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/request_common.rs deleted file mode 100644 index c4bc907b..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/request_common.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::time::Duration; - -use worldlabs_api_client::api::api_types::world_labs_model::WorldLabsModel; -use worldlabs_api_client::api::requests::generate_world::generate_world::{ - generate_world, GenerateWorldArgs, -}; -use worldlabs_api_client::api::requests::generate_world::http_request::WorldPrompt; -use worldlabs_api_client::pricing::check_pricing::InputType; - -use crate::client::router_worldlabs_client::RouterWorldLabsClient; -use crate::errors::artcraft_router_error::ArtcraftRouterError; -use crate::errors::provider_error::ProviderError; -use crate::generate::generate_splat::generate_splat_response::{ - GenerateSplatResponse, WorldLabsSplatResponsePayload, -}; - -/// How long to wait for the World Labs generate call to accept the request. -/// The call only starts the generation; status is polled separately via the -/// returned operation ID. -const GENERATE_REQUEST_TIMEOUT: Duration = Duration::from_secs(60); - -/// A fully materialized World Labs generation request, shared by all marble -/// model request states. Media references (if any) have already been -/// uploaded as World Labs media assets by the draft phase. -#[derive(Clone, Debug)] -pub struct WorldLabsSplatRequest { - pub model: WorldLabsModel, - pub world_prompt: WorldPrompt, -} - -impl WorldLabsSplatRequest { - pub async fn send(&self, client: &RouterWorldLabsClient) -> Result { - let response = generate_world(GenerateWorldArgs { - creds: &client.creds, - world_prompt: self.world_prompt.clone(), - display_name: None, - model: self.model, - seed: None, - tags: None, - permission: None, - request_timeout: Some(GENERATE_REQUEST_TIMEOUT), - }) - .await - .map_err(|err| ArtcraftRouterError::Provider(ProviderError::WorldLabs(err)))?; - - Ok(GenerateSplatResponse::WorldLabs(WorldLabsSplatResponsePayload { - operation_id: response.operation_id.as_str().to_string(), - done: response.done, - })) - } - - /// The pricing input type of the assembled prompt. - pub(crate) fn input_type(&self) -> InputType { - match &self.world_prompt { - WorldPrompt::Text { .. } => InputType::Text, - WorldPrompt::Image { is_pano, .. } => { - if is_pano.unwrap_or(false) { - InputType::ImagePanorama - } else { - InputType::ImageNonPanorama - } - } - WorldPrompt::MultiImage { .. } => InputType::MultiImage, - WorldPrompt::Video { .. } => InputType::Video, - } - } -} diff --git a/crates/router/src/generate/generate_splat/providers/worldlabs/resolve.rs b/crates/router/src/generate/generate_splat/providers/worldlabs/resolve.rs deleted file mode 100644 index d0f28f08..00000000 --- a/crates/router/src/generate/generate_splat/providers/worldlabs/resolve.rs +++ /dev/null @@ -1,208 +0,0 @@ -//! Input planning for the World Labs splat provider. -//! -//! The derivation is shared by request assembly and pricing so the two -//! always agree on the input type. - -use worldlabs_api_client::pricing::check_pricing::InputType; - -use crate::api::image_list_ref::ImageListRef; -use crate::api::image_ref::ImageRef; -use crate::api::video_ref::VideoRef; -use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; -use crate::errors::artcraft_router_error::ArtcraftRouterError; -use crate::errors::client_error::ClientError; - -/// The planned splat input, derived from the builder's media references. -#[derive(Clone, Debug)] -pub(crate) enum SplatInput { - Text, - Image { image: ImageRef, is_panoramic: bool }, - MultiImage { images: Vec }, - Video { video: VideoRef }, -} - -impl SplatInput { - pub(crate) fn to_input_type(&self) -> InputType { - match self { - SplatInput::Text => InputType::Text, - SplatInput::Image { is_panoramic: true, .. } => InputType::ImagePanorama, - SplatInput::Image { is_panoramic: false, .. } => InputType::ImageNonPanorama, - SplatInput::MultiImage { .. } => InputType::MultiImage, - SplatInput::Video { .. } => InputType::Video, - } - } -} - -/// Derive the splat input from the builder's references: -/// - a reference video wins (images alongside it reject under `ErrorOut` and -/// are dropped otherwise), -/// - two or more reference images are multi-image input, -/// - one reference image is a single panorama or non-panorama image, -/// - otherwise a prompt is required for text input. -pub(crate) fn plan_splat_input( - reference_images: Option, - reference_video: Option, - is_panoramic: Option, - has_prompt: bool, - strategy: RequestMismatchMitigationStrategy, -) -> Result { - let mut images = image_list_to_refs(reference_images); - - if let Some(video) = reference_video { - if !images.is_empty() { - if let RequestMismatchMitigationStrategy::ErrorOut = strategy { - return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { - field: "reference_images", - value: "Reference images cannot be combined with a reference video".to_string(), - })); - } - } - return Ok(SplatInput::Video { video }); - } - - match images.len() { - 0 => { - if has_prompt { - Ok(SplatInput::Text) - } else { - Err(ArtcraftRouterError::InvalidInput( - "A prompt, reference image, or reference video is required for splat generation".to_string(), - )) - } - } - 1 => Ok(SplatInput::Image { - image: images.remove(0), - is_panoramic: is_panoramic.unwrap_or(false), - }), - _ => Ok(SplatInput::MultiImage { images }), - } -} - -fn image_list_to_refs(reference_images: Option) -> Vec { - match reference_images { - None => Vec::new(), - Some(list) => list.into_refs(), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - const IMAGE_URL: &str = "https://example.com/room.png"; - const VIDEO_URL: &str = "https://example.com/room.mp4"; - - #[test] - fn video_wins() { - let input = plan_splat_input( - None, - Some(VideoRef::Url(VIDEO_URL.to_string())), - None, - true, - RequestMismatchMitigationStrategy::ErrorOut, - ).expect("plan"); - assert!(matches!(input, SplatInput::Video { .. })); - assert_eq!(input.to_input_type(), InputType::Video); - } - - #[test] - fn video_with_images_errors_out_under_error_out() { - let result = plan_splat_input( - Some(ImageListRef::Urls(vec![IMAGE_URL.to_string()])), - Some(VideoRef::Url(VIDEO_URL.to_string())), - None, - false, - RequestMismatchMitigationStrategy::ErrorOut, - ); - assert!(result.is_err()); - } - - #[test] - fn video_with_images_drops_the_images_under_lenient_strategies() { - let input = plan_splat_input( - Some(ImageListRef::Urls(vec![IMAGE_URL.to_string()])), - Some(VideoRef::Url(VIDEO_URL.to_string())), - None, - false, - RequestMismatchMitigationStrategy::PayMoreUpgrade, - ).expect("plan"); - assert!(matches!(input, SplatInput::Video { .. })); - } - - #[test] - fn two_images_are_multi_image() { - let input = plan_splat_input( - Some(ImageListRef::Urls(vec![ - IMAGE_URL.to_string(), - "https://example.com/other.png".to_string(), - ])), - None, - None, - false, - RequestMismatchMitigationStrategy::ErrorOut, - ).expect("plan"); - assert_eq!(input.to_input_type(), InputType::MultiImage); - } - - #[test] - fn one_panoramic_image_is_image_panorama() { - let input = plan_splat_input( - Some(ImageListRef::Urls(vec![IMAGE_URL.to_string()])), - None, - Some(true), - false, - RequestMismatchMitigationStrategy::ErrorOut, - ).expect("plan"); - assert_eq!(input.to_input_type(), InputType::ImagePanorama); - } - - #[test] - fn one_image_is_image_non_panorama() { - for is_panoramic in [None, Some(false)] { - let input = plan_splat_input( - Some(ImageListRef::Urls(vec![IMAGE_URL.to_string()])), - None, - is_panoramic, - false, - RequestMismatchMitigationStrategy::ErrorOut, - ).expect("plan"); - assert_eq!(input.to_input_type(), InputType::ImageNonPanorama); - } - } - - #[test] - fn prompt_only_is_text() { - let input = plan_splat_input( - None, - None, - None, - true, - RequestMismatchMitigationStrategy::ErrorOut, - ).expect("plan"); - assert_eq!(input.to_input_type(), InputType::Text); - } - - #[test] - fn empty_input_is_rejected() { - let result = plan_splat_input( - None, - None, - None, - false, - RequestMismatchMitigationStrategy::PayMoreUpgrade, - ); - assert!(matches!(result, Err(ArtcraftRouterError::InvalidInput(_)))); - } - - #[test] - fn empty_image_list_counts_as_no_images() { - let input = plan_splat_input( - Some(ImageListRef::Urls(vec![])), - None, - None, - true, - RequestMismatchMitigationStrategy::ErrorOut, - ).expect("plan"); - assert_eq!(input.to_input_type(), InputType::Text); - } -} diff --git a/crates/router/src/generate/generate_splat/splat_generation_draft.rs b/crates/router/src/generate/generate_splat/splat_generation_draft.rs deleted file mode 100644 index 381b6867..00000000 --- a/crates/router/src/generate/generate_splat/splat_generation_draft.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::api::router_provider::RouterProvider; -use crate::errors::artcraft_router_error::ArtcraftRouterError; -use crate::generate::generate_splat::providers::worldlabs::marble_1p0::cost::WorldLabsMarble1p0CostState; -use crate::generate::generate_splat::providers::worldlabs::marble_1p0::draft::WorldLabsMarble1p0DraftState; -use crate::generate::generate_splat::providers::worldlabs::marble_1p0_draft::cost::WorldLabsMarble1p0DraftModelCostState; -use crate::generate::generate_splat::providers::worldlabs::marble_1p0_draft::draft::WorldLabsMarble1p0DraftModelDraftState; -use crate::generate::generate_splat::providers::worldlabs::marble_1p1::cost::WorldLabsMarble1p1CostState; -use crate::generate::generate_splat::providers::worldlabs::marble_1p1::draft::WorldLabsMarble1p1DraftState; -use crate::generate::generate_splat::providers::worldlabs::marble_1p1_plus::cost::WorldLabsMarble1p1PlusCostState; -use crate::generate::generate_splat::providers::worldlabs::marble_1p1_plus::draft::WorldLabsMarble1p1PlusDraftState; -use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate; -use crate::generate::generate_splat::splat_generation_draft_context::SplatGenerationDraftContext; -use crate::generate::generate_splat::splat_generation_request::SplatGenerationRequest; - -/** - * Wrapper for all splat generation draft requests. - * - * Only the World Labs media inputs need a draft phase: reference images and - * videos must be downloaded from our CDN and re-uploaded to World Labs media - * assets before the request can be sent. Text-only prompts (and the Artcraft - * provider, which takes media tokens directly) return a `Request` from - * `build2()`. - */ -#[derive(Clone, Debug)] -pub enum SplatGenerationDraftRequest { - WorldLabsMarble1p0(WorldLabsMarble1p0DraftState), - WorldLabsMarble1p0Draft(WorldLabsMarble1p0DraftModelDraftState), - WorldLabsMarble1p1(WorldLabsMarble1p1DraftState), - WorldLabsMarble1p1Plus(WorldLabsMarble1p1PlusDraftState), -} - -impl SplatGenerationDraftRequest { - - pub fn get_provider(&self) -> RouterProvider { - match self { - Self::WorldLabsMarble1p0(_) => RouterProvider::WorldLabs, - Self::WorldLabsMarble1p0Draft(_) => RouterProvider::WorldLabs, - Self::WorldLabsMarble1p1(_) => RouterProvider::WorldLabs, - Self::WorldLabsMarble1p1Plus(_) => RouterProvider::WorldLabs, - } - } - - /// Return a cost estimate to fulfill the request. - pub fn estimate_cost(&self) -> Result { - match self { - SplatGenerationDraftRequest::WorldLabsMarble1p0(draft) => Ok(WorldLabsMarble1p0CostState::from_draft(draft).estimate_cost()), - SplatGenerationDraftRequest::WorldLabsMarble1p0Draft(draft) => Ok(WorldLabsMarble1p0DraftModelCostState::from_draft(draft).estimate_cost()), - SplatGenerationDraftRequest::WorldLabsMarble1p1(draft) => Ok(WorldLabsMarble1p1CostState::from_draft(draft).estimate_cost()), - SplatGenerationDraftRequest::WorldLabsMarble1p1Plus(draft) => Ok(WorldLabsMarble1p1PlusCostState::from_draft(draft).estimate_cost()), - } - } - - /// Finalize the draft request before generation - /// This may involve uploading media to the provider. - pub async fn finalize(self, draft_context: SplatGenerationDraftContext<'_>) -> Result { - match self { - SplatGenerationDraftRequest::WorldLabsMarble1p0(draft) => { - let result = draft.to_request(&draft_context).await?; - Ok(SplatGenerationRequest::WorldLabsMarble1p0(result)) - }, - SplatGenerationDraftRequest::WorldLabsMarble1p0Draft(draft) => { - let result = draft.to_request(&draft_context).await?; - Ok(SplatGenerationRequest::WorldLabsMarble1p0Draft(result)) - }, - SplatGenerationDraftRequest::WorldLabsMarble1p1(draft) => { - let result = draft.to_request(&draft_context).await?; - Ok(SplatGenerationRequest::WorldLabsMarble1p1(result)) - }, - SplatGenerationDraftRequest::WorldLabsMarble1p1Plus(draft) => { - let result = draft.to_request(&draft_context).await?; - Ok(SplatGenerationRequest::WorldLabsMarble1p1Plus(result)) - }, - } - } -} diff --git a/crates/router/src/generate/generate_splat/splat_generation_draft_context.rs b/crates/router/src/generate/generate_splat/splat_generation_draft_context.rs deleted file mode 100644 index 6ff06ecf..00000000 --- a/crates/router/src/generate/generate_splat/splat_generation_draft_context.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::collections::HashMap; -use std::fmt::Debug; - -use sqlite_identifiers::ids::media_file_token::MediaFileToken; - -use crate::client::router_client::RouterClient; -use crate::client::router_worldlabs_client::RouterWorldLabsClient; -use crate::errors::artcraft_router_error::ArtcraftRouterError; -use crate::errors::client_error::ClientError; - -#[derive(Clone, Default)] -pub struct SplatGenerationDraftContext<'a> { - /// Optional: the router client, needed for providers that require authentication. - pub client: Option<&'a RouterClient>, - - /// Optional context: a map of Media File Tokens to their ArtCraft URLs - /// Only needed if we have to fetch these assets and upload them to another provider. - pub media_file_to_artcraft_url_map: Option<&'a HashMap>, -} - -impl <'a> SplatGenerationDraftContext<'a> { - pub fn get_worldlabs_client_ref(&self) -> Result<&RouterWorldLabsClient, ArtcraftRouterError> { - let client = self.client.ok_or(ArtcraftRouterError::Client(ClientError::RouterClientNotProvided))?; - client.get_worldlabs_client_ref() - .map_err(|err| ArtcraftRouterError::Client(err)) - } -} - -impl Debug for SplatGenerationDraftContext<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("SplatGenerationDraftContext") - .field("client", &self.client.is_some()) - .field("media_file_to_artcraft_url_map", &self.media_file_to_artcraft_url_map) - .finish() - } -} diff --git a/crates/router/src/generate/generate_splat/splat_generation_draft_or_request.rs b/crates/router/src/generate/generate_splat/splat_generation_draft_or_request.rs deleted file mode 100644 index 5046e6c2..00000000 --- a/crates/router/src/generate/generate_splat/splat_generation_draft_or_request.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::api::router_provider::RouterProvider; -use crate::errors::artcraft_router_error::ArtcraftRouterError; -use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate; -use crate::generate::generate_splat::splat_generation_draft::SplatGenerationDraftRequest; -use crate::generate::generate_splat::splat_generation_request::SplatGenerationRequest; - -#[derive(Debug, Clone)] -pub enum SplatGenerationDraftOrRequest { - Draft(SplatGenerationDraftRequest), - Request(SplatGenerationRequest), -} - -impl SplatGenerationDraftOrRequest { - - pub fn get_provider(&self) -> RouterProvider { - match self { - Self::Draft(draft) => draft.get_provider(), - Self::Request(request) => request.get_provider(), - } - } - - pub fn estimate_cost(&self) -> Result { - match self { - SplatGenerationDraftOrRequest::Draft(draft) => draft.estimate_cost(), - SplatGenerationDraftOrRequest::Request(request) => request.estimate_cost(), - } - } -} diff --git a/crates/router/src/generate/generate_splat/splat_generation_request.rs b/crates/router/src/generate/generate_splat/splat_generation_request.rs index 769cff5d..256d2c8c 100644 --- a/crates/router/src/generate/generate_splat/splat_generation_request.rs +++ b/crates/router/src/generate/generate_splat/splat_generation_request.rs @@ -14,14 +14,6 @@ use crate::generate::generate_splat::providers::artcraft::triposplat::cost::Artc use crate::generate::generate_splat::providers::artcraft::triposplat::request::ArtcraftTripoSplatRequestState; use crate::generate::generate_splat::providers::fal::triposplat::cost::FalTripoSplatCostState; use crate::generate::generate_splat::providers::fal::triposplat::request::FalTripoSplatRequestState; -use crate::generate::generate_splat::providers::worldlabs::marble_1p0::cost::WorldLabsMarble1p0CostState; -use crate::generate::generate_splat::providers::worldlabs::marble_1p0::request::WorldLabsMarble1p0RequestState; -use crate::generate::generate_splat::providers::worldlabs::marble_1p0_draft::cost::WorldLabsMarble1p0DraftModelCostState; -use crate::generate::generate_splat::providers::worldlabs::marble_1p0_draft::request::WorldLabsMarble1p0DraftModelRequestState; -use crate::generate::generate_splat::providers::worldlabs::marble_1p1::cost::WorldLabsMarble1p1CostState; -use crate::generate::generate_splat::providers::worldlabs::marble_1p1::request::WorldLabsMarble1p1RequestState; -use crate::generate::generate_splat::providers::worldlabs::marble_1p1_plus::cost::WorldLabsMarble1p1PlusCostState; -use crate::generate::generate_splat::providers::worldlabs::marble_1p1_plus::request::WorldLabsMarble1p1PlusRequestState; use crate::generate::generate_splat::splat_generation_cost_estimate::SplatGenerationCostEstimate; #[derive(Clone, Debug)] @@ -32,10 +24,6 @@ pub enum SplatGenerationRequest { ArtcraftMarble1p1Plus(ArtcraftMarble1p1PlusRequestState), ArtcraftTripoSplat(ArtcraftTripoSplatRequestState), FalTripoSplat(FalTripoSplatRequestState), - WorldLabsMarble1p0(WorldLabsMarble1p0RequestState), - WorldLabsMarble1p0Draft(WorldLabsMarble1p0DraftModelRequestState), - WorldLabsMarble1p1(WorldLabsMarble1p1RequestState), - WorldLabsMarble1p1Plus(WorldLabsMarble1p1PlusRequestState), } impl SplatGenerationRequest { @@ -48,10 +36,6 @@ impl SplatGenerationRequest { Self::ArtcraftMarble1p1Plus(_) => RouterProvider::Artcraft, Self::ArtcraftTripoSplat(_) => RouterProvider::Artcraft, Self::FalTripoSplat(_) => RouterProvider::Fal, - Self::WorldLabsMarble1p0(_) => RouterProvider::WorldLabs, - Self::WorldLabsMarble1p0Draft(_) => RouterProvider::WorldLabs, - Self::WorldLabsMarble1p1(_) => RouterProvider::WorldLabs, - Self::WorldLabsMarble1p1Plus(_) => RouterProvider::WorldLabs, } } @@ -64,10 +48,6 @@ impl SplatGenerationRequest { SplatGenerationRequest::ArtcraftMarble1p1Plus(request) => Ok(ArtcraftMarble1p1PlusCostState::from_request(request).estimate_cost()), SplatGenerationRequest::ArtcraftTripoSplat(request) => Ok(ArtcraftTripoSplatCostState::from_request(request).estimate_cost()), SplatGenerationRequest::FalTripoSplat(request) => Ok(FalTripoSplatCostState::from_request(request).estimate_cost()), - SplatGenerationRequest::WorldLabsMarble1p0(request) => Ok(WorldLabsMarble1p0CostState::from_request(request).estimate_cost()), - SplatGenerationRequest::WorldLabsMarble1p0Draft(request) => Ok(WorldLabsMarble1p0DraftModelCostState::from_request(request).estimate_cost()), - SplatGenerationRequest::WorldLabsMarble1p1(request) => Ok(WorldLabsMarble1p1CostState::from_request(request).estimate_cost()), - SplatGenerationRequest::WorldLabsMarble1p1Plus(request) => Ok(WorldLabsMarble1p1PlusCostState::from_request(request).estimate_cost()), } } @@ -99,22 +79,6 @@ impl SplatGenerationRequest { let client_ref = client.get_fal_client_ref()?; request.send(client_ref).await }, - SplatGenerationRequest::WorldLabsMarble1p0(request) => { - let client_ref = client.get_worldlabs_client_ref()?; - request.send(client_ref).await - }, - SplatGenerationRequest::WorldLabsMarble1p0Draft(request) => { - let client_ref = client.get_worldlabs_client_ref()?; - request.send(client_ref).await - }, - SplatGenerationRequest::WorldLabsMarble1p1(request) => { - let client_ref = client.get_worldlabs_client_ref()?; - request.send(client_ref).await - }, - SplatGenerationRequest::WorldLabsMarble1p1Plus(request) => { - let client_ref = client.get_worldlabs_client_ref()?; - request.send(client_ref).await - }, } } } diff --git a/frontend/libs/common/src/lib/enums/GenerationProvider.ts b/frontend/libs/common/src/lib/enums/GenerationProvider.ts index 409e8cea..b8038c57 100644 --- a/frontend/libs/common/src/lib/enums/GenerationProvider.ts +++ b/frontend/libs/common/src/lib/enums/GenerationProvider.ts @@ -4,5 +4,4 @@ export enum GenerationProvider { Higgsfield = "higgsfield", Fal = "fal", Midjourney = "midjourney", - WorldLabs = "world_labs", } diff --git a/frontend/libs/components/model-selector/src/lib/provider-icons.tsx b/frontend/libs/components/model-selector/src/lib/provider-icons.tsx index c79df2f8..1f7e8f23 100644 --- a/frontend/libs/components/model-selector/src/lib/provider-icons.tsx +++ b/frontend/libs/components/model-selector/src/lib/provider-icons.tsx @@ -8,7 +8,6 @@ const GENERATION_PROVIDER_TO_CREATOR: Partial return "Higgsfield"; case GenerationProvider.Midjourney: return "Midjourney"; - case GenerationProvider.WorldLabs: - return "World Labs"; default: return "Unknown Provider"; } diff --git a/frontend/libs/components/model-selector/src/lib/selection-rules.spec.ts b/frontend/libs/components/model-selector/src/lib/selection-rules.spec.ts index c22c699a..e325811f 100644 --- a/frontend/libs/components/model-selector/src/lib/selection-rules.spec.ts +++ b/frontend/libs/components/model-selector/src/lib/selection-rules.spec.ts @@ -35,6 +35,7 @@ describe("providerForService", () => { expect(providerForService("higgsfield_cookies")).toBe(GenerationProvider.Higgsfield); expect(providerForService("higgsfield")).toBe(GenerationProvider.Higgsfield); expect(providerForService("runway_cookies")).toBeUndefined(); + expect(providerForService("worldlabs_cookies")).toBeUndefined(); }); }); @@ -54,7 +55,7 @@ describe("chooseModelForProvider", () => { expect(chooseModelForProvider(GenerationProvider.Fal, page)).toBe(flux); }); it("is undefined when the provider offers nothing on the page", () => { - expect(chooseModelForProvider(GenerationProvider.WorldLabs, page)).toBeUndefined(); + expect(chooseModelForProvider(GenerationProvider.Higgsfield, page)).toBeUndefined(); }); }); diff --git a/frontend/libs/components/model-selector/src/lib/selection-rules.ts b/frontend/libs/components/model-selector/src/lib/selection-rules.ts index f5554120..3ed28f17 100644 --- a/frontend/libs/components/model-selector/src/lib/selection-rules.ts +++ b/frontend/libs/components/model-selector/src/lib/selection-rules.ts @@ -37,9 +37,6 @@ export const providerForService = (service: string): GenerationProvider | undefi case "midjourney": case "midjourney_cookies": return GenerationProvider.Midjourney; - case "world_labs": - case "worldlabs_cookies": - return GenerationProvider.WorldLabs; default: return undefined; } diff --git a/frontend/libs/components/provider-billing-modal/src/lib/others/GenericProviderBillingBlock.tsx b/frontend/libs/components/provider-billing-modal/src/lib/others/GenericProviderBillingBlock.tsx index 4bd573d2..bce54978 100644 --- a/frontend/libs/components/provider-billing-modal/src/lib/others/GenericProviderBillingBlock.tsx +++ b/frontend/libs/components/provider-billing-modal/src/lib/others/GenericProviderBillingBlock.tsx @@ -28,8 +28,6 @@ function getServiceProviderName(provider: GenerationProvider) : string { return "Higgsfield"; case GenerationProvider.Midjourney: return "Midjourney"; - case GenerationProvider.WorldLabs: - return "World Labs"; case GenerationProvider.Artcraft: default: return "Artcraft"; diff --git a/frontend/libs/components/provider-billing-modal/src/lib/provider-billing-modal.tsx b/frontend/libs/components/provider-billing-modal/src/lib/provider-billing-modal.tsx index d08b5df5..516f0b45 100644 --- a/frontend/libs/components/provider-billing-modal/src/lib/provider-billing-modal.tsx +++ b/frontend/libs/components/provider-billing-modal/src/lib/provider-billing-modal.tsx @@ -34,7 +34,6 @@ export function ProviderBillingModal({ case GenerationProvider.Grok: case GenerationProvider.Higgsfield: case GenerationProvider.Midjourney: - case GenerationProvider.WorldLabs: // NB: We're just going to ask users to set up billing on the provider's website. block = ; break; diff --git a/frontend/libs/components/provider-setup-modal/src/lib/provider-setup-modal.tsx b/frontend/libs/components/provider-setup-modal/src/lib/provider-setup-modal.tsx index 4e3b2bf5..f06c65a1 100644 --- a/frontend/libs/components/provider-setup-modal/src/lib/provider-setup-modal.tsx +++ b/frontend/libs/components/provider-setup-modal/src/lib/provider-setup-modal.tsx @@ -34,9 +34,6 @@ export function ProviderSetupModal({ const buttonOnClick = async () => { switch (provider) { - case GenerationProvider.WorldLabs: - await invoke("worldlabs_open_login_command"); - break; case GenerationProvider.Grok: await invoke("grok_open_login_command"); break; @@ -117,8 +114,6 @@ function getServiceProviderName(provider: GenerationProvider) : string { return "Fal"; case GenerationProvider.Midjourney: return "Midjourney"; - case GenerationProvider.WorldLabs: - return "WorldLabs"; case GenerationProvider.Artcraft: default: return "Artcraft"; diff --git a/frontend/libs/model-list/src/lib/listing/ListingCommon.ts b/frontend/libs/model-list/src/lib/listing/ListingCommon.ts index 2b183ed8..a9e2dd0e 100644 --- a/frontend/libs/model-list/src/lib/listing/ListingCommon.ts +++ b/frontend/libs/model-list/src/lib/listing/ListingCommon.ts @@ -3,7 +3,7 @@ // ids the backend accepts on generate requests. // `models::enums::GenerationProvider` -export type ListingProvider = "artcraft" | "fal" | "grok" | "higgsfield" | "midjourney" | "world_labs"; +export type ListingProvider = "artcraft" | "fal" | "grok" | "higgsfield" | "midjourney"; // `models::enums::ModelCreator` (snake_case) export type ListingCreator = string; diff --git a/frontend/libs/tauri-api/src/index.ts b/frontend/libs/tauri-api/src/index.ts index f7d6c1c4..a605b3a4 100644 --- a/frontend/libs/tauri-api/src/index.ts +++ b/frontend/libs/tauri-api/src/index.ts @@ -44,7 +44,6 @@ export * from "./lib/promptbox/PromptboxState.js"; export * from "./lib/settings/LoadCustomSound.js"; export * from "./lib/system/GetAppInfo.js"; export * from "./lib/util/LoadWithoutCors.js"; -export * from "./lib/worldlabs/WorldLabsGetCredentialInfo.js"; export * from "./lib/cost_estimate/EstimateImageCost.js"; export * from "./lib/cost_estimate/EstimateAudioCost.js"; export * from "./lib/cost_estimate/EstimateMeshCost.js"; diff --git a/frontend/libs/tauri-api/src/lib/worldlabs/WorldLabsGetCredentialInfo.ts b/frontend/libs/tauri-api/src/lib/worldlabs/WorldLabsGetCredentialInfo.ts deleted file mode 100644 index 6b7e2a22..00000000 --- a/frontend/libs/tauri-api/src/lib/worldlabs/WorldLabsGetCredentialInfo.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { invoke } from "@tauri-apps/api/core"; -import { CommandResult } from "../common/CommandStatus"; - -export interface WorldLabsGetCredentialInfoPayload { - maybe_email?: string; - can_clear_state: boolean; -} - -export interface WorldLabsGetCredentialInfoSuccess extends CommandResult { - payload: WorldLabsGetCredentialInfoPayload; -} - -export const WorldLabsGetCredentialInfo = async (): Promise => { - const result = await invoke("worldlabs_get_credential_info_command"); - return result as WorldLabsGetCredentialInfoSuccess; -};