worldlabs marble 1.1 client library (#1379)

This commit is contained in:
Brandon Thomas authored and GitHub committed 2026-04-15 22:01:13 -04:00
1 parent d51c001835
commit 49e49efa3c
6 files changed
+364 -2

No files matched your search

Generated
+1
View File
@@ -14793,6 +14793,7 @@ dependencies = [
"serde",
"serde_json",
"strum",
"test_data",
"tokio",
"uuid 1.22.0",
"workspace-build-acceleration",
@@ -34,3 +34,4 @@ env_logger.workspace = true
env.workspace = true
chrono.workspace = true
strum.workspace = true
test_data.workspace = true
@@ -4,12 +4,26 @@ pub enum WorldLabsModel {
/// `Marble 0.1-mini` is good for quick drafts
/// Generation time: 30-45 seconds
/// Cost: 150-330 credits
#[deprecated(note="Marble 0.1-mini is deprecated. Use `marble-1.0-draft` instead.")]
Marble0p1Mini,
/// `Marble 0.1-plus` is best for final renders
/// Generation time: ~5 minutes,
/// Cost: 1500-1600 credits
#[deprecated(note="Marble 0.1-plus is deprecated. Use `marble-1.0` instead.")]
Marble0p1Plus,
/// marble-1.0
Marble1p0,
/// marble-1.0-draft
Marble1p0Draft,
/// marble-1.1
Marble1p1,
/// marble-1.1-plus
Marble1p1Plus,
}
impl WorldLabsModel {
@@ -18,6 +32,159 @@ impl WorldLabsModel {
match self {
Self::Marble0p1Mini => "Marble 0.1-mini",
Self::Marble0p1Plus => "Marble 0.1-plus",
Self::Marble1p0 => "marble-1.0",
Self::Marble1p0Draft => "marble-1.0-draft",
Self::Marble1p1 => "marble-1.1",
Self::Marble1p1Plus => "marble-1.1-plus",
}
}
pub fn is_deprecated(&self) -> bool {
match self {
Self::Marble0p1Mini | Self::Marble0p1Plus => true,
_ => false,
}
}
pub fn to_new_value(self) -> Self {
match self {
Self::Marble0p1Mini => Self::Marble1p0Draft,
Self::Marble0p1Plus => Self::Marble1p0,
_ => self,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
mod api_name {
use super::*;
#[allow(deprecated)]
#[test]
fn marble_0p1_mini() {
assert_eq!(WorldLabsModel::Marble0p1Mini.get_model_api_name_str(), "Marble 0.1-mini");
}
#[allow(deprecated)]
#[test]
fn marble_0p1_plus() {
assert_eq!(WorldLabsModel::Marble0p1Plus.get_model_api_name_str(), "Marble 0.1-plus");
}
#[test]
fn marble_1p0() {
assert_eq!(WorldLabsModel::Marble1p0.get_model_api_name_str(), "marble-1.0");
}
#[test]
fn marble_1p0_draft() {
assert_eq!(WorldLabsModel::Marble1p0Draft.get_model_api_name_str(), "marble-1.0-draft");
}
#[test]
fn marble_1p1() {
assert_eq!(WorldLabsModel::Marble1p1.get_model_api_name_str(), "marble-1.1");
}
#[test]
fn marble_1p1_plus() {
assert_eq!(WorldLabsModel::Marble1p1Plus.get_model_api_name_str(), "marble-1.1-plus");
}
}
mod is_deprecated {
use super::*;
#[allow(deprecated)]
#[test]
fn marble_0p1_mini_is_deprecated() {
assert!(WorldLabsModel::Marble0p1Mini.is_deprecated());
}
#[allow(deprecated)]
#[test]
fn marble_0p1_plus_is_deprecated() {
assert!(WorldLabsModel::Marble0p1Plus.is_deprecated());
}
#[test]
fn marble_1p0_is_not_deprecated() {
assert!(!WorldLabsModel::Marble1p0.is_deprecated());
}
#[test]
fn marble_1p0_draft_is_not_deprecated() {
assert!(!WorldLabsModel::Marble1p0Draft.is_deprecated());
}
#[test]
fn marble_1p1_is_not_deprecated() {
assert!(!WorldLabsModel::Marble1p1.is_deprecated());
}
#[test]
fn marble_1p1_plus_is_not_deprecated() {
assert!(!WorldLabsModel::Marble1p1Plus.is_deprecated());
}
}
mod to_new_value {
use super::*;
#[allow(deprecated)]
#[test]
fn marble_0p1_mini_maps_to_1p0_draft() {
let new = WorldLabsModel::Marble0p1Mini.to_new_value();
assert_eq!(new.get_model_api_name_str(), "marble-1.0-draft");
assert!(!new.is_deprecated());
}
#[allow(deprecated)]
#[test]
fn marble_0p1_plus_maps_to_1p0() {
let new = WorldLabsModel::Marble0p1Plus.to_new_value();
assert_eq!(new.get_model_api_name_str(), "marble-1.0");
assert!(!new.is_deprecated());
}
#[test]
fn marble_1p0_stays_as_1p0() {
let new = WorldLabsModel::Marble1p0.to_new_value();
assert_eq!(new.get_model_api_name_str(), "marble-1.0");
}
#[test]
fn marble_1p0_draft_stays_as_1p0_draft() {
let new = WorldLabsModel::Marble1p0Draft.to_new_value();
assert_eq!(new.get_model_api_name_str(), "marble-1.0-draft");
}
#[test]
fn marble_1p1_stays_as_1p1() {
let new = WorldLabsModel::Marble1p1.to_new_value();
assert_eq!(new.get_model_api_name_str(), "marble-1.1");
}
#[test]
fn marble_1p1_plus_stays_as_1p1_plus() {
let new = WorldLabsModel::Marble1p1Plus.to_new_value();
assert_eq!(new.get_model_api_name_str(), "marble-1.1-plus");
}
#[test]
fn new_models_are_not_deprecated_after_conversion() {
for model in [
WorldLabsModel::Marble1p0,
WorldLabsModel::Marble1p0Draft,
WorldLabsModel::Marble1p1,
WorldLabsModel::Marble1p1Plus,
] {
let new = model.to_new_value();
assert!(!new.is_deprecated(), "{:?} should not be deprecated after to_new_value", model);
}
}
}
}
@@ -33,10 +33,15 @@ pub struct GenerateWorldResponse {
pub async fn generate_world(args: GenerateWorldArgs<'_>) -> Result<GenerateWorldResponse, WorldLabsError> {
let client = Client::new();
let model_name = args.model
.to_new_value()
.get_model_api_name_str()
.to_string();
let payload = RawRequest {
world_prompt: args.world_prompt,
display_name: args.display_name,
model: Some(args.model.get_model_api_name_str().to_string()),
model: Some(model_name),
seed: args.seed,
tags: args.tags,
permission: args.permission,
@@ -347,4 +352,98 @@ mod tests {
assert_eq!(1, 2, "Inspect output above");
}
/// Marble 1.1: generate world from a mountain/tree image via URI.
#[tokio::test]
#[ignore]
async fn test_generate_world_marble_1p1_image_uri() {
setup_test_logging(LevelFilter::Debug);
let creds = get_test_api_key().unwrap();
let image_url = test_data::web::image_urls::MOUNTAIN_TREE_IMAGE_URL;
println!("=== generate_world with Marble 1.1 ===");
println!("Image URL: {}", image_url);
let result = generate_world(GenerateWorldArgs {
creds: &creds,
world_prompt: WorldPrompt::Image {
image_prompt: ContentReference::Uri {
uri: image_url.to_string(),
},
text_prompt: Some("Mountain landscape with a tree".to_string()),
is_pano: None,
disable_recaption: None,
},
display_name: None,
model: WorldLabsModel::Marble1p1,
seed: None,
tags: None,
permission: None,
request_timeout: None,
}).await;
match &result {
Ok(r) => {
println!("SUCCESS — Marble 1.1");
println!(" Operation ID: {}", r.operation_id.as_str());
println!(" Done: {}", r.done);
}
Err(err) => {
println!("ERROR — Marble 1.1");
println!(" Error (Debug): {:?}", err);
println!(" Error (Display): {}", err);
}
}
assert_eq!(1, 2, "Inspect output above");
}
/// Marble 1.1-plus: generate world from a wide fall mountains image via URI.
#[tokio::test]
#[ignore]
async fn test_generate_world_marble_1p1_plus_image_uri() {
setup_test_logging(LevelFilter::Debug);
let creds = get_test_api_key().unwrap();
let image_url = test_data::web::image_urls::SUPER_WIDE_FALL_MOUNTAINS_IMAGE_URL;
println!("=== generate_world with Marble 1.1-plus ===");
println!("Image URL: {}", image_url);
let result = generate_world(GenerateWorldArgs {
creds: &creds,
world_prompt: WorldPrompt::Image {
image_prompt: ContentReference::Uri {
uri: image_url.to_string(),
},
text_prompt: Some("Autumn mountain panorama".to_string()),
is_pano: None,
disable_recaption: None,
},
display_name: None,
model: WorldLabsModel::Marble1p1Plus,
seed: None,
tags: None,
permission: None,
request_timeout: None,
}).await;
match &result {
Ok(r) => {
println!("SUCCESS — Marble 1.1-plus");
println!(" Operation ID: {}", r.operation_id.as_str());
println!(" Done: {}", r.done);
}
Err(err) => {
println!("ERROR — Marble 1.1-plus");
println!(" Error (Debug): {:?}", err);
println!(" Error (Display): {}", err);
}
}
assert_eq!(1, 2, "Inspect output above");
}
}
@@ -195,7 +195,8 @@ mod tests {
let creds = get_test_api_key().unwrap();
let operation_id = OperationId("1fab3bf1-05a1-4929-907e-c6df07c539e2".to_string());
//let operation_id = OperationId("1fab3bf1-05a1-4929-907e-c6df07c539e2".to_string());
let operation_id = OperationId("a0894fa6-594d-4680-8f3a-5109f22664b1".to_string());
let response = get_operation(GetOperationArgs {
creds: &creds,
@@ -42,6 +42,38 @@ fn credits_for(model: WorldLabsModel, input_type: InputType) -> u32 {
(WorldLabsModel::Marble0p1Plus, InputType::ImageNonPanorama) => 1580,
(WorldLabsModel::Marble0p1Plus, InputType::MultiImage) => 1600,
(WorldLabsModel::Marble0p1Plus, InputType::Video) => 1600,
// Marble 1.0 (successor to 0.1-plus)
(WorldLabsModel::Marble1p0, InputType::ImagePanorama) => 1500,
(WorldLabsModel::Marble1p0, InputType::Text) => 1580,
(WorldLabsModel::Marble1p0, InputType::ImageNonPanorama) => 1580,
(WorldLabsModel::Marble1p0, InputType::MultiImage) => 1600,
(WorldLabsModel::Marble1p0, InputType::Video) => 1600,
// Marble 1.0-draft (successor to 0.1-mini)
(WorldLabsModel::Marble1p0Draft, InputType::ImagePanorama) => 150,
(WorldLabsModel::Marble1p0Draft, InputType::Text) => 230,
(WorldLabsModel::Marble1p0Draft, InputType::ImageNonPanorama) => 230,
(WorldLabsModel::Marble1p0Draft, InputType::MultiImage) => 250,
(WorldLabsModel::Marble1p0Draft, InputType::Video) => 250,
// Marble 1.1
(WorldLabsModel::Marble1p1, InputType::ImagePanorama) => 1500,
(WorldLabsModel::Marble1p1, InputType::Text) => 1580,
(WorldLabsModel::Marble1p1, InputType::ImageNonPanorama) => 1580,
(WorldLabsModel::Marble1p1, InputType::MultiImage) => 1600,
(WorldLabsModel::Marble1p1, InputType::Video) => 1600,
// TODO: Looks like there's no way for us to know what the price will be
// for us to bill this accurately, we'll have to bill this after the fact.
// also - they don't have an API for us to ascertain this??! Wat.
// Marble 1.1-plus (range pricing — we use the max to charge enough upfront)
// Actual range: 1500–3000 / 1580–3080 / 1600–3100
(WorldLabsModel::Marble1p1Plus, InputType::ImagePanorama) => 3000,
(WorldLabsModel::Marble1p1Plus, InputType::Text) => 3080,
(WorldLabsModel::Marble1p1Plus, InputType::ImageNonPanorama) => 3080,
(WorldLabsModel::Marble1p1Plus, InputType::MultiImage) => 3100,
(WorldLabsModel::Marble1p1Plus, InputType::Video) => 3100,
}
}
@@ -64,6 +96,10 @@ mod tests {
let models = [
WorldLabsModel::Marble0p1Mini,
WorldLabsModel::Marble0p1Plus,
WorldLabsModel::Marble1p0,
WorldLabsModel::Marble1p0Draft,
WorldLabsModel::Marble1p1,
WorldLabsModel::Marble1p1Plus,
];
println!("\n{:<20} {:<20} {:>10} {:>12}", "Model", "Input Type", "Credits", "USD Cents");
@@ -101,6 +137,59 @@ mod tests {
assert_eq!(calculate_cost(WorldLabsModel::Marble0p1Plus, InputType::Video).worldlabs_credits, 1600);
}
#[test]
fn test_marble_1p0_draft_credits() {
// Same as Marble 0.1-mini (its successor)
assert_eq!(calculate_cost(WorldLabsModel::Marble1p0Draft, InputType::Text).worldlabs_credits, 230);
assert_eq!(calculate_cost(WorldLabsModel::Marble1p0Draft, InputType::ImageNonPanorama).worldlabs_credits, 230);
assert_eq!(calculate_cost(WorldLabsModel::Marble1p0Draft, InputType::ImagePanorama).worldlabs_credits, 150);
assert_eq!(calculate_cost(WorldLabsModel::Marble1p0Draft, InputType::MultiImage).worldlabs_credits, 250);
assert_eq!(calculate_cost(WorldLabsModel::Marble1p0Draft, InputType::Video).worldlabs_credits, 250);
}
#[test]
fn test_marble_1p0_credits() {
// Standard model, same pricing as Marble 0.1-plus
assert_eq!(calculate_cost(WorldLabsModel::Marble1p0, InputType::Text).worldlabs_credits, 1580);
assert_eq!(calculate_cost(WorldLabsModel::Marble1p0, InputType::ImageNonPanorama).worldlabs_credits, 1580);
assert_eq!(calculate_cost(WorldLabsModel::Marble1p0, InputType::ImagePanorama).worldlabs_credits, 1500);
assert_eq!(calculate_cost(WorldLabsModel::Marble1p0, InputType::MultiImage).worldlabs_credits, 1600);
assert_eq!(calculate_cost(WorldLabsModel::Marble1p0, InputType::Video).worldlabs_credits, 1600);
}
#[test]
fn test_marble_1p1_credits() {
// Standard model, same pricing tier as 1.0
assert_eq!(calculate_cost(WorldLabsModel::Marble1p1, InputType::Text).worldlabs_credits, 1580);
assert_eq!(calculate_cost(WorldLabsModel::Marble1p1, InputType::ImageNonPanorama).worldlabs_credits, 1580);
assert_eq!(calculate_cost(WorldLabsModel::Marble1p1, InputType::ImagePanorama).worldlabs_credits, 1500);
assert_eq!(calculate_cost(WorldLabsModel::Marble1p1, InputType::MultiImage).worldlabs_credits, 1600);
assert_eq!(calculate_cost(WorldLabsModel::Marble1p1, InputType::Video).worldlabs_credits, 1600);
}
#[test]
fn test_marble_1p1_plus_credits() {
// Plus model uses max of the range pricing (1500–3000, 1580–3080, 1600–3100)
assert_eq!(calculate_cost(WorldLabsModel::Marble1p1Plus, InputType::Text).worldlabs_credits, 3080);
assert_eq!(calculate_cost(WorldLabsModel::Marble1p1Plus, InputType::ImageNonPanorama).worldlabs_credits, 3080);
assert_eq!(calculate_cost(WorldLabsModel::Marble1p1Plus, InputType::ImagePanorama).worldlabs_credits, 3000);
assert_eq!(calculate_cost(WorldLabsModel::Marble1p1Plus, InputType::MultiImage).worldlabs_credits, 3100);
assert_eq!(calculate_cost(WorldLabsModel::Marble1p1Plus, InputType::Video).worldlabs_credits, 3100);
}
#[test]
fn test_marble_1p1_plus_is_more_expensive_than_standard() {
for input_type in InputType::iter() {
let standard = calculate_cost(WorldLabsModel::Marble1p1, input_type);
let plus = calculate_cost(WorldLabsModel::Marble1p1Plus, input_type);
assert!(
plus.worldlabs_credits > standard.worldlabs_credits,
"1.1-plus should cost more than 1.1 for {:?}: {} vs {}",
input_type, plus.worldlabs_credits, standard.worldlabs_credits,
);
}
}
#[test]
fn test_dollar_conversion() {
// 1250 credits = $1.00 = 100 cents
@@ -120,6 +209,10 @@ mod tests {
let models = [
WorldLabsModel::Marble0p1Mini,
WorldLabsModel::Marble0p1Plus,
WorldLabsModel::Marble1p0,
WorldLabsModel::Marble1p0Draft,
WorldLabsModel::Marble1p1,
WorldLabsModel::Marble1p1Plus,
];
for model in &models {