diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0/build.rs index f58e0f4283..f34ef986df 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0/build.rs @@ -1,7 +1,7 @@ -use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{ - Seedance2p0AspectRatio, Seedance2p0BatchCount, Seedance2p0MultiFunctionVideoGenRequest, - Seedance2p0OutputResolution, -}; +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::audio_list_ref::AudioListRef; use crate::api::character_list_ref::CharacterListRef; @@ -22,39 +22,43 @@ pub fn build_artcraft_seedance_2p0(mut builder: GenerateVideoRequestBuilder) -> let strategy = builder.request_mismatch_mitigation_strategy; let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio.take(), strategy)?; - let output_resolution = plan_output_resolution(builder.resolution.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_media_token = resolve_image_ref(builder.start_frame.take())?; - let end_frame_media_token = resolve_image_ref(builder.end_frame.take())?; - let reference_image_media_tokens = resolve_image_list_ref(builder.reference_images.take())?; - let reference_video_media_tokens = resolve_video_list_ref(builder.reference_videos.take())?; - let reference_audio_media_tokens = resolve_audio_list_ref(builder.reference_audio.take())?; - let reference_character_tokens = resolve_character_list_ref(builder.reference_character_tokens.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 = Seedance2p0MultiFunctionVideoGenRequest { - uuid_idempotency_token: idempotency_token, + let request = OmniGenVideoCostAndGenerateRequest { + model: Some(CommonVideoModelEnum::Seedance2p0), + idempotency_token: Some(idempotency_token), prompt, - start_frame_media_token, - end_frame_media_token, - reference_image_media_tokens, - reference_video_media_tokens, - reference_audio_media_tokens, - reference_character_tokens, + 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, - output_resolution, - duration_seconds, - batch_count: Some(batch_count), + 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 = ArtcraftSeedance2p0RequestState { request }; Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftSeedance2p0(state))) } -// ── Resolve helpers ── +// -- Resolve helpers -- fn resolve_image_ref( image_ref: Option, @@ -113,38 +117,30 @@ fn resolve_character_list_ref( } } -// ── Plan helpers ── +// -- Plan helpers -- -// Supported aspect ratios and their AR values (width / height): -// Portrait9x16 = 0.5625, Portrait3x4 = 0.75, Square1x1 = 1.0, Standard4x3 = 1.33, Landscape16x9 = 1.78 -// -// All supported ratios cost the same, so PayMoreUpgrade and PayLessDowngrade both -// select the nearest match rather than rounding in a specific direction. fn plan_aspect_ratio( aspect_ratio: Option, strategy: RequestMismatchMitigationStrategy, -) -> Result, ArtcraftRouterError> { +) -> Result, ArtcraftRouterError> { match aspect_ratio { - // No preference or auto — let the model decide None | Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto4k) => Ok(None), - // Direct mappings Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => { - Ok(Some(Seedance2p0AspectRatio::Landscape16x9)) + Ok(Some(CommonAspectRatioEnum::WideSixteenByNine)) } Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => { - Ok(Some(Seedance2p0AspectRatio::Portrait9x16)) + Ok(Some(CommonAspectRatioEnum::TallNineBySixteen)) } Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => { - Ok(Some(Seedance2p0AspectRatio::Square1x1)) + Ok(Some(CommonAspectRatioEnum::Square)) } - Some(CommonAspectRatio::WideFourByThree) => Ok(Some(Seedance2p0AspectRatio::Standard4x3)), - Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(Seedance2p0AspectRatio::Portrait3x4)), + Some(CommonAspectRatio::WideFourByThree) => Ok(Some(CommonAspectRatioEnum::WideFourByThree)), + Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(CommonAspectRatioEnum::TallThreeByFour)), - // Mismatches — apply strategy Some(unsupported) => match strategy { RequestMismatchMitigationStrategy::ErrorOut => { Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { @@ -160,16 +156,15 @@ fn plan_aspect_ratio( } } -/// Pick the nearest supported aspect ratio by AR value (width / height). -fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> Seedance2p0AspectRatio { +fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> CommonAspectRatioEnum { match aspect_ratio { - CommonAspectRatio::WideFiveByFour => Seedance2p0AspectRatio::Standard4x3, // 1.25, nearest 1.33 - CommonAspectRatio::WideThreeByTwo => Seedance2p0AspectRatio::Standard4x3, // 1.50, nearest 1.33 - CommonAspectRatio::WideTwentyOneByNine => Seedance2p0AspectRatio::Landscape16x9, // 2.33, nearest 1.78 - CommonAspectRatio::TallFourByFive => Seedance2p0AspectRatio::Portrait3x4, // 0.80, nearest 0.75 - CommonAspectRatio::TallTwoByThree => Seedance2p0AspectRatio::Portrait3x4, // 0.67, nearest 0.75 - CommonAspectRatio::TallNineByTwentyOne => Seedance2p0AspectRatio::Portrait9x16, // 0.43, nearest 0.56 - _ => Seedance2p0AspectRatio::Square1x1, + CommonAspectRatio::WideFiveByFour => CommonAspectRatioEnum::WideFourByThree, + CommonAspectRatio::WideThreeByTwo => CommonAspectRatioEnum::WideFourByThree, + CommonAspectRatio::WideTwentyOneByNine => CommonAspectRatioEnum::WideSixteenByNine, + CommonAspectRatio::TallFourByFive => CommonAspectRatioEnum::TallThreeByFour, + CommonAspectRatio::TallTwoByThree => CommonAspectRatioEnum::TallThreeByFour, + CommonAspectRatio::TallNineByTwentyOne => CommonAspectRatioEnum::TallNineBySixteen, + _ => CommonAspectRatioEnum::Square, } } @@ -177,16 +172,14 @@ fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> Seedance2p0AspectRat fn plan_output_resolution( resolution: Option, strategy: RequestMismatchMitigationStrategy, -) -> Result, ArtcraftRouterError> { +) -> Result, ArtcraftRouterError> { match resolution { None => Ok(None), - // Direct mappings - Some(CommonResolution::FourEightyP) => Ok(Some(Seedance2p0OutputResolution::FourEightyP)), - Some(CommonResolution::SevenTwentyP) => Ok(Some(Seedance2p0OutputResolution::SevenTwentyP)), - Some(CommonResolution::TenEightyP) => Ok(Some(Seedance2p0OutputResolution::TenEightyP)), + Some(CommonResolution::FourEightyP) => Ok(Some(CommonResolutionEnum::FourEightyP)), + Some(CommonResolution::SevenTwentyP) => Ok(Some(CommonResolutionEnum::SevenTwentyP)), + Some(CommonResolution::TenEightyP) => Ok(Some(CommonResolutionEnum::TenEightyP)), - // Mismatches Some(unsupported) => match strategy { RequestMismatchMitigationStrategy::ErrorOut => { Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { @@ -196,31 +189,29 @@ fn plan_output_resolution( } RequestMismatchMitigationStrategy::PayMoreUpgrade => { Ok(Some(match unsupported { - CommonResolution::HalfK => Seedance2p0OutputResolution::FourEightyP, - _ => Seedance2p0OutputResolution::TenEightyP, + CommonResolution::HalfK => CommonResolutionEnum::FourEightyP, + _ => CommonResolutionEnum::TenEightyP, })) } RequestMismatchMitigationStrategy::PayLessDowngrade => { Ok(Some(match unsupported { - CommonResolution::HalfK => Seedance2p0OutputResolution::FourEightyP, - _ => Seedance2p0OutputResolution::TenEightyP, + CommonResolution::HalfK => CommonResolutionEnum::FourEightyP, + _ => CommonResolutionEnum::TenEightyP, })) } }, } } -// Seedance2p0 supports batch counts of 1, 2, and 4 only. +// Batch counts: 1, 2, 4. fn plan_batch_count( video_batch_count: Option, strategy: RequestMismatchMitigationStrategy, -) -> Result { +) -> Result { let count = video_batch_count.unwrap_or(1); match count { 0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)), - 1 => Ok(Seedance2p0BatchCount::One), - 2 => Ok(Seedance2p0BatchCount::Two), - 4 => Ok(Seedance2p0BatchCount::Four), + 1 | 2 | 4 => Ok(count), _ => match strategy { RequestMismatchMitigationStrategy::ErrorOut => { Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { @@ -228,17 +219,15 @@ fn plan_batch_count( value: format!("{}", count), })) } - RequestMismatchMitigationStrategy::PayMoreUpgrade => { - Ok(if count < 4 { Seedance2p0BatchCount::Four } else { Seedance2p0BatchCount::Four }) - } + RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(4), RequestMismatchMitigationStrategy::PayLessDowngrade => { - Ok(if count < 4 { Seedance2p0BatchCount::Two } else { Seedance2p0BatchCount::Four }) + Ok(if count < 4 { 2 } else { 4 }) } }, } } -// Seedance2p0 supports duration of 4–15 seconds. +// Duration: 4-15 seconds. fn plan_duration( duration_seconds: Option, strategy: RequestMismatchMitigationStrategy, @@ -262,350 +251,150 @@ fn plan_duration( #[cfg(test)] mod tests { - use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{ - Seedance2p0AspectRatio, Seedance2p0BatchCount, Seedance2p0OutputResolution, - }; + 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_aspect_ratio::CommonAspectRatio; 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::api::video_list_ref::VideoListRef; - 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::seedance_2p0::request::ArtcraftSeedance2p0RequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; use super::*; - // ── Materialized field conversions ── + // ── Field conversions ── - mod materialized_field_conversions { + mod field_conversions { use super::*; #[test] - fn prompt_is_passed_through() { - let state = unwrap_request(build_artcraft_seedance_2p0(artcraft_builder())); - assert_eq!(state.request.prompt.as_deref(), Some("a cat dancing")); + fn model_is_seedance_2p0() { + let req = unwrap_request(make_builder(|_| {})); + assert!(matches!(req.request.model, Some(CommonVideoModelEnum::Seedance2p0))); } #[test] - fn prompt_defaults_to_none() { - let builder = GenerateVideoRequestBuilder { prompt: None, ..artcraft_builder() }; - let state = unwrap_request(build_artcraft_seedance_2p0(builder)); - assert!(state.request.prompt.is_none()); + 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_seconds_converted() { - let builder = GenerateVideoRequestBuilder { duration_seconds: Some(10), ..artcraft_builder() }; - let state = unwrap_request(build_artcraft_seedance_2p0(builder)); - assert_eq!(state.request.duration_seconds, Some(10)); - } - - #[test] - fn duration_defaults_to_none() { - let builder = GenerateVideoRequestBuilder { duration_seconds: None, ..artcraft_builder() }; - let state = unwrap_request(build_artcraft_seedance_2p0(builder)); - assert!(state.request.duration_seconds.is_none()); + 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 builder = GenerateVideoRequestBuilder { - duration_seconds: Some(99), - ..artcraft_builder() - }; - let state = unwrap_request(build_artcraft_seedance_2p0(builder)); - assert_eq!(state.request.duration_seconds, Some(15)); + let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(99); })); + assert_eq!(req.request.duration_seconds, Some(15)); } #[test] - fn batch_count_one() { - let builder = GenerateVideoRequestBuilder { video_batch_count: Some(1), ..artcraft_builder() }; - let state = unwrap_request(build_artcraft_seedance_2p0(builder)); - assert!(matches!(state.request.batch_count, Some(Seedance2p0BatchCount::One))); - } - - #[test] - fn batch_count_two() { - let builder = GenerateVideoRequestBuilder { video_batch_count: Some(2), ..artcraft_builder() }; - let state = unwrap_request(build_artcraft_seedance_2p0(builder)); - assert!(matches!(state.request.batch_count, Some(Seedance2p0BatchCount::Two))); - } - - #[test] - fn batch_count_four() { - let builder = GenerateVideoRequestBuilder { video_batch_count: Some(4), ..artcraft_builder() }; - let state = unwrap_request(build_artcraft_seedance_2p0(builder)); - assert!(matches!(state.request.batch_count, Some(Seedance2p0BatchCount::Four))); + 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)); } } - // ── Aspect ratio conversions ── + // ── Resolution ── - mod aspect_ratio_conversions { + mod resolution_tests { use super::*; #[test] - fn aspect_ratio_wide() { - let builder = GenerateVideoRequestBuilder { - aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), - ..artcraft_builder() - }; - let state = unwrap_request(build_artcraft_seedance_2p0(builder)); - assert!(matches!(state.request.aspect_ratio, Some(Seedance2p0AspectRatio::Landscape16x9))); + 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 aspect_ratio_tall() { - let builder = GenerateVideoRequestBuilder { - aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen), - ..artcraft_builder() - }; - let state = unwrap_request(build_artcraft_seedance_2p0(builder)); - assert!(matches!(state.request.aspect_ratio, Some(Seedance2p0AspectRatio::Portrait9x16))); + 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 aspect_ratio_square() { - let builder = GenerateVideoRequestBuilder { - aspect_ratio: Some(CommonAspectRatio::Square), - ..artcraft_builder() - }; - let state = unwrap_request(build_artcraft_seedance_2p0(builder)); - assert!(matches!(state.request.aspect_ratio, Some(Seedance2p0AspectRatio::Square1x1))); + 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 aspect_ratio_defaults_to_none() { - let builder = GenerateVideoRequestBuilder { aspect_ratio: None, ..artcraft_builder() }; - let state = unwrap_request(build_artcraft_seedance_2p0(builder)); - assert!(state.request.aspect_ratio.is_none()); + fn none_stays_none() { + let req = unwrap_request(make_builder(|_| {})); + assert!(req.request.resolution.is_none()); } } - // ── Resolution conversions ── + // ── Media tokens ── - mod resolution_conversions { + mod media_token_tests { use super::*; #[test] - fn resolution_480p() { - let builder = GenerateVideoRequestBuilder { - resolution: Some(CommonResolution::FourEightyP), - ..artcraft_builder() - }; - let state = unwrap_request(build_artcraft_seedance_2p0(builder)); - assert!(matches!(state.request.output_resolution, Some(Seedance2p0OutputResolution::FourEightyP))); + 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 resolution_720p() { - let builder = GenerateVideoRequestBuilder { - resolution: Some(CommonResolution::SevenTwentyP), - ..artcraft_builder() - }; - let state = unwrap_request(build_artcraft_seedance_2p0(builder)); - assert!(matches!(state.request.output_resolution, Some(Seedance2p0OutputResolution::SevenTwentyP))); + fn url_start_frame_rejected() { + let result = build_artcraft_seedance_2p0(GenerateVideoRequestBuilder { + start_frame: Some(ImageRef::Url("https://example.com".to_string())), + ..base_builder() + }); + assert!(result.is_err()); } #[test] - fn resolution_1080p() { - let builder = GenerateVideoRequestBuilder { - resolution: Some(CommonResolution::TenEightyP), - ..artcraft_builder() - }; - let state = unwrap_request(build_artcraft_seedance_2p0(builder)); - assert!(matches!(state.request.output_resolution, Some(Seedance2p0OutputResolution::TenEightyP))); + 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 resolution_none() { - let builder = GenerateVideoRequestBuilder { resolution: None, ..artcraft_builder() }; - let state = unwrap_request(build_artcraft_seedance_2p0(builder)); - assert!(state.request.output_resolution.is_none()); + 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)); } - - #[test] - fn unsupported_resolution_error_out() { - let builder = GenerateVideoRequestBuilder { - resolution: Some(CommonResolution::FourK), - request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, - ..artcraft_builder() - }; - assert!(build_artcraft_seedance_2p0(builder).is_err()); - } - - #[test] - fn unsupported_resolution_rounds_up() { - let builder = GenerateVideoRequestBuilder { - resolution: Some(CommonResolution::FourK), - request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade, - ..artcraft_builder() - }; - let state = unwrap_request(build_artcraft_seedance_2p0(builder)); - assert!(matches!(state.request.output_resolution, Some(Seedance2p0OutputResolution::TenEightyP))); - } - } - - // ── Media token resolution ── - - mod media_token_resolution { - use super::*; - - #[test] - fn start_frame_media_token() { - let builder = GenerateVideoRequestBuilder { - start_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_start123".to_string()))), - ..artcraft_builder() - }; - let state = unwrap_request(build_artcraft_seedance_2p0(builder)); - assert_eq!(state.request.start_frame_media_token.as_ref().unwrap().as_str(), "mf_start123"); - } - - #[test] - fn end_frame_media_token() { - let builder = GenerateVideoRequestBuilder { - end_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_end456".to_string()))), - ..artcraft_builder() - }; - let state = unwrap_request(build_artcraft_seedance_2p0(builder)); - assert_eq!(state.request.end_frame_media_token.as_ref().unwrap().as_str(), "mf_end456"); - } - - #[test] - fn reference_image_media_tokens() { - let builder = GenerateVideoRequestBuilder { - reference_images: Some(ImageListRef::MediaFileTokens(vec![ - MediaFileToken::new("mf_ref1".to_string()), - MediaFileToken::new("mf_ref2".to_string()), - ])), - ..artcraft_builder() - }; - let state = unwrap_request(build_artcraft_seedance_2p0(builder)); - let tokens = state.request.reference_image_media_tokens.unwrap(); - assert_eq!(tokens.len(), 2); - assert_eq!(tokens[0].as_str(), "mf_ref1"); - assert_eq!(tokens[1].as_str(), "mf_ref2"); - } - - #[test] - fn reference_video_media_tokens() { - let builder = GenerateVideoRequestBuilder { - reference_videos: Some(VideoListRef::MediaFileTokens(vec![ - MediaFileToken::new("mf_vid1".to_string()), - ])), - ..artcraft_builder() - }; - let state = unwrap_request(build_artcraft_seedance_2p0(builder)); - let tokens = state.request.reference_video_media_tokens.unwrap(); - assert_eq!(tokens.len(), 1); - assert_eq!(tokens[0].as_str(), "mf_vid1"); - } - - #[test] - fn character_tokens() { - let builder = GenerateVideoRequestBuilder { - reference_character_tokens: Some(CharacterListRef::CharacterTokens(vec![ - CharacterToken::new("char_abc".to_string()), - CharacterToken::new("char_def".to_string()), - ])), - ..artcraft_builder() - }; - let state = unwrap_request(build_artcraft_seedance_2p0(builder)); - let tokens = state.request.reference_character_tokens.unwrap(); - assert_eq!(tokens.len(), 2); - assert_eq!(tokens[0].as_str(), "char_abc"); - assert_eq!(tokens[1].as_str(), "char_def"); - } - - #[test] - fn url_image_ref_returns_error() { - let builder = GenerateVideoRequestBuilder { - start_frame: Some(ImageRef::Url("https://example.com/image.jpg".to_string())), - ..artcraft_builder() - }; - assert!(matches!( - build_artcraft_seedance_2p0(builder), - Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens)) - )); - } - - #[test] - fn empty_refs_are_none() { - let state = unwrap_request(build_artcraft_seedance_2p0(artcraft_builder())); - assert!(state.request.start_frame_media_token.is_none()); - assert!(state.request.end_frame_media_token.is_none()); - assert!(state.request.reference_image_media_tokens.is_none()); - assert!(state.request.reference_video_media_tokens.is_none()); - assert!(state.request.reference_audio_media_tokens.is_none()); - assert!(state.request.reference_character_tokens.is_none()); - } - } - - // ── Returns Request, not Draft ── - - #[test] - fn build_returns_request_variant() { - let result = build_artcraft_seedance_2p0(artcraft_builder()).expect("build should succeed"); - assert!(matches!(result, VideoGenerationDraftOrRequest::Request(_))); - } - - // ── Full combination ── - - #[test] - fn full_request_all_fields() { - let builder = GenerateVideoRequestBuilder { - prompt: Some("full test".to_string()), - aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen), - resolution: Some(CommonResolution::TenEightyP), - duration_seconds: Some(10), - video_batch_count: Some(4), - start_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_start".to_string()))), - end_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_end".to_string()))), - reference_images: Some(ImageListRef::MediaFileTokens(vec![MediaFileToken::new("mf_img".to_string())])), - reference_videos: Some(VideoListRef::MediaFileTokens(vec![MediaFileToken::new("mf_vid".to_string())])), - reference_character_tokens: Some(CharacterListRef::CharacterTokens(vec![ - CharacterToken::new("char_xyz".to_string()), - ])), - ..artcraft_builder() - }; - let state = unwrap_request(build_artcraft_seedance_2p0(builder)); - - assert_eq!(state.request.prompt.as_deref(), Some("full test")); - assert!(matches!(state.request.aspect_ratio, Some(Seedance2p0AspectRatio::Portrait9x16))); - assert!(matches!(state.request.output_resolution, Some(Seedance2p0OutputResolution::TenEightyP))); - assert_eq!(state.request.duration_seconds, Some(10)); - assert!(matches!(state.request.batch_count, Some(Seedance2p0BatchCount::Four))); - assert!(state.request.start_frame_media_token.is_some()); - assert!(state.request.end_frame_media_token.is_some()); - assert!(state.request.reference_image_media_tokens.is_some()); - assert!(state.request.reference_video_media_tokens.is_some()); - assert!(state.request.reference_character_tokens.is_some()); } // ── Helpers ── - fn artcraft_builder() -> GenerateVideoRequestBuilder { + fn base_builder() -> GenerateVideoRequestBuilder { GenerateVideoRequestBuilder { provider: Provider::Artcraft, - prompt: Some("a cat dancing".to_string()), duration_seconds: Some(5), video_batch_count: Some(1), ..Default::default() } } - fn unwrap_request(result: Result) -> ArtcraftSeedance2p0RequestState { - match result.expect("build should succeed") { + fn make_builder(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder { + let mut builder = base_builder(); + f(&mut builder); + builder + } + + fn unwrap_request(builder: GenerateVideoRequestBuilder) -> ArtcraftSeedance2p0RequestState { + let result = build_artcraft_seedance_2p0(builder).expect("build should succeed"); + match result { VideoGenerationDraftOrRequest::Request( VideoGenerationRequest::ArtcraftSeedance2p0(state) ) => state, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0/cost.rs index 6a5fed8a02..0c0cd17325 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0/cost.rs @@ -1,6 +1,4 @@ -use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{ - Seedance2p0BatchCount, Seedance2p0OutputResolution, -}; +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::request::ArtcraftSeedance2p0RequestState; @@ -13,7 +11,7 @@ use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0::reque // from the upstream credit rates and their credit-package prices, // then set ArtCraft credits = cents. -/// USD cents per second by resolution, derived from Upstream rates: +/// USD cents per second by resolution, derived from upstream rates: /// 480p: 15 upstream-credits/sec ÷ 193 upstream-credits/$1 × 100 ≈ 7.772 ¢/s /// 720p: 40 upstream-credits/sec ÷ 250 upstream-credits/$1 × 100 = 16.0 ¢/s /// 1080p: 90 upstream-credits/sec ÷ 193 upstream-credits/$1 × 100 ≈ 46.632 ¢/s @@ -25,19 +23,18 @@ const CENTS_PER_SECOND_720P: f64 = 16.0; const CENTS_PER_SECOND_1080P: f64 = 46.632; pub struct ArtcraftSeedance2p0CostState { - pub resolution: Seedance2p0OutputResolution, - pub duration_seconds: u8, - pub batch_count: Seedance2p0BatchCount, + pub resolution: CommonResolution, + pub duration_seconds: u16, + pub batch_count: u16, pub has_video_reference: bool, } impl ArtcraftSeedance2p0CostState { pub fn from_request(request: &ArtcraftSeedance2p0RequestState) -> Self { - let resolution = request.request.output_resolution - .unwrap_or(Seedance2p0OutputResolution::SevenTwentyP); + let resolution = request.request.resolution + .unwrap_or(CommonResolution::SevenTwentyP); let duration_seconds = request.request.duration_seconds.unwrap_or(5); - let batch_count = request.request.batch_count - .unwrap_or(Seedance2p0BatchCount::One); + let batch_count = request.request.video_batch_count.unwrap_or(1); let has_video_reference = request.request.reference_video_media_tokens .as_ref() .is_some_and(|tokens| !tokens.is_empty()); @@ -47,18 +44,13 @@ impl ArtcraftSeedance2p0CostState { pub fn estimate_cost(&self) -> VideoGenerationCostEstimate { let cents_per_second = match self.resolution { - Seedance2p0OutputResolution::FourEightyP => CENTS_PER_SECOND_480P, - Seedance2p0OutputResolution::SevenTwentyP => CENTS_PER_SECOND_720P, - Seedance2p0OutputResolution::TenEightyP => CENTS_PER_SECOND_1080P, + CommonResolution::FourEightyP => CENTS_PER_SECOND_480P, + CommonResolution::TenEightyP => CENTS_PER_SECOND_1080P, + // Everything else (including 720p) prices at 720p. + _ => CENTS_PER_SECOND_720P, }; - let batch_multiplier: f64 = match self.batch_count { - Seedance2p0BatchCount::One => 1.0, - Seedance2p0BatchCount::Two => 2.0, - Seedance2p0BatchCount::Four => 4.0, - }; - - let usd_cents = (self.duration_seconds as f64 * cents_per_second * batch_multiplier).round() as u64; + let usd_cents = (self.duration_seconds as f64 * cents_per_second * self.batch_count as f64).round() as u64; // ArtCraft credits: 100 credits = $1.00, so credits = cents. VideoGenerationCostEstimate { @@ -74,137 +66,132 @@ impl ArtcraftSeedance2p0CostState { #[cfg(test)] mod tests { - use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{ - Seedance2p0BatchCount, Seedance2p0MultiFunctionVideoGenRequest, Seedance2p0OutputResolution, - }; + use enums::common::generation::common_resolution::CommonResolution; use tokens::tokens::media_files::MediaFileToken; - use crate::api::common_resolution::CommonResolution; + use crate::api::common_resolution::CommonResolution as CommonResolutionRouter; + use crate::api::common_video_model::CommonVideoModel; use crate::api::provider::Provider; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; - use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0::request::ArtcraftSeedance2p0RequestState; use super::*; - // ── 720p pricing ── + // -- 720p pricing -- mod pricing_720p { use super::*; #[test] - fn cost_720p_batch_1() { - assert_eq!(usd_cents(Seedance2p0OutputResolution::SevenTwentyP, 4, Seedance2p0BatchCount::One), 64); - assert_eq!(usd_cents(Seedance2p0OutputResolution::SevenTwentyP, 5, Seedance2p0BatchCount::One), 80); - assert_eq!(usd_cents(Seedance2p0OutputResolution::SevenTwentyP, 6, Seedance2p0BatchCount::One), 96); - assert_eq!(usd_cents(Seedance2p0OutputResolution::SevenTwentyP, 7, Seedance2p0BatchCount::One), 112); - assert_eq!(usd_cents(Seedance2p0OutputResolution::SevenTwentyP, 10, Seedance2p0BatchCount::One), 160); - assert_eq!(usd_cents(Seedance2p0OutputResolution::SevenTwentyP, 15, Seedance2p0BatchCount::One), 240); + fn batch_1() { + assert_eq!(usd_cents(CommonResolution::SevenTwentyP, 4, 1), 64); + assert_eq!(usd_cents(CommonResolution::SevenTwentyP, 5, 1), 80); + assert_eq!(usd_cents(CommonResolution::SevenTwentyP, 6, 1), 96); + assert_eq!(usd_cents(CommonResolution::SevenTwentyP, 10, 1), 160); + assert_eq!(usd_cents(CommonResolution::SevenTwentyP, 15, 1), 240); } #[test] - fn cost_720p_batch_2() { - assert_eq!(usd_cents(Seedance2p0OutputResolution::SevenTwentyP, 4, Seedance2p0BatchCount::Two), 128); - assert_eq!(usd_cents(Seedance2p0OutputResolution::SevenTwentyP, 5, Seedance2p0BatchCount::Two), 160); - assert_eq!(usd_cents(Seedance2p0OutputResolution::SevenTwentyP, 15, Seedance2p0BatchCount::Two), 480); + fn batch_2() { + assert_eq!(usd_cents(CommonResolution::SevenTwentyP, 4, 2), 128); + assert_eq!(usd_cents(CommonResolution::SevenTwentyP, 5, 2), 160); + assert_eq!(usd_cents(CommonResolution::SevenTwentyP, 15, 2), 480); } #[test] - fn cost_720p_batch_4() { - assert_eq!(usd_cents(Seedance2p0OutputResolution::SevenTwentyP, 4, Seedance2p0BatchCount::Four), 256); - assert_eq!(usd_cents(Seedance2p0OutputResolution::SevenTwentyP, 5, Seedance2p0BatchCount::Four), 320); - assert_eq!(usd_cents(Seedance2p0OutputResolution::SevenTwentyP, 15, Seedance2p0BatchCount::Four), 960); + fn batch_4() { + assert_eq!(usd_cents(CommonResolution::SevenTwentyP, 4, 4), 256); + assert_eq!(usd_cents(CommonResolution::SevenTwentyP, 5, 4), 320); + assert_eq!(usd_cents(CommonResolution::SevenTwentyP, 15, 4), 960); } } - // ── 480p pricing ── + // -- 480p pricing -- mod pricing_480p { use super::*; #[test] - fn cost_480p_batch_1() { - assert_eq!(usd_cents(Seedance2p0OutputResolution::FourEightyP, 4, Seedance2p0BatchCount::One), 31); - assert_eq!(usd_cents(Seedance2p0OutputResolution::FourEightyP, 5, Seedance2p0BatchCount::One), 39); - assert_eq!(usd_cents(Seedance2p0OutputResolution::FourEightyP, 10, Seedance2p0BatchCount::One), 78); - assert_eq!(usd_cents(Seedance2p0OutputResolution::FourEightyP, 15, Seedance2p0BatchCount::One), 117); + fn batch_1() { + assert_eq!(usd_cents(CommonResolution::FourEightyP, 4, 1), 31); + assert_eq!(usd_cents(CommonResolution::FourEightyP, 5, 1), 39); + assert_eq!(usd_cents(CommonResolution::FourEightyP, 10, 1), 78); + assert_eq!(usd_cents(CommonResolution::FourEightyP, 15, 1), 117); } #[test] - fn cost_480p_batch_2() { - assert_eq!(usd_cents(Seedance2p0OutputResolution::FourEightyP, 5, Seedance2p0BatchCount::Two), 78); + fn batch_2() { + assert_eq!(usd_cents(CommonResolution::FourEightyP, 5, 2), 78); } #[test] - fn cost_480p_batch_4() { - assert_eq!(usd_cents(Seedance2p0OutputResolution::FourEightyP, 5, Seedance2p0BatchCount::Four), 155); + fn batch_4() { + assert_eq!(usd_cents(CommonResolution::FourEightyP, 5, 4), 155); } } - // ── 1080p pricing ── + // -- 1080p pricing -- mod pricing_1080p { use super::*; #[test] - fn cost_1080p_batch_1() { - assert_eq!(usd_cents(Seedance2p0OutputResolution::TenEightyP, 4, Seedance2p0BatchCount::One), 187); - assert_eq!(usd_cents(Seedance2p0OutputResolution::TenEightyP, 5, Seedance2p0BatchCount::One), 233); - assert_eq!(usd_cents(Seedance2p0OutputResolution::TenEightyP, 10, Seedance2p0BatchCount::One), 466); - assert_eq!(usd_cents(Seedance2p0OutputResolution::TenEightyP, 15, Seedance2p0BatchCount::One), 699); + fn batch_1() { + assert_eq!(usd_cents(CommonResolution::TenEightyP, 4, 1), 187); + assert_eq!(usd_cents(CommonResolution::TenEightyP, 5, 1), 233); + assert_eq!(usd_cents(CommonResolution::TenEightyP, 10, 1), 466); + assert_eq!(usd_cents(CommonResolution::TenEightyP, 15, 1), 699); } #[test] - fn cost_1080p_batch_2() { - assert_eq!(usd_cents(Seedance2p0OutputResolution::TenEightyP, 5, Seedance2p0BatchCount::Two), 466); + fn batch_2() { + assert_eq!(usd_cents(CommonResolution::TenEightyP, 5, 2), 466); } #[test] - fn cost_1080p_batch_4() { - assert_eq!(usd_cents(Seedance2p0OutputResolution::TenEightyP, 5, Seedance2p0BatchCount::Four), 933); + fn batch_4() { + assert_eq!(usd_cents(CommonResolution::TenEightyP, 5, 4), 933); } } - // ── Relative pricing ── + // -- Relative pricing -- mod relative_pricing_tests { use super::*; #[test] fn cost_480p_cheaper_than_720p_cheaper_than_1080p() { - let c480 = usd_cents(Seedance2p0OutputResolution::FourEightyP, 5, Seedance2p0BatchCount::One); - let c720 = usd_cents(Seedance2p0OutputResolution::SevenTwentyP, 5, Seedance2p0BatchCount::One); - let c1080 = usd_cents(Seedance2p0OutputResolution::TenEightyP, 5, Seedance2p0BatchCount::One); - assert!(c480 < c720, "480p ({}) should be cheaper than 720p ({})", c480, c720); - assert!(c720 < c1080, "720p ({}) should be cheaper than 1080p ({})", c720, c1080); + let c480 = usd_cents(CommonResolution::FourEightyP, 5, 1); + let c720 = usd_cents(CommonResolution::SevenTwentyP, 5, 1); + let c1080 = usd_cents(CommonResolution::TenEightyP, 5, 1); + assert!(c480 < c720); + assert!(c720 < c1080); } #[test] fn cost_scales_with_duration() { - let c4 = usd_cents(Seedance2p0OutputResolution::SevenTwentyP, 4, Seedance2p0BatchCount::One); - let c10 = usd_cents(Seedance2p0OutputResolution::SevenTwentyP, 10, Seedance2p0BatchCount::One); - let c15 = usd_cents(Seedance2p0OutputResolution::SevenTwentyP, 15, Seedance2p0BatchCount::One); + let c4 = usd_cents(CommonResolution::SevenTwentyP, 4, 1); + let c10 = usd_cents(CommonResolution::SevenTwentyP, 10, 1); + let c15 = usd_cents(CommonResolution::SevenTwentyP, 15, 1); assert!(c4 < c10); assert!(c10 < c15); } #[test] fn cost_scales_with_batch() { - let b1 = usd_cents(Seedance2p0OutputResolution::TenEightyP, 5, Seedance2p0BatchCount::One); - let b2 = usd_cents(Seedance2p0OutputResolution::TenEightyP, 5, Seedance2p0BatchCount::Two); - let b4 = usd_cents(Seedance2p0OutputResolution::TenEightyP, 5, Seedance2p0BatchCount::Four); + let b1 = usd_cents(CommonResolution::TenEightyP, 5, 1); + let b2 = usd_cents(CommonResolution::TenEightyP, 5, 2); + let b4 = usd_cents(CommonResolution::TenEightyP, 5, 4); assert!(b1 < b2); assert!(b2 < b4); } } - // ── Video reference does NOT affect cost (yet) ── - #[test] fn video_reference_does_not_affect_cost() { let base = ArtcraftSeedance2p0CostState { - resolution: Seedance2p0OutputResolution::SevenTwentyP, + resolution: CommonResolution::SevenTwentyP, duration_seconds: 5, - batch_count: Seedance2p0BatchCount::One, + batch_count: 1, has_video_reference: false, }; let without = base.estimate_cost(); @@ -213,70 +200,18 @@ mod tests { assert_eq!(without.cost_in_credits, with.cost_in_credits); } - // ── from_request() tests ── - - mod from_request_tests { - use super::*; - - #[test] - fn from_request_720p() { - let req = make_request_state(Some(Seedance2p0OutputResolution::SevenTwentyP), 5, Seedance2p0BatchCount::One, false); - let cost = ArtcraftSeedance2p0CostState::from_request(&req); - assert!(matches!(cost.resolution, Seedance2p0OutputResolution::SevenTwentyP)); - assert_eq!(cost.duration_seconds, 5); - assert!(matches!(cost.batch_count, Seedance2p0BatchCount::One)); - assert!(!cost.has_video_reference); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(80)); - } - - #[test] - fn from_request_none_defaults_to_720p() { - let req = make_request_state(None, 5, Seedance2p0BatchCount::One, false); - let cost = ArtcraftSeedance2p0CostState::from_request(&req); - assert!(matches!(cost.resolution, Seedance2p0OutputResolution::SevenTwentyP)); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(80)); - } - - #[test] - fn from_request_480p() { - let req = make_request_state(Some(Seedance2p0OutputResolution::FourEightyP), 5, Seedance2p0BatchCount::One, false); - let cost = ArtcraftSeedance2p0CostState::from_request(&req); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(39)); - } - - #[test] - fn from_request_1080p_batch_2() { - let req = make_request_state(Some(Seedance2p0OutputResolution::TenEightyP), 5, Seedance2p0BatchCount::Two, false); - let cost = ArtcraftSeedance2p0CostState::from_request(&req); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(466)); - } - - #[test] - fn from_request_with_video_reference() { - let req = make_request_state(Some(Seedance2p0OutputResolution::SevenTwentyP), 5, Seedance2p0BatchCount::One, true); - let cost = ArtcraftSeedance2p0CostState::from_request(&req); - assert!(cost.has_video_reference); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(80)); - } - } - - // ── Cross-check: Artcraft v2 matches Kinovi v2 via GenerateVideoRequestBuilder ── + // -- Cross-check with Kinovi via builder -- mod cross_check_with_kinovi_via_builder { - use crate::api::common_video_model::CommonVideoModel; - use super::*; - /// Combinatorial test: build the same request with Provider::Artcraft and - /// Provider::Seedance2Pro, then compare USD cents. They must match for all - /// resolution × duration × batch combinations. #[test] fn artcraft_matches_kinovi_all_combos() { let resolutions = [ - Some(CommonResolution::FourEightyP), - Some(CommonResolution::SevenTwentyP), - None, // defaults to 720p - Some(CommonResolution::TenEightyP), + Some(CommonResolutionRouter::FourEightyP), + Some(CommonResolutionRouter::SevenTwentyP), + None, + Some(CommonResolutionRouter::TenEightyP), ]; let durations: [u16; 4] = [4, 5, 10, 15]; let batches: [u16; 3] = [1, 2, 4]; @@ -285,7 +220,6 @@ mod tests { for dur in &durations { for batch in &batches { let artcraft = GenerateVideoRequestBuilder { - model: CommonVideoModel::Seedance2p0, provider: Provider::Artcraft, resolution: *res, duration_seconds: Some(*dur), @@ -294,7 +228,6 @@ mod tests { }; let kinovi = GenerateVideoRequestBuilder { - model: CommonVideoModel::Seedance2p0, provider: Provider::Seedance2Pro, resolution: *res, duration_seconds: Some(*dur), @@ -303,20 +236,19 @@ mod tests { }; let artcraft_cost = artcraft.build2() - .expect("artcraft build2 should succeed") + .expect("artcraft build2") .estimate_cost() - .expect("artcraft estimate_cost should succeed"); + .expect("artcraft estimate_cost"); let kinovi_cost = kinovi.build2() - .expect("kinovi build2 should succeed") + .expect("kinovi build2") .estimate_cost() - .expect("kinovi estimate_cost should succeed"); + .expect("kinovi estimate_cost"); assert_eq!( artcraft_cost.cost_in_usd_cents, kinovi_cost.cost_in_usd_cents, - "USD cents mismatch: res={:?} dur={}s batch={} — artcraft={:?}, kinovi={:?}", + "USD cents mismatch: res={:?} dur={}s batch={}", res, dur, batch, - artcraft_cost.cost_in_usd_cents, kinovi_cost.cost_in_usd_cents, ); } } @@ -324,124 +256,28 @@ mod tests { } } - // ── Cross-check: Artcraft v2 matches v1 estimate_video_cost_artcraft_seedance2p0 ── - - mod cross_check_with_v1 { - use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0AspectRatio; - use tokens::tokens::media_files::MediaFileToken; - - use crate::generate::generate_video::cost::artcraft::estimate_video_cost_artcraft_seedance2p0::estimate_video_cost_artcraft_seedance2p0; - use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::PlanArtcraftSeedance2p0; - - use super::*; - - /// Combinatorial test: compare v2 ArtcraftSeedance2p0CostState against - /// v1 estimate_video_cost_artcraft_seedance2p0 for all resolution × - /// duration × batch combinations. - #[test] - fn v2_matches_v1_all_combos() { - let resolutions = [ - (Some(CommonResolution::FourEightyP), Seedance2p0OutputResolution::FourEightyP), - (Some(CommonResolution::SevenTwentyP), Seedance2p0OutputResolution::SevenTwentyP), - (None, Seedance2p0OutputResolution::SevenTwentyP), // None defaults to 720p - (Some(CommonResolution::TenEightyP), Seedance2p0OutputResolution::TenEightyP), - ]; - let durations: [u8; 4] = [4, 5, 10, 15]; - let batches = [ - (Seedance2p0BatchCount::One, Seedance2p0BatchCount::One), - (Seedance2p0BatchCount::Two, Seedance2p0BatchCount::Two), - (Seedance2p0BatchCount::Four, Seedance2p0BatchCount::Four), - ]; - - for (v1_res, v2_res) in &resolutions { - for dur in &durations { - for (v1_batch, v2_batch) in &batches { - // v1: construct a PlanArtcraftSeedance2p0 and call the v1 cost function - let v1_plan = PlanArtcraftSeedance2p0 { - prompt: None, - start_frame: None, - end_frame: None, - reference_images: None, - reference_videos: None, - reference_audio: None, - reference_characters: None, - aspect_ratio: Some(Seedance2p0AspectRatio::Square1x1), - resolution: *v1_res, - duration_seconds: Some(*dur), - batch_count: *v1_batch, - idempotency_token: "test".to_string(), - }; - let v1_cost = estimate_video_cost_artcraft_seedance2p0(&v1_plan); - - // v2: construct cost state directly - let v2_cost = ArtcraftSeedance2p0CostState { - resolution: *v2_res, - duration_seconds: *dur, - batch_count: *v2_batch, - has_video_reference: false, - }.estimate_cost(); - - assert_eq!( - v2_cost.cost_in_usd_cents, v1_cost.cost_in_usd_cents, - "USD cents mismatch: res={:?} dur={}s batch={:?} — v2={:?}, v1={:?}", - v1_res, dur, v1_batch, - v2_cost.cost_in_usd_cents, v1_cost.cost_in_usd_cents, - ); - - assert_eq!( - v2_cost.cost_in_credits, v1_cost.cost_in_credits, - "Credits mismatch: res={:?} dur={}s batch={:?} — v2={:?}, v1={:?}", - v1_res, dur, v1_batch, - v2_cost.cost_in_credits, v1_cost.cost_in_credits, - ); - } - } - } - } - } - - // ── Credits spot checks ── + // -- Credits -- mod credits_tests { use super::*; #[test] fn credits_equal_usd_cents() { - // ArtCraft credits: 100 credits = $1.00, so credits always equal cents. - for res in [ - Seedance2p0OutputResolution::FourEightyP, - Seedance2p0OutputResolution::SevenTwentyP, - Seedance2p0OutputResolution::TenEightyP, - ] { + for res in [CommonResolution::FourEightyP, CommonResolution::SevenTwentyP, CommonResolution::TenEightyP] { for dur in [4, 5, 10, 15] { - for batch in [Seedance2p0BatchCount::One, Seedance2p0BatchCount::Two, Seedance2p0BatchCount::Four] { - assert_eq!( - credits(res, dur, batch), - usd_cents(res, dur, batch), - "credits should equal usd_cents for {:?} {}s {:?}", res, dur, batch, - ); + for batch in [1, 2, 4] { + let state = ArtcraftSeedance2p0CostState { + resolution: res, duration_seconds: dur, batch_count: batch, has_video_reference: false, + }; + let cost = state.estimate_cost(); + assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents); } } } } - - #[test] - fn credits_720p() { - assert_eq!(credits(Seedance2p0OutputResolution::SevenTwentyP, 5, Seedance2p0BatchCount::One), 80); - } - - #[test] - fn credits_480p() { - assert_eq!(credits(Seedance2p0OutputResolution::FourEightyP, 5, Seedance2p0BatchCount::One), 39); - } - - #[test] - fn credits_1080p() { - assert_eq!(credits(Seedance2p0OutputResolution::TenEightyP, 5, Seedance2p0BatchCount::One), 233); - } } - // ── Builder round-trip ── + // -- Builder round-trip -- mod builder_round_trip { use super::*; @@ -450,13 +286,12 @@ mod tests { fn build2_then_estimate_cost_720p() { let builder = GenerateVideoRequestBuilder { provider: Provider::Artcraft, - resolution: Some(CommonResolution::SevenTwentyP), + resolution: Some(CommonResolutionRouter::SevenTwentyP), duration_seconds: Some(5), video_batch_count: Some(1), ..Default::default() }; - let result = builder.build2().expect("build2 should succeed"); - let cost = result.estimate_cost().expect("estimate_cost should succeed"); + let cost = builder.build2().unwrap().estimate_cost().unwrap(); assert_eq!(cost.cost_in_usd_cents, Some(80)); } @@ -464,68 +299,22 @@ mod tests { fn build2_then_estimate_cost_1080p_batch_4() { let builder = GenerateVideoRequestBuilder { provider: Provider::Artcraft, - resolution: Some(CommonResolution::TenEightyP), + resolution: Some(CommonResolutionRouter::TenEightyP), duration_seconds: Some(5), video_batch_count: Some(4), ..Default::default() }; - let result = builder.build2().expect("build2 should succeed"); - let cost = result.estimate_cost().expect("estimate_cost should succeed"); + let cost = builder.build2().unwrap().estimate_cost().unwrap(); assert_eq!(cost.cost_in_usd_cents, Some(933)); } } - // ── Helpers ── + // -- Helpers -- - fn usd_cents( - resolution: Seedance2p0OutputResolution, - duration_seconds: u8, - batch_count: Seedance2p0BatchCount, - ) -> u64 { + fn usd_cents(resolution: CommonResolution, duration_seconds: u16, batch_count: u16) -> u64 { ArtcraftSeedance2p0CostState { resolution, duration_seconds, batch_count, has_video_reference: false } .estimate_cost() .cost_in_usd_cents .unwrap() } - - fn credits( - resolution: Seedance2p0OutputResolution, - duration_seconds: u8, - batch_count: Seedance2p0BatchCount, - ) -> u64 { - ArtcraftSeedance2p0CostState { resolution, duration_seconds, batch_count, has_video_reference: false } - .estimate_cost() - .cost_in_credits - .unwrap() - } - - fn make_request_state( - resolution: Option, - duration_seconds: u8, - batch_count: Seedance2p0BatchCount, - with_video_ref: bool, - ) -> ArtcraftSeedance2p0RequestState { - let reference_video_media_tokens = if with_video_ref { - Some(vec![MediaFileToken::new("mf_testvid".to_string())]) - } else { - None - }; - - ArtcraftSeedance2p0RequestState { - request: Seedance2p0MultiFunctionVideoGenRequest { - uuid_idempotency_token: "test-idem".to_string(), - prompt: Some("test".to_string()), - start_frame_media_token: None, - end_frame_media_token: None, - reference_image_media_tokens: None, - reference_video_media_tokens, - reference_audio_media_tokens: None, - reference_character_tokens: None, - aspect_ratio: None, - output_resolution: resolution, - duration_seconds: Some(duration_seconds), - batch_count: Some(batch_count), - }, - } - } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0/request.rs index 353ea596f0..c2a91c75a2 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0/request.rs @@ -1,5 +1,5 @@ -use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0MultiFunctionVideoGenRequest; -use artcraft_client::endpoints::generate::video::multi_function::seedance_2p0_multi_function_video_gen::seedance_2p0_multi_function_video_gen; +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; @@ -10,13 +10,13 @@ use crate::generate::generate_video::generate_video_response::{ #[derive(Clone, Debug)] pub struct ArtcraftSeedance2p0RequestState { - /// Final materialized request; ready to fire. - pub request: Seedance2p0MultiFunctionVideoGenRequest, + /// Final materialized request; ready to fire via the omni-gen video endpoint. + pub request: OmniGenVideoCostAndGenerateRequest, } impl ArtcraftSeedance2p0RequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - let response = seedance_2p0_multi_function_video_gen( + let response = omni_gen_video_generate( &client.api_host, Some(&client.credentials), self.request.clone(), @@ -25,8 +25,8 @@ impl ArtcraftSeedance2p0RequestState { .map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?; Ok(GenerateVideoResponse::Artcraft(ArtcraftVideoResponsePayload { - inference_job_token: response.inference_job_token, - all_inference_job_tokens: response.all_inference_job_tokens, + inference_job_token: response.inference_job_token.clone(), + all_inference_job_tokens: vec![response.inference_job_token], })) } } @@ -42,7 +42,6 @@ mod tests { use crate::api::image_list_ref::ImageListRef; use crate::api::image_ref::ImageRef; use crate::api::provider::Provider; - use crate::api::video_list_ref::VideoListRef; use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::client::router_client::RouterClient; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; @@ -58,14 +57,13 @@ mod tests { WHITE_HOUSE_SUNSET_PRODUCTION_MEDIA_TOKEN, }; - - // ── Aspect ratio tests ── + // -- Aspect ratio tests -- mod aspect_ratio_tests { use super::*; #[tokio::test] - #[ignore] // manually run — fires a real API request and incurs cost + #[ignore] async fn landscape() { let response = run_pipeline(GenerateVideoRequestBuilder { prompt: Some("A corgi running through a field of wildflowers at sunset.".to_string()), @@ -77,7 +75,7 @@ mod tests { } #[tokio::test] - #[ignore] // manually run — fires a real API request and incurs cost + #[ignore] async fn portrait() { let response = run_pipeline(GenerateVideoRequestBuilder { prompt: Some("A cat sitting on a windowsill watching rain.".to_string()), @@ -89,7 +87,7 @@ mod tests { } #[tokio::test] - #[ignore] // manually run — fires a real API request and incurs cost + #[ignore] async fn square() { let response = run_pipeline(GenerateVideoRequestBuilder { prompt: Some("A hummingbird hovering near a flower.".to_string()), @@ -101,13 +99,13 @@ mod tests { } } - // ── Resolution tests ── + // -- Resolution tests -- mod resolution_tests { use super::*; #[tokio::test] - #[ignore] // manually run — fires a real API request and incurs cost + #[ignore] async fn res_480p() { let response = run_pipeline(GenerateVideoRequestBuilder { prompt: Some("A shiba inu playing in autumn leaves.".to_string()), @@ -119,7 +117,7 @@ mod tests { } #[tokio::test] - #[ignore] // manually run — fires a real API request and incurs cost + #[ignore] async fn res_720p() { let response = run_pipeline(GenerateVideoRequestBuilder { prompt: Some("A golden retriever catching a frisbee on the beach.".to_string()), @@ -131,7 +129,7 @@ mod tests { } #[tokio::test] - #[ignore] // manually run — fires a real API request and incurs cost + #[ignore] async fn res_1080p() { let response = run_pipeline(GenerateVideoRequestBuilder { prompt: Some("A fox walking through a snowy forest.".to_string()), @@ -143,13 +141,14 @@ mod tests { } } - // ── Modality tests ── + // -- Modality tests -- mod modality_tests { use super::*; + use test_data::web::image_media_tokens::TALL_MOCHI_WITH_GLASSES_PRODUCTION_MEDIA_TOKEN; #[tokio::test] - #[ignore] // manually run — fires a real API request and incurs cost + #[ignore] async fn text_to_video() { let response = run_pipeline(GenerateVideoRequestBuilder { prompt: Some("A whale breaching in the open ocean at dawn, cinematic.".to_string()), @@ -161,10 +160,8 @@ mod tests { } #[tokio::test] - #[ignore] // manually run — fires a real API request and incurs cost + #[ignore] async fn keyframe_start_and_end_frame() { - // Artcraft uses media file tokens directly — no URL resolution needed. - // Tokens from test_data::web::image_urls comments. let response = run_pipeline(GenerateVideoRequestBuilder { prompt: Some("The dog walks from the lake to the forest.".to_string()), start_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new(JUNO_AT_LAKE_PRODUCTION_MEDIA_TOKEN.to_string()))), @@ -177,15 +174,13 @@ mod tests { } #[tokio::test] - #[ignore] // manually run — fires a real API request and incurs cost + #[ignore] async fn image_references() { - // Artcraft uses media file tokens directly. - // Tokens from test_data::web::image_urls comments. let response = run_pipeline(GenerateVideoRequestBuilder { prompt: Some("The dog in @2 runs through the scenery in @1 towards the building in @3.".to_string()), reference_images: Some(ImageListRef::MediaFileTokens(vec![ MediaFileToken::new(FOREST_BACKDROP_PRODUCTION_MEDIA_TOKEN.to_string()), - MediaFileToken::new(JUNO_AT_LAKE_PRODUCTION_MEDIA_TOKEN.to_string()), + MediaFileToken::new(TALL_MOCHI_WITH_GLASSES_PRODUCTION_MEDIA_TOKEN.to_string()), MediaFileToken::new(WHITE_HOUSE_SUNSET_PRODUCTION_MEDIA_TOKEN.to_string()), ])), aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), @@ -196,25 +191,8 @@ mod tests { } #[tokio::test] - #[ignore] // manually run — fires a real API request and incurs cost - async fn video_references() { - // TODO: Needs a real video media file token to test properly. - let response = run_pipeline(GenerateVideoRequestBuilder { - prompt: Some("Change @video1 to a nighttime scene with moonlight.".to_string()), - reference_videos: Some(VideoListRef::MediaFileTokens(vec![ - MediaFileToken::new(JUNO_AT_LAKE_PRODUCTION_MEDIA_TOKEN.to_string()), // Using an image token as placeholder - ])), - aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), - ..artcraft_builder() - }).await; - assert!(matches!(response, GenerateVideoResponse::Artcraft(_))); - assert_eq!(1, 2, "Inspect output above"); - } - - #[tokio::test] - #[ignore] // manually run — fires a real API request and incurs cost + #[ignore] async fn character_references() { - // Character tokens are passed through directly to the Artcraft API. let response = run_pipeline(GenerateVideoRequestBuilder { prompt: Some("@Jim and @Knight are sparring in a medieval arena.".to_string()), reference_character_tokens: Some(CharacterListRef::CharacterTokens(vec![ @@ -227,11 +205,9 @@ mod tests { assert!(matches!(response, GenerateVideoResponse::Artcraft(_))); assert_eq!(1, 2, "Inspect output above"); } - - // TODO: Add audio reference test once a media file token for a test audio file is available. } - // ── Helpers ── + // -- Helpers -- fn artcraft_builder() -> GenerateVideoRequestBuilder { GenerateVideoRequestBuilder { diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_fast/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_fast/build.rs index f5e587b8d5..a5948cc389 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_fast/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_fast/build.rs @@ -50,8 +50,8 @@ pub fn build_artcraft_seedance_2p0_fast(mut builder: GenerateVideoRequestBuilder duration_seconds: duration_seconds.map(|d| d as u16), video_batch_count: Some(batch_count), negative_prompt: None, - quality: None, generate_audio: None, + quality: None, }; let state = ArtcraftSeedance2p0FastRequestState { request };