Skip to content

Commit d77de15

Browse files
authored
feat: video api (64bit#449)
* video api * video example * videos * Update README with output prompt and video Update README with output prompt and video
1 parent 837eede commit d77de15

File tree

11 files changed

+395
-5
lines changed

11 files changed

+395
-5
lines changed

async-openai/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
- [x] Realtime (Beta) (partially implemented)
3939
- [x] Responses (partially implemented)
4040
- [x] Uploads
41+
- [x] Videos
4142
- Bring your own custom types for Request or Response objects.
4243
- SSE streaming on available APIs
4344
- Requests (except SSE streaming) including form submissions are retried with exponential backoff when [rate limited](https://platform.openai.com/docs/guides/rate-limits).

async-openai/src/client.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use crate::{
1414
moderation::Moderations,
1515
traits::AsyncTryFrom,
1616
Assistants, Audio, AuditLogs, Batches, Chat, Completions, Embeddings, FineTuning, Invites,
17-
Models, Projects, Responses, Threads, Uploads, Users, VectorStores,
17+
Models, Projects, Responses, Threads, Uploads, Users, VectorStores, Videos,
1818
};
1919

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

125+
/// To call [Videos] group related APIs using this client.
126+
pub fn videos(&self) -> Videos<C> {
127+
Videos::new(self)
128+
}
129+
125130
/// To call [Assistants] group related APIs using this client.
126131
pub fn assistants(&self) -> Assistants<C> {
127132
Assistants::new(self)
@@ -238,6 +243,27 @@ impl<C: Config> Client<C> {
238243
self.execute_raw(request_maker).await
239244
}
240245

246+
pub(crate) async fn get_raw_with_query<Q>(
247+
&self,
248+
path: &str,
249+
query: &Q,
250+
) -> Result<Bytes, OpenAIError>
251+
where
252+
Q: Serialize + ?Sized,
253+
{
254+
let request_maker = || async {
255+
Ok(self
256+
.http_client
257+
.get(self.config.url(path))
258+
.query(&self.config.query())
259+
.query(query)
260+
.headers(self.config.headers())
261+
.build()?)
262+
};
263+
264+
self.execute_raw(request_maker).await
265+
}
266+
241267
/// Make a POST request to {path} and return the response body
242268
pub(crate) async fn post_raw<I>(&self, path: &str, request: I) -> Result<Bytes, OpenAIError>
243269
where

async-openai/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ mod util;
174174
mod vector_store_file_batches;
175175
mod vector_store_files;
176176
mod vector_stores;
177+
mod video;
177178

178179
pub use assistants::Assistants;
179180
pub use audio::Audio;
@@ -203,3 +204,4 @@ pub use users::Users;
203204
pub use vector_store_file_batches::VectorStoreFileBatches;
204205
pub use vector_store_files::VectorStoreFiles;
205206
pub use vector_stores::VectorStores;
207+
pub use video::Videos;

async-openai/src/types/impls.rs

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::{
77
download::{download_url, save_b64},
88
error::OpenAIError,
99
traits::AsyncTryFrom,
10-
types::InputSource,
10+
types::{InputSource, VideoSize},
1111
util::{create_all_dir, create_file_part},
1212
};
1313

@@ -26,9 +26,9 @@ use super::{
2626
ChatCompletionRequestUserMessage, ChatCompletionRequestUserMessageContent,
2727
ChatCompletionRequestUserMessageContentPart, ChatCompletionToolChoiceOption, CreateFileRequest,
2828
CreateImageEditRequest, CreateImageVariationRequest, CreateMessageRequestContent,
29-
CreateSpeechResponse, CreateTranscriptionRequest, CreateTranslationRequest, DallE2ImageSize,
30-
EmbeddingInput, FileExpiresAfterAnchor, FileInput, FilePurpose, FunctionName, Image,
31-
ImageInput, ImageModel, ImageResponseFormat, ImageSize, ImageUrl, ImagesResponse,
29+
CreateSpeechResponse, CreateTranscriptionRequest, CreateTranslationRequest, CreateVideoRequest,
30+
DallE2ImageSize, EmbeddingInput, FileExpiresAfterAnchor, FileInput, FilePurpose, FunctionName,
31+
Image, ImageInput, ImageModel, ImageResponseFormat, ImageSize, ImageUrl, ImagesResponse,
3232
ModerationInput, Prompt, Role, Stop, TimestampGranularity,
3333
};
3434

@@ -161,6 +161,21 @@ impl_input!(AudioInput);
161161
impl_input!(FileInput);
162162
impl_input!(ImageInput);
163163

164+
impl Display for VideoSize {
165+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166+
write!(
167+
f,
168+
"{}",
169+
match self {
170+
Self::S720x1280 => "720x1280",
171+
Self::S1280x720 => "1280x720",
172+
Self::S1024x1792 => "1024x1792",
173+
Self::S1792x1024 => "1792x1024",
174+
}
175+
)
176+
}
177+
}
178+
164179
impl Display for ImageSize {
165180
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166181
write!(
@@ -1005,6 +1020,31 @@ impl AsyncTryFrom<AddUploadPartRequest> for reqwest::multipart::Form {
10051020
}
10061021
}
10071022

1023+
impl AsyncTryFrom<CreateVideoRequest> for reqwest::multipart::Form {
1024+
type Error = OpenAIError;
1025+
1026+
async fn try_from(request: CreateVideoRequest) -> Result<Self, Self::Error> {
1027+
let mut form = reqwest::multipart::Form::new().text("model", request.model);
1028+
1029+
form = form.text("prompt", request.prompt);
1030+
1031+
if request.size.is_some() {
1032+
form = form.text("size", request.size.unwrap().to_string());
1033+
}
1034+
1035+
if request.seconds.is_some() {
1036+
form = form.text("seconds", request.seconds.unwrap());
1037+
}
1038+
1039+
if request.input_reference.is_some() {
1040+
let image_part = create_file_part(request.input_reference.unwrap().source).await?;
1041+
form = form.part("input_reference", image_part);
1042+
}
1043+
1044+
Ok(form)
1045+
}
1046+
}
1047+
10081048
// end: types to multipart form
10091049

10101050
impl Default for Input {

async-openai/src/types/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ mod thread;
3131
mod upload;
3232
mod users;
3333
mod vector_store;
34+
mod video;
3435

3536
pub use assistant::*;
3637
pub use assistant_stream::*;
@@ -58,6 +59,7 @@ pub use thread::*;
5859
pub use upload::*;
5960
pub use users::*;
6061
pub use vector_store::*;
62+
pub use video::*;
6163

6264
mod impls;
6365
use derive_builder::UninitializedFieldError;

async-openai/src/types/video.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
use derive_builder::Builder;
2+
use serde::{Deserialize, Serialize};
3+
4+
use crate::{error::OpenAIError, types::ImageInput};
5+
6+
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
7+
pub enum VideoSize {
8+
#[default]
9+
#[serde(rename = "720x1280")]
10+
S720x1280,
11+
#[serde(rename = "1280x720")]
12+
S1280x720,
13+
#[serde(rename = "1024x1792")]
14+
S1024x1792,
15+
#[serde(rename = "1792x1024")]
16+
S1792x1024,
17+
}
18+
19+
#[derive(Clone, Default, Debug, Builder, PartialEq)]
20+
#[builder(name = "CreateVideoRequestArgs")]
21+
#[builder(pattern = "mutable")]
22+
#[builder(setter(into, strip_option), default)]
23+
#[builder(derive(Debug))]
24+
#[builder(build_fn(error = "OpenAIError"))]
25+
pub struct CreateVideoRequest {
26+
/// ID of the model to use.
27+
pub model: String,
28+
29+
/// The prompt to generate video from.
30+
pub prompt: String,
31+
32+
pub size: Option<VideoSize>,
33+
34+
pub seconds: Option<String>,
35+
36+
pub input_reference: Option<ImageInput>,
37+
}
38+
39+
#[derive(Clone, Default, Debug, Builder, PartialEq, Serialize)]
40+
#[builder(name = "RemixVideoRequestArgs")]
41+
#[builder(pattern = "mutable")]
42+
#[builder(setter(into, strip_option), default)]
43+
#[builder(derive(Debug))]
44+
#[builder(build_fn(error = "OpenAIError"))]
45+
pub struct RemixVideoRequest {
46+
pub prompt: String,
47+
}
48+
49+
#[derive(Debug, Clone, Serialize, Deserialize)]
50+
pub struct VideoJobError {
51+
pub code: String,
52+
pub message: String,
53+
}
54+
55+
/// Structured information describing a generated video job.
56+
#[derive(Debug, Clone, Serialize, Deserialize)]
57+
pub struct VideoJob {
58+
/// Unix timestamp (seconds) for when the job completed, if finished.
59+
pub completed_at: Option<u32>,
60+
61+
/// Unix timestamp (seconds) for when the job was created.
62+
pub created_at: u32,
63+
64+
/// Error payload that explains why generation failed, if applicable.
65+
pub error: Option<VideoJobError>,
66+
67+
/// Unix timestamp (seconds) for when the downloadable assets expire, if set.
68+
pub expires_at: Option<u32>,
69+
70+
/// Unique identifier for the video job.
71+
pub id: String,
72+
73+
/// The video generation model that produced the job.
74+
pub model: String,
75+
76+
/// The object type, which is always video.
77+
pub object: String,
78+
79+
/// Approximate completion percentage for the generation task.
80+
pub progress: u8,
81+
82+
/// Identifier of the source video if this video is a remix.
83+
pub remixed_from_video_id: Option<String>,
84+
85+
/// Duration of the generated clip in seconds.
86+
pub seconds: String,
87+
88+
/// The resolution of the generated video.
89+
pub size: String,
90+
91+
/// Current lifecycle status of the video job.
92+
pub status: String,
93+
}
94+
95+
#[derive(Debug, Clone, Serialize, Deserialize)]
96+
pub struct VideoJobMetadata {
97+
pub id: String,
98+
pub object: String,
99+
pub deleted: bool,
100+
}
101+
102+
#[derive(Debug, Clone, Serialize, Deserialize)]
103+
pub struct ListVideosResponse {
104+
pub data: Vec<VideoJob>,
105+
pub object: String,
106+
}
107+
108+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
109+
#[serde(rename_all = "lowercase")]
110+
pub enum VideoVariant {
111+
#[default]
112+
Video,
113+
Thumbnail,
114+
Spritesheet,
115+
}

async-openai/src/video.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
use crate::{
2+
config::Config,
3+
error::OpenAIError,
4+
types::{
5+
CreateVideoRequest, ListVideosResponse, RemixVideoRequest, VideoJob, VideoJobMetadata,
6+
VideoVariant,
7+
},
8+
Client,
9+
};
10+
use bytes::Bytes;
11+
use serde::Serialize;
12+
13+
/// Video generation with Sora
14+
/// Related guide: [Video generation](https://platform.openai.com/docs/guides/video-generation)
15+
pub struct Videos<'c, C: Config> {
16+
client: &'c Client<C>,
17+
}
18+
19+
impl<'c, C: Config> Videos<'c, C> {
20+
pub fn new(client: &'c Client<C>) -> Self {
21+
Self { client }
22+
}
23+
24+
/// Create a video
25+
#[crate::byot(
26+
T0 = Clone,
27+
R = serde::de::DeserializeOwned,
28+
where_clause = "reqwest::multipart::Form: crate::traits::AsyncTryFrom<T0, Error = OpenAIError>",
29+
)]
30+
pub async fn create(&self, request: CreateVideoRequest) -> Result<VideoJob, OpenAIError> {
31+
self.client.post_form("/videos", request).await
32+
}
33+
34+
/// Create a video remix
35+
#[crate::byot(T0 = std::fmt::Display, T1 = serde::Serialize, R = serde::de::DeserializeOwned)]
36+
pub async fn remix(
37+
&self,
38+
video_id: &str,
39+
request: RemixVideoRequest,
40+
) -> Result<VideoJob, OpenAIError> {
41+
self.client
42+
.post(&format!("/videos/{video_id}/remix"), request)
43+
.await
44+
}
45+
46+
/// Retrieves a video by its ID.
47+
#[crate::byot(T0 = std::fmt::Display, R = serde::de::DeserializeOwned)]
48+
pub async fn retrieve(&self, video_id: &str) -> Result<VideoJob, OpenAIError> {
49+
self.client.get(&format!("/videos/{}", video_id)).await
50+
}
51+
52+
/// Delete a Video
53+
#[crate::byot(T0 = std::fmt::Display, R = serde::de::DeserializeOwned)]
54+
pub async fn delete(&self, video_id: &str) -> Result<VideoJobMetadata, OpenAIError> {
55+
self.client.delete(&format!("/videos/{}", video_id)).await
56+
}
57+
58+
/// List Videos
59+
#[crate::byot(T0 = serde::Serialize, R = serde::de::DeserializeOwned)]
60+
pub async fn list<Q>(&self, query: &Q) -> Result<ListVideosResponse, OpenAIError>
61+
where
62+
Q: Serialize + ?Sized,
63+
{
64+
self.client.get_with_query("/videos", &query).await
65+
}
66+
67+
/// Download video content
68+
pub async fn download_content(
69+
&self,
70+
video_id: &str,
71+
variant: VideoVariant,
72+
) -> Result<Bytes, OpenAIError> {
73+
self.client
74+
.get_raw_with_query(
75+
&format!("/videos/{video_id}/content"),
76+
&[("variant", variant)],
77+
)
78+
.await
79+
}
80+
}

examples/video/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "video"
3+
version = "0.1.0"
4+
edition = "2021"
5+
publish = false
6+
7+
[dependencies]
8+
async-openai = {path = "../../async-openai"}
9+
tokio = { version = "1.43.0", features = ["full"] }
10+
bytes = "1.9.0"
11+

examples/video/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Image Credits:
2+
3+
https://cdn.openai.com/API/docs/images/sora/monster_original_720p.jpeg
4+
5+
### Output
6+
7+
Prompt: "Fridge opens, cat walks out, and celebrates a birthday party"
8+
9+
https://github.com/user-attachments/assets/90dfe49d-4435-4e2a-8ea8-cb556c764414
10+
338 KB
Loading

0 commit comments

Comments
 (0)