From b94ce8943a2bfe4e3911fd54dbd3808a6c85b70b Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Mon, 18 May 2026 09:33:33 -0400 Subject: [PATCH] preview model --- .../generate_video_request_builder.rs | 7 + .../providers/artcraft/mod.rs | 2 + .../providers/artcraft/preview_model/build.rs | 344 ++++++++++++++++ .../providers/artcraft/preview_model/cost.rs | 233 +++++++++++ .../providers/artcraft/preview_model/mod.rs | 3 + .../artcraft/preview_model/request.rs | 32 ++ .../artcraft/preview_model_fast/build.rs | 367 ++++++++++++++++++ .../artcraft/preview_model_fast/cost.rs | 203 ++++++++++ .../artcraft/preview_model_fast/mod.rs | 3 + .../artcraft/preview_model_fast/request.rs | 32 ++ .../video_generation_request.rs | 18 + 11 files changed, 1244 insertions(+) create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/request.rs diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request_builder.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request_builder.rs index 8f8245f33f..c32eb76728 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request_builder.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/generate_video_request_builder.rs @@ -48,6 +48,8 @@ use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_see use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0_fast::plan_generate_video_seedance2pro_seedance2p0_fast; use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan; use crate::generate::generate_video_v2::providers::artcraft::happy_horse_1p0::build::build_artcraft_happy_horse_1p0; +use crate::generate::generate_video_v2::providers::artcraft::preview_model::build::build_artcraft_preview_model; +use crate::generate::generate_video_v2::providers::artcraft::preview_model_fast::build::build_artcraft_preview_model_fast; use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0::build::build_artcraft_seedance_2p0; use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_fast::build::build_artcraft_seedance_2p0_fast; use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_g::build::build_artcraft_seedance_2p0_g; @@ -153,7 +155,10 @@ impl GenerateVideoRequestBuilder { (Provider::Artcraft, CommonVideoModel::Seedance2p0Fast) => true, (Provider::Artcraft, CommonVideoModel::Seedance2p0Global) => true, (Provider::Artcraft, CommonVideoModel::Seedance2p0FastGlobal) => true, + (Provider::Artcraft, CommonVideoModel::PreviewModel) => true, + (Provider::Artcraft, CommonVideoModel::PreviewModelFast) => true, (Provider::GmiCloud, CommonVideoModel::Seedance2p0Global) => true, + (Provider::GmiCloud, CommonVideoModel::Seedance2p0FastGlobal) => true, (Provider::Seedance2Pro, CommonVideoModel::HappyHorse1p0) => true, (Provider::Seedance2Pro, CommonVideoModel::Seedance2p0) => true, @@ -170,6 +175,8 @@ impl GenerateVideoRequestBuilder { (Provider::Artcraft, CommonVideoModel::Seedance2p0Fast) => build_artcraft_seedance_2p0_fast(self), (Provider::Artcraft, CommonVideoModel::Seedance2p0Global) => build_artcraft_seedance_2p0_g(self), (Provider::Artcraft, CommonVideoModel::Seedance2p0FastGlobal) => build_artcraft_seedance_2p0_fast_g(self), + (Provider::Artcraft, CommonVideoModel::PreviewModel) => build_artcraft_preview_model(self), + (Provider::Artcraft, CommonVideoModel::PreviewModelFast) => build_artcraft_preview_model_fast(self), (Provider::GmiCloud, CommonVideoModel::Seedance2p0Global) => build_gmicloud_seedance_2p0_g(self), (Provider::GmiCloud, CommonVideoModel::Seedance2p0FastGlobal) => build_gmicloud_seedance_2p0_fast_g(self), (Provider::Seedance2Pro, CommonVideoModel::HappyHorse1p0) => build_kinovi_happy_horse_1p0(self), diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/mod.rs index 72c963e5a9..35de4da111 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/mod.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/mod.rs @@ -1,4 +1,6 @@ pub mod happy_horse_1p0; +pub mod preview_model; +pub mod preview_model_fast; pub(crate) mod resolve; pub mod seedance_2p0; pub mod seedance_2p0_fast; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/build.rs new file mode 100644 index 0000000000..e8b555aaef --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/build.rs @@ -0,0 +1,344 @@ +use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest; +use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum; +use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum; +use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum; + +use crate::api::common_aspect_ratio::CommonAspectRatio; +use crate::api::common_resolution::CommonResolution; +use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::client_error::ClientError; +use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; +use crate::generate::generate_video_v2::providers::artcraft::resolve::{ + resolve_audio_list_ref, resolve_character_list_ref, resolve_image_list_ref, + resolve_image_ref, resolve_video_list_ref, +}; +use crate::generate::generate_video_v2::providers::artcraft::preview_model::request::ArtcraftPreviewModelRequestState; +use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; +use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; + +pub fn build_artcraft_preview_model(mut builder: GenerateVideoRequestBuilder) -> Result { + let strategy = builder.request_mismatch_mitigation_strategy; + + let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio.take(), strategy)?; + let resolution = plan_output_resolution(builder.resolution.take(), strategy)?; + let batch_count = plan_batch_count(builder.video_batch_count.take(), strategy)?; + let duration_seconds = plan_duration(builder.duration_seconds.take(), strategy)?; + let prompt = builder.prompt.take(); + + let start_frame = resolve_image_ref(builder.start_frame.take())?; + let end_frame = resolve_image_ref(builder.end_frame.take())?; + let reference_images = resolve_image_list_ref(builder.reference_images.take())?; + let reference_videos = resolve_video_list_ref(builder.reference_videos.take())?; + let reference_audio = resolve_audio_list_ref(builder.reference_audio.take())?; + let reference_characters = resolve_character_list_ref(builder.reference_character_tokens.take()); + let idempotency_token = builder.get_or_generate_idempotency_token(); + + let request = OmniGenVideoCostAndGenerateRequest { + model: Some(CommonVideoModelEnum::PreviewModel), + idempotency_token: Some(idempotency_token), + prompt, + start_frame_image_media_token: start_frame, + end_frame_image_media_token: end_frame, + reference_image_media_tokens: reference_images, + reference_video_media_tokens: reference_videos, + reference_audio_media_tokens: reference_audio, + reference_character_tokens: reference_characters, + resolution, + aspect_ratio, + duration_seconds: duration_seconds.map(|d| d as u16), + video_batch_count: Some(batch_count), + negative_prompt: None, + generate_audio: None, + quality: None, + }; + + let state = ArtcraftPreviewModelRequestState { request }; + Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftPreviewModel(state))) +} + +// -- Plan helpers -- + +fn plan_aspect_ratio( + aspect_ratio: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + match aspect_ratio { + None + | Some(CommonAspectRatio::Auto) + | Some(CommonAspectRatio::Auto2k) + | Some(CommonAspectRatio::Auto4k) => Ok(None), + + Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => { + Ok(Some(CommonAspectRatioEnum::WideSixteenByNine)) + } + Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => { + Ok(Some(CommonAspectRatioEnum::TallNineBySixteen)) + } + Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => { + Ok(Some(CommonAspectRatioEnum::Square)) + } + Some(CommonAspectRatio::WideFourByThree) => Ok(Some(CommonAspectRatioEnum::WideFourByThree)), + Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(CommonAspectRatioEnum::TallThreeByFour)), + Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(CommonAspectRatioEnum::WideTwentyOneByNine)), + + Some(unsupported) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "aspect_ratio", + value: format!("{:?}", unsupported), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade + | RequestMismatchMitigationStrategy::PayLessDowngrade => { + Ok(Some(nearest_aspect_ratio(unsupported))) + } + }, + } +} + +fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> CommonAspectRatioEnum { + match aspect_ratio { + CommonAspectRatio::WideFiveByFour => CommonAspectRatioEnum::WideFourByThree, + CommonAspectRatio::WideThreeByTwo => CommonAspectRatioEnum::WideFourByThree, + CommonAspectRatio::TallFourByFive => CommonAspectRatioEnum::TallThreeByFour, + CommonAspectRatio::TallTwoByThree => CommonAspectRatioEnum::TallThreeByFour, + CommonAspectRatio::TallNineByTwentyOne => CommonAspectRatioEnum::TallNineBySixteen, + _ => CommonAspectRatioEnum::Square, + } +} + +// PreviewModel supports output resolutions: 480p, 720p, 1080p. +fn plan_output_resolution( + resolution: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + match resolution { + None => Ok(None), + + Some(CommonResolution::FourEightyP) => Ok(Some(CommonResolutionEnum::FourEightyP)), + Some(CommonResolution::SevenTwentyP) => Ok(Some(CommonResolutionEnum::SevenTwentyP)), + Some(CommonResolution::TenEightyP) => Ok(Some(CommonResolutionEnum::TenEightyP)), + + Some(unsupported) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "resolution", + value: format!("{:?}", unsupported), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => { + Ok(Some(match unsupported { + CommonResolution::HalfK => CommonResolutionEnum::FourEightyP, + _ => CommonResolutionEnum::TenEightyP, + })) + } + RequestMismatchMitigationStrategy::PayLessDowngrade => { + Ok(Some(match unsupported { + CommonResolution::HalfK => CommonResolutionEnum::FourEightyP, + _ => CommonResolutionEnum::TenEightyP, + })) + } + }, + } +} + +// Batch counts: 1, 2, 4. +fn plan_batch_count( + video_batch_count: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + let count = video_batch_count.unwrap_or(1); + match count { + 0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)), + 1 | 2 | 4 => Ok(count), + _ => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "video_batch_count", + value: format!("{}", count), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(4), + RequestMismatchMitigationStrategy::PayLessDowngrade => { + Ok(if count < 4 { 2 } else { 4 }) + } + }, + } +} + +// Duration: 4-15 seconds. +fn plan_duration( + duration_seconds: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + const MIN: u16 = 4; + const MAX: u16 = 15; + match duration_seconds { + None => Ok(None), + Some(d) if d >= MIN && d <= MAX => Ok(Some(d as u8)), + Some(d) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "duration_seconds", + value: format!("{}", d), + })) + } + _ => Ok(Some(d.clamp(MIN, MAX) as u8)), + }, + } +} + +#[cfg(test)] +mod tests { + use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum; + use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum; + use tokens::tokens::characters::CharacterToken; + use tokens::tokens::media_files::MediaFileToken; + + use crate::api::character_list_ref::CharacterListRef; + use crate::api::common_resolution::CommonResolution; + use crate::api::image_list_ref::ImageListRef; + use crate::api::image_ref::ImageRef; + use crate::api::provider::Provider; + use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; + use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; + use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; + + use super::*; + + // ── Field conversions ── + + mod field_conversions { + use super::*; + + #[test] + fn model_is_preview_model() { + let req = unwrap_request(make_builder(|_| {})); + assert!(matches!(req.request.model, Some(CommonVideoModelEnum::PreviewModel))); + } + + #[test] + fn prompt_passed_through() { + let req = unwrap_request(make_builder(|b| { b.prompt = Some("test".to_string()); })); + assert_eq!(req.request.prompt, Some("test".to_string())); + } + + #[test] + fn duration_passed_through() { + let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(10); })); + assert_eq!(req.request.duration_seconds, Some(10)); + } + + #[test] + fn duration_clamped_to_max() { + let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(99); })); + assert_eq!(req.request.duration_seconds, Some(15)); + } + + #[test] + fn batch_count_passed_through() { + let req = unwrap_request(make_builder(|b| { b.video_batch_count = Some(4); })); + assert_eq!(req.request.video_batch_count, Some(4)); + } + } + + // ── Resolution ── + + mod resolution_tests { + use super::*; + + #[test] + fn res_480p() { + let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::FourEightyP); })); + assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourEightyP)); + } + + #[test] + fn res_720p() { + let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::SevenTwentyP); })); + assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP)); + } + + #[test] + fn res_1080p() { + let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::TenEightyP); })); + assert_eq!(req.request.resolution, Some(CommonResolutionEnum::TenEightyP)); + } + + #[test] + fn none_stays_none() { + let req = unwrap_request(make_builder(|_| {})); + assert!(req.request.resolution.is_none()); + } + } + + // ── Media tokens ── + + mod media_token_tests { + use super::*; + + #[test] + fn start_frame_token_passed_through() { + let token = MediaFileToken::new("mf_start".to_string()); + let req = unwrap_request(make_builder(|b| { + b.start_frame = Some(ImageRef::MediaFileToken(token.clone())); + })); + assert_eq!(req.request.start_frame_image_media_token, Some(token)); + } + + #[test] + fn url_start_frame_rejected() { + let result = build_artcraft_preview_model(GenerateVideoRequestBuilder { + start_frame: Some(ImageRef::Url("https://example.com".to_string())), + ..base_builder() + }); + assert!(result.is_err()); + } + + #[test] + fn reference_image_tokens_passed_through() { + let tokens = vec![MediaFileToken::new("mf_a".to_string()), MediaFileToken::new("mf_b".to_string())]; + let req = unwrap_request(make_builder(|b| { + b.reference_images = Some(ImageListRef::MediaFileTokens(tokens.clone())); + })); + assert_eq!(req.request.reference_image_media_tokens, Some(tokens)); + } + + #[test] + fn character_tokens_passed_through() { + let tokens = vec![CharacterToken::new("char_a".to_string()), CharacterToken::new("char_b".to_string())]; + let req = unwrap_request(make_builder(|b| { + b.reference_character_tokens = Some(CharacterListRef::CharacterTokens(tokens.clone())); + })); + assert_eq!(req.request.reference_character_tokens, Some(tokens)); + } + } + + // ── Helpers ── + + fn base_builder() -> GenerateVideoRequestBuilder { + GenerateVideoRequestBuilder { + provider: Provider::Artcraft, + duration_seconds: Some(5), + video_batch_count: Some(1), + ..Default::default() + } + } + + fn make_builder(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder { + let mut builder = base_builder(); + f(&mut builder); + builder + } + + fn unwrap_request(builder: GenerateVideoRequestBuilder) -> ArtcraftPreviewModelRequestState { + let result = build_artcraft_preview_model(builder).expect("build should succeed"); + match result { + VideoGenerationDraftOrRequest::Request( + VideoGenerationRequest::ArtcraftPreviewModel(state) + ) => state, + _ => panic!("expected ArtcraftPreviewModel request"), + } + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/cost.rs new file mode 100644 index 0000000000..56ad1865b4 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/cost.rs @@ -0,0 +1,233 @@ +use enums::common::generation::common_resolution::CommonResolution; + +use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; +use crate::generate::generate_video_v2::providers::artcraft::preview_model::request::ArtcraftPreviewModelRequestState; + +// ── Pricing constants ── +// +// ArtCraft credits: 100 credits = $1.00. Credits always equal USD cents. + +/// USD cents per second by resolution: +/// 480p: $0.10/s = 10.0 ¢/s +/// 720p: $0.25/s = 25.0 ¢/s +/// 1080p: $0.50/s = 50.0 ¢/s +const CENTS_PER_SECOND_480P: f64 = 10.0; +const CENTS_PER_SECOND_720P: f64 = 25.0; +const CENTS_PER_SECOND_1080P: f64 = 50.0; + +pub struct ArtcraftPreviewModelCostState { + pub resolution: CommonResolution, + pub duration_seconds: u16, + pub batch_count: u16, +} + +impl ArtcraftPreviewModelCostState { + pub fn from_request(request: &ArtcraftPreviewModelRequestState) -> Self { + let resolution = request.request.resolution + .unwrap_or(CommonResolution::SevenTwentyP); + let duration_seconds = request.request.duration_seconds.unwrap_or(5); + let batch_count = request.request.video_batch_count.unwrap_or(1); + + Self { resolution, duration_seconds, batch_count } + } + + pub fn estimate_cost(&self) -> VideoGenerationCostEstimate { + let cents_per_second = match self.resolution { + CommonResolution::FourEightyP => CENTS_PER_SECOND_480P, + CommonResolution::TenEightyP => CENTS_PER_SECOND_1080P, + // Everything else (including 720p) prices at 720p. + _ => CENTS_PER_SECOND_720P, + }; + + let usd_cents = (self.duration_seconds as f64 * cents_per_second * self.batch_count as f64).round() as u64; + + VideoGenerationCostEstimate { + cost_in_credits: Some(usd_cents), + cost_in_usd_cents: Some(usd_cents), + is_free: false, + is_unlimited: false, + is_rate_limited: false, + has_watermark: false, + failures_are_refunded: None, + } + } +} + +#[cfg(test)] +mod tests { + use crate::api::common_resolution::CommonResolution; + use crate::api::common_video_model::CommonVideoModel; + use crate::api::provider::Provider; + use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; + + // -- 720p pricing -- + + mod pricing_720p { + use super::*; + + #[test] + fn batch_1() { + assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 4, 1), 100); + assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1), 125); + assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 10, 1), 250); + assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 15, 1), 375); + } + + #[test] + fn batch_2() { + assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 2), 250); + assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 15, 2), 750); + } + + #[test] + fn batch_4() { + assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 4), 500); + assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 15, 4), 1500); + } + + #[test] + fn none_defaults_to_720p() { + assert_eq!(cost_cents(None, 5, 1), cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1)); + } + } + + // -- 480p pricing -- + + mod pricing_480p { + use super::*; + + #[test] + fn batch_1() { + assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 4, 1), 40); + assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5, 1), 50); + assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 10, 1), 100); + assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 15, 1), 150); + } + + #[test] + fn batch_2() { + assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5, 2), 100); + } + + #[test] + fn batch_4() { + assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5, 4), 200); + } + } + + // -- 1080p pricing -- + + mod pricing_1080p { + use super::*; + + #[test] + fn batch_1() { + assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 4, 1), 200); + assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 5, 1), 250); + assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 10, 1), 500); + assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 15, 1), 750); + } + + #[test] + fn batch_2() { + assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 5, 2), 500); + } + + #[test] + fn batch_4() { + assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 5, 4), 1000); + } + } + + // -- Relative pricing -- + + mod relative_pricing_tests { + use super::*; + + #[test] + fn cost_480p_cheaper_than_720p_cheaper_than_1080p() { + let c480 = cost_cents(Some(CommonResolution::FourEightyP), 5, 1); + let c720 = cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1); + let c1080 = cost_cents(Some(CommonResolution::TenEightyP), 5, 1); + assert!(c480 < c720); + assert!(c720 < c1080); + } + + #[test] + fn cost_scales_with_duration() { + let c4 = cost_cents(Some(CommonResolution::SevenTwentyP), 4, 1); + let c10 = cost_cents(Some(CommonResolution::SevenTwentyP), 10, 1); + let c15 = cost_cents(Some(CommonResolution::SevenTwentyP), 15, 1); + assert!(c4 < c10); + assert!(c10 < c15); + } + + #[test] + fn cost_scales_with_batch() { + let b1 = cost_cents(Some(CommonResolution::TenEightyP), 5, 1); + let b2 = cost_cents(Some(CommonResolution::TenEightyP), 5, 2); + let b4 = cost_cents(Some(CommonResolution::TenEightyP), 5, 4); + assert!(b1 < b2); + assert!(b2 < b4); + } + } + + // -- Credits equal cents -- + + mod credits_tests { + use super::*; + + #[test] + fn credits_equal_usd_cents_all_combos() { + let resolutions = [ + Some(CommonResolution::FourEightyP), + Some(CommonResolution::SevenTwentyP), + Some(CommonResolution::TenEightyP), + None, + ]; + for res in resolutions { + for dur in [4, 5, 10, 15] { + for batch in [1, 2, 4] { + let cost = build_cost(res, dur, batch); + assert_eq!( + cost.cost_in_credits, cost.cost_in_usd_cents, + "credits should equal cents for res={:?} dur={}s batch={}", + res, dur, batch, + ); + } + } + } + } + } + + // -- Helpers -- + + fn build_cost( + resolution: Option, + duration_seconds: u16, + video_batch_count: u16, + ) -> crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate { + let builder = GenerateVideoRequestBuilder { + model: CommonVideoModel::PreviewModel, + provider: Provider::Artcraft, + resolution, + duration_seconds: Some(duration_seconds), + video_batch_count: Some(video_batch_count), + ..Default::default() + }; + builder.build2() + .expect("build2 should succeed") + .estimate_cost() + .expect("estimate_cost should succeed") + } + + fn cost_cents( + resolution: Option, + duration_seconds: u16, + video_batch_count: u16, + ) -> u64 { + build_cost(resolution, duration_seconds, video_batch_count) + .cost_in_usd_cents + .unwrap() + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/request.rs new file mode 100644 index 0000000000..ebf5a64e67 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/request.rs @@ -0,0 +1,32 @@ +use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest; +use artcraft_client::endpoints::omni_gen::generate::video::omni_gen_video::omni_gen_video_generate; + +use crate::client::router_artcraft_client::RouterArtcraftClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::provider_error::ProviderError; +use crate::generate::generate_video::generate_video_response::{ + ArtcraftVideoResponsePayload, GenerateVideoResponse, +}; + +#[derive(Clone, Debug)] +pub struct ArtcraftPreviewModelRequestState { + /// Final materialized request; ready to fire via the omni-gen video endpoint. + pub request: OmniGenVideoCostAndGenerateRequest, +} + +impl ArtcraftPreviewModelRequestState { + pub async fn send(&self, client: &RouterArtcraftClient) -> Result { + let response = omni_gen_video_generate( + &client.api_host, + Some(&client.credentials), + self.request.clone(), + ) + .await + .map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?; + + Ok(GenerateVideoResponse::Artcraft(ArtcraftVideoResponsePayload { + inference_job_token: response.inference_job_token.clone(), + all_inference_job_tokens: vec![response.inference_job_token], + })) + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/build.rs new file mode 100644 index 0000000000..36d9537342 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/build.rs @@ -0,0 +1,367 @@ +use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest; +use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum; +use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum; +use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum; + +use crate::api::common_aspect_ratio::CommonAspectRatio; +use crate::api::common_resolution::CommonResolution; +use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::client_error::ClientError; +use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; +use crate::generate::generate_video_v2::providers::artcraft::resolve::{ + resolve_audio_list_ref, resolve_character_list_ref, resolve_image_list_ref, + resolve_image_ref, resolve_video_list_ref, +}; +use crate::generate::generate_video_v2::providers::artcraft::preview_model_fast::request::ArtcraftPreviewModelFastRequestState; +use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; +use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; + +pub fn build_artcraft_preview_model_fast(mut builder: GenerateVideoRequestBuilder) -> Result { + let strategy = builder.request_mismatch_mitigation_strategy; + + let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio.take(), strategy)?; + let resolution = plan_output_resolution(builder.resolution.take(), strategy)?; + let batch_count = plan_batch_count(builder.video_batch_count.take(), strategy)?; + let duration_seconds = plan_duration(builder.duration_seconds.take(), strategy)?; + let prompt = builder.prompt.take(); + + let start_frame = resolve_image_ref(builder.start_frame.take())?; + let end_frame = resolve_image_ref(builder.end_frame.take())?; + let reference_images = resolve_image_list_ref(builder.reference_images.take())?; + let reference_videos = resolve_video_list_ref(builder.reference_videos.take())?; + let reference_audio = resolve_audio_list_ref(builder.reference_audio.take())?; + let reference_characters = resolve_character_list_ref(builder.reference_character_tokens.take()); + let idempotency_token = builder.get_or_generate_idempotency_token(); + + let request = OmniGenVideoCostAndGenerateRequest { + model: Some(CommonVideoModelEnum::PreviewModelFast), + idempotency_token: Some(idempotency_token), + prompt, + start_frame_image_media_token: start_frame, + end_frame_image_media_token: end_frame, + reference_image_media_tokens: reference_images, + reference_video_media_tokens: reference_videos, + reference_audio_media_tokens: reference_audio, + reference_character_tokens: reference_characters, + resolution, + aspect_ratio, + duration_seconds: duration_seconds.map(|d| d as u16), + video_batch_count: Some(batch_count), + negative_prompt: None, + generate_audio: None, + quality: None, + }; + + let state = ArtcraftPreviewModelFastRequestState { request }; + Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftPreviewModelFast(state))) +} + +// -- Plan helpers -- + +fn plan_aspect_ratio( + aspect_ratio: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + match aspect_ratio { + None + | Some(CommonAspectRatio::Auto) + | Some(CommonAspectRatio::Auto2k) + | Some(CommonAspectRatio::Auto4k) => Ok(None), + + Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => { + Ok(Some(CommonAspectRatioEnum::WideSixteenByNine)) + } + Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => { + Ok(Some(CommonAspectRatioEnum::TallNineBySixteen)) + } + Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => { + Ok(Some(CommonAspectRatioEnum::Square)) + } + Some(CommonAspectRatio::WideFourByThree) => Ok(Some(CommonAspectRatioEnum::WideFourByThree)), + Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(CommonAspectRatioEnum::TallThreeByFour)), + Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(CommonAspectRatioEnum::WideTwentyOneByNine)), + + Some(unsupported) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "aspect_ratio", + value: format!("{:?}", unsupported), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade + | RequestMismatchMitigationStrategy::PayLessDowngrade => { + Ok(Some(nearest_aspect_ratio(unsupported))) + } + }, + } +} + +fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> CommonAspectRatioEnum { + match aspect_ratio { + CommonAspectRatio::WideFiveByFour => CommonAspectRatioEnum::WideFourByThree, + CommonAspectRatio::WideThreeByTwo => CommonAspectRatioEnum::WideFourByThree, + CommonAspectRatio::TallFourByFive => CommonAspectRatioEnum::TallThreeByFour, + CommonAspectRatio::TallTwoByThree => CommonAspectRatioEnum::TallThreeByFour, + CommonAspectRatio::TallNineByTwentyOne => CommonAspectRatioEnum::TallNineBySixteen, + _ => CommonAspectRatioEnum::Square, + } +} + +// PreviewModelFast supports output resolutions: 480p and 720p only. +fn plan_output_resolution( + resolution: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + match resolution { + None => Ok(None), + + Some(CommonResolution::FourEightyP) => Ok(Some(CommonResolutionEnum::FourEightyP)), + Some(CommonResolution::SevenTwentyP) => Ok(Some(CommonResolutionEnum::SevenTwentyP)), + + // 1080p not supported for Fast + Some(CommonResolution::TenEightyP) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "resolution", + value: format!("{:?}", CommonResolution::TenEightyP), + })) + } + _ => Ok(Some(CommonResolutionEnum::SevenTwentyP)), + }, + + Some(unsupported) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "resolution", + value: format!("{:?}", unsupported), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => { + Ok(Some(match unsupported { + CommonResolution::HalfK => CommonResolutionEnum::FourEightyP, + _ => CommonResolutionEnum::SevenTwentyP, + })) + } + RequestMismatchMitigationStrategy::PayLessDowngrade => { + Ok(Some(match unsupported { + CommonResolution::HalfK => CommonResolutionEnum::FourEightyP, + _ => CommonResolutionEnum::SevenTwentyP, + })) + } + }, + } +} + +// Batch counts: 1, 2, 4. +fn plan_batch_count( + video_batch_count: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + let count = video_batch_count.unwrap_or(1); + match count { + 0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)), + 1 | 2 | 4 => Ok(count), + _ => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "video_batch_count", + value: format!("{}", count), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(4), + RequestMismatchMitigationStrategy::PayLessDowngrade => { + Ok(if count < 4 { 2 } else { 4 }) + } + }, + } +} + +// Duration: 4-15 seconds. +fn plan_duration( + duration_seconds: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + const MIN: u16 = 4; + const MAX: u16 = 15; + match duration_seconds { + None => Ok(None), + Some(d) if d >= MIN && d <= MAX => Ok(Some(d as u8)), + Some(d) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "duration_seconds", + value: format!("{}", d), + })) + } + _ => Ok(Some(d.clamp(MIN, MAX) as u8)), + }, + } +} + +#[cfg(test)] +mod tests { + use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum; + use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum; + use tokens::tokens::characters::CharacterToken; + use tokens::tokens::media_files::MediaFileToken; + + use crate::api::character_list_ref::CharacterListRef; + use crate::api::common_resolution::CommonResolution; + use crate::api::common_video_model::CommonVideoModel; + use crate::api::image_list_ref::ImageListRef; + use crate::api::image_ref::ImageRef; + use crate::api::provider::Provider; + use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; + use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; + use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; + use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; + + use super::*; + + // ── Field conversions ── + + mod field_conversions { + use super::*; + + #[test] + fn model_is_preview_model_fast() { + let req = unwrap_request(make_builder(|_| {})); + assert!(matches!(req.request.model, Some(CommonVideoModelEnum::PreviewModelFast))); + } + + #[test] + fn prompt_passed_through() { + let req = unwrap_request(make_builder(|b| { b.prompt = Some("test".to_string()); })); + assert_eq!(req.request.prompt, Some("test".to_string())); + } + + #[test] + fn duration_passed_through() { + let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(10); })); + assert_eq!(req.request.duration_seconds, Some(10)); + } + + #[test] + fn duration_clamped_to_max() { + let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(99); })); + assert_eq!(req.request.duration_seconds, Some(15)); + } + + #[test] + fn batch_count_passed_through() { + let req = unwrap_request(make_builder(|b| { b.video_batch_count = Some(4); })); + assert_eq!(req.request.video_batch_count, Some(4)); + } + } + + // ── Resolution ── + + mod resolution_tests { + use super::*; + + #[test] + fn res_480p() { + let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::FourEightyP); })); + assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourEightyP)); + } + + #[test] + fn res_720p() { + let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::SevenTwentyP); })); + assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP)); + } + + #[test] + fn res_1080p_downgrades_to_720p() { + let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::TenEightyP); })); + assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP)); + } + + #[test] + fn res_1080p_error_out() { + let result = build_artcraft_preview_model_fast(GenerateVideoRequestBuilder { + model: CommonVideoModel::PreviewModelFast, + provider: Provider::Artcraft, + resolution: Some(CommonResolution::TenEightyP), + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, + ..Default::default() + }); + assert!(result.is_err()); + } + + #[test] + fn none_stays_none() { + let req = unwrap_request(make_builder(|_| {})); + assert!(req.request.resolution.is_none()); + } + } + + // ── Media tokens ── + + mod media_token_tests { + use super::*; + + #[test] + fn start_frame_token_passed_through() { + let token = MediaFileToken::new("mf_start".to_string()); + let req = unwrap_request(make_builder(|b| { + b.start_frame = Some(ImageRef::MediaFileToken(token.clone())); + })); + assert_eq!(req.request.start_frame_image_media_token, Some(token)); + } + + #[test] + fn url_start_frame_rejected() { + let result = build_artcraft_preview_model_fast(GenerateVideoRequestBuilder { + model: CommonVideoModel::PreviewModelFast, + provider: Provider::Artcraft, + start_frame: Some(ImageRef::Url("https://example.com".to_string())), + ..Default::default() + }); + assert!(result.is_err()); + } + + #[test] + fn reference_image_tokens_passed_through() { + let tokens = vec![MediaFileToken::new("mf_a".to_string()), MediaFileToken::new("mf_b".to_string())]; + let req = unwrap_request(make_builder(|b| { + b.reference_images = Some(ImageListRef::MediaFileTokens(tokens.clone())); + })); + assert_eq!(req.request.reference_image_media_tokens, Some(tokens)); + } + + #[test] + fn character_tokens_passed_through() { + let tokens = vec![CharacterToken::new("char_a".to_string()), CharacterToken::new("char_b".to_string())]; + let req = unwrap_request(make_builder(|b| { + b.reference_character_tokens = Some(CharacterListRef::CharacterTokens(tokens.clone())); + })); + assert_eq!(req.request.reference_character_tokens, Some(tokens)); + } + } + + // ── Helpers ── + + fn make_builder(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder { + let mut builder = GenerateVideoRequestBuilder { + model: CommonVideoModel::PreviewModelFast, + provider: Provider::Artcraft, + duration_seconds: Some(5), + video_batch_count: Some(1), + ..Default::default() + }; + f(&mut builder); + builder + } + + fn unwrap_request(builder: GenerateVideoRequestBuilder) -> ArtcraftPreviewModelFastRequestState { + let result = build_artcraft_preview_model_fast(builder).expect("build should succeed"); + match result { + VideoGenerationDraftOrRequest::Request( + VideoGenerationRequest::ArtcraftPreviewModelFast(state) + ) => state, + _ => panic!("expected ArtcraftPreviewModelFast request"), + } + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/cost.rs new file mode 100644 index 0000000000..89116f7ad2 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/cost.rs @@ -0,0 +1,203 @@ +use enums::common::generation::common_resolution::CommonResolution; + +use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; +use crate::generate::generate_video_v2::providers::artcraft::preview_model_fast::request::ArtcraftPreviewModelFastRequestState; + +// ── Pricing constants ── +// +// ArtCraft credits: 100 credits = $1.00. Credits always equal USD cents. + +/// USD cents per second by resolution: +/// 480p: $0.09/s = 9.0 ¢/s +/// 720p: $0.20/s = 20.0 ¢/s +const CENTS_PER_SECOND_480P: f64 = 9.0; +const CENTS_PER_SECOND_720P: f64 = 20.0; + +pub struct ArtcraftPreviewModelFastCostState { + pub resolution: CommonResolution, + pub duration_seconds: u16, + pub batch_count: u16, +} + +impl ArtcraftPreviewModelFastCostState { + pub fn from_request(request: &ArtcraftPreviewModelFastRequestState) -> Self { + let resolution = request.request.resolution + .unwrap_or(CommonResolution::SevenTwentyP); + let duration_seconds = request.request.duration_seconds.unwrap_or(5); + let batch_count = request.request.video_batch_count.unwrap_or(1); + + Self { resolution, duration_seconds, batch_count } + } + + pub fn estimate_cost(&self) -> VideoGenerationCostEstimate { + let cents_per_second = match self.resolution { + CommonResolution::FourEightyP => CENTS_PER_SECOND_480P, + // Everything else (including 720p and unsupported resolutions) prices at 720p. + _ => CENTS_PER_SECOND_720P, + }; + + let usd_cents = (self.duration_seconds as f64 * cents_per_second * self.batch_count as f64).round() as u64; + + VideoGenerationCostEstimate { + cost_in_credits: Some(usd_cents), + cost_in_usd_cents: Some(usd_cents), + is_free: false, + is_unlimited: false, + is_rate_limited: false, + has_watermark: false, + failures_are_refunded: None, + } + } +} + +#[cfg(test)] +mod tests { + use crate::api::common_resolution::CommonResolution; + use crate::api::common_video_model::CommonVideoModel; + use crate::api::provider::Provider; + use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; + + // -- 720p pricing -- + + mod pricing_720p { + use super::*; + + #[test] + fn batch_1() { + assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 4, 1), 80); + assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1), 100); + assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 10, 1), 200); + assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 15, 1), 300); + } + + #[test] + fn batch_2() { + assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 2), 200); + assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 15, 2), 600); + } + + #[test] + fn batch_4() { + assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 4), 400); + assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 15, 4), 1200); + } + + #[test] + fn none_defaults_to_720p() { + assert_eq!(cost_cents(None, 5, 1), cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1)); + } + } + + // -- 480p pricing -- + + mod pricing_480p { + use super::*; + + #[test] + fn batch_1() { + assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 4, 1), 36); + assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5, 1), 45); + assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 10, 1), 90); + assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 15, 1), 135); + } + + #[test] + fn batch_2() { + assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5, 2), 90); + } + + #[test] + fn batch_4() { + assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5, 4), 180); + } + } + + // -- Relative pricing -- + + mod relative_pricing_tests { + use super::*; + + #[test] + fn cost_480p_cheaper_than_720p() { + let c480 = cost_cents(Some(CommonResolution::FourEightyP), 5, 1); + let c720 = cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1); + assert!(c480 < c720); + } + + #[test] + fn cost_scales_with_duration() { + let c4 = cost_cents(Some(CommonResolution::SevenTwentyP), 4, 1); + let c10 = cost_cents(Some(CommonResolution::SevenTwentyP), 10, 1); + let c15 = cost_cents(Some(CommonResolution::SevenTwentyP), 15, 1); + assert!(c4 < c10); + assert!(c10 < c15); + } + + #[test] + fn cost_scales_with_batch() { + let b1 = cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1); + let b2 = cost_cents(Some(CommonResolution::SevenTwentyP), 5, 2); + let b4 = cost_cents(Some(CommonResolution::SevenTwentyP), 5, 4); + assert!(b1 < b2); + assert!(b2 < b4); + } + } + + // -- Credits equal cents -- + + mod credits_tests { + use super::*; + + #[test] + fn credits_equal_usd_cents_all_combos() { + let resolutions = [ + Some(CommonResolution::FourEightyP), + Some(CommonResolution::SevenTwentyP), + None, + ]; + for res in resolutions { + for dur in [4, 5, 10, 15] { + for batch in [1, 2, 4] { + let cost = build_cost(res, dur, batch); + assert_eq!( + cost.cost_in_credits, cost.cost_in_usd_cents, + "credits should equal cents for res={:?} dur={}s batch={}", + res, dur, batch, + ); + } + } + } + } + } + + // -- Helpers -- + + fn build_cost( + resolution: Option, + duration_seconds: u16, + video_batch_count: u16, + ) -> crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate { + let builder = GenerateVideoRequestBuilder { + model: CommonVideoModel::PreviewModelFast, + provider: Provider::Artcraft, + resolution, + duration_seconds: Some(duration_seconds), + video_batch_count: Some(video_batch_count), + ..Default::default() + }; + builder.build2() + .expect("build2 should succeed") + .estimate_cost() + .expect("estimate_cost should succeed") + } + + fn cost_cents( + resolution: Option, + duration_seconds: u16, + video_batch_count: u16, + ) -> u64 { + build_cost(resolution, duration_seconds, video_batch_count) + .cost_in_usd_cents + .unwrap() + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/request.rs new file mode 100644 index 0000000000..6bfd7a90fa --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/request.rs @@ -0,0 +1,32 @@ +use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest; +use artcraft_client::endpoints::omni_gen::generate::video::omni_gen_video::omni_gen_video_generate; + +use crate::client::router_artcraft_client::RouterArtcraftClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::provider_error::ProviderError; +use crate::generate::generate_video::generate_video_response::{ + ArtcraftVideoResponsePayload, GenerateVideoResponse, +}; + +#[derive(Clone, Debug)] +pub struct ArtcraftPreviewModelFastRequestState { + /// Final materialized request; ready to fire via the omni-gen video endpoint. + pub request: OmniGenVideoCostAndGenerateRequest, +} + +impl ArtcraftPreviewModelFastRequestState { + pub async fn send(&self, client: &RouterArtcraftClient) -> Result { + let response = omni_gen_video_generate( + &client.api_host, + Some(&client.credentials), + self.request.clone(), + ) + .await + .map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?; + + Ok(GenerateVideoResponse::Artcraft(ArtcraftVideoResponsePayload { + inference_job_token: response.inference_job_token.clone(), + all_inference_job_tokens: vec![response.inference_job_token], + })) + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/video_generation_request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/video_generation_request.rs index 6e23ba3bc5..37283c785f 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/video_generation_request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/video_generation_request.rs @@ -5,6 +5,10 @@ use crate::generate::generate_video::generate_video_response::GenerateVideoRespo use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; use crate::generate::generate_video_v2::providers::artcraft::happy_horse_1p0::cost::ArtcraftHappyHorse1p0CostState; use crate::generate::generate_video_v2::providers::artcraft::happy_horse_1p0::request::ArtcraftHappyHorse1p0RequestState; +use crate::generate::generate_video_v2::providers::artcraft::preview_model::cost::ArtcraftPreviewModelCostState; +use crate::generate::generate_video_v2::providers::artcraft::preview_model::request::ArtcraftPreviewModelRequestState; +use crate::generate::generate_video_v2::providers::artcraft::preview_model_fast::cost::ArtcraftPreviewModelFastCostState; +use crate::generate::generate_video_v2::providers::artcraft::preview_model_fast::request::ArtcraftPreviewModelFastRequestState; use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0::cost::ArtcraftSeedance2p0CostState; use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0::request::ArtcraftSeedance2p0RequestState; use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_fast::cost::ArtcraftSeedance2p0FastCostState; @@ -27,6 +31,8 @@ use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0_fast::re #[derive(Clone, Debug)] pub enum VideoGenerationRequest { ArtcraftHappyHorse1p0(ArtcraftHappyHorse1p0RequestState), + ArtcraftPreviewModel(ArtcraftPreviewModelRequestState), + ArtcraftPreviewModelFast(ArtcraftPreviewModelFastRequestState), ArtcraftSeedance2p0(ArtcraftSeedance2p0RequestState), ArtcraftSeedance2p0Fast(ArtcraftSeedance2p0FastRequestState), ArtcraftSeedance2p0G(ArtcraftSeedance2p0GRequestState), @@ -43,6 +49,8 @@ impl VideoGenerationRequest { pub fn get_provider(&self) -> Provider { match self { Self::ArtcraftHappyHorse1p0(_) => Provider::Artcraft, + Self::ArtcraftPreviewModel(_) => Provider::Artcraft, + Self::ArtcraftPreviewModelFast(_) => Provider::Artcraft, Self::ArtcraftSeedance2p0(_) => Provider::Artcraft, Self::ArtcraftSeedance2p0Fast(_) => Provider::Artcraft, Self::ArtcraftSeedance2p0G(_) => Provider::Artcraft, @@ -59,6 +67,8 @@ impl VideoGenerationRequest { pub fn estimate_cost(&self) -> Result { match self { VideoGenerationRequest::ArtcraftHappyHorse1p0(request) => Ok(ArtcraftHappyHorse1p0CostState::from_request(request).estimate_cost()), + VideoGenerationRequest::ArtcraftPreviewModel(request) => Ok(ArtcraftPreviewModelCostState::from_request(request).estimate_cost()), + VideoGenerationRequest::ArtcraftPreviewModelFast(request) => Ok(ArtcraftPreviewModelFastCostState::from_request(request).estimate_cost()), VideoGenerationRequest::ArtcraftSeedance2p0(request) => Ok(ArtcraftSeedance2p0CostState::from_request(request).estimate_cost()), VideoGenerationRequest::ArtcraftSeedance2p0Fast(request) => Ok(ArtcraftSeedance2p0FastCostState::from_request(request).estimate_cost()), VideoGenerationRequest::ArtcraftSeedance2p0G(request) => Ok(ArtcraftSeedance2p0GCostState::from_request(request).estimate_cost()), @@ -79,6 +89,14 @@ impl VideoGenerationRequest { let client_ref = client.get_artcraft_client_ref()?; request.send(client_ref).await }, + VideoGenerationRequest::ArtcraftPreviewModel(request) => { + let client_ref = client.get_artcraft_client_ref()?; + request.send(client_ref).await + }, + VideoGenerationRequest::ArtcraftPreviewModelFast(request) => { + let client_ref = client.get_artcraft_client_ref()?; + request.send(client_ref).await + }, VideoGenerationRequest::ArtcraftSeedance2p0(request) => { let client_ref = client.get_artcraft_client_ref()?; request.send(client_ref).await