mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-28 12:32:51 -04:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b6e074878 | ||
|
|
3a800f5f7a | ||
|
|
1a5bc77ff3 | ||
|
|
fe2969a833 |
Generated
+1
@@ -15020,6 +15020,7 @@ dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.28.0",
|
||||
"tokio",
|
||||
"uuid 1.18.1",
|
||||
"workspace-build-acceleration",
|
||||
|
||||
@@ -33,3 +33,4 @@ filesys.workspace = true
|
||||
env_logger.workspace = true
|
||||
env.workspace = true
|
||||
chrono.workspace = true
|
||||
strum.workspace = true
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod media_asset_id;
|
||||
pub mod operation_id;
|
||||
pub mod world_id;
|
||||
pub mod world_labs_model;
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Type for World IDs.
|
||||
/// These are used to refer to worlds
|
||||
/// These appear to be bare UUIDs.
|
||||
/// Strongly-typed World ID
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WorldObjectId(pub String);
|
||||
|
||||
impl WorldObjectId {
|
||||
pub fn new() -> Self {
|
||||
let uuid = Uuid::new_v4().to_string();
|
||||
Self(uuid)
|
||||
}
|
||||
pub struct WorldId(pub String);
|
||||
|
||||
impl WorldId {
|
||||
pub fn from_str(s: &str) -> Self {
|
||||
Self(s.to_string())
|
||||
}
|
||||
@@ -19,8 +10,4 @@ impl WorldObjectId {
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> String {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/// The available World Labs generation models.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum WorldLabsModel {
|
||||
/// `Marble 0.1-mini` is good for quick drafts
|
||||
/// Generation time: 30-45 seconds
|
||||
/// Cost: 150-330 credits
|
||||
Marble0p1Mini,
|
||||
|
||||
/// `Marble 0.1-plus` is best for final renders
|
||||
/// Generation time: ~5 minutes,
|
||||
/// Cost: 1500-1600 credits
|
||||
Marble0p1Plus,
|
||||
}
|
||||
|
||||
impl WorldLabsModel {
|
||||
/// Returns the official API name string used in HTTP requests.
|
||||
pub fn get_model_api_name_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Marble0p1Mini => "Marble 0.1-mini",
|
||||
Self::Marble0p1Plus => "Marble 0.1-plus",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::api::api_types::world_id::WorldId;
|
||||
use crate::api::requests::delete_world::http_request::RawResponse;
|
||||
use crate::credentials::world_labs_api_creds::WorldLabsApiCreds;
|
||||
use crate::error::filter_world_labs_http_error::filter_world_labs_http_error;
|
||||
@@ -11,12 +12,12 @@ 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 world_id: &'a WorldId,
|
||||
pub request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
pub struct DeleteWorldResponse {
|
||||
pub world_id: String,
|
||||
pub world_id: WorldId,
|
||||
pub deleted: bool,
|
||||
}
|
||||
|
||||
@@ -26,7 +27,7 @@ pub struct DeleteWorldResponse {
|
||||
pub async fn delete_world(args: DeleteWorldArgs<'_>) -> Result<DeleteWorldResponse, WorldLabsError> {
|
||||
let client = Client::new();
|
||||
|
||||
let url = format!("{}/{}", BASE_URL, args.world_id);
|
||||
let url = format!("{}/{}", BASE_URL, args.world_id.as_str());
|
||||
|
||||
debug!("Requesting URL: {}", url);
|
||||
|
||||
@@ -65,7 +66,7 @@ pub async fn delete_world(args: DeleteWorldArgs<'_>) -> Result<DeleteWorldRespon
|
||||
.map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?;
|
||||
|
||||
Ok(DeleteWorldResponse {
|
||||
world_id: raw.world_id,
|
||||
world_id: WorldId(raw.world_id),
|
||||
deleted: raw.deleted,
|
||||
})
|
||||
}
|
||||
@@ -85,15 +86,15 @@ mod tests {
|
||||
let creds = get_test_api_key().unwrap();
|
||||
|
||||
// Use a known world_id to delete
|
||||
let world_id = "REPLACE_WITH_REAL_ID";
|
||||
let world_id = WorldId::from_str("REPLACE_WITH_REAL_ID");
|
||||
|
||||
let response = delete_world(DeleteWorldArgs {
|
||||
creds: &creds,
|
||||
world_id,
|
||||
world_id: &world_id,
|
||||
request_timeout: None,
|
||||
}).await.unwrap();
|
||||
|
||||
println!("World ID: {}", response.world_id);
|
||||
println!("World ID: {}", response.world_id.as_str());
|
||||
println!("Deleted: {}", response.deleted);
|
||||
|
||||
assert_eq!(1, 2);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::api::api_types::operation_id::OperationId;
|
||||
use crate::api::api_types::world_labs_model::WorldLabsModel;
|
||||
use crate::api::requests::generate_world::http_request::{RawRequest, RawResponse, WorldPrompt, Permission};
|
||||
use crate::credentials::world_labs_api_creds::WorldLabsApiCreds;
|
||||
use crate::error::filter_world_labs_http_error::filter_world_labs_http_error;
|
||||
@@ -14,7 +15,7 @@ pub struct GenerateWorldArgs<'a> {
|
||||
pub creds: &'a WorldLabsApiCreds,
|
||||
pub world_prompt: WorldPrompt,
|
||||
pub display_name: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub model: WorldLabsModel,
|
||||
pub seed: Option<u32>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub permission: Option<Permission>,
|
||||
@@ -35,7 +36,7 @@ pub async fn generate_world(args: GenerateWorldArgs<'_>) -> Result<GenerateWorld
|
||||
let payload = RawRequest {
|
||||
world_prompt: args.world_prompt,
|
||||
display_name: args.display_name,
|
||||
model: args.model,
|
||||
model: Some(args.model.get_model_api_name_str().to_string()),
|
||||
seed: args.seed,
|
||||
tags: args.tags,
|
||||
permission: args.permission,
|
||||
@@ -107,7 +108,7 @@ mod tests {
|
||||
disable_recaption: None,
|
||||
},
|
||||
display_name: Some("Test World".to_string()),
|
||||
model: Some("Marble 0.1-mini".to_string()),
|
||||
model: WorldLabsModel::Marble0p1Mini,
|
||||
seed: None,
|
||||
tags: None,
|
||||
permission: None,
|
||||
@@ -138,7 +139,7 @@ mod tests {
|
||||
disable_recaption: None,
|
||||
},
|
||||
display_name: None,
|
||||
model: Some("Marble 0.1-mini".to_string()),
|
||||
model: WorldLabsModel::Marble0p1Mini,
|
||||
seed: None,
|
||||
tags: None,
|
||||
permission: None,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use crate::api::api_types::operation_id::OperationId;
|
||||
use crate::api::requests::get_operation::http_request::RawResponse;
|
||||
use crate::api::api_types::world_id::WorldId;
|
||||
use crate::api::requests::get_operation::http_request::{RawResponse, RawWorld};
|
||||
use crate::credentials::world_labs_api_creds::WorldLabsApiCreds;
|
||||
use crate::error::filter_world_labs_http_error::filter_world_labs_http_error;
|
||||
use crate::error::world_labs_error::WorldLabsError;
|
||||
use crate::error::world_labs_generic_api_error::WorldLabsGenericApiError;
|
||||
use log::{debug, error};
|
||||
use log::{debug, error, warn};
|
||||
use std::time::Duration;
|
||||
use wreq::Client;
|
||||
|
||||
@@ -24,8 +25,8 @@ pub struct GetOperationResponse {
|
||||
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>,
|
||||
/// Structured World object, present when done=true and no error.
|
||||
pub world: Option<World>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -34,10 +35,47 @@ pub struct OperationError {
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
/// A completed World returned from an operation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct World {
|
||||
pub world_id: WorldId,
|
||||
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 assets: Option<WorldAssets>,
|
||||
}
|
||||
|
||||
/// Assets associated with a completed World.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WorldAssets {
|
||||
pub caption: Option<String>,
|
||||
pub thumbnail_url: Option<String>,
|
||||
pub pano_url: Option<String>,
|
||||
pub collider_mesh_url: Option<String>,
|
||||
pub splats: Option<SplatAssets>,
|
||||
}
|
||||
|
||||
/// Splat (3D Gaussian) assets for a World.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SplatAssets {
|
||||
/// Low-res 100k splat URL
|
||||
pub spz_url_100k: Option<String>,
|
||||
/// Medium-res 500k splat URL
|
||||
pub spz_url_500k: Option<String>,
|
||||
/// Full resolution splat URL
|
||||
pub spz_url_full_res: Option<String>,
|
||||
pub metric_scale_factor: Option<f64>,
|
||||
pub ground_plane_offset: Option<f64>,
|
||||
}
|
||||
|
||||
/// GET /marble/v1/operations/{operation_id}
|
||||
///
|
||||
/// Poll the status of an async operation.
|
||||
/// When `done` is true, `response` contains the World object.
|
||||
/// When `done` is true, `world` contains the structured World object.
|
||||
pub async fn get_operation(args: GetOperationArgs<'_>) -> Result<GetOperationResponse, WorldLabsError> {
|
||||
let client = Client::new();
|
||||
|
||||
@@ -79,6 +117,16 @@ pub async fn get_operation(args: GetOperationArgs<'_>) -> Result<GetOperationRes
|
||||
let raw: RawResponse = serde_json::from_str(&response_body)
|
||||
.map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?;
|
||||
|
||||
let world = raw.response.and_then(|raw_world| {
|
||||
match try_parse_world(raw_world) {
|
||||
Some(w) => Some(w),
|
||||
None => {
|
||||
warn!("Operation response present but missing world_id — skipping world parse");
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(GetOperationResponse {
|
||||
operation_id: raw.operation_id,
|
||||
done: raw.done.unwrap_or(false),
|
||||
@@ -90,7 +138,46 @@ pub async fn get_operation(args: GetOperationArgs<'_>) -> Result<GetOperationRes
|
||||
message: e.message,
|
||||
}),
|
||||
metadata: raw.metadata,
|
||||
response: raw.response,
|
||||
world,
|
||||
})
|
||||
}
|
||||
|
||||
/// Try to parse a RawWorld into a strongly-typed World.
|
||||
/// Returns None if world_id is missing (not a valid World).
|
||||
fn try_parse_world(raw: RawWorld) -> Option<World> {
|
||||
let world_id = raw.world_id?;
|
||||
|
||||
let assets = raw.assets.map(|a| {
|
||||
let splats = a.splats.map(|s| {
|
||||
let semantics = s.semantics_metadata;
|
||||
SplatAssets {
|
||||
spz_url_100k: s.spz_urls.as_ref().and_then(|u| u.low.clone()),
|
||||
spz_url_500k: s.spz_urls.as_ref().and_then(|u| u.medium.clone()),
|
||||
spz_url_full_res: s.spz_urls.as_ref().and_then(|u| u.full_res.clone()),
|
||||
metric_scale_factor: semantics.as_ref().and_then(|m| m.metric_scale_factor),
|
||||
ground_plane_offset: semantics.as_ref().and_then(|m| m.ground_plane_offset),
|
||||
}
|
||||
});
|
||||
|
||||
WorldAssets {
|
||||
caption: a.caption,
|
||||
thumbnail_url: a.thumbnail_url,
|
||||
pano_url: a.imagery.and_then(|i| i.pano_url),
|
||||
collider_mesh_url: a.mesh.and_then(|m| m.collider_mesh_url),
|
||||
splats,
|
||||
}
|
||||
});
|
||||
|
||||
Some(World {
|
||||
world_id: WorldId(world_id),
|
||||
display_name: raw.display_name,
|
||||
world_marble_url: raw.world_marble_url,
|
||||
created_at: raw.created_at,
|
||||
updated_at: raw.updated_at,
|
||||
model: raw.model,
|
||||
status: raw.status,
|
||||
tags: raw.tags,
|
||||
assets,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -108,7 +195,6 @@ mod tests {
|
||||
|
||||
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 {
|
||||
@@ -120,9 +206,46 @@ mod tests {
|
||||
println!("Operation ID: {}", response.operation_id);
|
||||
println!("Done: {}", response.done);
|
||||
println!("Created at: {:?}", response.created_at);
|
||||
println!("Updated at: {:?}", response.updated_at);
|
||||
println!("Expires at: {:?}", response.expires_at);
|
||||
println!("Error: {:?}", response.error);
|
||||
println!("Metadata: {:?}", response.metadata);
|
||||
println!("Response: {:?}", response.response);
|
||||
|
||||
match &response.world {
|
||||
None => println!("World: None (not yet complete)"),
|
||||
Some(world) => {
|
||||
println!("World:");
|
||||
println!(" World ID: {}", world.world_id.as_str());
|
||||
println!(" Display name: {:?}", world.display_name);
|
||||
println!(" Marble URL: {:?}", world.world_marble_url);
|
||||
println!(" Created at: {:?}", world.created_at);
|
||||
println!(" Updated at: {:?}", world.updated_at);
|
||||
println!(" Model: {:?}", world.model);
|
||||
println!(" Status: {:?}", world.status);
|
||||
println!(" Tags: {:?}", world.tags);
|
||||
match &world.assets {
|
||||
None => println!(" Assets: None"),
|
||||
Some(assets) => {
|
||||
println!(" Assets:");
|
||||
println!(" Caption: {:?}", assets.caption);
|
||||
println!(" Thumbnail URL: {:?}", assets.thumbnail_url);
|
||||
println!(" Pano URL: {:?}", assets.pano_url);
|
||||
println!(" Collider mesh URL: {:?}", assets.collider_mesh_url);
|
||||
match &assets.splats {
|
||||
None => println!(" Splats: None"),
|
||||
Some(splats) => {
|
||||
println!(" Splats:");
|
||||
println!(" SPZ 100k: {:?}", splats.spz_url_100k);
|
||||
println!(" SPZ 500k: {:?}", splats.spz_url_500k);
|
||||
println!(" SPZ full res: {:?}", splats.spz_url_full_res);
|
||||
println!(" Metric scale factor: {:?}", splats.metric_scale_factor);
|
||||
println!(" Ground plane offset: {:?}", splats.ground_plane_offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(1, 2);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ pub(crate) struct RawResponse {
|
||||
pub expires_at: Option<String>,
|
||||
pub error: Option<RawOperationError>,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
pub response: Option<serde_json::Value>,
|
||||
pub response: Option<RawWorld>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
@@ -17,3 +17,66 @@ pub(crate) struct RawOperationError {
|
||||
pub code: Option<i32>,
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
/// The World object embedded in `response` when the operation completes.
|
||||
/// All fields are optional to tolerate partial/unexpected shapes.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct RawWorld {
|
||||
pub world_id: Option<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 assets: Option<RawWorldAssets>,
|
||||
pub world_prompt: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
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)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct RawImagery {
|
||||
pub pano_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
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)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) struct RawSpzUrls {
|
||||
#[serde(rename = "100k")]
|
||||
pub low: Option<String>,
|
||||
#[serde(rename = "500k")]
|
||||
pub medium: Option<String>,
|
||||
pub full_res: Option<String>,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::api::api_types::world_id::WorldId;
|
||||
use crate::api::requests::get_world::http_request::RawResponse;
|
||||
use crate::credentials::world_labs_api_creds::WorldLabsApiCreds;
|
||||
use crate::error::filter_world_labs_http_error::filter_world_labs_http_error;
|
||||
@@ -11,12 +12,12 @@ 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 world_id: &'a WorldId,
|
||||
pub request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
pub struct GetWorldResponse {
|
||||
pub world_id: String,
|
||||
pub world_id: WorldId,
|
||||
pub display_name: Option<String>,
|
||||
pub world_marble_url: Option<String>,
|
||||
pub created_at: Option<String>,
|
||||
@@ -45,7 +46,7 @@ pub struct WorldAssets {
|
||||
pub async fn get_world(args: GetWorldArgs<'_>) -> Result<GetWorldResponse, WorldLabsError> {
|
||||
let client = Client::new();
|
||||
|
||||
let url = format!("{}/{}", BASE_URL, args.world_id);
|
||||
let url = format!("{}/{}", BASE_URL, args.world_id.as_str());
|
||||
|
||||
debug!("Requesting URL: {}", url);
|
||||
|
||||
@@ -97,7 +98,7 @@ pub async fn get_world(args: GetWorldArgs<'_>) -> Result<GetWorldResponse, World
|
||||
});
|
||||
|
||||
Ok(GetWorldResponse {
|
||||
world_id: raw.world_id,
|
||||
world_id: WorldId(raw.world_id),
|
||||
display_name: raw.display_name,
|
||||
world_marble_url: raw.world_marble_url,
|
||||
created_at: raw.created_at,
|
||||
@@ -125,16 +126,16 @@ mod tests {
|
||||
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 world_id = WorldId::from_str("0048d009-1c7a-4e13-9881-07e9b6ff32e1");
|
||||
//let world_id = WorldId::from_str("c8a89fb1-f8b4-44c6-ae50-525f7205c65f");
|
||||
|
||||
let response = get_world(GetWorldArgs {
|
||||
creds: &creds,
|
||||
world_id,
|
||||
world_id: &world_id,
|
||||
request_timeout: None,
|
||||
}).await.unwrap();
|
||||
|
||||
println!("World ID: {}", response.world_id);
|
||||
println!("World ID: {}", response.world_id.as_str());
|
||||
println!("Display name: {:?}", response.display_name);
|
||||
println!("Marble URL: {:?}", response.world_marble_url);
|
||||
println!("Created at: {:?}", response.created_at);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::api::api_types::world_id::WorldId;
|
||||
use crate::api::requests::list_worlds::http_request::{RawRequest, RawResponse};
|
||||
use crate::credentials::world_labs_api_creds::WorldLabsApiCreds;
|
||||
use crate::error::filter_world_labs_http_error::filter_world_labs_http_error;
|
||||
@@ -36,7 +37,7 @@ pub struct ListWorldsResponse {
|
||||
}
|
||||
|
||||
pub struct WorldSummary {
|
||||
pub world_id: String,
|
||||
pub world_id: WorldId,
|
||||
pub display_name: Option<String>,
|
||||
pub world_marble_url: Option<String>,
|
||||
pub created_at: Option<String>,
|
||||
@@ -104,7 +105,7 @@ pub async fn list_worlds(args: ListWorldsArgs<'_>) -> Result<ListWorldsResponse,
|
||||
.map_err(|err| WorldLabsGenericApiError::SerdeResponseParseErrorWithBody(err, response_body.to_string()))?;
|
||||
|
||||
let worlds = raw.worlds.into_iter().map(|w| WorldSummary {
|
||||
world_id: w.world_id,
|
||||
world_id: WorldId(w.world_id),
|
||||
display_name: w.display_name,
|
||||
world_marble_url: w.world_marble_url,
|
||||
created_at: w.created_at,
|
||||
@@ -154,7 +155,7 @@ mod tests {
|
||||
|
||||
for (i, world) in response.worlds.iter().enumerate() {
|
||||
println!("--- World {} ---", i + 1);
|
||||
println!(" World ID: {}", world.world_id);
|
||||
println!(" World ID: {}", world.world_id.as_str());
|
||||
println!(" Display name: {:?}", world.display_name);
|
||||
println!(" Marble URL: {:?}", world.world_marble_url);
|
||||
println!(" Created at: {:?}", world.created_at);
|
||||
|
||||
@@ -5,4 +5,5 @@ pub (crate) mod test_utils;
|
||||
pub mod api;
|
||||
pub mod credentials;
|
||||
pub mod error;
|
||||
pub mod pricing;
|
||||
pub mod recipes;
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
use crate::api::api_types::world_labs_model::WorldLabsModel;
|
||||
|
||||
/// The type of input used to generate a world.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[cfg_attr(test, derive(strum::EnumIter, strum::Display))]
|
||||
pub enum InputType {
|
||||
Text,
|
||||
ImageNonPanorama,
|
||||
ImagePanorama,
|
||||
MultiImage,
|
||||
Video,
|
||||
}
|
||||
|
||||
/// Cost breakdown for a World Labs generation request.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct WorldLabsCost {
|
||||
pub worldlabs_credits: u32,
|
||||
pub us_dollar_cents: u32,
|
||||
}
|
||||
|
||||
/// Calculate the cost of a World Labs generation request.
|
||||
pub fn calculate_cost(model: WorldLabsModel, input_type: InputType) -> WorldLabsCost {
|
||||
let worldlabs_credits = credits_for(model, input_type);
|
||||
let us_dollar_cents = credits_to_us_dollar_cents(worldlabs_credits);
|
||||
WorldLabsCost { worldlabs_credits, us_dollar_cents }
|
||||
}
|
||||
|
||||
/// Returns the credit cost for a given model and input type,
|
||||
/// based on the official pricing table at https://docs.worldlabs.ai/api/pricing
|
||||
fn credits_for(model: WorldLabsModel, input_type: InputType) -> u32 {
|
||||
match (model, input_type) {
|
||||
// Marble 0.1-mini
|
||||
(WorldLabsModel::Marble0p1Mini, InputType::ImagePanorama) => 150,
|
||||
(WorldLabsModel::Marble0p1Mini, InputType::Text) => 230,
|
||||
(WorldLabsModel::Marble0p1Mini, InputType::ImageNonPanorama) => 230,
|
||||
(WorldLabsModel::Marble0p1Mini, InputType::MultiImage) => 250,
|
||||
(WorldLabsModel::Marble0p1Mini, InputType::Video) => 250,
|
||||
|
||||
// Marble 0.1-plus
|
||||
(WorldLabsModel::Marble0p1Plus, InputType::ImagePanorama) => 1500,
|
||||
(WorldLabsModel::Marble0p1Plus, InputType::Text) => 1580,
|
||||
(WorldLabsModel::Marble0p1Plus, InputType::ImageNonPanorama) => 1580,
|
||||
(WorldLabsModel::Marble0p1Plus, InputType::MultiImage) => 1600,
|
||||
(WorldLabsModel::Marble0p1Plus, InputType::Video) => 1600,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts credits to US dollar cents.
|
||||
/// Rate: $1.00 USD per 1,250 credits → 100 cents per 1,250 credits.
|
||||
fn credits_to_us_dollar_cents(credits: u32) -> u32 {
|
||||
// (credits * 100) / 1250 = (credits * 4) / 50 = (credits * 2) / 25
|
||||
// Use u64 to avoid overflow on large values.
|
||||
((credits as u64 * 100) / 1250) as u32
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
#[test]
|
||||
fn test_pricing_table() {
|
||||
let models = [
|
||||
WorldLabsModel::Marble0p1Mini,
|
||||
WorldLabsModel::Marble0p1Plus,
|
||||
];
|
||||
|
||||
println!("\n{:<20} {:<20} {:>10} {:>12}", "Model", "Input Type", "Credits", "USD Cents");
|
||||
println!("{}", "-".repeat(65));
|
||||
|
||||
for model in &models {
|
||||
for input_type in InputType::iter() {
|
||||
let cost = calculate_cost(*model, input_type);
|
||||
println!(
|
||||
"{:<20} {:<20} {:>10} {:>12}",
|
||||
model.get_model_api_name_str(),
|
||||
input_type,
|
||||
cost.worldlabs_credits,
|
||||
cost.us_dollar_cents,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mini_credits() {
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Mini, InputType::Text).worldlabs_credits, 230);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Mini, InputType::ImageNonPanorama).worldlabs_credits, 230);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Mini, InputType::ImagePanorama).worldlabs_credits, 150);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Mini, InputType::MultiImage).worldlabs_credits, 250);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Mini, InputType::Video).worldlabs_credits, 250);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plus_credits() {
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Plus, InputType::Text).worldlabs_credits, 1580);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Plus, InputType::ImageNonPanorama).worldlabs_credits, 1580);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Plus, InputType::ImagePanorama).worldlabs_credits, 1500);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Plus, InputType::MultiImage).worldlabs_credits, 1600);
|
||||
assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Plus, InputType::Video).worldlabs_credits, 1600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dollar_conversion() {
|
||||
// 1250 credits = $1.00 = 100 cents
|
||||
assert_eq!(credits_to_us_dollar_cents(1250), 100);
|
||||
// 150 credits = $0.12 = 12 cents
|
||||
assert_eq!(credits_to_us_dollar_cents(150), 12);
|
||||
// 230 credits = $0.184 = 18 cents (truncated)
|
||||
assert_eq!(credits_to_us_dollar_cents(230), 18);
|
||||
// 1580 credits = $1.264 = 126 cents (truncated)
|
||||
assert_eq!(credits_to_us_dollar_cents(1580), 126);
|
||||
// 1600 credits = $1.28 = 128 cents
|
||||
assert_eq!(credits_to_us_dollar_cents(1600), 128);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_combinations_have_costs() {
|
||||
let models = [
|
||||
WorldLabsModel::Marble0p1Mini,
|
||||
WorldLabsModel::Marble0p1Plus,
|
||||
];
|
||||
|
||||
for model in &models {
|
||||
for input_type in InputType::iter() {
|
||||
let cost = calculate_cost(*model, input_type);
|
||||
assert!(cost.worldlabs_credits > 0, "credits should be > 0 for {:?} {:?}", model, input_type);
|
||||
assert!(cost.us_dollar_cents > 0, "dollar cents should be > 0 for {:?} {:?}", model, input_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod check_pricing;
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
use crate::api::api_types::media_asset_id::MediaAssetId;
|
||||
use crate::api::api_types::operation_id::OperationId;
|
||||
use crate::api::api_types::world_labs_model::WorldLabsModel;
|
||||
use crate::api::requests::generate_world::generate_world::{generate_world, GenerateWorldArgs};
|
||||
use crate::api::requests::generate_world::http_request::{ContentReference, WorldPrompt};
|
||||
use crate::api::requests::prepare_upload::prepare_upload::{prepare_upload, PrepareUploadArgs, MediaAssetKind};
|
||||
@@ -16,7 +17,7 @@ pub struct UploadImageAndCreateWorldArgs<'a> {
|
||||
pub creds: &'a WorldLabsApiCreds,
|
||||
pub file: FileBytesOrPath,
|
||||
pub text_prompt: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub model: WorldLabsModel,
|
||||
pub individual_request_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
@@ -145,7 +146,7 @@ mod tests {
|
||||
individual_request_timeout: None,
|
||||
file: FileBytesOrPath::Bytes(file_bytes),
|
||||
text_prompt: None,
|
||||
model: None,
|
||||
model: WorldLabsModel::Marble0p1Plus,
|
||||
}).await.unwrap();
|
||||
|
||||
println!("Operation ID: {}", results.operation_id.as_str());
|
||||
|
||||
Reference in New Issue
Block a user