diff --git a/Cargo.lock b/Cargo.lock index a1ba3f7d7a..518bd33e36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14793,6 +14793,7 @@ dependencies = [ "serde", "serde_json", "strum", + "test_data", "tokio", "uuid 1.22.0", "workspace-build-acceleration", diff --git a/crates/api_clients/worldlabs_api_client/Cargo.toml b/crates/api_clients/worldlabs_api_client/Cargo.toml index 8f71b7cc2a..7340fdb19a 100644 --- a/crates/api_clients/worldlabs_api_client/Cargo.toml +++ b/crates/api_clients/worldlabs_api_client/Cargo.toml @@ -34,3 +34,4 @@ env_logger.workspace = true env.workspace = true chrono.workspace = true strum.workspace = true +test_data.workspace = true diff --git a/crates/api_clients/worldlabs_api_client/src/api/api_types/world_labs_model.rs b/crates/api_clients/worldlabs_api_client/src/api/api_types/world_labs_model.rs index 7b7be76ebd..c73fe75e34 100644 --- a/crates/api_clients/worldlabs_api_client/src/api/api_types/world_labs_model.rs +++ b/crates/api_clients/worldlabs_api_client/src/api/api_types/world_labs_model.rs @@ -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); + } } } } diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/generate_world/generate_world.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/generate_world/generate_world.rs index 4e3131b7ce..7c12eaf75c 100644 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/generate_world/generate_world.rs +++ b/crates/api_clients/worldlabs_api_client/src/api/requests/generate_world/generate_world.rs @@ -33,10 +33,15 @@ pub struct GenerateWorldResponse { pub async fn generate_world(args: GenerateWorldArgs<'_>) -> Result { 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"); + } } diff --git a/crates/api_clients/worldlabs_api_client/src/api/requests/get_operation/get_operation.rs b/crates/api_clients/worldlabs_api_client/src/api/requests/get_operation/get_operation.rs index 4c230330e1..6833b1a3d4 100644 --- a/crates/api_clients/worldlabs_api_client/src/api/requests/get_operation/get_operation.rs +++ b/crates/api_clients/worldlabs_api_client/src/api/requests/get_operation/get_operation.rs @@ -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, diff --git a/crates/api_clients/worldlabs_api_client/src/pricing/check_pricing.rs b/crates/api_clients/worldlabs_api_client/src/pricing/check_pricing.rs index 33e072796e..8bb334b363 100644 --- a/crates/api_clients/worldlabs_api_client/src/pricing/check_pricing.rs +++ b/crates/api_clients/worldlabs_api_client/src/pricing/check_pricing.rs @@ -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 {