use serde::Serialize;
use validator::*;
use super::super::traits::*;
#[derive(Debug, Clone, Validate, Serialize)]
#[validate(schema(function = "validate_prompt_or_image"))]
pub struct VideoBody<N>
where
N: VideoGen,
{
pub model: N,
#[serde(skip_serializing_if = "Option::is_none")]
pub image_url: Option<ImageUrl>,
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(length(max = 512))]
pub prompt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quality: Option<VideoQuality>,
#[serde(skip_serializing_if = "Option::is_none")]
pub with_audio: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub watermark_enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<VideoSize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fps: Option<Fps>,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<VideoDuration>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[validate(length(min = 6, max = 128))]
pub user_id: Option<String>,
}
impl<N> VideoBody<N>
where
N: VideoGen,
{
pub fn new(model: N) -> Self {
Self {
model,
prompt: None,
quality: None,
with_audio: None,
watermark_enabled: None,
image_url: None,
size: None,
fps: None,
duration: None,
request_id: None,
user_id: None,
}
}
pub fn with_prompt(mut self, prompt: impl Into<String>) -> Self {
self.prompt = Some(prompt.into());
self
}
pub fn with_quality(mut self, quality: VideoQuality) -> Self {
self.quality = Some(quality);
self
}
pub fn with_audio(mut self, with_audio: bool) -> Self {
self.with_audio = Some(with_audio);
self
}
pub fn with_watermark_enabled(mut self, watermark_enabled: bool) -> Self {
self.watermark_enabled = Some(watermark_enabled);
self
}
pub fn with_image_url(mut self, image_url: ImageUrl) -> Self {
self.image_url = Some(image_url);
self
}
pub fn with_size(mut self, size: VideoSize) -> Self {
self.size = Some(size);
self
}
pub fn with_fps(mut self, fps: Fps) -> Self {
self.fps = Some(fps);
self
}
pub fn with_duration(mut self, duration: VideoDuration) -> Self {
self.duration = Some(duration);
self
}
pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
self.request_id = Some(request_id.into());
self
}
pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
self.user_id = Some(user_id.into());
self
}
pub fn prompt_only(model: N, prompt: impl Into<String>) -> Self {
Self::new(model).with_prompt(prompt)
}
pub fn with_single_image(
model: N,
image_url: impl Into<String>,
prompt: impl Into<String>,
) -> Self {
Self::new(model)
.with_image_url(ImageUrl::from_url(image_url))
.with_prompt(prompt)
}
pub fn with_multiple_images(
model: N,
image_urls: Vec<impl Into<String>>,
prompt: impl Into<String>,
) -> Result<Self, crate::ZaiError> {
let count = image_urls.len();
if !(1..=2).contains(&count) {
return Err(crate::ZaiError::ApiError {
code: crate::client::error::codes::SDK_VALIDATION,
message: "with_multiple_images requires 1 or 2 URLs".to_string(),
});
}
let mut image_urls = image_urls.into_iter();
let first = image_urls.next().ok_or_else(|| crate::ZaiError::ApiError {
code: crate::client::error::codes::SDK_VALIDATION,
message: "with_multiple_images requires 1 or 2 URLs".to_string(),
})?;
let image_url = match image_urls.next() {
Some(second) => ImageUrl::from_two_urls(first, second),
None => ImageUrl::from_url(first),
};
Ok(Self::new(model)
.with_image_url(image_url)
.with_prompt(prompt))
}
pub fn validate_body(&self) -> crate::ZaiResult<()> {
self.validate()
.map_err(crate::client::error::ZaiError::from)
}
}
fn validate_prompt_or_image<N>(body: &VideoBody<N>) -> Result<(), validator::ValidationError>
where
N: VideoGen,
{
let has_prompt = body.prompt.is_some();
if body
.prompt
.as_deref()
.is_some_and(|prompt| prompt.trim().is_empty())
{
return Err(validator::ValidationError::new("prompt_must_not_be_blank"));
}
let image_is_valid = match body.image_url.as_ref() {
Some(ImageUrl::Url(url)) => is_http_url(url),
Some(ImageUrl::Base64(data)) => is_image_data_url(data),
Some(ImageUrl::VecUrl(urls)) => urls.len() == 2 && urls.iter().all(|url| is_http_url(url)),
None => true,
};
if !image_is_valid {
return Err(validator::ValidationError::new("invalid_image_url"));
}
if !has_prompt && body.image_url.is_none() {
return Err(validator::ValidationError::new("prompt_or_image_required"));
}
Ok(())
}
fn is_http_url(value: &str) -> bool {
value
.parse::<url::Url>()
.is_ok_and(|url| matches!(url.scheme(), "http" | "https"))
}
fn is_image_data_url(value: &str) -> bool {
let Some((metadata, payload)) = value.trim().split_once(',') else {
return false;
};
metadata.starts_with("data:image/")
&& metadata.ends_with(";base64")
&& !payload.trim().is_empty()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum VideoQuality {
Speed,
Quality,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(untagged)]
pub enum ImageUrl {
Url(String),
Base64(String),
VecUrl(Vec<String>),
}
impl ImageUrl {
pub fn base64(data: impl Into<String>) -> Self {
ImageUrl::Base64(data.into())
}
pub fn from_url(url: impl Into<String>) -> Self {
ImageUrl::Url(url.into())
}
pub fn from_two_urls(u1: impl Into<String>, u2: impl Into<String>) -> Self {
ImageUrl::VecUrl(vec![u1.into(), u2.into()])
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum VideoSize {
#[serde(rename = "1280x720")]
Size1280x720,
#[serde(rename = "720x1280")]
Size720x1280,
#[serde(rename = "1024x1024")]
Size1024x1024,
#[serde(rename = "1920x1080")]
Size1920x1080,
#[serde(rename = "1080x1920")]
Size1080x1920,
#[serde(rename = "2048x1080")]
Size2048x1080,
#[serde(rename = "3840x2160")]
Size3840x2160,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Fps {
Fps30,
Fps60,
}
impl Serialize for Fps {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u8(match self {
Self::Fps30 => 30,
Self::Fps60 => 60,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VideoDuration {
Duration5,
Duration10,
}
impl Serialize for VideoDuration {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u8(match self {
Self::Duration5 => 5,
Self::Duration10 => 10,
})
}
}