mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 09:22:41 -04:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d14629225d | ||
|
|
298e7df459 | ||
|
|
d1b9331df1 | ||
|
|
164dbdde33 | ||
|
|
910a31dc90 | ||
|
|
2db6b28704 |
Generated
+19
@@ -15007,6 +15007,25 @@ dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "world_labs_api"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"env",
|
||||
"env_logger 0.11.9",
|
||||
"errors",
|
||||
"filesys",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"uuid 1.18.1",
|
||||
"workspace-build-acceleration",
|
||||
"wreq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "world_labs_client"
|
||||
version = "0.0.1"
|
||||
|
||||
@@ -19,6 +19,7 @@ members = [
|
||||
"crates/api_clients/seedance2pro",
|
||||
"crates/api_clients/runwayml_client",
|
||||
"crates/api_clients/thumbnail_generator",
|
||||
"crates/api_clients/world_labs_api",
|
||||
"crates/api_clients/world_labs_client",
|
||||
|
||||
# CLI tools for operations, etc.
|
||||
@@ -248,6 +249,7 @@ tokens = { path = "crates/schema/public/tokens" }
|
||||
url_utils = { path = "crates/lib/url_utils" }
|
||||
users = { path = "crates/lib/users" }
|
||||
web_base64 = { path = "crates/lib/data/web_base64" }
|
||||
world_labs_api = { path = "crates/api_clients/world_labs_api" }
|
||||
world_labs_client = { path = "crates/api_clients/world_labs_client" }
|
||||
|
||||
# External libs
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
[package]
|
||||
name = "world_labs_api"
|
||||
edition = "2024"
|
||||
version = "0.0.1"
|
||||
publish = false
|
||||
|
||||
[package.metadata.cargo-udeps.ignore]
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[lib]
|
||||
name = "world_labs_api"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration.workspace = true
|
||||
|
||||
# Internal
|
||||
errors.workspace = true
|
||||
filesys.workspace = true
|
||||
|
||||
# External
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
uuid.workspace = true
|
||||
wreq.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
filesys.workspace = true
|
||||
env_logger.workspace = true
|
||||
env.workspace = true
|
||||
chrono.workspace = true
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod media_asset_id;
|
||||
pub mod operation_id;
|
||||
pub mod world_id;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Type for World IDs.
|
||||
/// These are used to refer to worlds
|
||||
/// These appear to be bare UUIDs.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WorldObjectId(pub String);
|
||||
|
||||
impl WorldObjectId {
|
||||
pub fn new() -> Self {
|
||||
let uuid = Uuid::new_v4().to_string();
|
||||
Self(uuid)
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
Self(s.to_string())
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> String {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod api_types;
|
||||
pub mod requests;
|
||||
@@ -0,0 +1,101 @@
|
||||
use crate::api::requests::delete_world::http_request::RawResponse;
|
||||
use crate::credentials::world_labs_api_creds::WorldLabsApiCreds;
|
||||
use crate::error::filter_world_labs_http_error::filter_world_labs_http_error;
|
||||
use crate::error::world_labs_error::WorldLabsError;
|
||||
use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError;
|
||||
use log::{debug, error};
|
||||
use std::time::Duration;
|
||||
use wreq::Client;
|
||||
|
||||
const BASE_URL: &str = "https://api.worldlabs.ai/marble/v1/worlds";
|
||||
|
||||
pub struct DeleteWorldArgs<'a> {
|
||||
pub creds: &'a WorldLabsApiCreds,
|
||||
pub world_id: &'a str,
|
||||
pub request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
pub struct DeleteWorldResponse {
|
||||
pub world_id: String,
|
||||
pub deleted: bool,
|
||||
}
|
||||
|
||||
/// DELETE /marble/v1/worlds/{world_id}
|
||||
///
|
||||
/// Permanently delete a world and its assets. Only the owner can delete.
|
||||
pub async fn delete_world(args: DeleteWorldArgs<'_>) -> Result<DeleteWorldResponse, WorldLabsError> {
|
||||
let client = Client::new();
|
||||
|
||||
let url = format!("{}/{}", BASE_URL, args.world_id);
|
||||
|
||||
debug!("Requesting URL: {}", url);
|
||||
|
||||
let mut request_builder = client.delete(&url)
|
||||
.header("WLT-Api-Key", args.creds.api_key());
|
||||
|
||||
if let Some(timeout) = args.request_timeout {
|
||||
request_builder = request_builder.timeout(timeout);
|
||||
}
|
||||
|
||||
let response = request_builder.send()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error during delete_world request: {:?}", err);
|
||||
WorldLabsGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
let response_body = response.text()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error reading response body: {:?}", err);
|
||||
WorldLabsGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
if !status.is_success() {
|
||||
error!("delete_world returned error (code {}): {:?}", status.as_u16(), response_body);
|
||||
}
|
||||
|
||||
filter_world_labs_http_error(status, Some(&response_body))?;
|
||||
|
||||
debug!("Response body (200): {}", response_body);
|
||||
|
||||
let raw: RawResponse = serde_json::from_str(&response_body)
|
||||
.map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?;
|
||||
|
||||
Ok(DeleteWorldResponse {
|
||||
world_id: raw.world_id,
|
||||
deleted: raw.deleted,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::get_test_api_key::get_test_api_key;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
use log::LevelFilter;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_delete_world() {
|
||||
setup_test_logging(LevelFilter::Debug);
|
||||
|
||||
let creds = get_test_api_key().unwrap();
|
||||
|
||||
// Use a known world_id to delete
|
||||
let world_id = "REPLACE_WITH_REAL_ID";
|
||||
|
||||
let response = delete_world(DeleteWorldArgs {
|
||||
creds: &creds,
|
||||
world_id,
|
||||
request_timeout: None,
|
||||
}).await.unwrap();
|
||||
|
||||
println!("World ID: {}", response.world_id);
|
||||
println!("Deleted: {}", response.deleted);
|
||||
|
||||
assert_eq!(1, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(crate) struct RawResponse {
|
||||
pub world_id: String,
|
||||
pub deleted: bool,
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod delete_world;
|
||||
pub(crate) mod http_request;
|
||||
@@ -0,0 +1,153 @@
|
||||
use crate::api::api_types::operation_id::OperationId;
|
||||
use crate::api::requests::generate_world::http_request::{RawRequest, RawResponse, WorldPrompt, Permission};
|
||||
use crate::credentials::world_labs_api_creds::WorldLabsApiCreds;
|
||||
use crate::error::filter_world_labs_http_error::filter_world_labs_http_error;
|
||||
use crate::error::world_labs_error::WorldLabsError;
|
||||
use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError;
|
||||
use log::{debug, error};
|
||||
use std::time::Duration;
|
||||
use wreq::Client;
|
||||
|
||||
const URL: &str = "https://api.worldlabs.ai/marble/v1/worlds:generate";
|
||||
|
||||
pub struct GenerateWorldArgs<'a> {
|
||||
pub creds: &'a WorldLabsApiCreds,
|
||||
pub world_prompt: WorldPrompt,
|
||||
pub display_name: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub seed: Option<u32>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub permission: Option<Permission>,
|
||||
pub request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
pub struct GenerateWorldResponse {
|
||||
pub operation_id: OperationId,
|
||||
pub done: bool,
|
||||
}
|
||||
|
||||
/// POST /marble/v1/worlds:generate
|
||||
///
|
||||
/// Start world generation. Returns an operation_id to poll with get_operation.
|
||||
pub async fn generate_world(args: GenerateWorldArgs<'_>) -> Result<GenerateWorldResponse, WorldLabsError> {
|
||||
let client = Client::new();
|
||||
|
||||
let payload = RawRequest {
|
||||
world_prompt: args.world_prompt,
|
||||
display_name: args.display_name,
|
||||
model: args.model,
|
||||
seed: args.seed,
|
||||
tags: args.tags,
|
||||
permission: args.permission,
|
||||
};
|
||||
|
||||
debug!("Requesting URL: {}", URL);
|
||||
|
||||
let mut request_builder = client.post(URL)
|
||||
.header("WLT-Api-Key", args.creds.api_key())
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&payload);
|
||||
|
||||
if let Some(timeout) = args.request_timeout {
|
||||
request_builder = request_builder.timeout(timeout);
|
||||
}
|
||||
|
||||
let response = request_builder.send()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error during generate_world request: {:?}", err);
|
||||
WorldLabsGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
let response_body = response.text()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error reading response body: {:?}", err);
|
||||
WorldLabsGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
if !status.is_success() {
|
||||
error!("generate_world returned error (code {}): {:?}", status.as_u16(), response_body);
|
||||
}
|
||||
|
||||
filter_world_labs_http_error(status, Some(&response_body))?;
|
||||
|
||||
debug!("Response body (200): {}", response_body);
|
||||
|
||||
let raw: RawResponse = serde_json::from_str(&response_body)
|
||||
.map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?;
|
||||
|
||||
Ok(GenerateWorldResponse {
|
||||
operation_id: OperationId(raw.operation_id),
|
||||
done: raw.done.unwrap_or(false),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::api::requests::generate_world::http_request::{ContentReference, WorldPrompt};
|
||||
use crate::test_utils::get_test_api_key::get_test_api_key;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
use log::LevelFilter;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_generate_world_text_prompt() {
|
||||
setup_test_logging(LevelFilter::Debug);
|
||||
|
||||
let creds = get_test_api_key().unwrap();
|
||||
|
||||
let response = generate_world(GenerateWorldArgs {
|
||||
creds: &creds,
|
||||
world_prompt: WorldPrompt::Text {
|
||||
text_prompt: Some("A cozy cabin in the snowy mountains".to_string()),
|
||||
disable_recaption: None,
|
||||
},
|
||||
display_name: Some("Test World".to_string()),
|
||||
model: Some("Marble 0.1-mini".to_string()),
|
||||
seed: None,
|
||||
tags: None,
|
||||
permission: None,
|
||||
request_timeout: None,
|
||||
}).await.unwrap();
|
||||
|
||||
println!("Operation ID: {}", response.operation_id.as_str());
|
||||
println!("Done: {}", response.done);
|
||||
|
||||
assert_eq!(1, 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_generate_world_image_uri() {
|
||||
setup_test_logging(LevelFilter::Debug);
|
||||
|
||||
let creds = get_test_api_key().unwrap();
|
||||
|
||||
let response = generate_world(GenerateWorldArgs {
|
||||
creds: &creds,
|
||||
world_prompt: WorldPrompt::Image {
|
||||
image_prompt: ContentReference::Uri {
|
||||
uri: "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/280px-PNG_transparency_demonstration_1.png".to_string(),
|
||||
},
|
||||
text_prompt: None,
|
||||
is_pano: None,
|
||||
disable_recaption: None,
|
||||
},
|
||||
display_name: None,
|
||||
model: Some("Marble 0.1-mini".to_string()),
|
||||
seed: None,
|
||||
tags: None,
|
||||
permission: None,
|
||||
request_timeout: None,
|
||||
}).await.unwrap();
|
||||
|
||||
println!("Operation ID: {}", response.operation_id.as_str());
|
||||
println!("Done: {}", response.done);
|
||||
|
||||
assert_eq!(1, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ── Request types ──
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct RawRequest {
|
||||
pub world_prompt: WorldPrompt,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub display_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub model: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub seed: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tags: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub permission: Option<Permission>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct Permission {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub public: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub allow_id_access: Option<bool>,
|
||||
}
|
||||
|
||||
/// The world prompt — a tagged union describing the input.
|
||||
///
|
||||
/// Discriminated on the `type` field:
|
||||
/// - `"text"` — text-only prompt
|
||||
/// - `"image"` — single image (+ optional text)
|
||||
/// - `"multi-image"` — multiple images with optional azimuth
|
||||
/// - `"video"` — video input (+ optional text)
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
pub enum WorldPrompt {
|
||||
Text {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
text_prompt: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
disable_recaption: Option<bool>,
|
||||
},
|
||||
Image {
|
||||
image_prompt: ContentReference,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
text_prompt: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
is_pano: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
disable_recaption: Option<bool>,
|
||||
},
|
||||
MultiImage {
|
||||
multi_image_prompt: Vec<SphericallyLocatedContent>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
text_prompt: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
reconstruct_images: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
disable_recaption: Option<bool>,
|
||||
},
|
||||
Video {
|
||||
video_prompt: ContentReference,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
text_prompt: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
disable_recaption: Option<bool>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A content reference — tagged union on `source`.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(tag = "source", rename_all = "snake_case")]
|
||||
pub enum ContentReference {
|
||||
Uri {
|
||||
uri: String,
|
||||
},
|
||||
MediaAsset {
|
||||
media_asset_id: String,
|
||||
},
|
||||
DataBase64 {
|
||||
data_base64: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
extension: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A spherically-located content item for multi-image prompts.
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct SphericallyLocatedContent {
|
||||
pub content: ContentReference,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub azimuth: Option<f64>,
|
||||
}
|
||||
|
||||
// ── Response types ──
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct RawResponse {
|
||||
pub operation_id: String,
|
||||
pub done: Option<bool>,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
pub expires_at: Option<String>,
|
||||
pub error: Option<RawOperationError>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct RawOperationError {
|
||||
pub code: Option<i32>,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod generate_world;
|
||||
pub(crate) mod http_request;
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
use crate::api::requests::get_media_asset::http_request::RawResponse;
|
||||
use crate::credentials::world_labs_api_creds::WorldLabsApiCreds;
|
||||
use crate::error::filter_world_labs_http_error::filter_world_labs_http_error;
|
||||
use crate::error::world_labs_error::WorldLabsError;
|
||||
use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError;
|
||||
use log::{debug, error};
|
||||
use std::time::Duration;
|
||||
use wreq::Client;
|
||||
|
||||
const BASE_URL: &str = "https://api.worldlabs.ai/marble/v1/media-assets";
|
||||
|
||||
pub struct GetMediaAssetArgs<'a> {
|
||||
pub creds: &'a WorldLabsApiCreds,
|
||||
pub media_asset_id: &'a str,
|
||||
pub request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
pub struct GetMediaAssetResponse {
|
||||
pub media_asset_id: String,
|
||||
pub file_name: String,
|
||||
pub kind: String,
|
||||
pub extension: Option<String>,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
|
||||
/// GET /marble/v1/media-assets/{media_asset_id}
|
||||
///
|
||||
/// Get info about a previously uploaded media asset.
|
||||
pub async fn get_media_asset(args: GetMediaAssetArgs<'_>) -> Result<GetMediaAssetResponse, WorldLabsError> {
|
||||
let client = Client::new();
|
||||
|
||||
let url = format!("{}/{}", BASE_URL, args.media_asset_id);
|
||||
|
||||
debug!("Requesting URL: {}", url);
|
||||
|
||||
let mut request_builder = client.get(&url)
|
||||
.header("WLT-Api-Key", args.creds.api_key());
|
||||
|
||||
if let Some(timeout) = args.request_timeout {
|
||||
request_builder = request_builder.timeout(timeout);
|
||||
}
|
||||
|
||||
let response = request_builder.send()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error during get_media_asset request: {:?}", err);
|
||||
WorldLabsGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
let response_body = response.text()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error reading response body: {:?}", err);
|
||||
WorldLabsGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
if !status.is_success() {
|
||||
error!("get_media_asset returned error (code {}): {:?}", status.as_u16(), response_body);
|
||||
}
|
||||
|
||||
filter_world_labs_http_error(status, Some(&response_body))?;
|
||||
|
||||
debug!("Response body (200): {}", response_body);
|
||||
|
||||
let raw: RawResponse = serde_json::from_str(&response_body)
|
||||
.map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?;
|
||||
|
||||
Ok(GetMediaAssetResponse {
|
||||
media_asset_id: raw.media_asset_id,
|
||||
file_name: raw.file_name,
|
||||
kind: raw.kind,
|
||||
extension: raw.extension,
|
||||
created_at: raw.created_at,
|
||||
updated_at: raw.updated_at,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::get_test_api_key::get_test_api_key;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
use log::LevelFilter;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_get_media_asset() {
|
||||
setup_test_logging(LevelFilter::Debug);
|
||||
|
||||
let creds = get_test_api_key().unwrap();
|
||||
|
||||
// Use a known media_asset_id from a previous prepare_upload call
|
||||
let media_asset_id = "REPLACE_WITH_REAL_ID";
|
||||
|
||||
let response = get_media_asset(GetMediaAssetArgs {
|
||||
creds: &creds,
|
||||
media_asset_id,
|
||||
request_timeout: None,
|
||||
}).await.unwrap();
|
||||
|
||||
println!("Media asset ID: {}", response.media_asset_id);
|
||||
println!("File name: {}", response.file_name);
|
||||
println!("Kind: {}", response.kind);
|
||||
println!("Extension: {:?}", response.extension);
|
||||
println!("Created at: {:?}", response.created_at);
|
||||
|
||||
assert_eq!(1, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct RawResponse {
|
||||
pub media_asset_id: String,
|
||||
pub file_name: String,
|
||||
pub kind: String,
|
||||
pub extension: Option<String>,
|
||||
pub metadata: Option<HashMap<String, String>>,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod get_media_asset;
|
||||
pub(crate) mod http_request;
|
||||
@@ -0,0 +1,129 @@
|
||||
use crate::api::api_types::operation_id::OperationId;
|
||||
use crate::api::requests::get_operation::http_request::RawResponse;
|
||||
use crate::credentials::world_labs_api_creds::WorldLabsApiCreds;
|
||||
use crate::error::filter_world_labs_http_error::filter_world_labs_http_error;
|
||||
use crate::error::world_labs_error::WorldLabsError;
|
||||
use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError;
|
||||
use log::{debug, error};
|
||||
use std::time::Duration;
|
||||
use wreq::Client;
|
||||
|
||||
const BASE_URL: &str = "https://api.worldlabs.ai/marble/v1/operations";
|
||||
|
||||
pub struct GetOperationArgs<'a> {
|
||||
pub creds: &'a WorldLabsApiCreds,
|
||||
pub operation_id: &'a OperationId,
|
||||
pub request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
pub struct GetOperationResponse {
|
||||
pub operation_id: String,
|
||||
pub done: bool,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
pub expires_at: Option<String>,
|
||||
pub error: Option<OperationError>,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
/// When done=true and no error, contains the World object.
|
||||
pub response: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OperationError {
|
||||
pub code: Option<i32>,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
/// GET /marble/v1/operations/{operation_id}
|
||||
///
|
||||
/// Poll the status of an async operation.
|
||||
/// When `done` is true, `response` contains the World object.
|
||||
pub async fn get_operation(args: GetOperationArgs<'_>) -> Result<GetOperationResponse, WorldLabsError> {
|
||||
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),
|
||||
created_at: raw.created_at,
|
||||
updated_at: raw.updated_at,
|
||||
expires_at: raw.expires_at,
|
||||
error: raw.error.map(|e| OperationError {
|
||||
code: e.code,
|
||||
message: e.message,
|
||||
}),
|
||||
metadata: raw.metadata,
|
||||
response: raw.response,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::get_test_api_key::get_test_api_key;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
use log::LevelFilter;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_get_operation() {
|
||||
setup_test_logging(LevelFilter::Debug);
|
||||
|
||||
let creds = get_test_api_key().unwrap();
|
||||
|
||||
// Use a known operation_id from a previous generate_world call
|
||||
let operation_id = OperationId("1fab3bf1-05a1-4929-907e-c6df07c539e2".to_string());
|
||||
|
||||
let response = get_operation(GetOperationArgs {
|
||||
creds: &creds,
|
||||
operation_id: &operation_id,
|
||||
request_timeout: None,
|
||||
}).await.unwrap();
|
||||
|
||||
println!("Operation ID: {}", response.operation_id);
|
||||
println!("Done: {}", response.done);
|
||||
println!("Created at: {:?}", response.created_at);
|
||||
println!("Error: {:?}", response.error);
|
||||
println!("Metadata: {:?}", response.metadata);
|
||||
println!("Response: {:?}", response.response);
|
||||
|
||||
assert_eq!(1, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(crate) struct RawResponse {
|
||||
pub operation_id: String,
|
||||
pub done: Option<bool>,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
pub expires_at: Option<String>,
|
||||
pub error: Option<RawOperationError>,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
pub response: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub(crate) struct RawOperationError {
|
||||
pub code: Option<i32>,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod get_operation;
|
||||
pub(crate) mod http_request;
|
||||
@@ -0,0 +1,162 @@
|
||||
use crate::api::requests::get_world::http_request::RawResponse;
|
||||
use crate::credentials::world_labs_api_creds::WorldLabsApiCreds;
|
||||
use crate::error::filter_world_labs_http_error::filter_world_labs_http_error;
|
||||
use crate::error::world_labs_error::WorldLabsError;
|
||||
use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError;
|
||||
use log::{debug, error};
|
||||
use std::time::Duration;
|
||||
use wreq::Client;
|
||||
|
||||
const BASE_URL: &str = "https://api.worldlabs.ai/marble/v1/worlds";
|
||||
|
||||
pub struct GetWorldArgs<'a> {
|
||||
pub creds: &'a WorldLabsApiCreds,
|
||||
pub world_id: &'a str,
|
||||
pub request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
pub struct GetWorldResponse {
|
||||
pub world_id: String,
|
||||
pub display_name: Option<String>,
|
||||
pub world_marble_url: Option<String>,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
pub model: Option<String>,
|
||||
/// e.g. "SUCCEEDED", "PENDING", "FAILED", "RUNNING"
|
||||
pub status: Option<String>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub is_public: Option<bool>,
|
||||
pub assets: Option<WorldAssets>,
|
||||
}
|
||||
|
||||
pub struct WorldAssets {
|
||||
pub caption: Option<String>,
|
||||
pub thumbnail_url: Option<String>,
|
||||
pub pano_url: Option<String>,
|
||||
pub collider_mesh_url: Option<String>,
|
||||
pub spz_url_100k: Option<String>,
|
||||
pub spz_url_500k: Option<String>,
|
||||
pub spz_url_full_res: Option<String>,
|
||||
}
|
||||
|
||||
/// GET /marble/v1/worlds/{world_id}
|
||||
///
|
||||
/// Retrieve a world by its ID.
|
||||
pub async fn get_world(args: GetWorldArgs<'_>) -> Result<GetWorldResponse, WorldLabsError> {
|
||||
let client = Client::new();
|
||||
|
||||
let url = format!("{}/{}", BASE_URL, args.world_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_world request: {:?}", err);
|
||||
WorldLabsGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
let response_body = response.text()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error reading response body: {:?}", err);
|
||||
WorldLabsGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
if !status.is_success() {
|
||||
error!("get_world returned error (code {}): {:?}", status.as_u16(), response_body);
|
||||
}
|
||||
|
||||
filter_world_labs_http_error(status, Some(&response_body))?;
|
||||
|
||||
debug!("Response body (200): {}", response_body);
|
||||
|
||||
let raw: RawResponse = serde_json::from_str(&response_body)
|
||||
.map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?;
|
||||
|
||||
let assets = raw.assets.map(|a| {
|
||||
let spz_urls = a.splats.and_then(|s| s.spz_urls);
|
||||
WorldAssets {
|
||||
caption: a.caption,
|
||||
thumbnail_url: a.thumbnail_url,
|
||||
pano_url: a.imagery.and_then(|i| i.pano_url),
|
||||
collider_mesh_url: a.mesh.and_then(|m| m.collider_mesh_url),
|
||||
spz_url_100k: spz_urls.as_ref().and_then(|u| u.low.clone()),
|
||||
spz_url_500k: spz_urls.as_ref().and_then(|u| u.medium.clone()),
|
||||
spz_url_full_res: spz_urls.as_ref().and_then(|u| u.full_res.clone()),
|
||||
}
|
||||
});
|
||||
|
||||
Ok(GetWorldResponse {
|
||||
world_id: raw.world_id,
|
||||
display_name: raw.display_name,
|
||||
world_marble_url: raw.world_marble_url,
|
||||
created_at: raw.created_at,
|
||||
updated_at: raw.updated_at,
|
||||
model: raw.model,
|
||||
status: raw.status,
|
||||
tags: raw.tags,
|
||||
is_public: raw.permission.and_then(|p| p.public),
|
||||
assets,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::get_test_api_key::get_test_api_key;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
use log::LevelFilter;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_get_world() {
|
||||
setup_test_logging(LevelFilter::Debug);
|
||||
|
||||
let creds = get_test_api_key().unwrap();
|
||||
|
||||
// Use a known world_id from a previous generation
|
||||
let world_id = "0048d009-1c7a-4e13-9881-07e9b6ff32e1";
|
||||
//let world_id = "c8a89fb1-f8b4-44c6-ae50-525f7205c65f";
|
||||
|
||||
let response = get_world(GetWorldArgs {
|
||||
creds: &creds,
|
||||
world_id,
|
||||
request_timeout: None,
|
||||
}).await.unwrap();
|
||||
|
||||
println!("World ID: {}", response.world_id);
|
||||
println!("Display name: {:?}", response.display_name);
|
||||
println!("Marble URL: {:?}", response.world_marble_url);
|
||||
println!("Created at: {:?}", response.created_at);
|
||||
println!("Updated at: {:?}", response.updated_at);
|
||||
println!("Model: {:?}", response.model);
|
||||
println!("Status: {:?}", response.status);
|
||||
println!("Tags: {:?}", response.tags);
|
||||
println!("Is public: {:?}", response.is_public);
|
||||
match &response.assets {
|
||||
None => println!("Assets: None"),
|
||||
Some(assets) => {
|
||||
println!("Assets:");
|
||||
println!(" Caption: {:?}", assets.caption);
|
||||
println!(" Thumbnail URL: {:?}", assets.thumbnail_url);
|
||||
println!(" Pano URL: {:?}", assets.pano_url);
|
||||
println!(" Collider mesh URL: {:?}", assets.collider_mesh_url);
|
||||
println!(" SPZ 100k: {:?}", assets.spz_url_100k);
|
||||
println!(" SPZ 500k: {:?}", assets.spz_url_500k);
|
||||
println!(" SPZ full res: {:?}", assets.spz_url_full_res);
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(1, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct RawResponse {
|
||||
pub world_id: String,
|
||||
pub display_name: Option<String>,
|
||||
pub world_marble_url: Option<String>,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub permission: Option<RawPermission>,
|
||||
pub assets: Option<RawWorldAssets>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct RawPermission {
|
||||
pub public: Option<bool>,
|
||||
pub allow_id_access: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct RawWorldAssets {
|
||||
pub caption: Option<String>,
|
||||
pub thumbnail_url: Option<String>,
|
||||
pub imagery: Option<RawImagery>,
|
||||
pub mesh: Option<RawMesh>,
|
||||
pub splats: Option<RawSplats>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct RawImagery {
|
||||
pub pano_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct RawMesh {
|
||||
pub collider_mesh_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct RawSplats {
|
||||
pub spz_urls: Option<RawSpzUrls>,
|
||||
pub semantics_metadata: Option<RawSemanticsMetadata>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct RawSemanticsMetadata {
|
||||
pub metric_scale_factor: Option<f64>,
|
||||
pub ground_plane_offset: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct RawSpzUrls {
|
||||
#[serde(rename = "100k")]
|
||||
pub low: Option<String>,
|
||||
#[serde(rename = "500k")]
|
||||
pub medium: Option<String>,
|
||||
pub full_res: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod get_world;
|
||||
pub(crate) mod http_request;
|
||||
@@ -0,0 +1,41 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct RawRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub page_size: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub page_token: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub status: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub model: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tags: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_public: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub created_after: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub created_before: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sort_by: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(crate) struct RawResponse {
|
||||
pub worlds: Vec<RawWorldSummary>,
|
||||
pub next_page_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(crate) struct RawWorldSummary {
|
||||
pub world_id: String,
|
||||
pub display_name: Option<String>,
|
||||
pub world_marble_url: Option<String>,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
use crate::api::requests::list_worlds::http_request::{RawRequest, RawResponse};
|
||||
use crate::credentials::world_labs_api_creds::WorldLabsApiCreds;
|
||||
use crate::error::filter_world_labs_http_error::filter_world_labs_http_error;
|
||||
use crate::error::world_labs_error::WorldLabsError;
|
||||
use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError;
|
||||
use log::{debug, error};
|
||||
use std::time::Duration;
|
||||
use wreq::Client;
|
||||
|
||||
const URL: &str = "https://api.worldlabs.ai/marble/v1/worlds:list";
|
||||
|
||||
pub struct ListWorldsArgs<'a> {
|
||||
pub creds: &'a WorldLabsApiCreds,
|
||||
/// 1-100, default 20
|
||||
pub page_size: Option<u32>,
|
||||
/// Opaque cursor from a previous response
|
||||
pub page_token: Option<String>,
|
||||
/// "SUCCEEDED", "PENDING", "FAILED", "RUNNING"
|
||||
pub status: Option<String>,
|
||||
/// "Marble 0.1-mini" or "Marble 0.1-plus"
|
||||
pub model: Option<String>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub is_public: Option<bool>,
|
||||
/// ISO 8601 datetime
|
||||
pub created_after: Option<String>,
|
||||
/// ISO 8601 datetime
|
||||
pub created_before: Option<String>,
|
||||
/// "created_at" or "updated_at"
|
||||
pub sort_by: Option<String>,
|
||||
pub request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
pub struct ListWorldsResponse {
|
||||
pub worlds: Vec<WorldSummary>,
|
||||
pub next_page_token: Option<String>,
|
||||
}
|
||||
|
||||
pub struct WorldSummary {
|
||||
pub world_id: String,
|
||||
pub display_name: Option<String>,
|
||||
pub world_marble_url: Option<String>,
|
||||
pub created_at: Option<String>,
|
||||
pub updated_at: Option<String>,
|
||||
pub model: Option<String>,
|
||||
/// e.g. "SUCCEEDED", "PENDING", "FAILED", "RUNNING"
|
||||
pub status: Option<String>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// POST /marble/v1/worlds:list
|
||||
///
|
||||
/// List worlds with optional filtering and pagination.
|
||||
pub async fn list_worlds(args: ListWorldsArgs<'_>) -> Result<ListWorldsResponse, WorldLabsError> {
|
||||
let client = Client::new();
|
||||
|
||||
let payload = RawRequest {
|
||||
page_size: args.page_size,
|
||||
page_token: args.page_token,
|
||||
status: args.status,
|
||||
model: args.model,
|
||||
tags: args.tags,
|
||||
is_public: args.is_public,
|
||||
created_after: args.created_after,
|
||||
created_before: args.created_before,
|
||||
sort_by: args.sort_by,
|
||||
};
|
||||
|
||||
debug!("Requesting URL: {}", URL);
|
||||
|
||||
let mut request_builder = client.post(URL)
|
||||
.header("WLT-Api-Key", args.creds.api_key())
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&payload);
|
||||
|
||||
if let Some(timeout) = args.request_timeout {
|
||||
request_builder = request_builder.timeout(timeout);
|
||||
}
|
||||
|
||||
let response = request_builder.send()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error during list_worlds request: {:?}", err);
|
||||
WorldLabsGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
let response_body = response.text()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error reading response body: {:?}", err);
|
||||
WorldLabsGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
if !status.is_success() {
|
||||
error!("list_worlds returned error (code {}): {:?}", status.as_u16(), response_body);
|
||||
}
|
||||
|
||||
filter_world_labs_http_error(status, Some(&response_body))?;
|
||||
|
||||
debug!("Response body (200): {}", response_body);
|
||||
|
||||
let raw: RawResponse = serde_json::from_str(&response_body)
|
||||
.map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?;
|
||||
|
||||
let worlds = raw.worlds.into_iter().map(|w| WorldSummary {
|
||||
world_id: w.world_id,
|
||||
display_name: w.display_name,
|
||||
world_marble_url: w.world_marble_url,
|
||||
created_at: w.created_at,
|
||||
updated_at: w.updated_at,
|
||||
model: w.model,
|
||||
status: w.status,
|
||||
tags: w.tags,
|
||||
}).collect();
|
||||
|
||||
Ok(ListWorldsResponse {
|
||||
worlds,
|
||||
next_page_token: raw.next_page_token,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::get_test_api_key::get_test_api_key;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
use log::LevelFilter;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_list_worlds() {
|
||||
setup_test_logging(LevelFilter::Debug);
|
||||
|
||||
let creds = get_test_api_key().unwrap();
|
||||
|
||||
let response = list_worlds(ListWorldsArgs {
|
||||
creds: &creds,
|
||||
page_size: Some(5),
|
||||
page_token: None,
|
||||
status: None,
|
||||
model: None,
|
||||
tags: None,
|
||||
is_public: None,
|
||||
created_after: None,
|
||||
created_before: None,
|
||||
sort_by: None,
|
||||
request_timeout: None,
|
||||
}).await.unwrap();
|
||||
|
||||
println!("Worlds count: {}", response.worlds.len());
|
||||
println!("Next page token: {:?}", response.next_page_token);
|
||||
println!();
|
||||
|
||||
for (i, world) in response.worlds.iter().enumerate() {
|
||||
println!("--- World {} ---", i + 1);
|
||||
println!(" World ID: {}", world.world_id);
|
||||
println!(" Display name: {:?}", world.display_name);
|
||||
println!(" Marble URL: {:?}", world.world_marble_url);
|
||||
println!(" Created at: {:?}", world.created_at);
|
||||
println!(" Updated at: {:?}", world.updated_at);
|
||||
println!(" Model: {:?}", world.model);
|
||||
println!(" Status: {:?}", world.status);
|
||||
println!(" Tags: {:?}", world.tags);
|
||||
}
|
||||
|
||||
assert_eq!(1, 2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod list_worlds;
|
||||
pub(crate) mod http_request;
|
||||
@@ -0,0 +1,8 @@
|
||||
pub mod delete_world;
|
||||
pub mod generate_world;
|
||||
pub mod get_media_asset;
|
||||
pub mod get_operation;
|
||||
pub mod get_world;
|
||||
pub mod list_worlds;
|
||||
pub mod prepare_upload;
|
||||
pub mod upload_to_signed_url;
|
||||
@@ -0,0 +1,34 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct RawRequest {
|
||||
pub file_name: String,
|
||||
pub kind: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extension: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub metadata: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(crate) struct RawResponse {
|
||||
pub media_asset: RawMediaAsset,
|
||||
pub upload_info: RawUploadInfo,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct RawMediaAsset {
|
||||
pub media_asset_id: String,
|
||||
pub file_name: String,
|
||||
pub kind: String,
|
||||
pub extension: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(crate) struct RawUploadInfo {
|
||||
pub upload_url: String,
|
||||
pub upload_method: String,
|
||||
pub required_headers: Option<HashMap<String, String>>,
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod prepare_upload;
|
||||
pub(crate) mod http_request;
|
||||
@@ -0,0 +1,151 @@
|
||||
use crate::api::api_types::media_asset_id::MediaAssetId;
|
||||
use crate::api::requests::prepare_upload::http_request::{RawRequest, RawResponse};
|
||||
use crate::credentials::world_labs_api_creds::WorldLabsApiCreds;
|
||||
use crate::error::filter_world_labs_http_error::filter_world_labs_http_error;
|
||||
use crate::error::world_labs_error::WorldLabsError;
|
||||
use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError;
|
||||
use log::{debug, error};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use wreq::Client;
|
||||
|
||||
const URL: &str = "https://api.worldlabs.ai/marble/v1/media-assets:prepare_upload";
|
||||
|
||||
/// The kind of media asset to upload.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum MediaAssetKind {
|
||||
Image,
|
||||
Video,
|
||||
}
|
||||
|
||||
impl MediaAssetKind {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Image => "image",
|
||||
Self::Video => "video",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PrepareUploadArgs<'a> {
|
||||
pub creds: &'a WorldLabsApiCreds,
|
||||
pub file_name: &'a str,
|
||||
pub kind: MediaAssetKind,
|
||||
pub request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
pub struct PrepareUploadResponse {
|
||||
pub media_asset_id: MediaAssetId,
|
||||
pub upload_url: String,
|
||||
pub upload_method: String,
|
||||
pub required_headers: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Parse the file extension from a file name, defaulting to "png".
|
||||
fn extension_from_file_name(file_name: &str) -> String {
|
||||
file_name
|
||||
.rsplit_once('.')
|
||||
.map(|(_, ext)| ext.to_lowercase())
|
||||
.unwrap_or_else(|| "png".to_string())
|
||||
}
|
||||
|
||||
/// POST /marble/v1/media-assets:prepare_upload
|
||||
///
|
||||
/// Get a signed upload URL and media_asset_id for uploading a file.
|
||||
/// The extension is parsed from `file_name`; defaults to "png" if none found.
|
||||
pub async fn prepare_upload(args: PrepareUploadArgs<'_>) -> Result<PrepareUploadResponse, WorldLabsError> {
|
||||
let client = Client::new();
|
||||
|
||||
let extension = extension_from_file_name(args.file_name);
|
||||
|
||||
let payload = RawRequest {
|
||||
file_name: args.file_name.to_string(),
|
||||
kind: args.kind.as_str().to_string(),
|
||||
extension: Some(extension),
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
debug!("Requesting URL: {}", URL);
|
||||
|
||||
let mut request_builder = client.post(URL)
|
||||
.header("WLT-Api-Key", args.creds.api_key())
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&payload);
|
||||
|
||||
if let Some(timeout) = args.request_timeout {
|
||||
request_builder = request_builder.timeout(timeout);
|
||||
}
|
||||
|
||||
let response = request_builder.send()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error during prepare_upload request: {:?}", err);
|
||||
WorldLabsGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
let response_body = response.text()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error reading response body: {:?}", err);
|
||||
WorldLabsGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
if !status.is_success() {
|
||||
error!("prepare_upload returned error (code {}): {:?}", status.as_u16(), response_body);
|
||||
}
|
||||
|
||||
filter_world_labs_http_error(status, Some(&response_body))?;
|
||||
|
||||
debug!("Response body (200): {}", response_body);
|
||||
|
||||
let raw: RawResponse = serde_json::from_str(&response_body)
|
||||
.map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?;
|
||||
|
||||
Ok(PrepareUploadResponse {
|
||||
media_asset_id: MediaAssetId(raw.media_asset.media_asset_id),
|
||||
upload_url: raw.upload_info.upload_url,
|
||||
upload_method: raw.upload_info.upload_method,
|
||||
required_headers: raw.upload_info.required_headers.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::get_test_api_key::get_test_api_key;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
use log::LevelFilter;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_prepare_upload() {
|
||||
setup_test_logging(LevelFilter::Debug);
|
||||
|
||||
let creds = get_test_api_key().unwrap();
|
||||
|
||||
let response = prepare_upload(PrepareUploadArgs {
|
||||
creds: &creds,
|
||||
file_name: "test_image.jpg",
|
||||
kind: MediaAssetKind::Image,
|
||||
request_timeout: None,
|
||||
}).await.unwrap();
|
||||
|
||||
println!("Media asset ID: {}", response.media_asset_id.as_str());
|
||||
println!("Upload URL: {}", response.upload_url);
|
||||
println!("Upload method: {}", response.upload_method);
|
||||
println!("Required headers: {:?}", response.required_headers);
|
||||
|
||||
assert_eq!(1, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extension_parsing() {
|
||||
assert_eq!(extension_from_file_name("photo.jpg"), "jpg");
|
||||
assert_eq!(extension_from_file_name("image.PNG"), "png");
|
||||
assert_eq!(extension_from_file_name("video.mp4"), "mp4");
|
||||
assert_eq!(extension_from_file_name("no_extension"), "png");
|
||||
assert_eq!(extension_from_file_name("multi.dots.webp"), "webp");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod upload_to_signed_url;
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
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<u8>,
|
||||
pub required_headers: &'a HashMap<String, String>,
|
||||
pub content_type: &'a str,
|
||||
pub request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
/// PUT {upload_url}
|
||||
///
|
||||
/// Upload raw file bytes to the signed URL returned by prepare_upload.
|
||||
/// Include any required_headers from the prepare_upload response.
|
||||
pub async fn upload_to_signed_url(args: UploadToSignedUrlArgs<'_>) -> Result<(), WorldLabsError> {
|
||||
let client = Client::new();
|
||||
|
||||
debug!("Uploading to signed URL: {}", args.upload_url);
|
||||
|
||||
let mut request_builder = client.put(args.upload_url)
|
||||
.header("Content-Type", args.content_type)
|
||||
.body(args.file_bytes);
|
||||
|
||||
for (key, value) in args.required_headers {
|
||||
request_builder = request_builder.header(key.as_str(), value.as_str());
|
||||
}
|
||||
|
||||
if let Some(timeout) = args.request_timeout {
|
||||
request_builder = request_builder.timeout(timeout);
|
||||
}
|
||||
|
||||
let response = request_builder.send()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error during signed URL upload: {:?}", err);
|
||||
WorldLabsGenericApiError::WreqError(err)
|
||||
})?;
|
||||
|
||||
let status = response.status();
|
||||
|
||||
if !status.is_success() {
|
||||
let body = response.text()
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
error!("Signed URL upload failed (code {}): {:?}", status.as_u16(), body);
|
||||
return Err(WorldLabsGenericApiError::GoogleUploadFailed {
|
||||
status_code: status,
|
||||
body,
|
||||
}.into());
|
||||
}
|
||||
|
||||
debug!("Upload to signed URL succeeded");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// Upload tests are covered by the end-to-end recipe test,
|
||||
// since this endpoint requires a fresh signed URL from prepare_upload.
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod world_labs_api_creds;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
use crate::error::world_labs_error::WorldLabsError;
|
||||
use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError;
|
||||
use crate::error::world_labs_specific_api_error::WorldLabsSpecificApiError;
|
||||
use wreq::StatusCode;
|
||||
|
||||
/// Detect error HTTP responses and coerce the response as a Rust Error.
|
||||
pub fn filter_world_labs_http_error(status_code: StatusCode, maybe_body: Option<&str>) -> Result<(), WorldLabsError> {
|
||||
if status_code.is_success() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match status_code {
|
||||
StatusCode::PAYMENT_REQUIRED => {
|
||||
return Err(WorldLabsSpecificApiError::InsufficientCredits.into());
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
|
||||
if let Some(body) = maybe_body {
|
||||
return Err(WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code,
|
||||
body: body.to_string(),
|
||||
}.into());
|
||||
}
|
||||
|
||||
Err(WorldLabsGenericApiError::UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code,
|
||||
body: String::new(),
|
||||
}.into())
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod world_labs_client_error;
|
||||
pub mod world_labs_error;
|
||||
pub mod world_labs_generic_api_error;
|
||||
pub mod world_labs_specific_api_error;
|
||||
pub (crate) mod filter_world_labs_http_error;
|
||||
@@ -0,0 +1,23 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum WorldLabsClientError {
|
||||
|
||||
/// An error was encountered in building the Wreq client
|
||||
WreqClientError(wreq::Error),
|
||||
|
||||
/// Can't read a local file for uploading.
|
||||
CannotReadLocalFileForUpload(std::io::Error),
|
||||
}
|
||||
|
||||
impl Error for WorldLabsClientError {}
|
||||
|
||||
impl Display for WorldLabsClientError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::WreqClientError(err) => write!(f, "Wreq client error (during client creation): {}", err),
|
||||
Self::CannotReadLocalFileForUpload(err) => write!(f, "Cannot read local file for upload: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
use crate::error::world_labs_client_error::WorldLabsClientError;
|
||||
use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError;
|
||||
use crate::error::world_labs_specific_api_error::WorldLabsSpecificApiError;
|
||||
use std::error::Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum WorldLabsError {
|
||||
Client(WorldLabsClientError),
|
||||
ApiSpecific(WorldLabsSpecificApiError),
|
||||
ApiGeneric(WorldLabsGenericApiError),
|
||||
}
|
||||
|
||||
impl Error for WorldLabsError {}
|
||||
|
||||
impl std::fmt::Display for WorldLabsError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Client(e) => write!(f, "WorldLabsClientError: {:?}", e),
|
||||
Self::ApiSpecific(e) => write!(f, "WorldLabsSpecificApiError: {:?}", e),
|
||||
Self::ApiGeneric(e) => write!(f, "WorldLabsGenericApiError: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WorldLabsClientError> for WorldLabsError {
|
||||
fn from(error: WorldLabsClientError) -> Self {
|
||||
Self::Client(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WorldLabsSpecificApiError> for WorldLabsError {
|
||||
fn from(error: WorldLabsSpecificApiError) -> Self {
|
||||
Self::ApiSpecific(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WorldLabsGenericApiError> for WorldLabsError {
|
||||
fn from(error: WorldLabsGenericApiError) -> Self {
|
||||
Self::ApiGeneric(error)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use wreq::StatusCode;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum WorldLabsGenericApiError {
|
||||
/// Unknown error occurred when uploading to signed URL
|
||||
GoogleUploadFailed {
|
||||
status_code: StatusCode,
|
||||
body: String,
|
||||
},
|
||||
|
||||
/// serde_json::Error, likely from JSON deserialization schema mismatch.
|
||||
/// Includes the original body.
|
||||
SerdeResponseParseErrorWithBody(serde_json::Error, String),
|
||||
|
||||
/// An uncategorized bad HTTP response.
|
||||
UncategorizedBadResponseWithStatusAndBody {
|
||||
status_code: StatusCode,
|
||||
body: String,
|
||||
},
|
||||
|
||||
/// An uncaught error from the API client.
|
||||
WreqError(wreq::Error),
|
||||
}
|
||||
|
||||
impl Error for WorldLabsGenericApiError {}
|
||||
|
||||
impl Display for WorldLabsGenericApiError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::GoogleUploadFailed { status_code, body } => write!(f, "Signed URL upload failed with HTTP {}: {:?}", status_code, body),
|
||||
Self::SerdeResponseParseErrorWithBody(err, body) => write!(f, "Failed to parse response body: {:?}. Body: {}", err, body),
|
||||
Self::UncategorizedBadResponseWithStatusAndBody { status_code, body } => write!(f, "Uncategorized bad response: status code {}, body: {}", status_code, body),
|
||||
Self::WreqError(err) => write!(f, "Wreq client error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum WorldLabsSpecificApiError {
|
||||
/// There aren't sufficient funds for generation.
|
||||
InsufficientCredits,
|
||||
}
|
||||
|
||||
impl Error for WorldLabsSpecificApiError {}
|
||||
|
||||
impl Display for WorldLabsSpecificApiError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::InsufficientCredits => write!(f, "Insufficient credits"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
#[cfg(test)]
|
||||
pub (crate) mod test_utils;
|
||||
|
||||
pub mod api;
|
||||
pub mod credentials;
|
||||
pub mod error;
|
||||
pub mod recipes;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod upload_image_and_create_world_with_retry;
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
use crate::api::api_types::media_asset_id::MediaAssetId;
|
||||
use crate::api::api_types::operation_id::OperationId;
|
||||
use crate::api::requests::generate_world::generate_world::{generate_world, GenerateWorldArgs};
|
||||
use crate::api::requests::generate_world::http_request::{ContentReference, WorldPrompt};
|
||||
use crate::api::requests::prepare_upload::prepare_upload::{prepare_upload, PrepareUploadArgs, MediaAssetKind};
|
||||
use crate::api::requests::upload_to_signed_url::upload_to_signed_url::{upload_to_signed_url, UploadToSignedUrlArgs};
|
||||
use crate::credentials::world_labs_api_creds::WorldLabsApiCreds;
|
||||
use crate::error::world_labs_client_error::WorldLabsClientError;
|
||||
use crate::error::world_labs_error::WorldLabsError;
|
||||
use filesys::file_read_bytes::file_read_bytes;
|
||||
use log::{error, info};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct UploadImageAndCreateWorldArgs<'a> {
|
||||
pub creds: &'a WorldLabsApiCreds,
|
||||
pub file: FileBytesOrPath,
|
||||
pub text_prompt: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub individual_request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
pub enum FileBytesOrPath {
|
||||
Bytes(Vec<u8>),
|
||||
Path(PathBuf),
|
||||
}
|
||||
|
||||
pub struct UploadImageAndCreateWorldResponse {
|
||||
pub operation_id: OperationId,
|
||||
pub media_asset_id: MediaAssetId,
|
||||
}
|
||||
|
||||
/// Official World Labs API: Upload an image and generate a world.
|
||||
///
|
||||
/// Flow (3 steps):
|
||||
/// 1. prepare_upload — get media_asset_id + signed upload URL
|
||||
/// 2. upload_to_signed_url — PUT file bytes to signed URL
|
||||
/// 3. generate_world — start world generation with media_asset source
|
||||
pub async fn upload_image_and_create_world(args: UploadImageAndCreateWorldArgs<'_>) -> Result<UploadImageAndCreateWorldResponse, WorldLabsError> {
|
||||
|
||||
info!("Checking file input...");
|
||||
|
||||
let file_bytes = match args.file {
|
||||
FileBytesOrPath::Bytes(bytes) => {
|
||||
info!("File bytes provided ({} bytes)", bytes.len());
|
||||
bytes
|
||||
}
|
||||
FileBytesOrPath::Path(path) => {
|
||||
info!("File path provided: {:?}", path);
|
||||
match file_read_bytes(&path) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(err) => {
|
||||
error!("Error reading file bytes from path: {:?} - error: {:?}", path, err);
|
||||
return Err(WorldLabsClientError::CannotReadLocalFileForUpload(err).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Step 1: Prepare upload
|
||||
info!("Step 1 of 3: prepare_upload ...");
|
||||
|
||||
let prepare_response = prepare_upload(PrepareUploadArgs {
|
||||
creds: args.creds,
|
||||
file_name: "upload.jpg",
|
||||
kind: MediaAssetKind::Image,
|
||||
request_timeout: args.individual_request_timeout,
|
||||
}).await?;
|
||||
|
||||
let media_asset_id = prepare_response.media_asset_id;
|
||||
let upload_url = prepare_response.upload_url;
|
||||
let required_headers = prepare_response.required_headers;
|
||||
|
||||
info!("Media asset ID: {}", media_asset_id.as_str());
|
||||
info!("Upload URL: {}", upload_url);
|
||||
|
||||
// Step 2: Upload to signed URL
|
||||
info!("Step 2 of 3: upload_to_signed_url ...");
|
||||
|
||||
upload_to_signed_url(UploadToSignedUrlArgs {
|
||||
upload_url: &upload_url,
|
||||
file_bytes,
|
||||
required_headers: &required_headers,
|
||||
content_type: "image/jpeg",
|
||||
request_timeout: args.individual_request_timeout,
|
||||
}).await?;
|
||||
|
||||
info!("Upload complete.");
|
||||
|
||||
// Step 3: Generate world
|
||||
info!("Step 3 of 3: generate_world ...");
|
||||
|
||||
let world_prompt = WorldPrompt::Image {
|
||||
image_prompt: ContentReference::MediaAsset {
|
||||
media_asset_id: media_asset_id.as_str().to_string(),
|
||||
},
|
||||
text_prompt: args.text_prompt,
|
||||
is_pano: None,
|
||||
disable_recaption: None,
|
||||
};
|
||||
|
||||
let generate_response = generate_world(GenerateWorldArgs {
|
||||
creds: args.creds,
|
||||
world_prompt,
|
||||
display_name: None,
|
||||
model: args.model,
|
||||
seed: None,
|
||||
tags: None,
|
||||
permission: None,
|
||||
request_timeout: args.individual_request_timeout,
|
||||
}).await?;
|
||||
|
||||
info!("Operation ID: {}", generate_response.operation_id.as_str());
|
||||
info!("Done: {}", generate_response.done);
|
||||
|
||||
Ok(UploadImageAndCreateWorldResponse {
|
||||
operation_id: generate_response.operation_id,
|
||||
media_asset_id,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_utils::get_test_api_key::get_test_api_key;
|
||||
use crate::test_utils::setup_test_logging::setup_test_logging;
|
||||
use filesys::file_read_bytes::file_read_bytes;
|
||||
use log::LevelFilter;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_upload_and_generate() {
|
||||
setup_test_logging(LevelFilter::Debug);
|
||||
|
||||
let creds = get_test_api_key().unwrap();
|
||||
|
||||
//let file_path = "/Users/bt/Pictures/Midjourney/moon_door.png";
|
||||
let file_path = "/Users/bt/Pictures/Midjourney/meadow_mirror.png";
|
||||
let file_bytes = file_read_bytes(file_path).unwrap();
|
||||
|
||||
println!("File bytes len: {}", file_bytes.len());
|
||||
|
||||
let results = upload_image_and_create_world(UploadImageAndCreateWorldArgs {
|
||||
creds: &creds,
|
||||
individual_request_timeout: None,
|
||||
file: FileBytesOrPath::Bytes(file_bytes),
|
||||
text_prompt: None,
|
||||
model: None,
|
||||
}).await.unwrap();
|
||||
|
||||
println!("Operation ID: {}", results.operation_id.as_str());
|
||||
println!("Media Asset ID: {}", results.media_asset_id.as_str());
|
||||
|
||||
/*
|
||||
00:55:36.237287 [DEBUG] - Requesting URL: https://api.worldlabs.ai/marble/v1/worlds:generate
|
||||
00:55:45.047221 [DEBUG] - Response body (200): {"operation_id":"1fab3bf1-05a1-4929-907e-c6df07c539e2","created_at":"2026-03-09T04:55:37Z","updated_at":"2026-03-09T04:55:37Z","expires_at":"2026-03-09T05:55:37Z","done":false,"error":null,"metadata":null,"response":null}
|
||||
00:55:45.047756 [INFO] - Operation ID: 1fab3bf1-05a1-4929-907e-c6df07c539e2
|
||||
00:55:45.047839 [INFO] - Done: false
|
||||
Operation ID: 1fab3bf1-05a1-4929-907e-c6df07c539e2
|
||||
Media Asset ID: bc57b2bc-542e-416d-8089-924294152f0d
|
||||
*/
|
||||
|
||||
assert_eq!(1, 2);
|
||||
}
|
||||
}
|
||||
@@ -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/world_labs_api_key.txt";
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn get_test_api_key() -> AnyhowResult<WorldLabsApiCreds> {
|
||||
let api_key = std::fs::read_to_string(API_KEY_FILE)?.trim().to_string();
|
||||
Ok(WorldLabsApiCreds::new(api_key))
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod get_test_api_key;
|
||||
pub mod setup_test_logging;
|
||||
@@ -0,0 +1,52 @@
|
||||
use chrono::Local;
|
||||
use env_logger::Builder;
|
||||
use log::{debug, error, info, trace, warn, LevelFilter};
|
||||
use std::io::Write;
|
||||
|
||||
const RUST_LOG : &str = "RUST_LOG";
|
||||
|
||||
pub fn setup_test_logging(level: LevelFilter) {
|
||||
let level_str = get_level_str(level);
|
||||
|
||||
println!("Previous log level: {:?}", env::var(RUST_LOG));
|
||||
|
||||
let is_unsafe = env::set_var(RUST_LOG, level_str);
|
||||
|
||||
if is_unsafe.is_none() {
|
||||
println!("Unsafely setting Rust Log Level...");
|
||||
unsafe { std::env::set_var(RUST_LOG, level_str) }
|
||||
}
|
||||
|
||||
println!("Log level: {:?}", env::var(RUST_LOG));
|
||||
|
||||
Builder::new()
|
||||
.is_test(true)
|
||||
.format(|buf, record| {
|
||||
writeln!(buf,
|
||||
"{} [{}] - {}",
|
||||
Local::now().format("%H:%M:%S%.6f"),
|
||||
record.level(),
|
||||
record.args()
|
||||
)
|
||||
})
|
||||
.filter(None, level)
|
||||
.filter_level(level)
|
||||
.init();
|
||||
|
||||
trace!("Test trace log");
|
||||
debug!("Test debug log");
|
||||
info!("Test info log");
|
||||
warn!("Test warn log");
|
||||
error!("Test error log");
|
||||
}
|
||||
|
||||
fn get_level_str(level: LevelFilter) -> &'static str {
|
||||
match level {
|
||||
LevelFilter::Error => "error",
|
||||
LevelFilter::Warn => "warn",
|
||||
LevelFilter::Info => "info",
|
||||
LevelFilter::Debug => "debug",
|
||||
LevelFilter::Trace => "trace",
|
||||
LevelFilter::Off => "off",
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user