diff --git a/crates/api_clients/comet_api_client/src/generate/mod.rs b/crates/api_clients/comet_api_client/src/generate/mod.rs new file mode 100644 index 0000000000..b04b953edb --- /dev/null +++ b/crates/api_clients/comet_api_client/src/generate/mod.rs @@ -0,0 +1 @@ +pub mod video; diff --git a/crates/api_clients/comet_api_client/src/generate/video/generate_doubao_seedance_1p0_pro.rs b/crates/api_clients/comet_api_client/src/generate/video/generate_doubao_seedance_1p0_pro.rs new file mode 100644 index 0000000000..878b500cd2 --- /dev/null +++ b/crates/api_clients/comet_api_client/src/generate/video/generate_doubao_seedance_1p0_pro.rs @@ -0,0 +1,146 @@ +use crate::creds::comet_api_key::CometApiKey; +use crate::error::comet_client_error::CometClientError; +use crate::error::comet_error::CometError; +use crate::requests::create_video::create_video::{ + create_video, CometVideoModelRaw, CometVideoSize, CreateVideoArgs, CreateVideoRequest, +}; +use crate::requests::video_task_status::CometVideoTaskStatus; + +/// Seedance 1.0 Pro supports 2-10 second durations. +const MIN_DURATION_SECONDS: u8 = 2; +const MAX_DURATION_SECONDS: u8 = 10; + +// ── Args ── + +pub struct GenerateDoubaoSeedance1p0ProArgs<'a> { + pub request: GenerateDoubaoSeedance1p0ProRequest, + pub api_key: &'a CometApiKey, +} + +// ── Request ── + +/// NB: Seedance 1.0 Pro is text-only — `input_reference` images are not +/// supported, so this request deliberately has no image field. +#[derive(Clone, Debug)] +pub struct GenerateDoubaoSeedance1p0ProRequest { + pub prompt: String, + + /// 2-10 seconds. `None` uses the API default (5). + pub duration_seconds: Option, + + pub size: Option, +} + +// ── Enums ── + +#[derive(Debug, Clone, Copy)] +pub enum DoubaoSeedance1p0ProSize { + Landscape16x9, + Standard4x3, + Square1x1, + Portrait3x4, + Portrait9x16, + UltraWide21x9, + Exact { width: u32, height: u32 }, +} + +// ── Response ── + +pub struct GenerateDoubaoSeedance1p0ProResponse { + /// Poll `GET /v1/videos/{task_id}` until terminal. + pub task_id: String, + pub status: CometVideoTaskStatus, +} + +// ── Entry point ── + +pub async fn generate_doubao_seedance_1p0_pro( + args: GenerateDoubaoSeedance1p0ProArgs<'_>, +) -> Result { + let raw_request = args.request.to_create_video_request()?; + + let result = create_video(CreateVideoArgs { + api_key: args.api_key, + request: raw_request, + }).await?; + + Ok(GenerateDoubaoSeedance1p0ProResponse { + task_id: result.task_id, + status: result.status, + }) +} + +impl GenerateDoubaoSeedance1p0ProRequest { + /// Validate and lower to the generic wire request. + pub fn to_create_video_request(&self) -> Result { + if let Some(seconds) = self.duration_seconds { + if !(MIN_DURATION_SECONDS..=MAX_DURATION_SECONDS).contains(&seconds) { + return Err(CometClientError::InvalidRequestField { + field: "duration_seconds", + raw_value: seconds.to_string(), + reason: format!("Seedance 1.0 Pro supports {MIN_DURATION_SECONDS}-{MAX_DURATION_SECONDS} second durations"), + }); + } + } + + Ok(CreateVideoRequest { + model: CometVideoModelRaw::DoubaoSeedance1p0Pro, + prompt: self.prompt.clone(), + maybe_seconds: self.duration_seconds, + maybe_size: self.size.map(map_size), + input_reference_images: vec![], + }) + } +} + +fn map_size(size: DoubaoSeedance1p0ProSize) -> CometVideoSize { + match size { + DoubaoSeedance1p0ProSize::Landscape16x9 => CometVideoSize::Landscape16x9, + DoubaoSeedance1p0ProSize::Standard4x3 => CometVideoSize::Standard4x3, + DoubaoSeedance1p0ProSize::Square1x1 => CometVideoSize::Square, + DoubaoSeedance1p0ProSize::Portrait3x4 => CometVideoSize::Portrait3x4, + DoubaoSeedance1p0ProSize::Portrait9x16 => CometVideoSize::Portrait9x16, + DoubaoSeedance1p0ProSize::UltraWide21x9 => CometVideoSize::UltraWide21x9, + DoubaoSeedance1p0ProSize::Exact { width, height } => CometVideoSize::Exact { width, height }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn maps_to_wire_request() { + let request = GenerateDoubaoSeedance1p0ProRequest { + prompt: "time lapse of a city at dusk".to_string(), + duration_seconds: Some(2), + size: Some(DoubaoSeedance1p0ProSize::Square1x1), + }; + + let raw = request.to_create_video_request().expect("should validate"); + assert_eq!(raw.text_form_fields(), vec![ + ("model", "doubao-seedance-1-0-pro".to_string()), + ("prompt", "time lapse of a city at dusk".to_string()), + ("seconds", "2".to_string()), + ("size", "1:1".to_string()), + ]); + assert!(raw.input_reference_images.is_empty()); + } + + #[test] + fn duration_bounds() { + assert!(request_with_seconds(Some(1)).to_create_video_request().is_err()); + assert!(request_with_seconds(Some(2)).to_create_video_request().is_ok()); + assert!(request_with_seconds(Some(10)).to_create_video_request().is_ok()); + assert!(request_with_seconds(Some(11)).to_create_video_request().is_err()); + assert!(request_with_seconds(None).to_create_video_request().is_ok()); + } + + fn request_with_seconds(duration_seconds: Option) -> GenerateDoubaoSeedance1p0ProRequest { + GenerateDoubaoSeedance1p0ProRequest { + prompt: "ok".to_string(), + duration_seconds, + size: None, + } + } +} diff --git a/crates/api_clients/comet_api_client/src/generate/video/generate_doubao_seedance_1p5_pro.rs b/crates/api_clients/comet_api_client/src/generate/video/generate_doubao_seedance_1p5_pro.rs new file mode 100644 index 0000000000..216d2c06a9 --- /dev/null +++ b/crates/api_clients/comet_api_client/src/generate/video/generate_doubao_seedance_1p5_pro.rs @@ -0,0 +1,146 @@ +use crate::creds::comet_api_key::CometApiKey; +use crate::error::comet_client_error::CometClientError; +use crate::error::comet_error::CometError; +use crate::requests::create_video::create_video::{ + create_video, CometVideoModelRaw, CometVideoSize, CreateVideoArgs, CreateVideoRequest, +}; +use crate::requests::video_task_status::CometVideoTaskStatus; + +/// Seedance 1.5 Pro supports 4-12 second durations. +const MIN_DURATION_SECONDS: u8 = 4; +const MAX_DURATION_SECONDS: u8 = 12; + +// ── Args ── + +pub struct GenerateDoubaoSeedance1p5ProArgs<'a> { + pub request: GenerateDoubaoSeedance1p5ProRequest, + pub api_key: &'a CometApiKey, +} + +// ── Request ── + +/// NB: Seedance 1.5 Pro is text-only — `input_reference` images are not +/// supported, so this request deliberately has no image field. +#[derive(Clone, Debug)] +pub struct GenerateDoubaoSeedance1p5ProRequest { + pub prompt: String, + + /// 4-12 seconds. `None` uses the API default (5). + pub duration_seconds: Option, + + pub size: Option, +} + +// ── Enums ── + +#[derive(Debug, Clone, Copy)] +pub enum DoubaoSeedance1p5ProSize { + Landscape16x9, + Standard4x3, + Square1x1, + Portrait3x4, + Portrait9x16, + UltraWide21x9, + Exact { width: u32, height: u32 }, +} + +// ── Response ── + +pub struct GenerateDoubaoSeedance1p5ProResponse { + /// Poll `GET /v1/videos/{task_id}` until terminal. + pub task_id: String, + pub status: CometVideoTaskStatus, +} + +// ── Entry point ── + +pub async fn generate_doubao_seedance_1p5_pro( + args: GenerateDoubaoSeedance1p5ProArgs<'_>, +) -> Result { + let raw_request = args.request.to_create_video_request()?; + + let result = create_video(CreateVideoArgs { + api_key: args.api_key, + request: raw_request, + }).await?; + + Ok(GenerateDoubaoSeedance1p5ProResponse { + task_id: result.task_id, + status: result.status, + }) +} + +impl GenerateDoubaoSeedance1p5ProRequest { + /// Validate and lower to the generic wire request. + pub fn to_create_video_request(&self) -> Result { + if let Some(seconds) = self.duration_seconds { + if !(MIN_DURATION_SECONDS..=MAX_DURATION_SECONDS).contains(&seconds) { + return Err(CometClientError::InvalidRequestField { + field: "duration_seconds", + raw_value: seconds.to_string(), + reason: format!("Seedance 1.5 Pro supports {MIN_DURATION_SECONDS}-{MAX_DURATION_SECONDS} second durations"), + }); + } + } + + Ok(CreateVideoRequest { + model: CometVideoModelRaw::DoubaoSeedance1p5Pro, + prompt: self.prompt.clone(), + maybe_seconds: self.duration_seconds, + maybe_size: self.size.map(map_size), + input_reference_images: vec![], + }) + } +} + +fn map_size(size: DoubaoSeedance1p5ProSize) -> CometVideoSize { + match size { + DoubaoSeedance1p5ProSize::Landscape16x9 => CometVideoSize::Landscape16x9, + DoubaoSeedance1p5ProSize::Standard4x3 => CometVideoSize::Standard4x3, + DoubaoSeedance1p5ProSize::Square1x1 => CometVideoSize::Square, + DoubaoSeedance1p5ProSize::Portrait3x4 => CometVideoSize::Portrait3x4, + DoubaoSeedance1p5ProSize::Portrait9x16 => CometVideoSize::Portrait9x16, + DoubaoSeedance1p5ProSize::UltraWide21x9 => CometVideoSize::UltraWide21x9, + DoubaoSeedance1p5ProSize::Exact { width, height } => CometVideoSize::Exact { width, height }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn maps_to_wire_request() { + let request = GenerateDoubaoSeedance1p5ProRequest { + prompt: "a paper boat in a storm drain".to_string(), + duration_seconds: Some(12), + size: Some(DoubaoSeedance1p5ProSize::Portrait9x16), + }; + + let raw = request.to_create_video_request().expect("should validate"); + assert_eq!(raw.text_form_fields(), vec![ + ("model", "doubao-seedance-1-5-pro".to_string()), + ("prompt", "a paper boat in a storm drain".to_string()), + ("seconds", "12".to_string()), + ("size", "9:16".to_string()), + ]); + assert!(raw.input_reference_images.is_empty()); + } + + #[test] + fn duration_bounds() { + assert!(request_with_seconds(Some(3)).to_create_video_request().is_err()); + assert!(request_with_seconds(Some(4)).to_create_video_request().is_ok()); + assert!(request_with_seconds(Some(12)).to_create_video_request().is_ok()); + assert!(request_with_seconds(Some(13)).to_create_video_request().is_err()); + assert!(request_with_seconds(None).to_create_video_request().is_ok()); + } + + fn request_with_seconds(duration_seconds: Option) -> GenerateDoubaoSeedance1p5ProRequest { + GenerateDoubaoSeedance1p5ProRequest { + prompt: "ok".to_string(), + duration_seconds, + size: None, + } + } +} diff --git a/crates/api_clients/comet_api_client/src/generate/video/generate_doubao_seedance_2p0.rs b/crates/api_clients/comet_api_client/src/generate/video/generate_doubao_seedance_2p0.rs new file mode 100644 index 0000000000..69a6447ec3 --- /dev/null +++ b/crates/api_clients/comet_api_client/src/generate/video/generate_doubao_seedance_2p0.rs @@ -0,0 +1,150 @@ +use crate::creds::comet_api_key::CometApiKey; +use crate::error::comet_client_error::CometClientError; +use crate::error::comet_error::CometError; +use crate::requests::create_video::create_video::{ + create_video, CometInputReferenceImage, CometVideoModelRaw, CometVideoSize, + CreateVideoArgs, CreateVideoRequest, +}; +use crate::requests::video_task_status::CometVideoTaskStatus; + +/// Seedance 2.0 supports 4-15 second durations. +const MIN_DURATION_SECONDS: u8 = 4; +const MAX_DURATION_SECONDS: u8 = 15; + +// ── Args ── + +pub struct GenerateDoubaoSeedance2p0Args<'a> { + pub request: GenerateDoubaoSeedance2p0Request, + pub api_key: &'a CometApiKey, +} + +// ── Request ── + +#[derive(Clone, Debug)] +pub struct GenerateDoubaoSeedance2p0Request { + /// Reference attached images in the prompt as `[Image 1]`, `[Image 2]`, etc. + pub prompt: String, + + /// 4-15 seconds. `None` uses the API default (5). + pub duration_seconds: Option, + + pub size: Option, + + /// Reference images (JPEG, PNG, WebP) for image-to-video. + pub input_reference_images: Vec, +} + +// ── Enums ── + +#[derive(Debug, Clone, Copy)] +pub enum DoubaoSeedance2p0Size { + Landscape16x9, + Standard4x3, + Square1x1, + Portrait3x4, + Portrait9x16, + UltraWide21x9, + Exact { width: u32, height: u32 }, +} + +// ── Response ── + +pub struct GenerateDoubaoSeedance2p0Response { + /// Poll `GET /v1/videos/{task_id}` until terminal. + pub task_id: String, + pub status: CometVideoTaskStatus, +} + +// ── Entry point ── + +pub async fn generate_doubao_seedance_2p0( + args: GenerateDoubaoSeedance2p0Args<'_>, +) -> Result { + let raw_request = args.request.to_create_video_request()?; + + let result = create_video(CreateVideoArgs { + api_key: args.api_key, + request: raw_request, + }).await?; + + Ok(GenerateDoubaoSeedance2p0Response { + task_id: result.task_id, + status: result.status, + }) +} + +impl GenerateDoubaoSeedance2p0Request { + /// Validate and lower to the generic wire request. + pub fn to_create_video_request(&self) -> Result { + if let Some(seconds) = self.duration_seconds { + if !(MIN_DURATION_SECONDS..=MAX_DURATION_SECONDS).contains(&seconds) { + return Err(CometClientError::InvalidRequestField { + field: "duration_seconds", + raw_value: seconds.to_string(), + reason: format!("Seedance 2.0 supports {MIN_DURATION_SECONDS}-{MAX_DURATION_SECONDS} second durations"), + }); + } + } + + Ok(CreateVideoRequest { + model: CometVideoModelRaw::DoubaoSeedance2p0, + prompt: self.prompt.clone(), + maybe_seconds: self.duration_seconds, + maybe_size: self.size.map(map_size), + input_reference_images: self.input_reference_images.clone(), + }) + } +} + +fn map_size(size: DoubaoSeedance2p0Size) -> CometVideoSize { + match size { + DoubaoSeedance2p0Size::Landscape16x9 => CometVideoSize::Landscape16x9, + DoubaoSeedance2p0Size::Standard4x3 => CometVideoSize::Standard4x3, + DoubaoSeedance2p0Size::Square1x1 => CometVideoSize::Square, + DoubaoSeedance2p0Size::Portrait3x4 => CometVideoSize::Portrait3x4, + DoubaoSeedance2p0Size::Portrait9x16 => CometVideoSize::Portrait9x16, + DoubaoSeedance2p0Size::UltraWide21x9 => CometVideoSize::UltraWide21x9, + DoubaoSeedance2p0Size::Exact { width, height } => CometVideoSize::Exact { width, height }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn maps_to_wire_request() { + let request = GenerateDoubaoSeedance2p0Request { + prompt: "animate [Image 1]".to_string(), + duration_seconds: Some(8), + size: Some(DoubaoSeedance2p0Size::UltraWide21x9), + input_reference_images: vec![], + }; + + let raw = request.to_create_video_request().expect("should validate"); + assert_eq!(raw.text_form_fields(), vec![ + ("model", "doubao-seedance-2-0".to_string()), + ("prompt", "animate [Image 1]".to_string()), + ("seconds", "8".to_string()), + ("size", "21:9".to_string()), + ]); + } + + #[test] + fn duration_bounds() { + assert!(request_with_seconds(Some(3)).to_create_video_request().is_err()); + assert!(request_with_seconds(Some(4)).to_create_video_request().is_ok()); + assert!(request_with_seconds(Some(15)).to_create_video_request().is_ok()); + assert!(request_with_seconds(Some(16)).to_create_video_request().is_err()); + assert!(request_with_seconds(None).to_create_video_request().is_ok()); + } + + fn request_with_seconds(duration_seconds: Option) -> GenerateDoubaoSeedance2p0Request { + GenerateDoubaoSeedance2p0Request { + prompt: "ok".to_string(), + duration_seconds, + size: None, + input_reference_images: vec![], + } + } +} diff --git a/crates/api_clients/comet_api_client/src/generate/video/generate_doubao_seedance_2p0_fast.rs b/crates/api_clients/comet_api_client/src/generate/video/generate_doubao_seedance_2p0_fast.rs new file mode 100644 index 0000000000..ab50b9f3f8 --- /dev/null +++ b/crates/api_clients/comet_api_client/src/generate/video/generate_doubao_seedance_2p0_fast.rs @@ -0,0 +1,150 @@ +use crate::creds::comet_api_key::CometApiKey; +use crate::error::comet_client_error::CometClientError; +use crate::error::comet_error::CometError; +use crate::requests::create_video::create_video::{ + create_video, CometInputReferenceImage, CometVideoModelRaw, CometVideoSize, + CreateVideoArgs, CreateVideoRequest, +}; +use crate::requests::video_task_status::CometVideoTaskStatus; + +/// Seedance 2.0 Fast supports 4-15 second durations. +const MIN_DURATION_SECONDS: u8 = 4; +const MAX_DURATION_SECONDS: u8 = 15; + +// ── Args ── + +pub struct GenerateDoubaoSeedance2p0FastArgs<'a> { + pub request: GenerateDoubaoSeedance2p0FastRequest, + pub api_key: &'a CometApiKey, +} + +// ── Request ── + +#[derive(Clone, Debug)] +pub struct GenerateDoubaoSeedance2p0FastRequest { + /// Reference attached images in the prompt as `[Image 1]`, `[Image 2]`, etc. + pub prompt: String, + + /// 4-15 seconds. `None` uses the API default (5). + pub duration_seconds: Option, + + pub size: Option, + + /// Reference images (JPEG, PNG, WebP) for image-to-video. + pub input_reference_images: Vec, +} + +// ── Enums ── + +#[derive(Debug, Clone, Copy)] +pub enum DoubaoSeedance2p0FastSize { + Landscape16x9, + Standard4x3, + Square1x1, + Portrait3x4, + Portrait9x16, + UltraWide21x9, + Exact { width: u32, height: u32 }, +} + +// ── Response ── + +pub struct GenerateDoubaoSeedance2p0FastResponse { + /// Poll `GET /v1/videos/{task_id}` until terminal. + pub task_id: String, + pub status: CometVideoTaskStatus, +} + +// ── Entry point ── + +pub async fn generate_doubao_seedance_2p0_fast( + args: GenerateDoubaoSeedance2p0FastArgs<'_>, +) -> Result { + let raw_request = args.request.to_create_video_request()?; + + let result = create_video(CreateVideoArgs { + api_key: args.api_key, + request: raw_request, + }).await?; + + Ok(GenerateDoubaoSeedance2p0FastResponse { + task_id: result.task_id, + status: result.status, + }) +} + +impl GenerateDoubaoSeedance2p0FastRequest { + /// Validate and lower to the generic wire request. + pub fn to_create_video_request(&self) -> Result { + if let Some(seconds) = self.duration_seconds { + if !(MIN_DURATION_SECONDS..=MAX_DURATION_SECONDS).contains(&seconds) { + return Err(CometClientError::InvalidRequestField { + field: "duration_seconds", + raw_value: seconds.to_string(), + reason: format!("Seedance 2.0 Fast supports {MIN_DURATION_SECONDS}-{MAX_DURATION_SECONDS} second durations"), + }); + } + } + + Ok(CreateVideoRequest { + model: CometVideoModelRaw::DoubaoSeedance2p0Fast, + prompt: self.prompt.clone(), + maybe_seconds: self.duration_seconds, + maybe_size: self.size.map(map_size), + input_reference_images: self.input_reference_images.clone(), + }) + } +} + +fn map_size(size: DoubaoSeedance2p0FastSize) -> CometVideoSize { + match size { + DoubaoSeedance2p0FastSize::Landscape16x9 => CometVideoSize::Landscape16x9, + DoubaoSeedance2p0FastSize::Standard4x3 => CometVideoSize::Standard4x3, + DoubaoSeedance2p0FastSize::Square1x1 => CometVideoSize::Square, + DoubaoSeedance2p0FastSize::Portrait3x4 => CometVideoSize::Portrait3x4, + DoubaoSeedance2p0FastSize::Portrait9x16 => CometVideoSize::Portrait9x16, + DoubaoSeedance2p0FastSize::UltraWide21x9 => CometVideoSize::UltraWide21x9, + DoubaoSeedance2p0FastSize::Exact { width, height } => CometVideoSize::Exact { width, height }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn maps_to_wire_request() { + let request = GenerateDoubaoSeedance2p0FastRequest { + prompt: "a corgi surfing".to_string(), + duration_seconds: Some(4), + size: Some(DoubaoSeedance2p0FastSize::Landscape16x9), + input_reference_images: vec![], + }; + + let raw = request.to_create_video_request().expect("should validate"); + assert_eq!(raw.text_form_fields(), vec![ + ("model", "doubao-seedance-2-0-fast".to_string()), + ("prompt", "a corgi surfing".to_string()), + ("seconds", "4".to_string()), + ("size", "16:9".to_string()), + ]); + } + + #[test] + fn duration_bounds() { + assert!(request_with_seconds(Some(3)).to_create_video_request().is_err()); + assert!(request_with_seconds(Some(4)).to_create_video_request().is_ok()); + assert!(request_with_seconds(Some(15)).to_create_video_request().is_ok()); + assert!(request_with_seconds(Some(16)).to_create_video_request().is_err()); + assert!(request_with_seconds(None).to_create_video_request().is_ok()); + } + + fn request_with_seconds(duration_seconds: Option) -> GenerateDoubaoSeedance2p0FastRequest { + GenerateDoubaoSeedance2p0FastRequest { + prompt: "ok".to_string(), + duration_seconds, + size: None, + input_reference_images: vec![], + } + } +} diff --git a/crates/api_clients/comet_api_client/src/generate/video/generate_vidu_q3.rs b/crates/api_clients/comet_api_client/src/generate/video/generate_vidu_q3.rs new file mode 100644 index 0000000000..80e9baa618 --- /dev/null +++ b/crates/api_clients/comet_api_client/src/generate/video/generate_vidu_q3.rs @@ -0,0 +1,162 @@ +use crate::creds::comet_api_key::CometApiKey; +use crate::error::comet_client_error::CometClientError; +use crate::error::comet_error::CometError; +use crate::requests::create_video::create_video::{ + create_video, CometInputReferenceImage, CometVideoModelRaw, CometVideoSize, + CreateVideoArgs, CreateVideoRequest, +}; +use crate::requests::video_task_status::CometVideoTaskStatus; + +/// Vidu Q3 supports 1-16 second durations. +const MIN_DURATION_SECONDS: u8 = 1; +const MAX_DURATION_SECONDS: u8 = 16; + +// ── Args ── + +pub struct GenerateViduQ3Args<'a> { + pub request: GenerateViduQ3Request, + pub api_key: &'a CometApiKey, +} + +// ── Request ── + +#[derive(Clone, Debug)] +pub struct GenerateViduQ3Request { + pub prompt: String, + + /// 1-16 seconds. `None` uses the API default (5). + pub duration_seconds: Option, + + pub size: Option, + + /// Reference images for image-to-video / start-end-to-video workflows. + pub input_reference_images: Vec, +} + +// ── Enums ── + +/// Vidu Q3 renders at 540p/720p/1080p (24 FPS). The API's `size` field takes +/// an aspect ratio preset or exact dimensions (eg. 960x528). +#[derive(Debug, Clone, Copy)] +pub enum ViduQ3Size { + Landscape16x9, + Standard4x3, + Square1x1, + Portrait3x4, + Portrait9x16, + Exact { width: u32, height: u32 }, +} + +// ── Pricing ── +// +// Vidu Q3 per-second USD pricing (via CometAPI): +// +// | Resolution | $/sec | +// |-------------|---------| +// | 360p/540p | 0.056 | +// | 720p/1080p | 0.1232 | +// +// Total cost = price per second × video duration (seconds). +// NB: Not modeled as an estimator because the `size` field is an aspect +// ratio in the common case, which doesn't determine the billed resolution. + +// ── Response ── + +pub struct GenerateViduQ3Response { + /// Poll `GET /v1/videos/{task_id}` until terminal. + pub task_id: String, + pub status: CometVideoTaskStatus, +} + +// ── Entry point ── + +pub async fn generate_vidu_q3( + args: GenerateViduQ3Args<'_>, +) -> Result { + let raw_request = args.request.to_create_video_request()?; + + let result = create_video(CreateVideoArgs { + api_key: args.api_key, + request: raw_request, + }).await?; + + Ok(GenerateViduQ3Response { + task_id: result.task_id, + status: result.status, + }) +} + +impl GenerateViduQ3Request { + /// Validate and lower to the generic wire request. + pub fn to_create_video_request(&self) -> Result { + if let Some(seconds) = self.duration_seconds { + if !(MIN_DURATION_SECONDS..=MAX_DURATION_SECONDS).contains(&seconds) { + return Err(CometClientError::InvalidRequestField { + field: "duration_seconds", + raw_value: seconds.to_string(), + reason: format!("Vidu Q3 supports {MIN_DURATION_SECONDS}-{MAX_DURATION_SECONDS} second durations"), + }); + } + } + + Ok(CreateVideoRequest { + model: CometVideoModelRaw::ViduQ3, + prompt: self.prompt.clone(), + maybe_seconds: self.duration_seconds, + maybe_size: self.size.map(map_size), + input_reference_images: self.input_reference_images.clone(), + }) + } +} + +fn map_size(size: ViduQ3Size) -> CometVideoSize { + match size { + ViduQ3Size::Landscape16x9 => CometVideoSize::Landscape16x9, + ViduQ3Size::Standard4x3 => CometVideoSize::Standard4x3, + ViduQ3Size::Square1x1 => CometVideoSize::Square, + ViduQ3Size::Portrait3x4 => CometVideoSize::Portrait3x4, + ViduQ3Size::Portrait9x16 => CometVideoSize::Portrait9x16, + ViduQ3Size::Exact { width, height } => CometVideoSize::Exact { width, height }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn maps_to_wire_request() { + let request = GenerateViduQ3Request { + prompt: "a lighthouse in fog".to_string(), + duration_seconds: Some(16), + size: Some(ViduQ3Size::Exact { width: 960, height: 528 }), + input_reference_images: vec![], + }; + + let raw = request.to_create_video_request().expect("should validate"); + assert_eq!(raw.text_form_fields(), vec![ + ("model", "viduq3".to_string()), + ("prompt", "a lighthouse in fog".to_string()), + ("seconds", "16".to_string()), + ("size", "960x528".to_string()), + ]); + } + + #[test] + fn duration_bounds() { + assert!(request_with_seconds(Some(0)).to_create_video_request().is_err()); + assert!(request_with_seconds(Some(1)).to_create_video_request().is_ok()); + assert!(request_with_seconds(Some(16)).to_create_video_request().is_ok()); + assert!(request_with_seconds(Some(17)).to_create_video_request().is_err()); + assert!(request_with_seconds(None).to_create_video_request().is_ok()); + } + + fn request_with_seconds(duration_seconds: Option) -> GenerateViduQ3Request { + GenerateViduQ3Request { + prompt: "ok".to_string(), + duration_seconds, + size: None, + input_reference_images: vec![], + } + } +} diff --git a/crates/api_clients/comet_api_client/src/generate/video/generate_vidu_q3_turbo.rs b/crates/api_clients/comet_api_client/src/generate/video/generate_vidu_q3_turbo.rs new file mode 100644 index 0000000000..325d3bc92d --- /dev/null +++ b/crates/api_clients/comet_api_client/src/generate/video/generate_vidu_q3_turbo.rs @@ -0,0 +1,162 @@ +use crate::creds::comet_api_key::CometApiKey; +use crate::error::comet_client_error::CometClientError; +use crate::error::comet_error::CometError; +use crate::requests::create_video::create_video::{ + create_video, CometInputReferenceImage, CometVideoModelRaw, CometVideoSize, + CreateVideoArgs, CreateVideoRequest, +}; +use crate::requests::video_task_status::CometVideoTaskStatus; + +/// Vidu Q3 Turbo supports 1-16 second durations. +const MIN_DURATION_SECONDS: u8 = 1; +const MAX_DURATION_SECONDS: u8 = 16; + +// ── Args ── + +pub struct GenerateViduQ3TurboArgs<'a> { + pub request: GenerateViduQ3TurboRequest, + pub api_key: &'a CometApiKey, +} + +// ── Request ── + +#[derive(Clone, Debug)] +pub struct GenerateViduQ3TurboRequest { + pub prompt: String, + + /// 1-16 seconds. `None` uses the API default (5). + pub duration_seconds: Option, + + pub size: Option, + + /// Reference images for image-to-video / start-end-to-video workflows. + pub input_reference_images: Vec, +} + +// ── Enums ── + +/// Vidu Q3 Turbo renders at 540p/720p/1080p (24 FPS). The API's `size` field +/// takes an aspect ratio preset or exact dimensions (eg. 960x528). +#[derive(Debug, Clone, Copy)] +pub enum ViduQ3TurboSize { + Landscape16x9, + Standard4x3, + Square1x1, + Portrait3x4, + Portrait9x16, + Exact { width: u32, height: u32 }, +} + +// ── Pricing ── +// +// Vidu Q3 Turbo per-second USD pricing (via CometAPI): +// +// | Resolution | $/sec | +// |-------------|---------| +// | 360p/540p | 0.028 | +// | 720p/1080p | 0.0616 | +// +// Total cost = price per second × video duration (seconds). +// NB: Not modeled as an estimator because the `size` field is an aspect +// ratio in the common case, which doesn't determine the billed resolution. + +// ── Response ── + +pub struct GenerateViduQ3TurboResponse { + /// Poll `GET /v1/videos/{task_id}` until terminal. + pub task_id: String, + pub status: CometVideoTaskStatus, +} + +// ── Entry point ── + +pub async fn generate_vidu_q3_turbo( + args: GenerateViduQ3TurboArgs<'_>, +) -> Result { + let raw_request = args.request.to_create_video_request()?; + + let result = create_video(CreateVideoArgs { + api_key: args.api_key, + request: raw_request, + }).await?; + + Ok(GenerateViduQ3TurboResponse { + task_id: result.task_id, + status: result.status, + }) +} + +impl GenerateViduQ3TurboRequest { + /// Validate and lower to the generic wire request. + pub fn to_create_video_request(&self) -> Result { + if let Some(seconds) = self.duration_seconds { + if !(MIN_DURATION_SECONDS..=MAX_DURATION_SECONDS).contains(&seconds) { + return Err(CometClientError::InvalidRequestField { + field: "duration_seconds", + raw_value: seconds.to_string(), + reason: format!("Vidu Q3 Turbo supports {MIN_DURATION_SECONDS}-{MAX_DURATION_SECONDS} second durations"), + }); + } + } + + Ok(CreateVideoRequest { + model: CometVideoModelRaw::ViduQ3Turbo, + prompt: self.prompt.clone(), + maybe_seconds: self.duration_seconds, + maybe_size: self.size.map(map_size), + input_reference_images: self.input_reference_images.clone(), + }) + } +} + +fn map_size(size: ViduQ3TurboSize) -> CometVideoSize { + match size { + ViduQ3TurboSize::Landscape16x9 => CometVideoSize::Landscape16x9, + ViduQ3TurboSize::Standard4x3 => CometVideoSize::Standard4x3, + ViduQ3TurboSize::Square1x1 => CometVideoSize::Square, + ViduQ3TurboSize::Portrait3x4 => CometVideoSize::Portrait3x4, + ViduQ3TurboSize::Portrait9x16 => CometVideoSize::Portrait9x16, + ViduQ3TurboSize::Exact { width, height } => CometVideoSize::Exact { width, height }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn maps_to_wire_request() { + let request = GenerateViduQ3TurboRequest { + prompt: "raindrops on a window".to_string(), + duration_seconds: Some(1), + size: Some(ViduQ3TurboSize::Portrait9x16), + input_reference_images: vec![], + }; + + let raw = request.to_create_video_request().expect("should validate"); + assert_eq!(raw.text_form_fields(), vec![ + ("model", "viduq3-turbo".to_string()), + ("prompt", "raindrops on a window".to_string()), + ("seconds", "1".to_string()), + ("size", "9:16".to_string()), + ]); + } + + #[test] + fn duration_bounds() { + assert!(request_with_seconds(Some(0)).to_create_video_request().is_err()); + assert!(request_with_seconds(Some(1)).to_create_video_request().is_ok()); + assert!(request_with_seconds(Some(16)).to_create_video_request().is_ok()); + assert!(request_with_seconds(Some(17)).to_create_video_request().is_err()); + assert!(request_with_seconds(None).to_create_video_request().is_ok()); + } + + fn request_with_seconds(duration_seconds: Option) -> GenerateViduQ3TurboRequest { + GenerateViduQ3TurboRequest { + prompt: "ok".to_string(), + duration_seconds, + size: None, + input_reference_images: vec![], + } + } +} diff --git a/crates/api_clients/comet_api_client/src/generate/video/generate_wan_2p6.rs b/crates/api_clients/comet_api_client/src/generate/video/generate_wan_2p6.rs new file mode 100644 index 0000000000..942c77dcbf --- /dev/null +++ b/crates/api_clients/comet_api_client/src/generate/video/generate_wan_2p6.rs @@ -0,0 +1,216 @@ +use crate::creds::comet_api_key::CometApiKey; +use crate::error::comet_client_error::CometClientError; +use crate::error::comet_error::CometError; +use crate::requests::create_video::create_video::{ + create_video, CometInputReferenceImage, CometVideoModelRaw, CometVideoSize, + CreateVideoArgs, CreateVideoRequest, +}; +use crate::requests::video_task_status::CometVideoTaskStatus; + +/// Wan 2.6 supports 2-15 second durations. +const MIN_DURATION_SECONDS: u8 = 2; +const MAX_DURATION_SECONDS: u8 = 15; + +/// The API default duration when `seconds` is omitted. +const DEFAULT_DURATION_SECONDS: u8 = 5; + +/// Wan 2.6 accepts up to 5 references. +const MAX_REFERENCE_IMAGES: usize = 5; + +// ── Args ── + +pub struct GenerateWan2p6Args<'a> { + pub request: GenerateWan2p6Request, + pub api_key: &'a CometApiKey, +} + +// ── Request ── + +#[derive(Clone, Debug)] +pub struct GenerateWan2p6Request { + pub prompt: String, + + /// 2-15 seconds. `None` uses the API default (5). + pub duration_seconds: Option, + + /// `None` uses the API default (720p). + pub size: Option, + + /// Reference images for image-to-video and reference-to-video workflows. + /// Wan 2.6 accepts up to 5 references. + pub input_reference_images: Vec, +} + +// ── Enums ── + +/// Wan 2.6 renders at 720p or 1080p. +#[derive(Debug, Clone, Copy)] +pub enum Wan2p6Size { + SevenTwentyP, + TenEightyP, +} + +// ── Pricing ── +// +// Wan 2.6 per-second USD pricing (via CometAPI): +// +// | Resolution | $/sec | +// |------------|-------| +// | 720p | 0.08 | +// | 1080p | 0.12 | +// +// Total cost = price per second × video duration (seconds). + +impl GenerateWan2p6Request { + /// Estimate the USD cost in cents for this generation request. + pub fn estimate_cost_in_usd_cents(&self) -> u64 { + let cents_per_second: u64 = match self.size { + Some(Wan2p6Size::TenEightyP) => 12, + // Default resolution (None) is 720p. + Some(Wan2p6Size::SevenTwentyP) | None => 8, + }; + let seconds = self.duration_seconds.unwrap_or(DEFAULT_DURATION_SECONDS); + u64::from(seconds) * cents_per_second + } +} + +// ── Response ── + +pub struct GenerateWan2p6Response { + /// Poll `GET /v1/videos/{task_id}` until terminal. + pub task_id: String, + pub status: CometVideoTaskStatus, +} + +// ── Entry point ── + +pub async fn generate_wan_2p6( + args: GenerateWan2p6Args<'_>, +) -> Result { + let raw_request = args.request.to_create_video_request()?; + + let result = create_video(CreateVideoArgs { + api_key: args.api_key, + request: raw_request, + }).await?; + + Ok(GenerateWan2p6Response { + task_id: result.task_id, + status: result.status, + }) +} + +impl GenerateWan2p6Request { + /// Validate and lower to the generic wire request. + pub fn to_create_video_request(&self) -> Result { + if let Some(seconds) = self.duration_seconds { + if !(MIN_DURATION_SECONDS..=MAX_DURATION_SECONDS).contains(&seconds) { + return Err(CometClientError::InvalidRequestField { + field: "duration_seconds", + raw_value: seconds.to_string(), + reason: format!("Wan 2.6 supports {MIN_DURATION_SECONDS}-{MAX_DURATION_SECONDS} second durations"), + }); + } + } + + if self.input_reference_images.len() > MAX_REFERENCE_IMAGES { + return Err(CometClientError::InvalidRequestField { + field: "input_reference_images", + raw_value: self.input_reference_images.len().to_string(), + reason: format!("Wan 2.6 accepts at most {MAX_REFERENCE_IMAGES} references"), + }); + } + + Ok(CreateVideoRequest { + model: CometVideoModelRaw::Wan2p6, + prompt: self.prompt.clone(), + maybe_seconds: self.duration_seconds, + maybe_size: self.size.map(map_size), + input_reference_images: self.input_reference_images.clone(), + }) + } +} + +fn map_size(size: Wan2p6Size) -> CometVideoSize { + match size { + Wan2p6Size::SevenTwentyP => CometVideoSize::Exact { width: 1280, height: 720 }, + Wan2p6Size::TenEightyP => CometVideoSize::Exact { width: 1920, height: 1080 }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn maps_to_wire_request() { + let request = GenerateWan2p6Request { + prompt: "a steam train crossing a viaduct".to_string(), + duration_seconds: Some(15), + size: Some(Wan2p6Size::TenEightyP), + input_reference_images: vec![], + }; + + let raw = request.to_create_video_request().expect("should validate"); + assert_eq!(raw.text_form_fields(), vec![ + ("model", "wan2.6".to_string()), + ("prompt", "a steam train crossing a viaduct".to_string()), + ("seconds", "15".to_string()), + ("size", "1920x1080".to_string()), + ]); + } + + #[test] + fn duration_bounds() { + assert!(request_with_seconds(Some(1)).to_create_video_request().is_err()); + assert!(request_with_seconds(Some(2)).to_create_video_request().is_ok()); + assert!(request_with_seconds(Some(15)).to_create_video_request().is_ok()); + assert!(request_with_seconds(Some(16)).to_create_video_request().is_err()); + assert!(request_with_seconds(None).to_create_video_request().is_ok()); + } + + #[test] + fn too_many_references_rejected() { + let mut request = request_with_seconds(Some(5)); + request.input_reference_images = (0..6) + .map(|i| CometInputReferenceImage { + file_bytes: vec![0u8], + filename: format!("ref_{i}.png"), + maybe_content_type: Some("image/png".to_string()), + }) + .collect(); + + let error = request.to_create_video_request().expect_err("should reject 6 references"); + assert!(matches!(error, CometClientError::InvalidRequestField { field: "input_reference_images", .. })); + + request.input_reference_images.truncate(5); + assert!(request.to_create_video_request().is_ok()); + } + + #[test] + fn cost_estimates() { + // 5 seconds at 720p: 5 × 8¢ = 40¢ + let mut request = request_with_seconds(Some(5)); + request.size = Some(Wan2p6Size::SevenTwentyP); + assert_eq!(request.estimate_cost_in_usd_cents(), 40); + + // 10 seconds at 1080p: 10 × 12¢ = 120¢ + request.duration_seconds = Some(10); + request.size = Some(Wan2p6Size::TenEightyP); + assert_eq!(request.estimate_cost_in_usd_cents(), 120); + + // Defaults (5 seconds, 720p): 40¢ + request.duration_seconds = None; + request.size = None; + assert_eq!(request.estimate_cost_in_usd_cents(), 40); + } + + fn request_with_seconds(duration_seconds: Option) -> GenerateWan2p6Request { + GenerateWan2p6Request { + prompt: "ok".to_string(), + duration_seconds, + size: None, + input_reference_images: vec![], + } + } +} diff --git a/crates/api_clients/comet_api_client/src/generate/video/generate_wan_2p7.rs b/crates/api_clients/comet_api_client/src/generate/video/generate_wan_2p7.rs new file mode 100644 index 0000000000..b944c41993 --- /dev/null +++ b/crates/api_clients/comet_api_client/src/generate/video/generate_wan_2p7.rs @@ -0,0 +1,186 @@ +use crate::creds::comet_api_key::CometApiKey; +use crate::error::comet_client_error::CometClientError; +use crate::error::comet_error::CometError; +use crate::requests::create_video::create_video::{ + create_video, CometInputReferenceImage, CometVideoModelRaw, CometVideoSize, + CreateVideoArgs, CreateVideoRequest, +}; +use crate::requests::video_task_status::CometVideoTaskStatus; + +/// Wan 2.7 supports 2-15 second durations. +const MIN_DURATION_SECONDS: u8 = 2; +const MAX_DURATION_SECONDS: u8 = 15; + +/// The API default duration when `seconds` is omitted. +const DEFAULT_DURATION_SECONDS: u8 = 5; + +// ── Args ── + +pub struct GenerateWan2p7Args<'a> { + pub request: GenerateWan2p7Request, + pub api_key: &'a CometApiKey, +} + +// ── Request ── + +#[derive(Clone, Debug)] +pub struct GenerateWan2p7Request { + pub prompt: String, + + /// 2-15 seconds. `None` uses the API default (5). + pub duration_seconds: Option, + + /// `None` uses the API default (720p). + pub size: Option, + + /// Reference images for image-to-video and first/last-frame workflows. + pub input_reference_images: Vec, +} + +// ── Enums ── + +/// Wan 2.7 renders at 720p or 1080p. +#[derive(Debug, Clone, Copy)] +pub enum Wan2p7Size { + SevenTwentyP, + TenEightyP, +} + +// ── Pricing ── +// +// Wan 2.7 per-second USD pricing (via CometAPI): +// +// | Resolution | $/sec | +// |------------|-------| +// | 720p | 0.08 | +// | 1080p | 0.12 | +// +// Total cost = price per second × video duration (seconds). + +impl GenerateWan2p7Request { + /// Estimate the USD cost in cents for this generation request. + pub fn estimate_cost_in_usd_cents(&self) -> u64 { + let cents_per_second: u64 = match self.size { + Some(Wan2p7Size::TenEightyP) => 12, + // Default resolution (None) is 720p. + Some(Wan2p7Size::SevenTwentyP) | None => 8, + }; + let seconds = self.duration_seconds.unwrap_or(DEFAULT_DURATION_SECONDS); + u64::from(seconds) * cents_per_second + } +} + +// ── Response ── + +pub struct GenerateWan2p7Response { + /// Poll `GET /v1/videos/{task_id}` until terminal. + pub task_id: String, + pub status: CometVideoTaskStatus, +} + +// ── Entry point ── + +pub async fn generate_wan_2p7( + args: GenerateWan2p7Args<'_>, +) -> Result { + let raw_request = args.request.to_create_video_request()?; + + let result = create_video(CreateVideoArgs { + api_key: args.api_key, + request: raw_request, + }).await?; + + Ok(GenerateWan2p7Response { + task_id: result.task_id, + status: result.status, + }) +} + +impl GenerateWan2p7Request { + /// Validate and lower to the generic wire request. + pub fn to_create_video_request(&self) -> Result { + if let Some(seconds) = self.duration_seconds { + if !(MIN_DURATION_SECONDS..=MAX_DURATION_SECONDS).contains(&seconds) { + return Err(CometClientError::InvalidRequestField { + field: "duration_seconds", + raw_value: seconds.to_string(), + reason: format!("Wan 2.7 supports {MIN_DURATION_SECONDS}-{MAX_DURATION_SECONDS} second durations"), + }); + } + } + + Ok(CreateVideoRequest { + model: CometVideoModelRaw::Wan2p7, + prompt: self.prompt.clone(), + maybe_seconds: self.duration_seconds, + maybe_size: self.size.map(map_size), + input_reference_images: self.input_reference_images.clone(), + }) + } +} + +fn map_size(size: Wan2p7Size) -> CometVideoSize { + match size { + Wan2p7Size::SevenTwentyP => CometVideoSize::Exact { width: 1280, height: 720 }, + Wan2p7Size::TenEightyP => CometVideoSize::Exact { width: 1920, height: 1080 }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn maps_to_wire_request() { + let request = GenerateWan2p7Request { + prompt: "a koi pond at golden hour".to_string(), + duration_seconds: Some(5), + size: Some(Wan2p7Size::SevenTwentyP), + input_reference_images: vec![], + }; + + let raw = request.to_create_video_request().expect("should validate"); + assert_eq!(raw.text_form_fields(), vec![ + ("model", "wan2.7".to_string()), + ("prompt", "a koi pond at golden hour".to_string()), + ("seconds", "5".to_string()), + ("size", "1280x720".to_string()), + ]); + } + + #[test] + fn duration_bounds() { + assert!(request_with_seconds(Some(1)).to_create_video_request().is_err()); + assert!(request_with_seconds(Some(2)).to_create_video_request().is_ok()); + assert!(request_with_seconds(Some(15)).to_create_video_request().is_ok()); + assert!(request_with_seconds(Some(16)).to_create_video_request().is_err()); + assert!(request_with_seconds(None).to_create_video_request().is_ok()); + } + + #[test] + fn cost_estimates() { + // 5 seconds at 720p: 5 × 8¢ = 40¢ + let mut request = request_with_seconds(Some(5)); + request.size = Some(Wan2p7Size::SevenTwentyP); + assert_eq!(request.estimate_cost_in_usd_cents(), 40); + + // 10 seconds at 1080p: 10 × 12¢ = 120¢ + request.duration_seconds = Some(10); + request.size = Some(Wan2p7Size::TenEightyP); + assert_eq!(request.estimate_cost_in_usd_cents(), 120); + + // Defaults (5 seconds, 720p): 40¢ + request.duration_seconds = None; + request.size = None; + assert_eq!(request.estimate_cost_in_usd_cents(), 40); + } + + fn request_with_seconds(duration_seconds: Option) -> GenerateWan2p7Request { + GenerateWan2p7Request { + prompt: "ok".to_string(), + duration_seconds, + size: None, + input_reference_images: vec![], + } + } +} diff --git a/crates/api_clients/comet_api_client/src/generate/video/mod.rs b/crates/api_clients/comet_api_client/src/generate/video/mod.rs new file mode 100644 index 0000000000..edc28e6d88 --- /dev/null +++ b/crates/api_clients/comet_api_client/src/generate/video/mod.rs @@ -0,0 +1,13 @@ +//! Concrete, strongly-typed per-model bindings over the generic +//! [`crate::requests::create_video`] call. Each module encodes one model's +//! constraints (duration range, size options, image-input support) in the +//! type system and validates before hitting the wire. + +pub mod generate_doubao_seedance_1p0_pro; +pub mod generate_doubao_seedance_1p5_pro; +pub mod generate_doubao_seedance_2p0; +pub mod generate_doubao_seedance_2p0_fast; +pub mod generate_vidu_q3; +pub mod generate_vidu_q3_turbo; +pub mod generate_wan_2p6; +pub mod generate_wan_2p7; diff --git a/crates/api_clients/comet_api_client/src/lib.rs b/crates/api_clients/comet_api_client/src/lib.rs index 1baa63ef76..964cb8a029 100644 --- a/crates/api_clients/comet_api_client/src/lib.rs +++ b/crates/api_clients/comet_api_client/src/lib.rs @@ -14,6 +14,7 @@ pub mod creds; pub mod error; +pub mod generate; pub mod requests; #[cfg(test)] diff --git a/crates/api_clients/comet_api_client/src/requests/create_video/create_video.rs b/crates/api_clients/comet_api_client/src/requests/create_video/create_video.rs index cb43b425cf..06c5cc572a 100644 --- a/crates/api_clients/comet_api_client/src/requests/create_video/create_video.rs +++ b/crates/api_clients/comet_api_client/src/requests/create_video/create_video.rs @@ -12,10 +12,6 @@ use crate::requests::comet_host::COMET_API_BASE_URL; use crate::requests::create_video::request_types::CreateVideoRawResponse; use crate::requests::video_task_status::CometVideoTaskStatus; -/// Seedance 2.0 models support 4-15 second durations. -const MIN_DURATION_SECONDS: u8 = 4; -const MAX_DURATION_SECONDS: u8 = 15; - // ── Public args ── /// Top-level argument to [`create_video`]. Borrows the API key separately @@ -27,16 +23,20 @@ pub struct CreateVideoArgs<'a> { } /// The material part of a video creation request (`POST /v1/videos`). +/// +/// This is the generic, model-agnostic wire shape. Per-model duration and +/// size constraints are NOT enforced here — use the strongly-typed bindings +/// in [`crate::generate::video`] which validate before delegating to this. #[derive(Clone)] pub struct CreateVideoRequest { - /// Which Doubao Seedance model fulfils the request. - pub model: DoubaoSeedance2p0Model, + /// Which video model fulfils the request. + pub model: CometVideoModelRaw, /// Text description of the desired video. Reference attached images in /// the prompt as `[Image 1]`, `[Image 2]`, etc. pub prompt: String, - /// Video duration in seconds (4-15 for Seedance 2.0 models). Default: 5. + /// Video duration in seconds. Valid ranges are model-specific. Default: 5. pub maybe_seconds: Option, /// Output size: an aspect-ratio preset or exact dimensions. @@ -44,16 +44,29 @@ pub struct CreateVideoRequest { /// Optional reference images (JPEG, PNG, WebP) for image-to-video. /// Each is sent as a repeated `input_reference` multipart field. + /// Not all models support image input. pub input_reference_images: Vec, } -/// The Doubao Seedance 2.0 models CometAPI fronts. +/// The video models CometAPI fronts on `POST /v1/videos`. #[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum DoubaoSeedance2p0Model { +pub enum CometVideoModelRaw { /// Seedance 2.0 DoubaoSeedance2p0, /// Seedance 2.0 Fast DoubaoSeedance2p0Fast, + /// Seedance 1.5 Pro (text-only) + DoubaoSeedance1p5Pro, + /// Seedance 1.0 Pro (text-only) + DoubaoSeedance1p0Pro, + /// Vidu Q3 + ViduQ3, + /// Vidu Q3 Turbo + ViduQ3Turbo, + /// Alibaba Wan 2.7 + Wan2p7, + /// Alibaba Wan 2.6 + Wan2p6, } /// Output video size: a ratio preset or exact pixel dimensions. @@ -199,25 +212,21 @@ impl CreateVideoRequest { }); } - if let Some(seconds) = self.maybe_seconds { - if !(MIN_DURATION_SECONDS..=MAX_DURATION_SECONDS).contains(&seconds) { - return Err(CometClientError::InvalidRequestField { - field: "seconds", - raw_value: seconds.to_string(), - reason: format!("duration must be {MIN_DURATION_SECONDS}-{MAX_DURATION_SECONDS} seconds for Seedance 2.0 models"), - }); - } - } - Ok(()) } } -impl DoubaoSeedance2p0Model { +impl CometVideoModelRaw { pub fn as_api_str(&self) -> &'static str { match self { Self::DoubaoSeedance2p0 => "doubao-seedance-2-0", Self::DoubaoSeedance2p0Fast => "doubao-seedance-2-0-fast", + Self::DoubaoSeedance1p5Pro => "doubao-seedance-1-5-pro", + Self::DoubaoSeedance1p0Pro => "doubao-seedance-1-0-pro", + Self::ViduQ3 => "viduq3", + Self::ViduQ3Turbo => "viduq3-turbo", + Self::Wan2p7 => "wan2.7", + Self::Wan2p6 => "wan2.6", } } } @@ -269,8 +278,14 @@ mod tests { #[test] fn model_api_strings() { - assert_eq!(DoubaoSeedance2p0Model::DoubaoSeedance2p0.as_api_str(), "doubao-seedance-2-0"); - assert_eq!(DoubaoSeedance2p0Model::DoubaoSeedance2p0Fast.as_api_str(), "doubao-seedance-2-0-fast"); + assert_eq!(CometVideoModelRaw::DoubaoSeedance2p0.as_api_str(), "doubao-seedance-2-0"); + assert_eq!(CometVideoModelRaw::DoubaoSeedance2p0Fast.as_api_str(), "doubao-seedance-2-0-fast"); + assert_eq!(CometVideoModelRaw::DoubaoSeedance1p5Pro.as_api_str(), "doubao-seedance-1-5-pro"); + assert_eq!(CometVideoModelRaw::DoubaoSeedance1p0Pro.as_api_str(), "doubao-seedance-1-0-pro"); + assert_eq!(CometVideoModelRaw::ViduQ3.as_api_str(), "viduq3"); + assert_eq!(CometVideoModelRaw::ViduQ3Turbo.as_api_str(), "viduq3-turbo"); + assert_eq!(CometVideoModelRaw::Wan2p7.as_api_str(), "wan2.7"); + assert_eq!(CometVideoModelRaw::Wan2p6.as_api_str(), "wan2.6"); } #[test] @@ -287,7 +302,7 @@ mod tests { #[test] fn minimal_form_fields() { let request = CreateVideoRequest { - model: DoubaoSeedance2p0Model::DoubaoSeedance2p0Fast, + model: CometVideoModelRaw::DoubaoSeedance2p0Fast, prompt: "a corgi running through a field".to_string(), maybe_seconds: None, maybe_size: None, @@ -303,7 +318,7 @@ mod tests { #[test] fn full_form_fields() { let request = CreateVideoRequest { - model: DoubaoSeedance2p0Model::DoubaoSeedance2p0, + model: CometVideoModelRaw::DoubaoSeedance2p0, prompt: "make [Image 1] come alive".to_string(), maybe_seconds: Some(10), maybe_size: Some(CometVideoSize::Portrait9x16), @@ -329,18 +344,12 @@ mod tests { assert!(matches!(error, CometClientError::InvalidRequestField { field: "prompt", .. })); } - #[test] - fn duration_bounds() { - assert!(request_with_prompt_and_seconds("ok", Some(3)).validate().is_err()); - assert!(request_with_prompt_and_seconds("ok", Some(4)).validate().is_ok()); - assert!(request_with_prompt_and_seconds("ok", Some(15)).validate().is_ok()); - assert!(request_with_prompt_and_seconds("ok", Some(16)).validate().is_err()); - assert!(request_with_prompt_and_seconds("ok", None).validate().is_ok()); - } + // NB: Duration ranges are model-specific and validated by the concrete + // bindings in `generate::video`; the generic layer accepts any value. fn request_with_prompt_and_seconds(prompt: &str, maybe_seconds: Option) -> CreateVideoRequest { CreateVideoRequest { - model: DoubaoSeedance2p0Model::DoubaoSeedance2p0, + model: CometVideoModelRaw::DoubaoSeedance2p0, prompt: prompt.to_string(), maybe_seconds, maybe_size: None, @@ -364,7 +373,7 @@ mod tests { let result = create_video(CreateVideoArgs { api_key: &api_key, request: CreateVideoRequest { - model: DoubaoSeedance2p0Model::DoubaoSeedance2p0Fast, + model: CometVideoModelRaw::DoubaoSeedance2p0Fast, prompt: "a corgi running through a sunny meadow, cinematic".to_string(), maybe_seconds: Some(4), maybe_size: Some(CometVideoSize::Landscape16x9), @@ -396,7 +405,7 @@ mod tests { let created = create_video(CreateVideoArgs { api_key: &api_key, request: CreateVideoRequest { - model: DoubaoSeedance2p0Model::DoubaoSeedance2p0Fast, + model: CometVideoModelRaw::DoubaoSeedance2p0Fast, prompt: "a corgi running through a sunny meadow, cinematic".to_string(), maybe_seconds: Some(4), maybe_size: Some(CometVideoSize::Landscape16x9),