Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions async-openai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
- [x] Realtime (Beta) (partially implemented)
- [x] Responses (partially implemented)
- [x] Uploads
- [x] Videos
- Bring your own custom types for Request or Response objects.
- SSE streaming on available APIs
- Requests (except SSE streaming) including form submissions are retried with exponential backoff when [rate limited](https://platform.openai.com/docs/guides/rate-limits).
Expand Down
28 changes: 27 additions & 1 deletion async-openai/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use crate::{
moderation::Moderations,
traits::AsyncTryFrom,
Assistants, Audio, AuditLogs, Batches, Chat, Completions, Embeddings, FineTuning, Invites,
Models, Projects, Responses, Threads, Uploads, Users, VectorStores,
Models, Projects, Responses, Threads, Uploads, Users, VectorStores, Videos,
};

#[derive(Debug, Clone, Default)]
Expand Down Expand Up @@ -122,6 +122,11 @@ impl<C: Config> Client<C> {
Audio::new(self)
}

/// To call [Videos] group related APIs using this client.
pub fn videos(&self) -> Videos<C> {
Videos::new(self)
}

/// To call [Assistants] group related APIs using this client.
pub fn assistants(&self) -> Assistants<C> {
Assistants::new(self)
Expand Down Expand Up @@ -238,6 +243,27 @@ impl<C: Config> Client<C> {
self.execute_raw(request_maker).await
}

pub(crate) async fn get_raw_with_query<Q>(
&self,
path: &str,
query: &Q,
) -> Result<Bytes, OpenAIError>
where
Q: Serialize + ?Sized,
{
let request_maker = || async {
Ok(self
.http_client
.get(self.config.url(path))
.query(&self.config.query())
.query(query)
.headers(self.config.headers())
.build()?)
};

self.execute_raw(request_maker).await
}

/// Make a POST request to {path} and return the response body
pub(crate) async fn post_raw<I>(&self, path: &str, request: I) -> Result<Bytes, OpenAIError>
where
Expand Down
2 changes: 2 additions & 0 deletions async-openai/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ mod util;
mod vector_store_file_batches;
mod vector_store_files;
mod vector_stores;
mod video;

pub use assistants::Assistants;
pub use audio::Audio;
Expand Down Expand Up @@ -203,3 +204,4 @@ pub use users::Users;
pub use vector_store_file_batches::VectorStoreFileBatches;
pub use vector_store_files::VectorStoreFiles;
pub use vector_stores::VectorStores;
pub use video::Videos;
48 changes: 44 additions & 4 deletions async-openai/src/types/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
download::{download_url, save_b64},
error::OpenAIError,
traits::AsyncTryFrom,
types::InputSource,
types::{InputSource, VideoSize},
util::{create_all_dir, create_file_part},
};

Expand All @@ -26,9 +26,9 @@ use super::{
ChatCompletionRequestUserMessage, ChatCompletionRequestUserMessageContent,
ChatCompletionRequestUserMessageContentPart, ChatCompletionToolChoiceOption, CreateFileRequest,
CreateImageEditRequest, CreateImageVariationRequest, CreateMessageRequestContent,
CreateSpeechResponse, CreateTranscriptionRequest, CreateTranslationRequest, DallE2ImageSize,
EmbeddingInput, FileExpiresAfterAnchor, FileInput, FilePurpose, FunctionName, Image,
ImageInput, ImageModel, ImageResponseFormat, ImageSize, ImageUrl, ImagesResponse,
CreateSpeechResponse, CreateTranscriptionRequest, CreateTranslationRequest, CreateVideoRequest,
DallE2ImageSize, EmbeddingInput, FileExpiresAfterAnchor, FileInput, FilePurpose, FunctionName,
Image, ImageInput, ImageModel, ImageResponseFormat, ImageSize, ImageUrl, ImagesResponse,
ModerationInput, Prompt, Role, Stop, TimestampGranularity,
};

Expand Down Expand Up @@ -161,6 +161,21 @@ impl_input!(AudioInput);
impl_input!(FileInput);
impl_input!(ImageInput);

impl Display for VideoSize {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::S720x1280 => "720x1280",
Self::S1280x720 => "1280x720",
Self::S1024x1792 => "1024x1792",
Self::S1792x1024 => "1792x1024",
}
)
}
}

impl Display for ImageSize {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
Expand Down Expand Up @@ -1005,6 +1020,31 @@ impl AsyncTryFrom<AddUploadPartRequest> for reqwest::multipart::Form {
}
}

impl AsyncTryFrom<CreateVideoRequest> for reqwest::multipart::Form {
type Error = OpenAIError;

async fn try_from(request: CreateVideoRequest) -> Result<Self, Self::Error> {
let mut form = reqwest::multipart::Form::new().text("model", request.model);

form = form.text("prompt", request.prompt);

if request.size.is_some() {
form = form.text("size", request.size.unwrap().to_string());
}

if request.seconds.is_some() {
form = form.text("seconds", request.seconds.unwrap());
}

if request.input_reference.is_some() {
let image_part = create_file_part(request.input_reference.unwrap().source).await?;
form = form.part("input_reference", image_part);
}

Ok(form)
}
}

// end: types to multipart form

impl Default for Input {
Expand Down
2 changes: 2 additions & 0 deletions async-openai/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ mod thread;
mod upload;
mod users;
mod vector_store;
mod video;

pub use assistant::*;
pub use assistant_stream::*;
Expand Down Expand Up @@ -58,6 +59,7 @@ pub use thread::*;
pub use upload::*;
pub use users::*;
pub use vector_store::*;
pub use video::*;

mod impls;
use derive_builder::UninitializedFieldError;
Expand Down
115 changes: 115 additions & 0 deletions async-openai/src/types/video.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use derive_builder::Builder;
use serde::{Deserialize, Serialize};

use crate::{error::OpenAIError, types::ImageInput};

#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub enum VideoSize {
#[default]
#[serde(rename = "720x1280")]
S720x1280,
#[serde(rename = "1280x720")]
S1280x720,
#[serde(rename = "1024x1792")]
S1024x1792,
#[serde(rename = "1792x1024")]
S1792x1024,
}

