Skip to main content

xai_rust/api/
videos.rs

1//! Videos API for generation and editing.
2
3use crate::client::XaiClient;
4use crate::models::videos::{
5    Video, VideoEditRequest, VideoGenerationRequest, VideoGenerationResponse, VideoResponseFormat,
6};
7use crate::{Error, Result};
8
9/// Videos API client.
10#[derive(Debug, Clone)]
11pub struct VideosApi {
12    client: XaiClient,
13}
14
15impl VideosApi {
16    pub(crate) fn new(client: XaiClient) -> Self {
17        Self { client }
18    }
19
20    /// Create a video generation request.
21    ///
22    /// # Example
23    ///
24    /// ```rust,no_run
25    /// use xai_rust::XaiClient;
26    ///
27    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
28    /// let client = XaiClient::from_env()?;
29    /// let response = client.videos().generate("grok-video", "A cat on screen").send().await?;
30    /// println!("videos: {:?}", response.first_url());
31    /// # Ok(())
32    /// # }
33    /// ```
34    pub fn generate(
35        &self,
36        model: impl Into<String>,
37        prompt: impl Into<String>,
38    ) -> VideoGenerationBuilder {
39        VideoGenerationBuilder::new(self.client.clone(), model.into(), prompt.into())
40    }
41
42    /// Create a video edit request.
43    pub fn edit(
44        &self,
45        model: impl Into<String>,
46        video: impl Into<String>,
47        prompt: impl Into<String>,
48    ) -> VideoEditBuilder {
49        VideoEditBuilder::new(
50            self.client.clone(),
51            model.into(),
52            video.into(),
53            prompt.into(),
54        )
55    }
56
57    /// Get a video by identifier.
58    pub async fn get(&self, video_id: &str) -> Result<Video> {
59        let id = XaiClient::encode_path(video_id);
60        let url = format!("{}/videos/{}", self.client.base_url(), id);
61
62        let response = self.client.send(self.client.http().get(&url)).await?;
63        if !response.status().is_success() {
64            return Err(Error::from_response(response).await);
65        }
66        Ok(response.json().await?)
67    }
68}
69
70/// Builder for generation requests.
71#[derive(Debug)]
72pub struct VideoGenerationBuilder {
73    client: XaiClient,
74    request: VideoGenerationRequest,
75}
76
77impl VideoGenerationBuilder {
78    fn new(client: XaiClient, model: String, prompt: String) -> Self {
79        Self {
80            client,
81            request: VideoGenerationRequest::new(model, prompt),
82        }
83    }
84
85    /// Set the output duration in seconds.
86    pub fn duration_seconds(mut self, duration_seconds: u32) -> Self {
87        self.request = self.request.duration_seconds(duration_seconds);
88        self
89    }
90
91    /// Set number of outputs.
92    pub fn n(mut self, n: u32) -> Self {
93        self.request.n = Some(n.clamp(1, 4));
94        self
95    }
96
97    /// Set response format.
98    pub fn response_format(mut self, format: VideoResponseFormat) -> Self {
99        self.request = self.request.response_format(format);
100        self
101    }
102
103    /// Request URL format.
104    pub fn url_format(self) -> Self {
105        self.response_format(VideoResponseFormat::Url)
106    }
107
108    /// Request base64 format.
109    pub fn base64_format(self) -> Self {
110        self.response_format(VideoResponseFormat::B64Json)
111    }
112
113    /// Send the request.
114    pub async fn send(self) -> Result<VideoGenerationResponse> {
115        let url = format!("{}/videos/generations", self.client.base_url());
116        let response = self
117            .client
118            .send(self.client.http().post(&url).json(&self.request))
119            .await?;
120
121        if !response.status().is_success() {
122            return Err(Error::from_response(response).await);
123        }
124        Ok(response.json().await?)
125    }
126}
127
128/// Builder for edit requests.
129#[derive(Debug)]
130pub struct VideoEditBuilder {
131    client: XaiClient,
132    request: VideoEditRequest,
133}
134
135impl VideoEditBuilder {
136    fn new(client: XaiClient, model: String, video: String, prompt: String) -> Self {
137        Self {
138            client,
139            request: VideoEditRequest::new(model, video, prompt),
140        }
141    }
142
143    /// Set output duration in seconds.
144    pub fn duration_seconds(mut self, duration_seconds: u32) -> Self {
145        self.request = self.request.duration_seconds(duration_seconds);
146        self
147    }
148
149    /// Set response format.
150    pub fn response_format(mut self, format: VideoResponseFormat) -> Self {
151        self.request = self.request.response_format(format);
152        self
153    }
154
155    /// Request URL format.
156    pub fn url_format(self) -> Self {
157        self.response_format(VideoResponseFormat::Url)
158    }
159
160    /// Request base64 format.
161    pub fn base64_format(self) -> Self {
162        self.response_format(VideoResponseFormat::B64Json)
163    }
164
165    /// Send the request.
166    pub async fn send(self) -> Result<VideoGenerationResponse> {
167        let url = format!("{}/videos/edits", self.client.base_url());
168        let response = self
169            .client
170            .send(self.client.http().post(&url).json(&self.request))
171            .await?;
172
173        if !response.status().is_success() {
174            return Err(Error::from_response(response).await);
175        }
176        Ok(response.json().await?)
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use serde_json::json;
184    use wiremock::matchers::{method, path};
185    use wiremock::{Mock, MockServer, ResponseTemplate};
186
187    #[tokio::test]
188    async fn generate_forwards_request_body() {
189        let server = MockServer::start().await;
190
191        Mock::given(method("POST"))
192            .and(path("/videos/generations"))
193            .respond_with(move |req: &wiremock::Request| {
194                let body = serde_json::from_slice::<serde_json::Value>(&req.body).unwrap();
195                assert_eq!(body["model"], "grok-video");
196                assert_eq!(body["prompt"], "cat running");
197                assert_eq!(body["n"], 2);
198                ResponseTemplate::new(200).set_body_json(json!({
199                    "created": 1700000000,
200                    "data": [{"url": "https://example.com/video.mp4"}]
201                }))
202            })
203            .mount(&server)
204            .await;
205
206        let client = XaiClient::builder()
207            .api_key("test-key")
208            .base_url(server.uri())
209            .build()
210            .unwrap();
211
212        let response = client
213            .videos()
214            .generate("grok-video", "cat running")
215            .n(2)
216            .send()
217            .await
218            .unwrap();
219        assert_eq!(response.first_url(), Some("https://example.com/video.mp4"));
220    }
221
222    #[tokio::test]
223    async fn edit_forwards_request_body() {
224        let server = MockServer::start().await;
225
226        Mock::given(method("POST"))
227            .and(path("/videos/edits"))
228            .respond_with(move |req: &wiremock::Request| {
229                let body = serde_json::from_slice::<serde_json::Value>(&req.body).unwrap();
230                assert_eq!(body["model"], "grok-video");
231                assert_eq!(body["video"], "video-1");
232                assert_eq!(body["prompt"], "slow motion");
233                ResponseTemplate::new(200).set_body_json(json!({
234                    "created": 1700000000,
235                    "data": [{"url": "https://example.com/video-edit.mp4"}]
236                }))
237            })
238            .mount(&server)
239            .await;
240
241        let client = XaiClient::builder()
242            .api_key("test-key")
243            .base_url(server.uri())
244            .build()
245            .unwrap();
246
247        let response = client
248            .videos()
249            .edit("grok-video", "video-1", "slow motion")
250            .send()
251            .await
252            .unwrap();
253        assert_eq!(
254            response.first_url(),
255            Some("https://example.com/video-edit.mp4")
256        );
257    }
258
259    #[tokio::test]
260    async fn get_video_encodes_id() {
261        let server = MockServer::start().await;
262
263        Mock::given(method("GET"))
264            .and(path("/videos/video%2Fencoded"))
265            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
266                "id": "video/encoded",
267                "status": "ready",
268                "url": "https://example.com/video.mp4"
269            })))
270            .mount(&server)
271            .await;
272
273        let client = XaiClient::builder()
274            .api_key("test-key")
275            .base_url(server.uri())
276            .build()
277            .unwrap();
278
279        let response = client.videos().get("video/encoded").await.unwrap();
280        assert_eq!(response.id, "video/encoded");
281        assert_eq!(
282            response.url.as_deref(),
283            Some("https://example.com/video.mp4")
284        );
285    }
286}