From 164dbdde33516f9004daeeff82410024ddfa8e82 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Mon, 9 Mar 2026 00:33:26 -0400 Subject: [PATCH] overhaul API --- Cargo.lock | 19 - crates/api_clients/world_labs_api/Cargo.toml | 20 - .../api/api_types/image_input_object_id.rs | 26 - .../src/api/api_types/media_asset_id.rs | 9 + .../src/api/api_types/meta_world_object_id.rs | 26 - .../world_labs_api/src/api/api_types/mod.rs | 11 +- .../src/api/api_types/operation_id.rs | 9 + .../src/api/api_types/pano_object_id.rs | 26 - .../src/api/api_types/run_object.rs | 79 --- .../src/api/api_types/run_object_id.rs | 15 - .../src/api/api_types/upload_mime_type.rs | 21 - .../src/api/api_types/upload_object_id.rs | 15 - .../src/api/common/common_header_values.rs | 6 - .../world_labs_api/src/api/common/mod.rs | 1 - .../api_clients/world_labs_api/src/api/mod.rs | 3 - .../google_refresh_token.rs | 154 ------ .../api/requests/google_refresh_token/mod.rs | 1 - .../google_upload/google_upload_image.rs | 91 ---- .../src/api/requests/google_upload/mod.rs | 1 - .../requests/media_assets/get_media_asset.rs | 80 +++ .../src/api/requests/media_assets/mod.rs | 3 + .../requests/media_assets/prepare_upload.rs | 107 +++++ .../media_assets/upload_to_signed_url.rs | 56 +++ .../world_labs_api/src/api/requests/mod.rs | 6 +- .../objects/begin_object_image_upload.rs | 147 ------ .../api/requests/objects/create_run_object.rs | 124 ----- .../requests/objects/create_upload_object.rs | 152 ------ .../objects/finalize_object_image_upload.rs | 172 ------- .../src/api/requests/objects/mod.rs | 6 - .../objects/update_run_object_with_upload.rs | 440 ----------------- .../objects/update_run_object_with_world.rs | 454 ------------------ .../api/requests/operations/get_operation.rs | 87 ++++ .../src/api/requests/operations/mod.rs | 1 + .../src/api/requests/recaption/mod.rs | 1 - .../api/requests/recaption/recaption_image.rs | 174 ------- .../src/api/requests/worlds/create_world.rs | 186 ------- .../src/api/requests/worlds/generate_world.rs | 148 ++++++ .../src/api/requests/worlds/mod.rs | 3 +- .../api/requests/worlds/poll_world_status.rs | 212 -------- .../world_labs_api/src/api/utils/mod.rs | 1 - .../src/api/utils/upload_id_to_image_url.rs | 38 -- .../src/client/browser_user_agents.rs | 10 - .../world_labs_api/src/client/mod.rs | 1 - .../src/credentials/jwt_claims.rs | 101 ---- .../world_labs_api/src/credentials/mod.rs | 5 +- .../src/credentials/world_labs_api_creds.rs | 16 + .../credentials/world_labs_bearer_token.rs | 69 --- .../src/credentials/world_labs_cookies.rs | 24 - .../credentials/worldlabs_refresh_token.rs | 25 - .../src/error/filter_world_labs_http_error.rs | 12 +- .../src/error/world_labs_client_error.rs | 135 ------ .../src/error/world_labs_error.rs | 18 - .../src/error/world_labs_generic_api_error.rs | 57 +-- .../error/world_labs_specific_api_error.rs | 71 --- crates/api_clients/world_labs_api/src/lib.rs | 1 - ...pload_image_and_create_world_with_retry.rs | 340 +++---------- .../src/test_utils/get_test_api_key.rs | 10 + .../src/test_utils/get_test_bearer_token.rs | 17 - .../src/test_utils/get_test_cookies.rs | 17 - .../src/test_utils/get_test_refresh_token.rs | 17 - .../world_labs_api/src/test_utils/mod.rs | 4 +- 61 files changed, 614 insertions(+), 3467 deletions(-) delete mode 100644 crates/api_clients/world_labs_api/src/api/api_types/image_input_object_id.rs create mode 100644 crates/api_clients/world_labs_api/src/api/api_types/media_asset_id.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/api_types/meta_world_object_id.rs create mode 100644 crates/api_clients/world_labs_api/src/api/api_types/operation_id.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/api_types/pano_object_id.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/api_types/run_object.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/api_types/run_object_id.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/api_types/upload_mime_type.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/api_types/upload_object_id.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/common/common_header_values.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/common/mod.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/requests/google_refresh_token/google_refresh_token.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/requests/google_refresh_token/mod.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/requests/google_upload/google_upload_image.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/requests/google_upload/mod.rs create mode 100644 crates/api_clients/world_labs_api/src/api/requests/media_assets/get_media_asset.rs create mode 100644 crates/api_clients/world_labs_api/src/api/requests/media_assets/mod.rs create mode 100644 crates/api_clients/world_labs_api/src/api/requests/media_assets/prepare_upload.rs create mode 100644 crates/api_clients/world_labs_api/src/api/requests/media_assets/upload_to_signed_url.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/requests/objects/begin_object_image_upload.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/requests/objects/create_run_object.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/requests/objects/create_upload_object.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/requests/objects/finalize_object_image_upload.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/requests/objects/mod.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/requests/objects/update_run_object_with_upload.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/requests/objects/update_run_object_with_world.rs create mode 100644 crates/api_clients/world_labs_api/src/api/requests/operations/get_operation.rs create mode 100644 crates/api_clients/world_labs_api/src/api/requests/operations/mod.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/requests/recaption/mod.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/requests/recaption/recaption_image.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/requests/worlds/create_world.rs create mode 100644 crates/api_clients/world_labs_api/src/api/requests/worlds/generate_world.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/requests/worlds/poll_world_status.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/utils/mod.rs delete mode 100644 crates/api_clients/world_labs_api/src/api/utils/upload_id_to_image_url.rs delete mode 100644 crates/api_clients/world_labs_api/src/client/browser_user_agents.rs delete mode 100644 crates/api_clients/world_labs_api/src/client/mod.rs delete mode 100644 crates/api_clients/world_labs_api/src/credentials/jwt_claims.rs create mode 100644 crates/api_clients/world_labs_api/src/credentials/world_labs_api_creds.rs delete mode 100644 crates/api_clients/world_labs_api/src/credentials/world_labs_bearer_token.rs delete mode 100644 crates/api_clients/world_labs_api/src/credentials/world_labs_cookies.rs delete mode 100644 crates/api_clients/world_labs_api/src/credentials/worldlabs_refresh_token.rs create mode 100644 crates/api_clients/world_labs_api/src/test_utils/get_test_api_key.rs delete mode 100644 crates/api_clients/world_labs_api/src/test_utils/get_test_bearer_token.rs delete mode 100644 crates/api_clients/world_labs_api/src/test_utils/get_test_cookies.rs delete mode 100644 crates/api_clients/world_labs_api/src/test_utils/get_test_refresh_token.rs diff --git a/Cargo.lock b/Cargo.lock index cd787ae731..6a74db7c0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15012,37 +15012,18 @@ name = "world_labs_api" version = "0.0.1" dependencies = [ "anyhow", - "base64 0.22.1", - "bstr", - "byteorder", "chrono", - "cloudflare_errors", - "cloudflare_mitigation", - "cookie_store 0.0.1", "env", "env_logger 0.11.9", "errors", "filesys", - "futures 0.3.31", - "http_headers", - "indexmap 2.9.0", - "jwt_light", "log", - "log4rs", - "once_cell", - "regex", - "scraper 0.24.0", "serde", "serde_json", - "sha2 0.11.0-rc.2", - "test_data", - "testing", "tokio", - "url", "uuid 1.18.1", "workspace-build-acceleration", "wreq", - "wreq-util", ] [[package]] diff --git a/crates/api_clients/world_labs_api/Cargo.toml b/crates/api_clients/world_labs_api/Cargo.toml index 158255dbce..9373558695 100644 --- a/crates/api_clients/world_labs_api/Cargo.toml +++ b/crates/api_clients/world_labs_api/Cargo.toml @@ -16,40 +16,20 @@ path = "src/lib.rs" workspace-build-acceleration.workspace = true # Internal -cloudflare_errors.workspace = true -cloudflare_mitigation.workspace = true -cookie_store.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 = "1.5.0" -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 -testing.workspace = true -log4rs = "1.4.0" env_logger.workspace = true env.workspace = true chrono.workspace = true diff --git a/crates/api_clients/world_labs_api/src/api/api_types/image_input_object_id.rs b/crates/api_clients/world_labs_api/src/api/api_types/image_input_object_id.rs deleted file mode 100644 index 207d6a5ead..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/api/api_types/media_asset_id.rs b/crates/api_clients/world_labs_api/src/api/api_types/media_asset_id.rs new file mode 100644 index 0000000000..604aa51fa0 --- /dev/null +++ b/crates/api_clients/world_labs_api/src/api/api_types/media_asset_id.rs @@ -0,0 +1,9 @@ +/// 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/world_labs_api/src/api/api_types/meta_world_object_id.rs b/crates/api_clients/world_labs_api/src/api/api_types/meta_world_object_id.rs deleted file mode 100644 index 54cd53a198..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/api/api_types/mod.rs b/crates/api_clients/world_labs_api/src/api/api_types/mod.rs index ed4b37eb86..1ea69e2a9a 100644 --- a/crates/api_clients/world_labs_api/src/api/api_types/mod.rs +++ b/crates/api_clients/world_labs_api/src/api/api_types/mod.rs @@ -1,8 +1,3 @@ -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 +pub mod media_asset_id; +pub mod operation_id; +pub mod world_id; diff --git a/crates/api_clients/world_labs_api/src/api/api_types/operation_id.rs b/crates/api_clients/world_labs_api/src/api/api_types/operation_id.rs new file mode 100644 index 0000000000..83504ee68e --- /dev/null +++ b/crates/api_clients/world_labs_api/src/api/api_types/operation_id.rs @@ -0,0 +1,9 @@ +/// 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/world_labs_api/src/api/api_types/pano_object_id.rs b/crates/api_clients/world_labs_api/src/api/api_types/pano_object_id.rs deleted file mode 100644 index 96c359f4c6..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/api/api_types/run_object.rs b/crates/api_clients/world_labs_api/src/api/api_types/run_object.rs deleted file mode 100644 index e923607c90..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/api/api_types/run_object_id.rs b/crates/api_clients/world_labs_api/src/api/api_types/run_object_id.rs deleted file mode 100644 index 73a50ae277..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/api/api_types/upload_mime_type.rs b/crates/api_clients/world_labs_api/src/api/api_types/upload_mime_type.rs deleted file mode 100644 index 72c60853d8..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/api/api_types/upload_object_id.rs b/crates/api_clients/world_labs_api/src/api/api_types/upload_object_id.rs deleted file mode 100644 index 26eeb401fd..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/api/common/common_header_values.rs b/crates/api_clients/world_labs_api/src/api/common/common_header_values.rs deleted file mode 100644 index 2df742bef2..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/api/common/mod.rs b/crates/api_clients/world_labs_api/src/api/common/mod.rs deleted file mode 100644 index 6356e9460a..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/api/mod.rs b/crates/api_clients/world_labs_api/src/api/mod.rs index 9729e3e72d..3a0804a33a 100644 --- a/crates/api_clients/world_labs_api/src/api/mod.rs +++ b/crates/api_clients/world_labs_api/src/api/mod.rs @@ -1,5 +1,2 @@ pub mod api_types; pub mod requests; - -pub (crate) mod common; -pub mod utils; diff --git a/crates/api_clients/world_labs_api/src/api/requests/google_refresh_token/google_refresh_token.rs b/crates/api_clients/world_labs_api/src/api/requests/google_refresh_token/google_refresh_token.rs deleted file mode 100644 index 7db5e6b9b1..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/api/requests/google_refresh_token/mod.rs b/crates/api_clients/world_labs_api/src/api/requests/google_refresh_token/mod.rs deleted file mode 100644 index cfade37fe6..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/api/requests/google_upload/google_upload_image.rs b/crates/api_clients/world_labs_api/src/api/requests/google_upload/google_upload_image.rs deleted file mode 100644 index 7d2e0dc9b4..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/api/requests/google_upload/mod.rs b/crates/api_clients/world_labs_api/src/api/requests/google_upload/mod.rs deleted file mode 100644 index 66f712bf42..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/api/requests/media_assets/get_media_asset.rs b/crates/api_clients/world_labs_api/src/api/requests/media_assets/get_media_asset.rs new file mode 100644 index 0000000000..c35b1a277c --- /dev/null +++ b/crates/api_clients/world_labs_api/src/api/requests/media_assets/get_media_asset.rs @@ -0,0 +1,80 @@ +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 serde::Deserialize; +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 status: String, +} + +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, + status: raw.status, + }) +} + +#[derive(Deserialize)] +struct RawResponse { + pub media_asset_id: String, + pub file_name: String, + pub kind: String, + pub status: String, +} diff --git a/crates/api_clients/world_labs_api/src/api/requests/media_assets/mod.rs b/crates/api_clients/world_labs_api/src/api/requests/media_assets/mod.rs new file mode 100644 index 0000000000..60d535b1ae --- /dev/null +++ b/crates/api_clients/world_labs_api/src/api/requests/media_assets/mod.rs @@ -0,0 +1,3 @@ +pub mod get_media_asset; +pub mod prepare_upload; +pub mod upload_to_signed_url; diff --git a/crates/api_clients/world_labs_api/src/api/requests/media_assets/prepare_upload.rs b/crates/api_clients/world_labs_api/src/api/requests/media_assets/prepare_upload.rs new file mode 100644 index 0000000000..5fe94f148e --- /dev/null +++ b/crates/api_clients/world_labs_api/src/api/requests/media_assets/prepare_upload.rs @@ -0,0 +1,107 @@ +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 serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::time::Duration; +use wreq::Client; + +const URL: &str = "https://api.worldlabs.ai/marble/v1/media-assets:prepare_upload"; + +pub struct PrepareUploadArgs<'a> { + pub creds: &'a WorldLabsApiCreds, + pub file_name: &'a str, + /// "image" or "video" + pub kind: &'a str, + pub extension: &'a str, + pub request_timeout: Option, +} + +pub struct PrepareUploadResponse { + pub media_asset_id: String, + pub upload_url: String, + pub upload_method: String, + pub required_headers: HashMap, +} + +pub async fn prepare_upload(args: PrepareUploadArgs<'_>) -> Result { + let client = Client::new(); + + let payload = RawRequest { + file_name: args.file_name.to_string(), + kind: args.kind.to_string(), + extension: args.extension.to_string(), + }; + + 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: 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(), + }) +} + +#[derive(Serialize)] +struct RawRequest { + pub file_name: String, + pub kind: String, + pub extension: String, +} + +#[derive(Deserialize)] +struct RawResponse { + pub media_asset: RawMediaAsset, + pub upload_info: RawUploadInfo, +} + +#[derive(Deserialize)] +struct RawMediaAsset { + pub media_asset_id: String, +} + +#[derive(Deserialize)] +struct RawUploadInfo { + pub upload_url: String, + pub upload_method: String, + pub required_headers: Option>, +} diff --git a/crates/api_clients/world_labs_api/src/api/requests/media_assets/upload_to_signed_url.rs b/crates/api_clients/world_labs_api/src/api/requests/media_assets/upload_to_signed_url.rs new file mode 100644 index 0000000000..c46aa74aa6 --- /dev/null +++ b/crates/api_clients/world_labs_api/src/api/requests/media_assets/upload_to_signed_url.rs @@ -0,0 +1,56 @@ +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 request_timeout: Option, +} + +/// Upload file bytes to a signed URL via PUT. +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", "application/octet-stream") + .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(()) +} diff --git a/crates/api_clients/world_labs_api/src/api/requests/mod.rs b/crates/api_clients/world_labs_api/src/api/requests/mod.rs index 618aa293b1..0a933d976f 100644 --- a/crates/api_clients/world_labs_api/src/api/requests/mod.rs +++ b/crates/api_clients/world_labs_api/src/api/requests/mod.rs @@ -1,5 +1,3 @@ -pub mod google_upload; -pub mod objects; -pub mod recaption; +pub mod media_assets; +pub mod operations; pub mod worlds; -pub mod google_refresh_token; \ No newline at end of file diff --git a/crates/api_clients/world_labs_api/src/api/requests/objects/begin_object_image_upload.rs b/crates/api_clients/world_labs_api/src/api/requests/objects/begin_object_image_upload.rs deleted file mode 100644 index 9e9fc34026..0000000000 --- a/crates/api_clients/world_labs_api/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; - - #[test] - fn test_get_url() { - let upload_id = UploadObjectId("foo-bar-baz-bin".to_string()); - let expected = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects/foo-bar-baz-bin:upload"; - assert_eq!(get_url(&upload_id), expected); - } -} diff --git a/crates/api_clients/world_labs_api/src/api/requests/objects/create_run_object.rs b/crates/api_clients/world_labs_api/src/api/requests/objects/create_run_object.rs deleted file mode 100644 index 341cc32129..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/api/requests/objects/create_upload_object.rs b/crates/api_clients/world_labs_api/src/api/requests/objects/create_upload_object.rs deleted file mode 100644 index 12feff00b1..0000000000 --- a/crates/api_clients/world_labs_api/src/api/requests/objects/create_upload_object.rs +++ /dev/null @@ -1,152 +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); - assert_eq!(request.display_name, "uploaded_image_jpeg"); - } -} diff --git a/crates/api_clients/world_labs_api/src/api/requests/objects/finalize_object_image_upload.rs b/crates/api_clients/world_labs_api/src/api/requests/objects/finalize_object_image_upload.rs deleted file mode 100644 index 303ef657e4..0000000000 --- a/crates/api_clients/world_labs_api/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}; - 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 = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects/foo-bar-baz-bin:complete"; - 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/world_labs_api/src/api/requests/objects/mod.rs b/crates/api_clients/world_labs_api/src/api/requests/objects/mod.rs deleted file mode 100644 index 3c27307f0d..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/api/requests/objects/update_run_object_with_upload.rs b/crates/api_clients/world_labs_api/src/api/requests/objects/update_run_object_with_upload.rs deleted file mode 100644 index e3e4aa2d1c..0000000000 --- a/crates/api_clients/world_labs_api/src/api/requests/objects/update_run_object_with_upload.rs +++ /dev/null @@ -1,440 +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}; - - #[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); - - let json = serde_json::to_string_pretty(&request).unwrap(); - println!("{}", json); - - assert_eq!(json, r#"{"type":"abcxyz_image_input_id","value":"image_input_id"}"#); - } -} diff --git a/crates/api_clients/world_labs_api/src/api/requests/objects/update_run_object_with_world.rs b/crates/api_clients/world_labs_api/src/api/requests/objects/update_run_object_with_world.rs deleted file mode 100644 index 9710ef2c32..0000000000 --- a/crates/api_clients/world_labs_api/src/api/requests/objects/update_run_object_with_world.rs +++ /dev/null @@ -1,454 +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}; - - #[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); - - let json = serde_json::to_string_pretty(&request).unwrap(); - println!("{}", json); - - assert_eq!(json, r#"{"type":"abcxyz_image_input_id","value":"image_input_id"}"#); - } -} diff --git a/crates/api_clients/world_labs_api/src/api/requests/operations/get_operation.rs b/crates/api_clients/world_labs_api/src/api/requests/operations/get_operation.rs new file mode 100644 index 0000000000..b9f3b03ea4 --- /dev/null +++ b/crates/api_clients/world_labs_api/src/api/requests/operations/get_operation.rs @@ -0,0 +1,87 @@ +use crate::api::api_types::operation_id::OperationId; +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 serde::Deserialize; +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 error: Option, + pub response: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct OperationError { + pub code: Option, + pub message: Option, +} + +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()))?; + + Ok(GetOperationResponse { + operation_id: raw.operation_id, + done: raw.done.unwrap_or(false), + error: raw.error, + response: raw.response, + }) +} + +#[derive(Deserialize)] +struct RawResponse { + pub operation_id: String, + pub done: Option, + pub error: Option, + pub response: Option, +} diff --git a/crates/api_clients/world_labs_api/src/api/requests/operations/mod.rs b/crates/api_clients/world_labs_api/src/api/requests/operations/mod.rs new file mode 100644 index 0000000000..e3cc796b78 --- /dev/null +++ b/crates/api_clients/world_labs_api/src/api/requests/operations/mod.rs @@ -0,0 +1 @@ +pub mod get_operation; diff --git a/crates/api_clients/world_labs_api/src/api/requests/recaption/mod.rs b/crates/api_clients/world_labs_api/src/api/requests/recaption/mod.rs deleted file mode 100644 index 58671219b6..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/api/requests/recaption/recaption_image.rs b/crates/api_clients/world_labs_api/src/api/requests/recaption/recaption_image.rs deleted file mode 100644 index 29c9732776..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/api/requests/worlds/create_world.rs b/crates/api_clients/world_labs_api/src/api/requests/worlds/create_world.rs deleted file mode 100644 index 5ebd570227..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/api/requests/worlds/generate_world.rs b/crates/api_clients/world_labs_api/src/api/requests/worlds/generate_world.rs new file mode 100644 index 0000000000..3d1ab7d198 --- /dev/null +++ b/crates/api_clients/world_labs_api/src/api/requests/worlds/generate_world.rs @@ -0,0 +1,148 @@ +use crate::api::api_types::operation_id::OperationId; +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 serde::{Deserialize, Serialize}; +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: Option, + pub request_timeout: Option, +} + +/// The world prompt — a tagged union describing the input. +#[derive(Clone, Debug, Serialize)] +#[serde(tag = "type", rename_all = "kebab-case")] +pub enum WorldPrompt { + Text { + text_prompt: String, + }, + Image { + image_prompt: ImagePrompt, + #[serde(skip_serializing_if = "Option::is_none")] + text_prompt: Option, + #[serde(skip_serializing_if = "Option::is_none")] + is_pano: Option, + }, + MultiImage { + multi_image_prompt: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + text_prompt: Option, + }, + Video { + video_prompt: VideoPrompt, + #[serde(skip_serializing_if = "Option::is_none")] + text_prompt: Option, + }, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(tag = "source", rename_all = "snake_case")] +pub enum ImagePrompt { + Uri { + uri: String, + }, + MediaAsset { + media_asset_id: String, + }, + DataBase64 { + data: String, + media_type: String, + }, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(tag = "source", rename_all = "snake_case")] +pub enum VideoPrompt { + Uri { + uri: String, + }, + MediaAsset { + media_asset_id: String, + }, + DataBase64 { + data: String, + media_type: String, + }, +} + +pub struct GenerateWorldResponse { + pub operation_id: OperationId, + pub done: bool, +} + +pub async fn generate_world(args: GenerateWorldArgs<'_>) -> Result { + let client = Client::new(); + + let payload = RawRequest { + world_prompt: args.world_prompt, + display_name: args.display_name, + model: args.model, + }; + + 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), + }) +} + +#[derive(Serialize)] +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, +} + +#[derive(Deserialize)] +struct RawResponse { + pub operation_id: String, + pub done: Option, +} diff --git a/crates/api_clients/world_labs_api/src/api/requests/worlds/mod.rs b/crates/api_clients/world_labs_api/src/api/requests/worlds/mod.rs index 58522dea54..18c05df2ae 100644 --- a/crates/api_clients/world_labs_api/src/api/requests/worlds/mod.rs +++ b/crates/api_clients/world_labs_api/src/api/requests/worlds/mod.rs @@ -1,2 +1 @@ -pub mod create_world; -pub mod poll_world_status; \ No newline at end of file +pub mod generate_world; diff --git a/crates/api_clients/world_labs_api/src/api/requests/worlds/poll_world_status.rs b/crates/api_clients/world_labs_api/src/api/requests/worlds/poll_world_status.rs deleted file mode 100644 index 48450a4d70..0000000000 --- a/crates/api_clients/world_labs_api/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}; - 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 = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/worlds/d60cfa21-5506-43de-9a9c-0707fc17a5ec"; - 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/world_labs_api/src/api/utils/mod.rs b/crates/api_clients/world_labs_api/src/api/utils/mod.rs deleted file mode 100644 index ff1db7c174..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/api/utils/upload_id_to_image_url.rs b/crates/api_clients/world_labs_api/src/api/utils/upload_id_to_image_url.rs deleted file mode 100644 index 3dc266bb5f..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/client/browser_user_agents.rs b/crates/api_clients/world_labs_api/src/client/browser_user_agents.rs deleted file mode 100644 index 94f3923b48..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/client/mod.rs b/crates/api_clients/world_labs_api/src/client/mod.rs deleted file mode 100644 index 82c176fa31..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/credentials/jwt_claims.rs b/crates/api_clients/world_labs_api/src/credentials/jwt_claims.rs deleted file mode 100644 index aa15ba35c2..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/credentials/mod.rs b/crates/api_clients/world_labs_api/src/credentials/mod.rs index f752f37ea7..d576a60ee4 100644 --- a/crates/api_clients/world_labs_api/src/credentials/mod.rs +++ b/crates/api_clients/world_labs_api/src/credentials/mod.rs @@ -1,4 +1 @@ -pub mod world_labs_bearer_token; -pub mod world_labs_cookies; -pub mod worldlabs_refresh_token; -pub mod jwt_claims; +pub mod world_labs_api_creds; diff --git a/crates/api_clients/world_labs_api/src/credentials/world_labs_api_creds.rs b/crates/api_clients/world_labs_api/src/credentials/world_labs_api_creds.rs new file mode 100644 index 0000000000..b16442bc8e --- /dev/null +++ b/crates/api_clients/world_labs_api/src/credentials/world_labs_api_creds.rs @@ -0,0 +1,16 @@ +/// 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/world_labs_api/src/credentials/world_labs_bearer_token.rs b/crates/api_clients/world_labs_api/src/credentials/world_labs_bearer_token.rs deleted file mode 100644 index 779b3968f0..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/credentials/world_labs_cookies.rs b/crates/api_clients/world_labs_api/src/credentials/world_labs_cookies.rs deleted file mode 100644 index f953c47dde..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/credentials/worldlabs_refresh_token.rs b/crates/api_clients/world_labs_api/src/credentials/worldlabs_refresh_token.rs deleted file mode 100644 index 5e305ec425..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/error/filter_world_labs_http_error.rs b/crates/api_clients/world_labs_api/src/error/filter_world_labs_http_error.rs index 46cc9547cc..39de60dc9e 100644 --- a/crates/api_clients/world_labs_api/src/error/filter_world_labs_http_error.rs +++ b/crates/api_clients/world_labs_api/src/error/filter_world_labs_http_error.rs @@ -1,7 +1,6 @@ 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. @@ -10,23 +9,22 @@ pub fn filter_world_labs_http_error(status_code: StatusCode, maybe_body: Option< 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()) + Err(WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody { + status_code, + body: String::new(), + }.into()) } diff --git a/crates/api_clients/world_labs_api/src/error/world_labs_client_error.rs b/crates/api_clients/world_labs_api/src/error/world_labs_client_error.rs index abcfb7f7af..5c2073cb7e 100644 --- a/crates/api_clients/world_labs_api/src/error/world_labs_client_error.rs +++ b/crates/api_clients/world_labs_api/src/error/world_labs_client_error.rs @@ -1,4 +1,3 @@ -use base64::DecodeError; use std::error::Error; use std::fmt::{Display, Formatter}; @@ -7,108 +6,9 @@ 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 {} @@ -117,42 +17,7 @@ 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/world_labs_api/src/error/world_labs_error.rs b/crates/api_clients/world_labs_api/src/error/world_labs_error.rs index 813240b08f..0e414ba4fd 100644 --- a/crates/api_clients/world_labs_api/src/error/world_labs_error.rs +++ b/crates/api_clients/world_labs_api/src/error/world_labs_error.rs @@ -1,7 +1,6 @@ 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)] @@ -11,17 +10,6 @@ pub enum WorldLabsError { 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 { @@ -51,9 +39,3 @@ impl From for WorldLabsError { Self::ApiGeneric(error) } } - -impl From for WorldLabsError { - fn from(error: CloudflareError) -> Self { - Self::ApiGeneric(WorldLabsGenericApiError::CloudflareError(error)) - } -} diff --git a/crates/api_clients/world_labs_api/src/error/world_labs_generic_api_error.rs b/crates/api_clients/world_labs_api/src/error/world_labs_generic_api_error.rs index b3c6f2f8ed..8d2ab6e3dd 100644 --- a/crates/api_clients/world_labs_api/src/error/world_labs_generic_api_error.rs +++ b/crates/api_clients/world_labs_api/src/error/world_labs_generic_api_error.rs @@ -1,65 +1,24 @@ -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 + /// Unknown error occurred when uploading to signed URL 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. + /// An uncategorized bad HTTP response. 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), @@ -70,19 +29,9 @@ 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::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::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/world_labs_api/src/error/world_labs_specific_api_error.rs b/crates/api_clients/world_labs_api/src/error/world_labs_specific_api_error.rs index aba508705e..487e870ff4 100644 --- a/crates/api_clients/world_labs_api/src/error/world_labs_specific_api_error.rs +++ b/crates/api_clients/world_labs_api/src/error/world_labs_specific_api_error.rs @@ -3,64 +3,8 @@ 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 {} @@ -68,22 +12,7 @@ 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/world_labs_api/src/lib.rs b/crates/api_clients/world_labs_api/src/lib.rs index dda4bd6712..f2b65a1e5a 100644 --- a/crates/api_clients/world_labs_api/src/lib.rs +++ b/crates/api_clients/world_labs_api/src/lib.rs @@ -3,7 +3,6 @@ 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/world_labs_api/src/recipes/upload_image_and_create_world_with_retry.rs b/crates/api_clients/world_labs_api/src/recipes/upload_image_and_create_world_with_retry.rs index b841da4f95..3872a1191d 100644 --- a/crates/api_clients/world_labs_api/src/recipes/upload_image_and_create_world_with_retry.rs +++ b/crates/api_clients/world_labs_api/src/recipes/upload_image_and_create_world_with_retry.rs @@ -1,39 +1,21 @@ -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::api::api_types::media_asset_id::MediaAssetId; +use crate::api::api_types::operation_id::OperationId; +use crate::api::requests::media_assets::prepare_upload::{prepare_upload, PrepareUploadArgs}; +use crate::api::requests::media_assets::upload_to_signed_url::{upload_to_signed_url, UploadToSignedUrlArgs}; +use crate::api::requests::worlds::generate_world::{generate_world, GenerateWorldArgs, ImagePrompt, WorldPrompt}; +use crate::credentials::world_labs_api_creds::WorldLabsApiCreds; 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 log::{error, info}; use std::path::PathBuf; use std::time::Duration; -use uuid::Uuid; -pub struct UploadImageAndCreateWorldWithRetryArgs<'a> { +pub struct UploadImageAndCreateWorldArgs<'a> { + pub creds: &'a WorldLabsApiCreds, pub file: FileBytesOrPath, - pub cookies: &'a WorldLabsCookies, - pub bearer_token: &'a WorldLabsBearerToken, - pub refresh_token: &'a WorldLabsRefreshToken, + pub text_prompt: Option, + pub model: Option, pub individual_request_timeout: Option, } @@ -42,83 +24,28 @@ pub enum FileBytesOrPath { 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 UploadImageAndCreateWorldResponse { + pub operation_id: OperationId, + pub media_asset_id: MediaAssetId, } -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); +/// Official World Labs API: Upload an image and generate a world. +/// +/// New 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"); + info!("File bytes provided ({} bytes)", bytes.len()); bytes } FileBytesOrPath::Path(path) => { - info!("File path provided"); + info!("File path provided: {:?}", path); match file_read_bytes(&path) { Ok(bytes) => bytes, Err(err) => { @@ -129,218 +56,95 @@ pub async fn upload_image_and_create_world_with_retry(args: UploadImageAndCreate } }; - info!("Request #1 of 10: create run object ..."); + // Step 1: Prepare upload + info!("Step 1 of 3: prepare_upload ..."); - let response = create_run_object(CreateRunObjectArgs { - cookies: args.cookies, - bearer_token: use_bearer_token, + let prepare_response = prepare_upload(PrepareUploadArgs { + creds: args.creds, + file_name: "upload.jpg", + kind: "image", + extension: "jpg", request_timeout: args.individual_request_timeout, }).await?; - let run_object_id = response.id; - let run_object = response.run_object; + let media_asset_id = prepare_response.media_asset_id; + let upload_url = prepare_response.upload_url; + let required_headers = prepare_response.required_headers; - info!("Object id: {}", run_object_id.0); + info!("Media asset ID: {}", media_asset_id); + info!("Upload URL: {}", upload_url); - // TODO - multiple types. - let upload_mime_type= UploadMimeType::ImageJpeg; + // Step 2: Upload to signed URL + info!("Step 2 of 3: upload_to_signed_url ..."); - 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, + upload_to_signed_url(UploadToSignedUrlArgs { + upload_url: &upload_url, file_bytes, + required_headers: &required_headers, request_timeout: args.individual_request_timeout, }).await?; - info!("Request #5 of 10: finalize object upload ..."); + info!("Upload complete."); - 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?; + // Step 3: Generate world + info!("Step 3 of 3: generate_world ..."); - 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, + let world_prompt = WorldPrompt::Image { + image_prompt: ImagePrompt::MediaAsset { + media_asset_id: media_asset_id.clone(), }, + text_prompt: args.text_prompt, + is_pano: None, + }; + + let generate_response = generate_world(GenerateWorldArgs { + creds: args.creds, + world_prompt, + display_name: None, + model: args.model, request_timeout: args.individual_request_timeout, }).await?; - let run_first_updated_at = response.run_updated_timestamp; + info!("Operation ID: {}", generate_response.operation_id.as_str()); + info!("Done: {}", generate_response.done); - 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, + Ok(UploadImageAndCreateWorldResponse { + operation_id: generate_response.operation_id, + media_asset_id: MediaAssetId(media_asset_id), }) } #[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::recipes::upload_image_and_create_world_with_retry::{upload_image_and_create_world, FileBytesOrPath, UploadImageAndCreateWorldArgs}; + 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] // Client side tests only - async fn test_requests() { + #[ignore] // Client side tests only — requires real API key + async fn test_upload_and_generate() { 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 creds = get_test_api_key().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, + let results = upload_image_and_create_world(UploadImageAndCreateWorldArgs { + creds: &creds, individual_request_timeout: None, file: FileBytesOrPath::Bytes(file_bytes), + text_prompt: None, + model: None, }).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()); + println!("Operation ID: {}", results.operation_id.as_str()); + println!("Media Asset ID: {}", results.media_asset_id.as_str()); 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/world_labs_api/src/test_utils/get_test_api_key.rs b/crates/api_clients/world_labs_api/src/test_utils/get_test_api_key.rs new file mode 100644 index 0000000000..9695fa5585 --- /dev/null +++ b/crates/api_clients/world_labs_api/src/test_utils/get_test_api_key.rs @@ -0,0 +1,10 @@ +use crate::credentials::world_labs_api_creds::WorldLabsApiCreds; +use anyhow::Result as AnyhowResult; + +const API_KEY_FILE: &str = "/Users/bt/Artcraft/credentials/worldlabs_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/world_labs_api/src/test_utils/get_test_bearer_token.rs b/crates/api_clients/world_labs_api/src/test_utils/get_test_bearer_token.rs deleted file mode 100644 index 93aabc641e..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/test_utils/get_test_cookies.rs b/crates/api_clients/world_labs_api/src/test_utils/get_test_cookies.rs deleted file mode 100644 index 3fd53c1a9c..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/test_utils/get_test_refresh_token.rs b/crates/api_clients/world_labs_api/src/test_utils/get_test_refresh_token.rs deleted file mode 100644 index 385a868a17..0000000000 --- a/crates/api_clients/world_labs_api/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/world_labs_api/src/test_utils/mod.rs b/crates/api_clients/world_labs_api/src/test_utils/mod.rs index ef9d0b8e3c..35f004a135 100644 --- a/crates/api_clients/world_labs_api/src/test_utils/mod.rs +++ b/crates/api_clients/world_labs_api/src/test_utils/mod.rs @@ -1,4 +1,2 @@ -pub mod get_test_cookies; -pub mod get_test_bearer_token; +pub mod get_test_api_key; pub mod setup_test_logging; -pub mod get_test_refresh_token; \ No newline at end of file