#[derive(Clone, Default, Debug, Builder, PartialEq)]
#[builder(name = "CreateVideoRequestArgs")]
#[builder(pattern = "mutable")]
#[builder(setter(into, strip_option), default)]
#[builder(derive(Debug))]
#[builder(build_fn(error = "OpenAIError"))]
pub struct CreateVideoRequest {
/// ID of the model to use.
pub model: String,

/// The prompt to generate video from.
pub prompt: String,

pub size: Option<VideoSize>,

pub seconds: Option<String>,

pub input_reference: Option<ImageInput>,
}

#[derive(Clone, Default, Debug, Builder, PartialEq, Serialize)]
#[builder(name = "RemixVideoRequestArgs")]
#[builder(pattern = "mutable")]
#[builder(setter(into, strip_option), default)]
#[builder(derive(Debug))]
#[builder(build_fn(error = "OpenAIError"))]
pub struct RemixVideoRequest {
pub prompt: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VideoJobError {
pub code: String,
pub message: String,
}

/// Structured information describing a generated video job.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VideoJob {
/// Unix timestamp (seconds) for when the job completed, if finished.
pub completed_at: Option<u32>,

/// Unix timestamp (seconds) for when the job was created.
pub created_at: u32,

/// Error payload that explains why generation failed, if applicable.
pub error: Option<VideoJobError>,

/// Unix timestamp (seconds) for when the downloadable assets expire, if set.
pub expires_at: Option<u32>,

/// Unique identifier for the video job.
pub id: String,

/// The video generation model that produced the job.
pub model: String,

/// The object type, which is always video.
pub object: String,

/// Approximate completion percentage for the generation task.
pub progress: u8,

/// Identifier of the source video if this video is a remix.
pub remixed_from_video_id: Option<String>,

/// Duration of the generated clip in seconds.
pub seconds: String,

/// The resolution of the generated video.
pub size: String,

/// Current lifecycle status of the video job.
pub status: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VideoJobMetadata {
pub id: String,
pub object: String,
pub deleted: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListVideosResponse {
pub data: Vec<VideoJob>,
pub object: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum VideoVariant {
#[default]
Video,
Thumbnail,
Spritesheet,
}
80 changes: 80 additions & 0 deletions async-openai/src/video.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use crate::{
config::Config,
error::OpenAIError,
types::{
CreateVideoRequest, ListVideosResponse, RemixVideoRequest, VideoJob, VideoJobMetadata,
VideoVariant,
},
Client,
};
use bytes::Bytes;
use serde::Serialize;

/// Video generation with Sora
/// Related guide: [Video generation](https://platform.openai.com/docs/guides/video-generation)
pub struct Videos<'c, C: Config> {
client: &'c Client<C>,
}

impl<'c, C: Config> Videos<'c, C> {
pub fn new(client: &'c Client<C>) -> Self {
Self { client }
}

/// Create a video
#[crate::byot(
T0 = Clone,
R = serde::de::DeserializeOwned,
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>",
)]
pub async fn create(&self, request: CreateVideoRequest) -> Result<VideoJob, OpenAIError> {
self.client.post_form("/videos", request).await
}

/// Create a video remix
#[crate::byot(T0 = std::fmt::Display, T1 = serde::Serialize, R = serde::de::DeserializeOwned)]
pub async fn remix(
&self,
video_id: &str,
request: RemixVideoRequest,
) -> Result<VideoJob, OpenAIError> {
self.client
.post(&format!("/videos/{video_id}/remix"), request)
.await
}

/// Retrieves a video by its ID.
#[crate::byot(T0 = std::fmt::Display, R = serde::de::DeserializeOwned)]
pub async fn retrieve(&self, video_id: &str) -> Result<VideoJob, OpenAIError> {
self.client.get(&format!("/videos/{}", video_id)).await
}

/// Delete a Video
#[crate::byot(T0 = std::fmt::Display, R = serde::de::DeserializeOwned)]
pub async fn delete(&self, video_id: &str) -> Result<VideoJobMetadata, OpenAIError> {
self.client.delete(&format!("/videos/{}", video_id)).await
}

/// List Videos
#[crate::byot(T0 = serde::Serialize, R = serde::de::DeserializeOwned)]
pub async fn list<Q>(&self, query: &Q) -> Result<ListVideosResponse, OpenAIError>
where
Q: Serialize + ?Sized,
{
self.client.get_with_query("/videos", &query).await
}

/// Download video content
pub async fn download_content(
&self,
video_id: &str,
variant: VideoVariant,
) -> Result<Bytes, OpenAIError> {
self.client
.get_raw_with_query(
&format!("/videos/{video_id}/content"),
&[("variant", variant)],
)
.await
}
}
11 changes: 11 additions & 0 deletions examples/video/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "video"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
async-openai = {path = "../../async-openai"}
tokio = { version = "1.43.0", features = ["full"] }
bytes = "1.9.0"

10 changes: 10 additions & 0 deletions examples/video/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Image Credits:

https://cdn.openai.com/API/docs/images/sora/monster_original_720p.jpeg

### Output

Prompt: "Fridge opens, cat walks out, and celebrates a birthday party"

https://github.com/user-attachments/assets/90dfe49d-4435-4e2a-8ea8-cb556c764414

Binary file added examples/video/input/monster_original_720p.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading