1use async_trait::async_trait;
9use bytes::Bytes;
10use serde::{Deserialize, Serialize};
11
12use crate::error::Result;
13use crate::shared::{FileBytes, Headers, ProviderMetadata, ProviderOptions, Warning};
14
15#[async_trait]
17pub trait VideoModel: Send + Sync + std::fmt::Debug {
18 fn provider(&self) -> &str;
20
21 fn model_id(&self) -> &str;
23
24 fn specification_version(&self) -> &'static str {
26 "v4"
27 }
28
29 async fn max_videos_per_call(&self) -> Option<u32> {
34 Some(1)
35 }
36
37 async fn do_generate(&self, options: VideoOptions) -> Result<VideoResult>;
44}
45
46#[derive(Debug, Clone, Default, Serialize, Deserialize)]
50pub struct VideoOptions {
51 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub prompt: Option<String>,
54 #[serde(default = "default_n")]
56 pub n: u32,
57 #[serde(
59 default,
60 rename = "aspectRatio",
61 skip_serializing_if = "Option::is_none"
62 )]
63 pub aspect_ratio: Option<String>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub resolution: Option<String>,
67 #[serde(default, rename = "duration", skip_serializing_if = "Option::is_none")]
74 pub duration_seconds: Option<f64>,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub fps: Option<u32>,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub seed: Option<u64>,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub image: Option<VideoFile>,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub headers: Option<Headers>,
87 #[serde(
89 default,
90 rename = "providerOptions",
91 skip_serializing_if = "Option::is_none"
92 )]
93 pub provider_options: Option<ProviderOptions>,
94}
95
96fn default_n() -> u32 {
97 1
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
104#[serde(tag = "type", rename_all = "kebab-case")]
105pub enum VideoFile {
106 File {
108 #[serde(rename = "mediaType")]
110 media_type: String,
111 data: FileBytes,
113 #[serde(
115 default,
116 rename = "providerOptions",
117 skip_serializing_if = "Option::is_none"
118 )]
119 provider_options: Option<ProviderOptions>,
120 },
121 Url {
123 url: String,
125 #[serde(
127 default,
128 rename = "providerOptions",
129 skip_serializing_if = "Option::is_none"
130 )]
131 provider_options: Option<ProviderOptions>,
132 },
133}
134
135#[derive(Debug, Clone)]
139pub struct VideoResult {
140 pub videos: Vec<VideoData>,
142 pub warnings: Vec<Warning>,
144 pub provider_metadata: Option<ProviderMetadata>,
146 pub response: VideoResponseInfo,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
160pub struct VideoResponseInfo {
161 pub timestamp: String,
163 #[serde(rename = "modelId")]
165 pub model_id: String,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub headers: Option<Headers>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
175#[serde(tag = "type", rename_all = "kebab-case")]
176pub enum VideoData {
177 Url {
179 url: String,
181 #[serde(rename = "mediaType")]
183 media_type: String,
184 },
185 Base64 {
187 data: String,
189 #[serde(rename = "mediaType")]
191 media_type: String,
192 },
193 Binary {
195 #[serde(with = "binary_serde")]
197 data: Bytes,
198 #[serde(rename = "mediaType")]
200 media_type: String,
201 },
202}
203
204mod binary_serde {
205 use bytes::Bytes;
206 use serde::{Deserialize, Deserializer, Serializer};
207
208 pub fn serialize<S: Serializer>(b: &Bytes, s: S) -> Result<S::Ok, S::Error> {
209 s.serialize_bytes(b)
210 }
211
212 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Bytes, D::Error> {
213 let v: Vec<u8> = Vec::deserialize(d)?;
214 Ok(Bytes::from(v))
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use serde_json::json;
222
223 #[test]
224 fn options_default_n_is_one() {
225 let v: VideoOptions = serde_json::from_value(json!({})).unwrap();
226 assert_eq!(v.n, 1);
227 }
228
229 #[test]
230 fn options_roundtrip_camelcase() {
231 let v = VideoOptions {
232 prompt: Some("a cat".into()),
233 n: 2,
234 aspect_ratio: Some("16:9".into()),
235 resolution: Some("1920x1080".into()),
236 duration_seconds: Some(5.0),
237 fps: Some(30),
238 seed: Some(42),
239 image: None,
240 headers: None,
241 provider_options: None,
242 };
243 let j = serde_json::to_value(&v).unwrap();
244 assert_eq!(j["aspectRatio"], "16:9");
245 assert_eq!(j["duration"], 5.0);
249 let back: VideoOptions = serde_json::from_value(j).unwrap();
250 assert_eq!(back.aspect_ratio.as_deref(), Some("16:9"));
251 assert_eq!(back.fps, Some(30));
252 }
253
254 #[test]
255 fn file_tagged_correctly() {
256 let f = VideoFile::Url {
257 url: "https://example.com/start.png".into(),
258 provider_options: None,
259 };
260 let j = serde_json::to_value(&f).unwrap();
261 assert_eq!(j["type"], "url");
262 }
263
264 #[test]
265 fn data_tagged_correctly() {
266 let d = VideoData::Url {
267 url: "https://example.com/x.mp4".into(),
268 media_type: "video/mp4".into(),
269 };
270 let j = serde_json::to_value(&d).unwrap();
271 assert_eq!(j["type"], "url");
272 assert_eq!(j["mediaType"], "video/mp4");
273 }
274}