use crate::transport::{BodyStream, HttpRequest};
use crate::LingerError;
use crate::RequestId;
use bytes::Bytes;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct Video {
pub id: String,
pub object: String,
pub model: String,
pub status: String,
pub progress: u32,
pub created_at: u64,
pub completed_at: Option<u64>,
pub expires_at: Option<u64>,
pub prompt: Option<String>,
pub size: String,
pub seconds: String,
pub remixed_from_video_id: Option<String>,
pub error: Option<VideoError>,
#[serde(skip)]
request_id: Option<RequestId>,
}
impl Video {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct VideoPage {
pub object: String,
#[serde(default)]
pub data: Vec<Video>,
pub first_id: Option<String>,
pub last_id: Option<String>,
pub has_more: bool,
#[serde(skip)]
request_id: Option<RequestId>,
}
impl VideoPage {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct CreateVideoRequest {
pub prompt: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_reference: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub seconds: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<String>,
}
impl CreateVideoRequest {
pub fn builder() -> CreateVideoRequestBuilder {
CreateVideoRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CreateVideoRequestBuilder {
prompt: Option<String>,
model: Option<String>,
input_reference: Option<serde_json::Value>,
seconds: Option<String>,
size: Option<String>,
}
impl CreateVideoRequestBuilder {
pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
self.prompt = Some(prompt.into());
self
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn input_reference(mut self, input_reference: serde_json::Value) -> Self {
self.input_reference = Some(input_reference);
self
}
pub fn seconds(mut self, seconds: impl Into<String>) -> Self {
self.seconds = Some(seconds.into());
self
}
pub fn size(mut self, size: impl Into<String>) -> Self {
self.size = Some(size.into());
self
}
pub fn build(self) -> Result<CreateVideoRequest, LingerError> {
let prompt = self
.prompt
.ok_or_else(|| LingerError::invalid_config("prompt is required"))?;
if prompt.trim().is_empty() {
return Err(LingerError::invalid_config("prompt must not be empty"));
}
Ok(CreateVideoRequest {
prompt,
model: self.model,
input_reference: self.input_reference,
seconds: self.seconds,
size: self.size,
})
}
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct VideoReferenceInput {
pub id: String,
}
impl VideoReferenceInput {
pub fn new(id: impl Into<String>) -> Result<Self, LingerError> {
let id = id.into();
let id = id.trim();
if id.is_empty() {
return Err(LingerError::invalid_config("video id is required"));
}
Ok(Self { id: id.to_owned() })
}
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct CreateVideoEditRequest {
pub video: VideoReferenceInput,
pub prompt: String,
}
impl CreateVideoEditRequest {
pub fn builder() -> CreateVideoEditRequestBuilder {
CreateVideoEditRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CreateVideoEditRequestBuilder {
video: Option<VideoReferenceInput>,
prompt: Option<String>,
}
impl CreateVideoEditRequestBuilder {
pub fn video_id(mut self, video_id: impl Into<String>) -> Self {
self.video = VideoReferenceInput::new(video_id).ok();
self
}
pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
self.prompt = Some(prompt.into());
self
}
pub fn build(self) -> Result<CreateVideoEditRequest, LingerError> {
let video = self
.video
.ok_or_else(|| LingerError::invalid_config("video id is required"))?;
let prompt = self
.prompt
.ok_or_else(|| LingerError::invalid_config("prompt is required"))?;
if prompt.trim().is_empty() {
return Err(LingerError::invalid_config("prompt must not be empty"));
}
Ok(CreateVideoEditRequest { video, prompt })
}
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct CreateVideoExtensionRequest {
pub video: VideoReferenceInput,
pub prompt: String,
pub seconds: String,
}
impl CreateVideoExtensionRequest {
pub fn builder() -> CreateVideoExtensionRequestBuilder {
CreateVideoExtensionRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CreateVideoExtensionRequestBuilder {
video: Option<VideoReferenceInput>,
prompt: Option<String>,
seconds: Option<String>,
}
impl CreateVideoExtensionRequestBuilder {
pub fn video_id(mut self, video_id: impl Into<String>) -> Self {
self.video = VideoReferenceInput::new(video_id).ok();
self
}
pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
self.prompt = Some(prompt.into());
self
}
pub fn seconds(mut self, seconds: impl Into<String>) -> Self {
self.seconds = Some(seconds.into());
self
}
pub fn build(self) -> Result<CreateVideoExtensionRequest, LingerError> {
let video = self
.video
.ok_or_else(|| LingerError::invalid_config("video id is required"))?;
let prompt = self
.prompt
.ok_or_else(|| LingerError::invalid_config("prompt is required"))?;
if prompt.trim().is_empty() {
return Err(LingerError::invalid_config("prompt must not be empty"));
}
let seconds = self
.seconds
.ok_or_else(|| LingerError::invalid_config("seconds is required"))?;
if seconds.trim().is_empty() {
return Err(LingerError::invalid_config("seconds must not be empty"));
}
Ok(CreateVideoExtensionRequest {
video,
prompt,
seconds,
})
}
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct CreateVideoRemixRequest {
pub prompt: String,
}
impl CreateVideoRemixRequest {
pub fn builder() -> CreateVideoRemixRequestBuilder {
CreateVideoRemixRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
pub struct CreateVideoRemixRequestBuilder {
prompt: Option<String>,
}
impl CreateVideoRemixRequestBuilder {
pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
self.prompt = Some(prompt.into());
self
}
pub fn build(self) -> Result<CreateVideoRemixRequest, LingerError> {
let prompt = self
.prompt
.ok_or_else(|| LingerError::invalid_config("prompt is required"))?;
if prompt.trim().is_empty() {
return Err(LingerError::invalid_config("prompt must not be empty"));
}
Ok(CreateVideoRemixRequest { prompt })
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct VideoUpload {
pub filename: String,
pub content_type: String,
content: Bytes,
}
impl VideoUpload {
pub fn from_bytes(
filename: impl Into<String>,
content: impl Into<Bytes>,
) -> Result<Self, LingerError> {
let filename = filename.into();
validate_header_param("filename", &filename)?;
Ok(Self {
filename,
content_type: "application/octet-stream".to_string(),
content: content.into(),
})
}
pub fn content_type(mut self, content_type: impl Into<String>) -> Result<Self, LingerError> {
let content_type = content_type.into();
validate_header_value("content_type", &content_type)?;
self.content_type = content_type;
Ok(self)
}
pub fn bytes(&self) -> Bytes {
self.content.clone()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct CreateVideoCharacterRequest {
pub name: String,
pub video: VideoUpload,
}
impl CreateVideoCharacterRequest {
pub fn builder() -> CreateVideoCharacterRequestBuilder {
CreateVideoCharacterRequestBuilder::default()
}
pub(crate) fn apply_multipart_body(&self, request: &mut HttpRequest) {
let boundary = video_multipart_boundary(&self.name, &self.video.content);
request.insert_header(
"content-type",
format!("multipart/form-data; boundary={boundary}"),
);
request.set_body_stream(self.multipart_stream(boundary));
}
fn multipart_stream(
&self,
boundary: String,
) -> impl futures_core::Stream<Item = Result<Bytes, LingerError>> {
let mut chunks = Vec::new();
chunks.push(Ok(Bytes::from(format!(
"--{boundary}\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n{}\r\n",
self.name
))));
chunks.push(Ok(Bytes::from(format!(
"--{boundary}\r\nContent-Disposition: form-data; name=\"video\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n",
escape_multipart_param(&self.video.filename),
self.video.content_type
))));
chunks.push(Ok(self.video.content.clone()));
chunks.push(Ok(Bytes::from(format!("\r\n--{boundary}--\r\n"))));
futures_util::stream::iter(chunks)
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CreateVideoCharacterRequestBuilder {
name: Option<String>,
video: Option<VideoUpload>,
}
impl CreateVideoCharacterRequestBuilder {
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn video(mut self, video: VideoUpload) -> Self {
self.video = Some(video);
self
}
pub fn build(self) -> Result<CreateVideoCharacterRequest, LingerError> {
let name = self
.name
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| LingerError::invalid_config("name is required"))?;
let video = self
.video
.ok_or_else(|| LingerError::invalid_config("video is required"))?;
Ok(CreateVideoCharacterRequest { name, video })
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct VideoCharacter {
pub id: Option<String>,
pub name: Option<String>,
pub created_at: u64,
#[serde(skip)]
request_id: Option<RequestId>,
}
impl VideoCharacter {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct VideoError {
pub code: String,
pub message: String,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct VideoDeletion {
pub object: String,
pub deleted: bool,
pub id: String,
#[serde(skip)]
request_id: Option<RequestId>,
}
impl VideoDeletion {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
pub struct VideoContent {
request_id: Option<RequestId>,
body: BodyStream,
}
impl VideoContent {
pub(crate) fn new(request_id: Option<RequestId>, body: BodyStream) -> Self {
Self { request_id, body }
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
pub fn into_stream(self) -> BodyStream {
self.body
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum VideoContentVariant {
Video,
Thumbnail,
Spritesheet,
}
impl VideoContentVariant {
pub(crate) fn as_query_value(self) -> &'static str {
match self {
Self::Video => "video",
Self::Thumbnail => "thumbnail",
Self::Spritesheet => "spritesheet",
}
}
}
fn video_multipart_boundary(name: &str, content: &Bytes) -> String {
for counter in 0.. {
let boundary = format!("linger-openai-sdk-video-boundary-{counter}");
let boundary_bytes = boundary.as_bytes();
if !contains_bytes(name.as_bytes(), boundary_bytes)
&& !contains_bytes(content, boundary_bytes)
{
return boundary;
}
}
unreachable!("unbounded boundary counter")
}
fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
if needle.is_empty() {
return true;
}
haystack
.windows(needle.len())
.any(|window| window == needle)
}
fn validate_header_param(name: &str, value: &str) -> Result<(), LingerError> {
if value.trim().is_empty() {
return Err(LingerError::invalid_config(format!("{name} is required")));
}
validate_header_value(name, value)
}
fn validate_header_value(name: &str, value: &str) -> Result<(), LingerError> {
if value.contains('\r') || value.contains('\n') {
return Err(LingerError::invalid_config(format!(
"{name} must not contain CR or LF"
)));
}
Ok(())
}
fn escape_multipart_param(value: &str) -> String {
value.replace('\\', "\\\\").replace('"', "\\\"")
}