1use crate::transport::{BodyStream, HttpRequest};
2use crate::LingerError;
3use crate::RequestId;
4use bytes::Bytes;
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
10#[non_exhaustive]
11pub struct Video {
12 pub id: String,
15 pub object: String,
18 pub model: String,
21 pub status: String,
24 pub progress: u32,
27 pub created_at: u64,
30 pub completed_at: Option<u64>,
33 pub expires_at: Option<u64>,
36 pub prompt: Option<String>,
39 pub size: String,
42 pub seconds: String,
45 pub remixed_from_video_id: Option<String>,
48 pub error: Option<VideoError>,
51 #[serde(skip)]
54 request_id: Option<RequestId>,
55}
56
57impl Video {
58 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
59 self.request_id = request_id;
60 self
61 }
62
63 pub fn request_id(&self) -> Option<&RequestId> {
66 self.request_id.as_ref()
67 }
68}
69
70#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
73#[non_exhaustive]
74pub struct VideoPage {
75 pub object: String,
78 #[serde(default)]
81 pub data: Vec<Video>,
82 pub first_id: Option<String>,
85 pub last_id: Option<String>,
88 pub has_more: bool,
91 #[serde(skip)]
94 request_id: Option<RequestId>,
95}
96
97impl VideoPage {
98 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
99 self.request_id = request_id;
100 self
101 }
102
103 pub fn request_id(&self) -> Option<&RequestId> {
106 self.request_id.as_ref()
107 }
108}
109
110#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
113#[non_exhaustive]
114pub struct CreateVideoRequest {
115 pub prompt: String,
118 #[serde(skip_serializing_if = "Option::is_none")]
121 pub model: Option<String>,
122 #[serde(skip_serializing_if = "Option::is_none")]
125 pub input_reference: Option<serde_json::Value>,
126 #[serde(skip_serializing_if = "Option::is_none")]
129 pub seconds: Option<String>,
130 #[serde(skip_serializing_if = "Option::is_none")]
133 pub size: Option<String>,
134}
135
136impl CreateVideoRequest {
137 pub fn builder() -> CreateVideoRequestBuilder {
140 CreateVideoRequestBuilder::default()
141 }
142}
143
144#[derive(Clone, Debug, Default)]
147#[non_exhaustive]
148pub struct CreateVideoRequestBuilder {
149 prompt: Option<String>,
150 model: Option<String>,
151 input_reference: Option<serde_json::Value>,
152 seconds: Option<String>,
153 size: Option<String>,
154}
155
156impl CreateVideoRequestBuilder {
157 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
160 self.prompt = Some(prompt.into());
161 self
162 }
163
164 pub fn model(mut self, model: impl Into<String>) -> Self {
167 self.model = Some(model.into());
168 self
169 }
170
171 pub fn input_reference(mut self, input_reference: serde_json::Value) -> Self {
174 self.input_reference = Some(input_reference);
175 self
176 }
177
178 pub fn seconds(mut self, seconds: impl Into<String>) -> Self {
181 self.seconds = Some(seconds.into());
182 self
183 }
184
185 pub fn size(mut self, size: impl Into<String>) -> Self {
188 self.size = Some(size.into());
189 self
190 }
191
192 pub fn build(self) -> Result<CreateVideoRequest, LingerError> {
195 let prompt = self
196 .prompt
197 .ok_or_else(|| LingerError::invalid_config("prompt is required"))?;
198 if prompt.trim().is_empty() {
199 return Err(LingerError::invalid_config("prompt must not be empty"));
200 }
201 Ok(CreateVideoRequest {
202 prompt,
203 model: self.model,
204 input_reference: self.input_reference,
205 seconds: self.seconds,
206 size: self.size,
207 })
208 }
209}
210
211#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
214#[non_exhaustive]
215pub struct VideoReferenceInput {
216 pub id: String,
219}
220
221impl VideoReferenceInput {
222 pub fn new(id: impl Into<String>) -> Result<Self, LingerError> {
225 let id = id.into();
226 let id = id.trim();
227 if id.is_empty() {
228 return Err(LingerError::invalid_config("video id is required"));
229 }
230 Ok(Self { id: id.to_owned() })
231 }
232}
233
234#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
237#[non_exhaustive]
238pub struct CreateVideoEditRequest {
239 pub video: VideoReferenceInput,
242 pub prompt: String,
245}
246
247impl CreateVideoEditRequest {
248 pub fn builder() -> CreateVideoEditRequestBuilder {
251 CreateVideoEditRequestBuilder::default()
252 }
253}
254
255#[derive(Clone, Debug, Default)]
258#[non_exhaustive]
259pub struct CreateVideoEditRequestBuilder {
260 video: Option<VideoReferenceInput>,
261 prompt: Option<String>,
262}
263
264impl CreateVideoEditRequestBuilder {
265 pub fn video_id(mut self, video_id: impl Into<String>) -> Self {
268 self.video = VideoReferenceInput::new(video_id).ok();
269 self
270 }
271
272 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
275 self.prompt = Some(prompt.into());
276 self
277 }
278
279 pub fn build(self) -> Result<CreateVideoEditRequest, LingerError> {
282 let video = self
283 .video
284 .ok_or_else(|| LingerError::invalid_config("video id is required"))?;
285 let prompt = self
286 .prompt
287 .ok_or_else(|| LingerError::invalid_config("prompt is required"))?;
288 if prompt.trim().is_empty() {
289 return Err(LingerError::invalid_config("prompt must not be empty"));
290 }
291 Ok(CreateVideoEditRequest { video, prompt })
292 }
293}
294
295#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
298#[non_exhaustive]
299pub struct CreateVideoExtensionRequest {
300 pub video: VideoReferenceInput,
303 pub prompt: String,
306 pub seconds: String,
309}
310
311impl CreateVideoExtensionRequest {
312 pub fn builder() -> CreateVideoExtensionRequestBuilder {
315 CreateVideoExtensionRequestBuilder::default()
316 }
317}
318
319#[derive(Clone, Debug, Default)]
322#[non_exhaustive]
323pub struct CreateVideoExtensionRequestBuilder {
324 video: Option<VideoReferenceInput>,
325 prompt: Option<String>,
326 seconds: Option<String>,
327}
328
329impl CreateVideoExtensionRequestBuilder {
330 pub fn video_id(mut self, video_id: impl Into<String>) -> Self {
333 self.video = VideoReferenceInput::new(video_id).ok();
334 self
335 }
336
337 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
340 self.prompt = Some(prompt.into());
341 self
342 }
343
344 pub fn seconds(mut self, seconds: impl Into<String>) -> Self {
347 self.seconds = Some(seconds.into());
348 self
349 }
350
351 pub fn build(self) -> Result<CreateVideoExtensionRequest, LingerError> {
354 let video = self
355 .video
356 .ok_or_else(|| LingerError::invalid_config("video id is required"))?;
357 let prompt = self
358 .prompt
359 .ok_or_else(|| LingerError::invalid_config("prompt is required"))?;
360 if prompt.trim().is_empty() {
361 return Err(LingerError::invalid_config("prompt must not be empty"));
362 }
363 let seconds = self
364 .seconds
365 .ok_or_else(|| LingerError::invalid_config("seconds is required"))?;
366 if seconds.trim().is_empty() {
367 return Err(LingerError::invalid_config("seconds must not be empty"));
368 }
369 Ok(CreateVideoExtensionRequest {
370 video,
371 prompt,
372 seconds,
373 })
374 }
375}
376
377#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
380#[non_exhaustive]
381pub struct CreateVideoRemixRequest {
382 pub prompt: String,
385}
386
387impl CreateVideoRemixRequest {
388 pub fn builder() -> CreateVideoRemixRequestBuilder {
391 CreateVideoRemixRequestBuilder::default()
392 }
393}
394
395#[derive(Clone, Debug, Default)]
398pub struct CreateVideoRemixRequestBuilder {
399 prompt: Option<String>,
400}
401
402impl CreateVideoRemixRequestBuilder {
403 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
406 self.prompt = Some(prompt.into());
407 self
408 }
409
410 pub fn build(self) -> Result<CreateVideoRemixRequest, LingerError> {
413 let prompt = self
414 .prompt
415 .ok_or_else(|| LingerError::invalid_config("prompt is required"))?;
416 if prompt.trim().is_empty() {
417 return Err(LingerError::invalid_config("prompt must not be empty"));
418 }
419 Ok(CreateVideoRemixRequest { prompt })
420 }
421}
422
423#[derive(Clone, Debug, PartialEq, Eq)]
426#[non_exhaustive]
427pub struct VideoUpload {
428 pub filename: String,
431 pub content_type: String,
434 content: Bytes,
435}
436
437impl VideoUpload {
438 pub fn from_bytes(
441 filename: impl Into<String>,
442 content: impl Into<Bytes>,
443 ) -> Result<Self, LingerError> {
444 let filename = filename.into();
445 validate_header_param("filename", &filename)?;
446 Ok(Self {
447 filename,
448 content_type: "application/octet-stream".to_string(),
449 content: content.into(),
450 })
451 }
452
453 pub fn content_type(mut self, content_type: impl Into<String>) -> Result<Self, LingerError> {
456 let content_type = content_type.into();
457 validate_header_value("content_type", &content_type)?;
458 self.content_type = content_type;
459 Ok(self)
460 }
461
462 pub fn bytes(&self) -> Bytes {
465 self.content.clone()
466 }
467}
468
469#[derive(Clone, Debug, PartialEq, Eq)]
472#[non_exhaustive]
473pub struct CreateVideoCharacterRequest {
474 pub name: String,
477 pub video: VideoUpload,
480}
481
482impl CreateVideoCharacterRequest {
483 pub fn builder() -> CreateVideoCharacterRequestBuilder {
486 CreateVideoCharacterRequestBuilder::default()
487 }
488
489 pub(crate) fn apply_multipart_body(&self, request: &mut HttpRequest) {
490 let boundary = video_multipart_boundary(&self.name, &self.video.content);
491 request.insert_header(
492 "content-type",
493 format!("multipart/form-data; boundary={boundary}"),
494 );
495 request.set_body_stream(self.multipart_stream(boundary));
496 }
497
498 fn multipart_stream(
499 &self,
500 boundary: String,
501 ) -> impl futures_core::Stream<Item = Result<Bytes, LingerError>> {
502 let mut chunks = Vec::new();
503 chunks.push(Ok(Bytes::from(format!(
504 "--{boundary}\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n{}\r\n",
505 self.name
506 ))));
507 chunks.push(Ok(Bytes::from(format!(
508 "--{boundary}\r\nContent-Disposition: form-data; name=\"video\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n",
509 escape_multipart_param(&self.video.filename),
510 self.video.content_type
511 ))));
512 chunks.push(Ok(self.video.content.clone()));
513 chunks.push(Ok(Bytes::from(format!("\r\n--{boundary}--\r\n"))));
514 futures_util::stream::iter(chunks)
515 }
516}
517
518#[derive(Clone, Debug, Default)]
521#[non_exhaustive]
522pub struct CreateVideoCharacterRequestBuilder {
523 name: Option<String>,
524 video: Option<VideoUpload>,
525}
526
527impl CreateVideoCharacterRequestBuilder {
528 pub fn name(mut self, name: impl Into<String>) -> Self {
531 self.name = Some(name.into());
532 self
533 }
534
535 pub fn video(mut self, video: VideoUpload) -> Self {
538 self.video = Some(video);
539 self
540 }
541
542 pub fn build(self) -> Result<CreateVideoCharacterRequest, LingerError> {
545 let name = self
546 .name
547 .filter(|value| !value.trim().is_empty())
548 .ok_or_else(|| LingerError::invalid_config("name is required"))?;
549 let video = self
550 .video
551 .ok_or_else(|| LingerError::invalid_config("video is required"))?;
552 Ok(CreateVideoCharacterRequest { name, video })
553 }
554}
555
556#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
559#[non_exhaustive]
560pub struct VideoCharacter {
561 pub id: Option<String>,
564 pub name: Option<String>,
567 pub created_at: u64,
570 #[serde(skip)]
573 request_id: Option<RequestId>,
574}
575
576impl VideoCharacter {
577 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
578 self.request_id = request_id;
579 self
580 }
581
582 pub fn request_id(&self) -> Option<&RequestId> {
585 self.request_id.as_ref()
586 }
587}
588
589#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
592#[non_exhaustive]
593pub struct VideoError {
594 pub code: String,
597 pub message: String,
600}
601
602#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
605#[non_exhaustive]
606pub struct VideoDeletion {
607 pub object: String,
610 pub deleted: bool,
613 pub id: String,
616 #[serde(skip)]
619 request_id: Option<RequestId>,
620}
621
622impl VideoDeletion {
623 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
624 self.request_id = request_id;
625 self
626 }
627
628 pub fn request_id(&self) -> Option<&RequestId> {
631 self.request_id.as_ref()
632 }
633}
634
635pub struct VideoContent {
638 request_id: Option<RequestId>,
639 body: BodyStream,
640}
641
642impl VideoContent {
643 pub(crate) fn new(request_id: Option<RequestId>, body: BodyStream) -> Self {
644 Self { request_id, body }
645 }
646
647 pub fn request_id(&self) -> Option<&RequestId> {
650 self.request_id.as_ref()
651 }
652
653 pub fn into_stream(self) -> BodyStream {
656 self.body
657 }
658}
659
660#[derive(Clone, Copy, Debug, PartialEq, Eq)]
663#[non_exhaustive]
664pub enum VideoContentVariant {
665 Video,
668 Thumbnail,
671 Spritesheet,
674}
675
676impl VideoContentVariant {
677 pub(crate) fn as_query_value(self) -> &'static str {
678 match self {
679 Self::Video => "video",
680 Self::Thumbnail => "thumbnail",
681 Self::Spritesheet => "spritesheet",
682 }
683 }
684}
685
686fn video_multipart_boundary(name: &str, content: &Bytes) -> String {
687 for counter in 0.. {
688 let boundary = format!("linger-openai-sdk-video-boundary-{counter}");
689 let boundary_bytes = boundary.as_bytes();
690 if !contains_bytes(name.as_bytes(), boundary_bytes)
691 && !contains_bytes(content, boundary_bytes)
692 {
693 return boundary;
694 }
695 }
696 unreachable!("unbounded boundary counter")
697}
698
699fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
700 if needle.is_empty() {
701 return true;
702 }
703 haystack
704 .windows(needle.len())
705 .any(|window| window == needle)
706}
707
708fn validate_header_param(name: &str, value: &str) -> Result<(), LingerError> {
709 if value.trim().is_empty() {
710 return Err(LingerError::invalid_config(format!("{name} is required")));
711 }
712 validate_header_value(name, value)
713}
714
715fn validate_header_value(name: &str, value: &str) -> Result<(), LingerError> {
716 if value.contains('\r') || value.contains('\n') {
717 return Err(LingerError::invalid_config(format!(
718 "{name} must not contain CR or LF"
719 )));
720 }
721 Ok(())
722}
723
724fn escape_multipart_param(value: &str) -> String {
725 value.replace('\\', "\\\\").replace('"', "\\\"")
726}