diff --git a/crates/api_clients/world_labs_client/src/api/requests/_metadata/request_flow_4/r6_objects_patch.txt b/crates/api_clients/world_labs_client/src/api/requests/_metadata/request_flow_4/r6_objects_patch.txt index b7872c7adc..cfb99fd6d5 100644 --- a/crates/api_clients/world_labs_client/src/api/requests/_metadata/request_flow_4/r6_objects_patch.txt +++ b/crates/api_clients/world_labs_client/src/api/requests/_metadata/request_flow_4/r6_objects_patch.txt @@ -25,8 +25,126 @@ fetch("https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects/6623842d-3669-49b "credentials": "include" }); +Request Payload: + +{"object":{"metadata":{"version":"0.0.1","createdAt":1765433415549,"updatedAt":1765433417487,"useAdvancedEditing":false,"draftMode":false,"nodes":{"9b864e10-5a9b-46a1-8a94-2b571649a60d":{"id":"9b864e10-5a9b-46a1-8a94-2b571649a60d","type":"input","parentId":null,"source":{"type":"image","image_url":"https://cdn.marble.worldlabs.ai/object/ee33d03e-b153-4d56-a7cb-a08bf86bd20e/asset.jpg"},"createdAt":1765433417487},"22b9587b-2b4d-49c2-a6eb-e985d890e7db":{"id":"22b9587b-2b4d-49c2-a6eb-e985d890e7db","type":"pano","parentId":"9b864e10-5a9b-46a1-8a94-2b571649a60d","source":{"type":"generate_world","world_id":"pending","status":"pending"},"createdAt":1765433417487},"8e63a168-0c85-470d-b655-b185e0e13fee":{"id":"8e63a168-0c85-470d-b655-b185e0e13fee","type":"world","parentId":"22b9587b-2b4d-49c2-a6eb-e985d890e7db","source":{"type":"generate_world","world_id":"pending","posed_cubemaps_url":"","minimap_url":"","minimap_metadata":"","status":"pending"},"createdAt":1765433417487}}}},"update_mask":["metadata"]} + +Request Payload Pretty Print: + +{ + "object": { + "metadata": { + "version": "0.0.1", + "createdAt": 1765433415549, + "updatedAt": 1765433417487, + "useAdvancedEditing": false, + "draftMode": false, + "nodes": { + "9b864e10-5a9b-46a1-8a94-2b571649a60d": { + "id": "9b864e10-5a9b-46a1-8a94-2b571649a60d", + "type": "input", + "parentId": null, + "source": { + "type": "image", + "image_url": "https://cdn.marble.worldlabs.ai/object/ee33d03e-b153-4d56-a7cb-a08bf86bd20e/asset.jpg" + }, + "createdAt": 1765433417487 + }, + "22b9587b-2b4d-49c2-a6eb-e985d890e7db": { + "id": "22b9587b-2b4d-49c2-a6eb-e985d890e7db", + "type": "pano", + "parentId": "9b864e10-5a9b-46a1-8a94-2b571649a60d", + "source": { + "type": "generate_world", + "world_id": "pending", + "status": "pending" + }, + "createdAt": 1765433417487 + }, + "8e63a168-0c85-470d-b655-b185e0e13fee": { + "id": "8e63a168-0c85-470d-b655-b185e0e13fee", + "type": "world", + "parentId": "22b9587b-2b4d-49c2-a6eb-e985d890e7db", + "source": { + "type": "generate_world", + "world_id": "pending", + "posed_cubemaps_url": "", + "minimap_url": "", + "minimap_metadata": "", + "status": "pending" + }, + "createdAt": 1765433417487 + } + } + } + }, + "update_mask": [ + "metadata" + ] +} + +Response: + {"context":null,"id":"6623842d-3669-49b9-976f-5b239c24e353","display_name":null,"owner_id":"hdYte3rVrWbswJZEglL5CdmKfxv1","status":"CREATED","created_at":1765433415,"updated_at":1765433417,"object_uri":null,"mime_type":"application/run+json","metadata":{"version":"0.0.1","createdAt":1765433415549,"updatedAt":1765433417487,"useAdvancedEditing":false,"draftMode":false,"nodes":{"9b864e10-5a9b-46a1-8a94-2b571649a60d":{"id":"9b864e10-5a9b-46a1-8a94-2b571649a60d","type":"input","parentId":null,"source":{"type":"image","image_url":"https://cdn.marble.worldlabs.ai/object/ee33d03e-b153-4d56-a7cb-a08bf86bd20e/asset.jpg"},"createdAt":1765433417487},"22b9587b-2b4d-49c2-a6eb-e985d890e7db":{"id":"22b9587b-2b4d-49c2-a6eb-e985d890e7db","type":"pano","parentId":"9b864e10-5a9b-46a1-8a94-2b571649a60d","source":{"type":"generate_world","world_id":"pending","status":"pending"},"createdAt":1765433417487},"8e63a168-0c85-470d-b655-b185e0e13fee":{"id":"8e63a168-0c85-470d-b655-b185e0e13fee","type":"world","parentId":"22b9587b-2b4d-49c2-a6eb-e985d890e7db","source":{"type":"generate_world","world_id":"pending","posed_cubemaps_url":"","minimap_url":"","minimap_metadata":"","status":"pending"},"createdAt":1765433417487}}}} +Response Pretty Print: + +{ + "context": null, + "id": "6623842d-3669-49b9-976f-5b239c24e353", + "display_name": null, + "owner_id": "hdYte3rVrWbswJZEglL5CdmKfxv1", + "status": "CREATED", + "created_at": 1765433415, + "updated_at": 1765433417, + "object_uri": null, + "mime_type": "application/run+json", + "metadata": { + "version": "0.0.1", + "createdAt": 1765433415549, + "updatedAt": 1765433417487, + "useAdvancedEditing": false, + "draftMode": false, + "nodes": { + "9b864e10-5a9b-46a1-8a94-2b571649a60d": { + "id": "9b864e10-5a9b-46a1-8a94-2b571649a60d", + "type": "input", + "parentId": null, + "source": { + "type": "image", + "image_url": "https://cdn.marble.worldlabs.ai/object/ee33d03e-b153-4d56-a7cb-a08bf86bd20e/asset.jpg" + }, + "createdAt": 1765433417487 + }, + "22b9587b-2b4d-49c2-a6eb-e985d890e7db": { + "id": "22b9587b-2b4d-49c2-a6eb-e985d890e7db", + "type": "pano", + "parentId": "9b864e10-5a9b-46a1-8a94-2b571649a60d", + "source": { + "type": "generate_world", + "world_id": "pending", + "status": "pending" + }, + "createdAt": 1765433417487 + }, + "8e63a168-0c85-470d-b655-b185e0e13fee": { + "id": "8e63a168-0c85-470d-b655-b185e0e13fee", + "type": "world", + "parentId": "22b9587b-2b4d-49c2-a6eb-e985d890e7db", + "source": { + "type": "generate_world", + "world_id": "pending", + "posed_cubemaps_url": "", + "minimap_url": "", + "minimap_metadata": "", + "status": "pending" + }, + "createdAt": 1765433417487 + } + } + } +} + The URL is accessible in the browser: https://cdn.marble.worldlabs.ai/object/ee33d03e-b153-4d56-a7cb-a08bf86bd20e/asset.jpg diff --git a/crates/api_clients/world_labs_client/src/api/requests/objects/mod.rs b/crates/api_clients/world_labs_client/src/api/requests/objects/mod.rs index 980fce9d8e..cdb900266f 100644 --- a/crates/api_clients/world_labs_client/src/api/requests/objects/mod.rs +++ b/crates/api_clients/world_labs_client/src/api/requests/objects/mod.rs @@ -1,4 +1,5 @@ -pub mod create_object; pub mod begin_object_image_attachment; pub mod begin_object_image_upload; -pub mod finalize_object_image_upload; \ No newline at end of file +pub mod create_object; +pub mod finalize_object_image_upload; +pub mod patch_object_run_after_upload; \ No newline at end of file diff --git a/crates/api_clients/world_labs_client/src/api/requests/objects/patch_object_run_after_upload.rs b/crates/api_clients/world_labs_client/src/api/requests/objects/patch_object_run_after_upload.rs new file mode 100644 index 0000000000..84ec25127d --- /dev/null +++ b/crates/api_clients/world_labs_client/src/api/requests/objects/patch_object_run_after_upload.rs @@ -0,0 +1,205 @@ +use crate::api::api_types::object_id::ObjectId; +use crate::api::api_types::upload_id::UploadId; +use crate::api::api_types::upload_mime_type::UploadMimeType; +use crate::api::api_types::world_id::WorldId; +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 BASE_URL : &str = "https://marble2-kgw-prod-iac1.wlt-ai.art/api/v1/objects"; + +fn get_url(run_id: &ObjectId) -> String { + format!("{}/{}", BASE_URL, run_id.0) +} + +pub struct PatchObjectRunAfterUploadArgs<'a> { + pub cookies: &'a WorldLabsCookies, + pub bearer_token: &'a WorldLabsBearerToken, + pub run_id: &'a ObjectId, + + pub upload_id: &'a UploadId, + pub upload_mime_type: UploadMimeType, + pub text_prompt: &'a str, + pub request_timeout: Option, +} + +pub struct PatchObjectRunAfterUploadResponse { + pub world_id: WorldId, +} + +/// Marble Image-to-World +/// This request patches an object. Seems to update it with the attached files. +/// Request #6 (of ~10) +pub async fn patch_object_run_after_upload(args: PatchObjectRunAfterUploadArgs<'_>) -> Result { + let client = Client::builder() + .emulation(Emulation::Firefox143) + .build() + .map_err(|err| WorldLabsClientError::WreqClientError(err))?; + + let url = get_url(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_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, args.text_prompt); + + 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(PatchObjectRunAfterUploadResponse { + world_id: WorldId(response.id), + }) +} + +#[derive(Serialize)] +struct RawRequest { + pub object: ObjectDef, + pub generation_input: GenerationInput, + pub context: Context, +} + +#[derive(Serialize)] +struct ObjectDef { + pub metadata: ObjectMetadata, +} + +#[derive(Serialize)] +struct ObjectMetadata { + pub version: String, + #[serde(rename = "createdAt")] + pub created_at: String, + #[serde(rename = "updatedAt")] + pub updated_at: String, + #[serde(rename = "usesAdvancedEditing")] + pub uses_advanced_editing: bool, + #[serde(rename = "draftMode")] + pub draft_mode: bool, +} + +#[derive(Serialize)] +struct GenerationInput { + pub model: String, + pub prompt: Prompt, +} + +#[derive(Serialize)] +struct Prompt { + 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: &UploadId, image_mime_type: UploadMimeType, run_id: &ObjectId, text_prompt: &str) -> Self { + let image_url = upload_id_to_image_url(image_upload_id, image_mime_type); + 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: image_url, + }, + text_prompt: text_prompt.to_string(), + }, + }, + context: Context { + run_id: run_id.0.clone(), + } + } + } +} + +#[derive(Deserialize)] +struct RawResponse { + pub id: String, +} + +#[cfg(test)] +mod tests { +}