mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-14 22:02:36 +00:00
concrete bindings
This commit is contained in:
1 parent
edb5a81e02
commit
e22585de06
12 files changed
+1377
-35
No files matched your search
@@ -0,0 +1 @@
|
||||
pub mod video;
|
||||
+146
@@ -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<u8>,
|
||||
|
||||
pub size: Option<DoubaoSeedance1p0ProSize>,
|
||||
}
|
||||
|
||||
// ── 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<GenerateDoubaoSeedance1p0ProResponse, CometError> {
|
||||
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<CreateVideoRequest, CometClientError> {
|
||||
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<u8>) -> GenerateDoubaoSeedance1p0ProRequest {
|
||||
GenerateDoubaoSeedance1p0ProRequest {
|
||||
prompt: "ok".to_string(),
|
||||
duration_seconds,
|
||||
size: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
+146
@@ -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<u8>,
|
||||
|
||||
pub size: Option<DoubaoSeedance1p5ProSize>,
|
||||
}
|
||||
|
||||
// ── 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<GenerateDoubaoSeedance1p5ProResponse, CometError> {
|
||||
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<CreateVideoRequest, CometClientError> {
|
||||
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<u8>) -> GenerateDoubaoSeedance1p5ProRequest {
|
||||
GenerateDoubaoSeedance1p5ProRequest {
|
||||
prompt: "ok".to_string(),
|
||||
duration_seconds,
|
||||
size: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
+150
@@ -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<u8>,
|
||||
|
||||
pub size: Option<DoubaoSeedance2p0Size>,
|
||||
|
||||
/// Reference images (JPEG, PNG, WebP) for image-to-video.
|
||||
pub input_reference_images: Vec<CometInputReferenceImage>,
|
||||
}
|
||||
|
||||
// ── 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<GenerateDoubaoSeedance2p0Response, CometError> {
|
||||
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<CreateVideoRequest, CometClientError> {
|
||||
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<u8>) -> GenerateDoubaoSeedance2p0Request {
|
||||
GenerateDoubaoSeedance2p0Request {
|
||||
prompt: "ok".to_string(),
|
||||
duration_seconds,
|
||||
size: None,
|
||||
input_reference_images: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
+150
@@ -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<u8>,
|
||||
|
||||
pub size: Option<DoubaoSeedance2p0FastSize>,
|
||||
|
||||
/// Reference images (JPEG, PNG, WebP) for image-to-video.
|
||||
pub input_reference_images: Vec<CometInputReferenceImage>,
|
||||
}
|
||||
|
||||
// ── 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<GenerateDoubaoSeedance2p0FastResponse, CometError> {
|
||||
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<CreateVideoRequest, CometClientError> {
|
||||
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<u8>) -> GenerateDoubaoSeedance2p0FastRequest {
|
||||
GenerateDoubaoSeedance2p0FastRequest {
|
||||
prompt: "ok".to_string(),
|
||||
duration_seconds,
|
||||
size: None,
|
||||
input_reference_images: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<u8>,
|
||||
|
||||
pub size: Option<ViduQ3Size>,
|
||||
|
||||
/// Reference images for image-to-video / start-end-to-video workflows.
|
||||
pub input_reference_images: Vec<CometInputReferenceImage>,
|
||||
}
|
||||
|
||||
// ── 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<GenerateViduQ3Response, CometError> {
|
||||
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<CreateVideoRequest, CometClientError> {
|
||||
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<u8>) -> GenerateViduQ3Request {
|
||||
GenerateViduQ3Request {
|
||||
prompt: "ok".to_string(),
|
||||
duration_seconds,
|
||||
size: None,
|
||||
input_reference_images: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<u8>,
|
||||
|
||||
pub size: Option<ViduQ3TurboSize>,
|
||||
|
||||
/// Reference images for image-to-video / start-end-to-video workflows.
|
||||
pub input_reference_images: Vec<CometInputReferenceImage>,
|
||||
}
|
||||
|
||||
// ── 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<GenerateViduQ3TurboResponse, CometError> {
|
||||
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<CreateVideoRequest, CometClientError> {
|
||||
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<u8>) -> GenerateViduQ3TurboRequest {
|
||||
GenerateViduQ3TurboRequest {
|
||||
prompt: "ok".to_string(),
|
||||
duration_seconds,
|
||||
size: None,
|
||||
input_reference_images: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<u8>,
|
||||
|
||||
/// `None` uses the API default (720p).
|
||||
pub size: Option<Wan2p6Size>,
|
||||
|
||||
/// Reference images for image-to-video and reference-to-video workflows.
|
||||
/// Wan 2.6 accepts up to 5 references.
|
||||
pub input_reference_images: Vec<CometInputReferenceImage>,
|
||||
}
|
||||
|
||||
// ── 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<GenerateWan2p6Response, CometError> {
|
||||
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<CreateVideoRequest, CometClientError> {
|
||||
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<u8>) -> GenerateWan2p6Request {
|
||||
GenerateWan2p6Request {
|
||||
prompt: "ok".to_string(),
|
||||
duration_seconds,
|
||||
size: None,
|
||||
input_reference_images: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<u8>,
|
||||
|
||||
/// `None` uses the API default (720p).
|
||||
pub size: Option<Wan2p7Size>,
|
||||
|
||||
/// Reference images for image-to-video and first/last-frame workflows.
|
||||
pub input_reference_images: Vec<CometInputReferenceImage>,
|
||||
}
|
||||
|
||||
// ── 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<GenerateWan2p7Response, CometError> {
|
||||
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<CreateVideoRequest, CometClientError> {
|
||||
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<u8>) -> GenerateWan2p7Request {
|
||||
GenerateWan2p7Request {
|
||||
prompt: "ok".to_string(),
|
||||
duration_seconds,
|
||||
size: None,
|
||||
input_reference_images: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
pub mod creds;
|
||||
pub mod error;
|
||||
pub mod generate;
|
||||
pub mod requests;
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -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<u8>,
|
||||
|
||||
/// 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<CometInputReferenceImage>,
|
||||
}
|
||||
|
||||
/// 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<u8>) -> 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),
|
||||
|
||||
Reference in new issue
Block a user