From 625ba414237edb378aa18fc2bdf9274246763d5c Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 21 Feb 2026 09:54:14 +0700 Subject: [PATCH] shouldn't be impacting --- .../src/generate/video/multi_function/mod.rs | 1 + .../seedance_2p0_multi_function_video_gen.rs | 68 +++++++++++++++ .../generate/video/multi_function/mod.rs | 1 + .../seedance_2p0_multi_function_video_gen.rs | 18 ++++ .../requests/generate_video/generate_video.rs | 86 +++++++++++++++++++ 5 files changed, 174 insertions(+) create mode 100644 crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/seedance_2p0_multi_function_video_gen.rs create mode 100644 crates/api_clients/artcraft/storyteller_client/src/endpoints/generate/video/multi_function/seedance_2p0_multi_function_video_gen.rs diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/mod.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/mod.rs index faf3e5d7c2..f8821c8a2c 100644 --- a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/mod.rs +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/mod.rs @@ -1,5 +1,6 @@ pub mod kling_2_5_turbo_multi_function_video_gen; pub mod kling_2_6_multi_function_video_gen; +pub mod seedance_2p0_multi_function_video_gen; pub mod sora_2_multi_function_video_gen; pub mod sora_2_pro_multi_function_video_gen; pub mod veo_3p1_fast_multi_function_video_gen; diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/seedance_2p0_multi_function_video_gen.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/seedance_2p0_multi_function_video_gen.rs new file mode 100644 index 0000000000..07bb3cd880 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/generate/video/multi_function/seedance_2p0_multi_function_video_gen.rs @@ -0,0 +1,68 @@ +use serde::{Deserialize, Serialize}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use tokens::tokens::media_files::MediaFileToken; +use utoipa::ToSchema; + +pub const SEEDANCE_2P0_MULTI_FUNCTION_VIDEO_GEN_PATH: &str = "/v1/generate/video/multi_function/seedance_2p0"; + +/// Seedance 2.0 multi-function video generation (text-to-video, image-to-video, and reference-based). +#[derive(Serialize, Deserialize, ToSchema)] +pub struct Seedance2p0MultiFunctionVideoGenRequest { + /// Idempotency token to prevent duplicate requests. + pub uuid_idempotency_token: String, + + /// Text prompt. + pub prompt: Option, + + /// Optional start frame image. + pub start_frame_media_token: Option, + + /// Optional end frame image. + pub end_frame_media_token: Option, + + /// Optional reference images (reference mode). When present, takes priority over start/end frames. + pub reference_image_media_tokens: Option>, + + /// Video aspect ratio / resolution. + pub aspect_ratio: Option, + + /// Duration in seconds (4–15). + pub duration_seconds: Option, + + /// Number of videos to generate. + pub batch_count: Option, +} + +#[derive(Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum Seedance2p0AspectRatio { + /// 16:9 landscape (1280x720) + Landscape16x9, + /// 9:16 portrait (720x1280) + Portrait9x16, + /// 1:1 square (720x720) + Square1x1, + /// 4:3 standard (960x720) + Standard4x3, + /// 3:4 portrait (720x960) + Portrait3x4, +} + +#[derive(Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum Seedance2p0BatchCount { + One, + Two, + Four, +} + +#[derive(Serialize, Deserialize, ToSchema)] +pub struct Seedance2p0MultiFunctionVideoGenResponse { + pub success: bool, + + /// The first inference job token (always present). + pub inference_job_token: InferenceJobToken, + + /// All inference job tokens for the batch (includes `inference_job_token` as the first entry). + pub all_inference_job_tokens: Vec, +} diff --git a/crates/api_clients/artcraft/storyteller_client/src/endpoints/generate/video/multi_function/mod.rs b/crates/api_clients/artcraft/storyteller_client/src/endpoints/generate/video/multi_function/mod.rs index 6c8d7eea98..afead5fe24 100644 --- a/crates/api_clients/artcraft/storyteller_client/src/endpoints/generate/video/multi_function/mod.rs +++ b/crates/api_clients/artcraft/storyteller_client/src/endpoints/generate/video/multi_function/mod.rs @@ -1,4 +1,5 @@ pub mod kling_2p5_turbo_pro_multi_function_video_gen; +pub mod seedance_2p0_multi_function_video_gen; pub mod kling_2p6_pro_multi_function_video_gen; pub mod sora_2_multi_function_video_gen; pub mod sora_2_pro_multi_function_video_gen; diff --git a/crates/api_clients/artcraft/storyteller_client/src/endpoints/generate/video/multi_function/seedance_2p0_multi_function_video_gen.rs b/crates/api_clients/artcraft/storyteller_client/src/endpoints/generate/video/multi_function/seedance_2p0_multi_function_video_gen.rs new file mode 100644 index 0000000000..b3c8c8b73a --- /dev/null +++ b/crates/api_clients/artcraft/storyteller_client/src/endpoints/generate/video/multi_function/seedance_2p0_multi_function_video_gen.rs @@ -0,0 +1,18 @@ +use crate::credentials::storyteller_credential_set::StorytellerCredentialSet; +use crate::error::storyteller_error::StorytellerError; +use crate::utils::api_host::ApiHost; +use crate::utils::basic_json_post_request::basic_json_post_request; +use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{Seedance2p0MultiFunctionVideoGenRequest, Seedance2p0MultiFunctionVideoGenResponse, SEEDANCE_2P0_MULTI_FUNCTION_VIDEO_GEN_PATH}; + +pub async fn seedance_2p0_multi_function_video_gen( + api_host: &ApiHost, + maybe_creds: Option<&StorytellerCredentialSet>, + request: Seedance2p0MultiFunctionVideoGenRequest, +) -> Result { + Ok(basic_json_post_request( + api_host, + SEEDANCE_2P0_MULTI_FUNCTION_VIDEO_GEN_PATH, + maybe_creds, + request, + ).await?) +} diff --git a/crates/api_clients/seedance2pro/src/requests/generate_video/generate_video.rs b/crates/api_clients/seedance2pro/src/requests/generate_video/generate_video.rs index fadf37d8e8..126bc52384 100644 --- a/crates/api_clients/seedance2pro/src/requests/generate_video/generate_video.rs +++ b/crates/api_clients/seedance2pro/src/requests/generate_video/generate_video.rs @@ -35,6 +35,33 @@ pub struct GenerateVideoArgs<'a> { pub reference_image_urls: Option>, } +impl GenerateVideoArgs<'_> { + /// Estimates the credit cost for this generation request. + /// + /// Pricing rules: + /// - 40 credits per second of video (4s = 160, 15s = 600) + /// - Resolution has no effect on cost + /// - Input mode (text, keyframe, reference) has no effect on cost + /// - Batch 1 = 1×, Batch 2 = 2×, Batch 4 = 3× (not 4×) + pub fn estimate_credits(&self) -> u32 { + let per_video = u32::from(self.duration_seconds) * 40; + let batch_multiplier = match self.batch_count { + BatchCount::One => 1, + BatchCount::Two => 2, + BatchCount::Four => 3, // NB: 3x, not 4x. + }; + per_video * batch_multiplier + } + + pub fn estimate_cost_in_usd_cents(&self) -> u64 { + // 25000 Credits costs $99.99 as of 2026-02-20 + // 250 for $1. + let credits = self.estimate_credits() as f64; + let cost = credits / 250.0 * 100.0; + cost.round() as u64 + } +} + // --- Public enums --- /// Video resolution / aspect ratio. @@ -220,6 +247,65 @@ mod tests { use errors::AnyhowResult; use log::LevelFilter; + fn dummy_session() -> Seedance2ProSession { + Seedance2ProSession::from_cookies_string(String::new()) + } + + fn args_with(duration_seconds: u8, batch_count: BatchCount) -> GenerateVideoArgs<'static> { + // Safety: the dummy session is leaked so the reference is 'static for test purposes. + let session = Box::leak(Box::new(dummy_session())); + GenerateVideoArgs { + session, + prompt: String::new(), + resolution: Resolution::Square1x1, + duration_seconds, + batch_count, + start_frame_url: None, + end_frame_url: None, + reference_image_urls: None, + } + } + + #[test] + fn test_estimate_credits() { + // 40 credits per second, batch 1 + assert_eq!(args_with(4, BatchCount::One).estimate_credits(), 160); + assert_eq!(args_with(5, BatchCount::One).estimate_credits(), 200); + assert_eq!(args_with(6, BatchCount::One).estimate_credits(), 240); + assert_eq!(args_with(7, BatchCount::One).estimate_credits(), 280); + assert_eq!(args_with(15, BatchCount::One).estimate_credits(), 600); + + // Batch 2 = 2× + assert_eq!(args_with(4, BatchCount::Two).estimate_credits(), 320); + assert_eq!(args_with(5, BatchCount::Two).estimate_credits(), 400); + assert_eq!(args_with(15, BatchCount::Two).estimate_credits(), 1200); + + // Batch 4 = 3× (not 4×) + assert_eq!(args_with(4, BatchCount::Four).estimate_credits(), 480); + assert_eq!(args_with(5, BatchCount::Four).estimate_credits(), 600); + assert_eq!(args_with(15, BatchCount::Four).estimate_credits(), 1800); + } + + #[test] + fn test_estimate_cost_usd_cents() { + // 40 credits per second, batch 1 + assert_eq!(args_with(4, BatchCount::One).estimate_cost_in_usd_cents(), 64); + assert_eq!(args_with(5, BatchCount::One).estimate_cost_in_usd_cents(), 80); + assert_eq!(args_with(6, BatchCount::One).estimate_cost_in_usd_cents(), 96); + assert_eq!(args_with(7, BatchCount::One).estimate_cost_in_usd_cents(), 112); + assert_eq!(args_with(15, BatchCount::One).estimate_cost_in_usd_cents(), 240); + + // Batch 2 = 2× + assert_eq!(args_with(4, BatchCount::Two).estimate_cost_in_usd_cents(), 128); + assert_eq!(args_with(5, BatchCount::Two).estimate_cost_in_usd_cents(), 160); + assert_eq!(args_with(15, BatchCount::Two).estimate_cost_in_usd_cents(), 480); + + // Batch 4 = 3× (not 4×) + assert_eq!(args_with(4, BatchCount::Four).estimate_cost_in_usd_cents(), 192); + assert_eq!(args_with(5, BatchCount::Four).estimate_cost_in_usd_cents(), 240); + assert_eq!(args_with(15, BatchCount::Four).estimate_cost_in_usd_cents(), 720); + } + fn test_session() -> AnyhowResult { let cookies = get_test_cookies()?; Ok(Seedance2ProSession::from_cookies_string(cookies))