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 c32eb76728..cdfd56b634 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 @@ -54,6 +54,8 @@ use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0::build 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; use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_fast_g::build::build_artcraft_seedance_2p0_fast_g; +use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_p::build::build_artcraft_seedance_2p0_p; +use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_p_fast::build::build_artcraft_seedance_2p0_p_fast; use crate::generate::generate_video_v2::providers::kinovi::happy_horse_1p0::build::build_kinovi_happy_horse_1p0; use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0::build::build_kinovi_seedance_2p0; use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::build::build_gmicloud_seedance_2p0_g; @@ -150,16 +152,20 @@ impl GenerateVideoRequestBuilder { pub fn use_new_builder(&self) -> bool { match (self.provider, self.model) { + // Artcraft (Provider::Artcraft, CommonVideoModel::HappyHorse1p0) => true, (Provider::Artcraft, CommonVideoModel::Seedance2p0) => true, (Provider::Artcraft, CommonVideoModel::Seedance2p0Fast) => true, (Provider::Artcraft, CommonVideoModel::Seedance2p0Global) => true, (Provider::Artcraft, CommonVideoModel::Seedance2p0FastGlobal) => true, + (Provider::Artcraft, CommonVideoModel::Seedance2p0Plus) => true, + (Provider::Artcraft, CommonVideoModel::Seedance2p0PlusFast) => true, (Provider::Artcraft, CommonVideoModel::PreviewModel) => true, (Provider::Artcraft, CommonVideoModel::PreviewModelFast) => true, + // GmiCloud (Provider::GmiCloud, CommonVideoModel::Seedance2p0Global) => true, - (Provider::GmiCloud, CommonVideoModel::Seedance2p0FastGlobal) => true, + // Kinovi (Provider::Seedance2Pro, CommonVideoModel::HappyHorse1p0) => true, (Provider::Seedance2Pro, CommonVideoModel::Seedance2p0) => true, (Provider::Seedance2Pro, CommonVideoModel::Seedance2p0Fast) => true, @@ -170,15 +176,20 @@ impl GenerateVideoRequestBuilder { // New builder (transitional state) pub fn build2(self) -> Result { match (self.provider, self.model) { + // Artcraft (Provider::Artcraft, CommonVideoModel::HappyHorse1p0) => build_artcraft_happy_horse_1p0(self), (Provider::Artcraft, CommonVideoModel::Seedance2p0) => build_artcraft_seedance_2p0(self), (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::Seedance2p0Plus) => build_artcraft_seedance_2p0_p(self), + (Provider::Artcraft, CommonVideoModel::Seedance2p0PlusFast) => build_artcraft_seedance_2p0_p_fast(self), (Provider::Artcraft, CommonVideoModel::PreviewModel) => build_artcraft_preview_model(self), (Provider::Artcraft, CommonVideoModel::PreviewModelFast) => build_artcraft_preview_model_fast(self), + // GmiCloud (Provider::GmiCloud, CommonVideoModel::Seedance2p0Global) => build_gmicloud_seedance_2p0_g(self), (Provider::GmiCloud, CommonVideoModel::Seedance2p0FastGlobal) => build_gmicloud_seedance_2p0_fast_g(self), + // Kinovi (Provider::Seedance2Pro, CommonVideoModel::HappyHorse1p0) => build_kinovi_happy_horse_1p0(self), (Provider::Seedance2Pro, CommonVideoModel::Seedance2p0) => build_kinovi_seedance_2p0(self), (Provider::Seedance2Pro, CommonVideoModel::Seedance2p0Fast) => build_kinovi_seedance_2p0_fast(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 35de4da111..0a89c7be29 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 @@ -6,3 +6,5 @@ pub mod seedance_2p0; pub mod seedance_2p0_fast; pub mod seedance_2p0_fast_g; pub mod seedance_2p0_g; +pub mod seedance_2p0_p; +pub mod seedance_2p0_p_fast; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_p/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_p/build.rs new file mode 100644 index 0000000000..2e6b80beda --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_p/build.rs @@ -0,0 +1,327 @@ +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::seedance_2p0_p::request::ArtcraftSeedance2p0PlusRequestState; +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_seedance_2p0_p(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::Seedance2p0Plus), + 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 = ArtcraftSeedance2p0PlusRequestState { request }; + Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0Plus(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, + } +} + +// 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 + | RequestMismatchMitigationStrategy::PayLessDowngrade => { + Ok(Some(match unsupported { + CommonResolution::HalfK => CommonResolutionEnum::FourEightyP, + _ => CommonResolutionEnum::TenEightyP, + })) + } + }, + } +} + +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 }) + } + }, + } +} + +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::*; + + mod field_conversions { + use super::*; + + #[test] + fn model_is_seedance_2p0_plus() { + let req = unwrap_request(make_builder(|_| {})); + assert!(matches!(req.request.model, Some(CommonVideoModelEnum::Seedance2p0Plus))); + } + + #[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)); + } + } + + 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()); + } + } + + 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_seedance_2p0_p(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)); + } + } + + 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) -> ArtcraftSeedance2p0PlusRequestState { + let result = build_artcraft_seedance_2p0_p(builder).expect("build should succeed"); + match result { + VideoGenerationDraftOrRequest::Request( + VideoGenerationRequest::ArtcraftSeedance2p0Plus(state) + ) => state, + _ => panic!("expected ArtcraftSeedance2p0Plus request"), + } + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_p/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_p/cost.rs new file mode 100644 index 0000000000..4a911d534a --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_p/cost.rs @@ -0,0 +1,175 @@ +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::seedance_2p0_p::request::ArtcraftSeedance2p0PlusRequestState; + +/// 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 ArtcraftSeedance2p0PlusCostState { + pub resolution: CommonResolution, + pub duration_seconds: u16, + pub batch_count: u16, +} + +impl ArtcraftSeedance2p0PlusCostState { + pub fn from_request(request: &ArtcraftSeedance2p0PlusRequestState) -> 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, + _ => 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; + + 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); + } + + #[test] + fn batch_4() { + assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 4), 500); + } + + #[test] + fn none_defaults_to_720p() { + assert_eq!(cost_cents(None, 5, 1), cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1)); + } + } + + 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); + } + } + + 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); + } + } + + 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); + } + } + + mod credits_tests { + use super::*; + + #[test] + fn credits_equal_usd_cents() { + for res in [Some(CommonResolution::FourEightyP), Some(CommonResolution::SevenTwentyP), Some(CommonResolution::TenEightyP), None] { + 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); + } + } + } + } + } + + fn build_cost( + resolution: Option, + duration_seconds: u16, + video_batch_count: u16, + ) -> crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate { + GenerateVideoRequestBuilder { + model: CommonVideoModel::Seedance2p0Plus, + provider: Provider::Artcraft, + resolution, + duration_seconds: Some(duration_seconds), + video_batch_count: Some(video_batch_count), + ..Default::default() + }.build2().expect("build2").estimate_cost().expect("estimate_cost") + } + + 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/seedance_2p0_p/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_p/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/seedance_2p0_p/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/seedance_2p0_p/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_p/request.rs new file mode 100644 index 0000000000..ead5564dc1 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_p/request.rs @@ -0,0 +1,31 @@ +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 ArtcraftSeedance2p0PlusRequestState { + pub request: OmniGenVideoCostAndGenerateRequest, +} + +impl ArtcraftSeedance2p0PlusRequestState { + 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/seedance_2p0_p_fast/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_p_fast/build.rs new file mode 100644 index 0000000000..8bc036e2cd --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_p_fast/build.rs @@ -0,0 +1,348 @@ +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::seedance_2p0_p_fast::request::ArtcraftSeedance2p0PlusFastRequestState; +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_seedance_2p0_p_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::Seedance2p0PlusFast), + 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 = ArtcraftSeedance2p0PlusFastRequestState { request }; + Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0PlusFast(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, + } +} + +// 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)), + 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 + | RequestMismatchMitigationStrategy::PayLessDowngrade => { + Ok(Some(match unsupported { + CommonResolution::HalfK => CommonResolutionEnum::FourEightyP, + _ => CommonResolutionEnum::SevenTwentyP, + })) + } + }, + } +} + +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 }) + } + }, + } +} + +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::*; + + mod field_conversions { + use super::*; + + #[test] + fn model_is_seedance_2p0_plus_fast() { + let req = unwrap_request(make_builder(|_| {})); + assert!(matches!(req.request.model, Some(CommonVideoModelEnum::Seedance2p0PlusFast))); + } + + #[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)); + } + } + + 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_seedance_2p0_p_fast(GenerateVideoRequestBuilder { + model: CommonVideoModel::Seedance2p0PlusFast, + 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()); + } + } + + 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_seedance_2p0_p_fast(GenerateVideoRequestBuilder { + model: CommonVideoModel::Seedance2p0PlusFast, + 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)); + } + } + + fn make_builder(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder { + let mut builder = GenerateVideoRequestBuilder { + model: CommonVideoModel::Seedance2p0PlusFast, + provider: Provider::Artcraft, + duration_seconds: Some(5), + video_batch_count: Some(1), + ..Default::default() + }; + f(&mut builder); + builder + } + + fn unwrap_request(builder: GenerateVideoRequestBuilder) -> ArtcraftSeedance2p0PlusFastRequestState { + let result = build_artcraft_seedance_2p0_p_fast(builder).expect("build should succeed"); + match result { + VideoGenerationDraftOrRequest::Request( + VideoGenerationRequest::ArtcraftSeedance2p0PlusFast(state) + ) => state, + _ => panic!("expected ArtcraftSeedance2p0PlusFast request"), + } + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_p_fast/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_p_fast/cost.rs new file mode 100644 index 0000000000..a951a24de9 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_p_fast/cost.rs @@ -0,0 +1,146 @@ +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::seedance_2p0_p_fast::request::ArtcraftSeedance2p0PlusFastRequestState; + +/// 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 ArtcraftSeedance2p0PlusFastCostState { + pub resolution: CommonResolution, + pub duration_seconds: u16, + pub batch_count: u16, +} + +impl ArtcraftSeedance2p0PlusFastCostState { + pub fn from_request(request: &ArtcraftSeedance2p0PlusFastRequestState) -> 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, + _ => 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; + + 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); + } + + #[test] + fn batch_4() { + assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 4), 400); + } + + #[test] + fn none_defaults_to_720p() { + assert_eq!(cost_cents(None, 5, 1), cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1)); + } + } + + 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); + } + } + + mod relative_pricing_tests { + use super::*; + + #[test] + fn cost_480p_cheaper_than_720p() { + assert!(cost_cents(Some(CommonResolution::FourEightyP), 5, 1) < cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1)); + } + } + + mod credits_tests { + use super::*; + + #[test] + fn credits_equal_usd_cents() { + for res in [Some(CommonResolution::FourEightyP), Some(CommonResolution::SevenTwentyP), None] { + 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); + } + } + } + } + } + + fn build_cost( + resolution: Option, + duration_seconds: u16, + video_batch_count: u16, + ) -> crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate { + GenerateVideoRequestBuilder { + model: CommonVideoModel::Seedance2p0PlusFast, + provider: Provider::Artcraft, + resolution, + duration_seconds: Some(duration_seconds), + video_batch_count: Some(video_batch_count), + ..Default::default() + }.build2().expect("build2").estimate_cost().expect("estimate_cost") + } + + 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/seedance_2p0_p_fast/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_p_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/seedance_2p0_p_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/seedance_2p0_p_fast/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_p_fast/request.rs new file mode 100644 index 0000000000..54acc15e6a --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_p_fast/request.rs @@ -0,0 +1,31 @@ +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 ArtcraftSeedance2p0PlusFastRequestState { + pub request: OmniGenVideoCostAndGenerateRequest, +} + +impl ArtcraftSeedance2p0PlusFastRequestState { + 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 37283c785f..61f27ab5e6 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 @@ -17,6 +17,10 @@ use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_g::cos use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_g::request::ArtcraftSeedance2p0GRequestState; use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_fast_g::cost::ArtcraftSeedance2p0FastGCostState; use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_fast_g::request::ArtcraftSeedance2p0FastGRequestState; +use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_p::cost::ArtcraftSeedance2p0PlusCostState; +use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_p::request::ArtcraftSeedance2p0PlusRequestState; +use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_p_fast::cost::ArtcraftSeedance2p0PlusFastCostState; +use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_p_fast::request::ArtcraftSeedance2p0PlusFastRequestState; use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::cost::GmiCloudSeedance2p0GCostState; use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::request::GmiCloudSeedance2p0GRequestState; use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::cost::GmiCloudSeedance2p0FastGCostState; @@ -37,6 +41,8 @@ pub enum VideoGenerationRequest { ArtcraftSeedance2p0Fast(ArtcraftSeedance2p0FastRequestState), ArtcraftSeedance2p0G(ArtcraftSeedance2p0GRequestState), ArtcraftSeedance2p0FastG(ArtcraftSeedance2p0FastGRequestState), + ArtcraftSeedance2p0Plus(ArtcraftSeedance2p0PlusRequestState), + ArtcraftSeedance2p0PlusFast(ArtcraftSeedance2p0PlusFastRequestState), GmiCloudSeedance2p0G(GmiCloudSeedance2p0GRequestState), GmiCloudSeedance2p0FastG(GmiCloudSeedance2p0FastGRequestState), KinoviHappyHorse1p0(KinoviHappyHorse1p0RequestState), @@ -55,6 +61,8 @@ impl VideoGenerationRequest { Self::ArtcraftSeedance2p0Fast(_) => Provider::Artcraft, Self::ArtcraftSeedance2p0G(_) => Provider::Artcraft, Self::ArtcraftSeedance2p0FastG(_) => Provider::Artcraft, + Self::ArtcraftSeedance2p0Plus(_) => Provider::Artcraft, + Self::ArtcraftSeedance2p0PlusFast(_) => Provider::Artcraft, Self::GmiCloudSeedance2p0G(_) => Provider::GmiCloud, Self::GmiCloudSeedance2p0FastG(_) => Provider::GmiCloud, Self::KinoviHappyHorse1p0(_) => Provider::Seedance2Pro, @@ -73,6 +81,8 @@ impl VideoGenerationRequest { VideoGenerationRequest::ArtcraftSeedance2p0Fast(request) => Ok(ArtcraftSeedance2p0FastCostState::from_request(request).estimate_cost()), VideoGenerationRequest::ArtcraftSeedance2p0G(request) => Ok(ArtcraftSeedance2p0GCostState::from_request(request).estimate_cost()), VideoGenerationRequest::ArtcraftSeedance2p0FastG(request) => Ok(ArtcraftSeedance2p0FastGCostState::from_request(request).estimate_cost()), + VideoGenerationRequest::ArtcraftSeedance2p0Plus(request) => Ok(ArtcraftSeedance2p0PlusCostState::from_request(request).estimate_cost()), + VideoGenerationRequest::ArtcraftSeedance2p0PlusFast(request) => Ok(ArtcraftSeedance2p0PlusFastCostState::from_request(request).estimate_cost()), VideoGenerationRequest::GmiCloudSeedance2p0G(request) => Ok(GmiCloudSeedance2p0GCostState::from_request(request).estimate_cost()), VideoGenerationRequest::GmiCloudSeedance2p0FastG(request) => Ok(GmiCloudSeedance2p0FastGCostState::from_request(request).estimate_cost()), VideoGenerationRequest::KinoviHappyHorse1p0(request) => Ok(KinoviHappyHorse1p0CostState::from_request(request).estimate_cost()), @@ -113,6 +123,14 @@ impl VideoGenerationRequest { let client_ref = client.get_artcraft_client_ref()?; request.send(client_ref).await }, + VideoGenerationRequest::ArtcraftSeedance2p0Plus(request) => { + let client_ref = client.get_artcraft_client_ref()?; + request.send(client_ref).await + }, + VideoGenerationRequest::ArtcraftSeedance2p0PlusFast(request) => { + let client_ref = client.get_artcraft_client_ref()?; + request.send(client_ref).await + }, VideoGenerationRequest::GmiCloudSeedance2p0G(request) => { let client_ref = client.get_gmicloud_client_ref()?; request.send(client_ref).await