Skip to content

Commit a4e69f8

Browse files
64bitifsheldon
authored andcommitted
feat: video api (64bit#449)
* video api * video example * videos * Update README with output prompt and video Update README with output prompt and video (cherry picked from commit d77de15) # Conflicts: # async-openai-wasm/src/client.rs # async-openai-wasm/src/types/impls.rs # async-openai-wasm/src/types/video.rs # async-openai-wasm/src/video.rs # async-openai/README.md
1 parent 729afc2 commit a4e69f8

File tree

7 files changed

+270
-9
lines changed

7 files changed

+270
-9
lines changed

async-openai-wasm/src/client.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use serde::{Serialize, de::DeserializeOwned};
1515
use crate::error::{ApiError, StreamError};
1616
use crate::{
1717
Assistants, Audio, AuditLogs, Batches, Chat, Completions, Embeddings, FineTuning, Invites,
18-
Models, Projects, Responses, Threads, Uploads, Users, VectorStores,
18+
Models, Projects, Responses, Threads, Uploads, Users, VectorStores, Videos,
1919
config::{Config, OpenAIConfig},
2020
error::{OpenAIError, WrappedError, map_deserialization_error},
2121
file::Files,
@@ -119,6 +119,11 @@ impl<C: Config> Client<C> {
119119
Audio::new(self)
120120
}
121121

122+
/// To call [Videos] group related APIs using this client.
123+
pub fn videos(&self) -> Videos<'_, C> {
124+
Videos::new(self)
125+
}
126+
122127
/// To call [Assistants] group related APIs using this client.
123128
pub fn assistants(&self) -> Assistants<'_, C> {
124129
Assistants::new(self)
@@ -231,6 +236,26 @@ impl<C: Config> Client<C> {
231236
.await
232237
}
233238

239+
pub(crate) async fn get_raw_with_query<Q>(
240+
&self,
241+
path: &str,
242+
query: &Q,
243+
) -> Result<Bytes, OpenAIError>
244+
where
245+
Q: Serialize + ?Sized,
246+
{
247+
self.execute_raw(async {
248+
Ok(self
249+
.http_client
250+
.get(self.config.url(path))
251+
.query(&self.config.query())
252+
.query(query)
253+
.headers(self.config.headers())
254+
.build()?)
255+
})
256+
.await
257+
}
258+
234259
/// Make a POST request to {path} and return the response body
235260
pub(crate) async fn post_raw<I>(&self, path: &str, request: I) -> Result<Bytes, OpenAIError>
236261
where

async-openai-wasm/src/error.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,7 @@ pub struct WrappedError {
7979

8080
pub(crate) fn map_deserialization_error(e: serde_json::Error, bytes: &[u8]) -> OpenAIError {
8181
let json_content = String::from_utf8_lossy(bytes);
82-
tracing::error!(
83-
"failed deserialization of: {}",
84-
json_content
85-
);
82+
tracing::error!("failed deserialization of: {}", json_content);
8683

8784
OpenAIError::JSONDeserialize(e, json_content.to_string())
8885
}

async-openai-wasm/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-wasm/src/types/impls.rs

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ use super::{
1414
ChatCompletionRequestUserMessage, ChatCompletionRequestUserMessageContent,
1515
ChatCompletionRequestUserMessageContentPart, ChatCompletionToolChoiceOption, CreateFileRequest,
1616
CreateImageEditRequest, CreateImageVariationRequest, CreateMessageRequestContent,
17-
CreateTranscriptionRequest, CreateTranslationRequest, DallE2ImageSize, EmbeddingInput,
18-
FileExpiresAfterAnchor, FileInput, FilePurpose, FunctionName, ImageInput, ImageModel,
19-
ImageResponseFormat, ImageSize, ImageUrl, ModerationInput, Prompt, Role, Stop,
20-
TimestampGranularity,
17+
CreateTranscriptionRequest, CreateTranslationRequest, CreateVideoRequest, DallE2ImageSize,
18+
EmbeddingInput, FileExpiresAfterAnchor, FileInput, FilePurpose, FunctionName, ImageInput,
19+
ImageModel, ImageResponseFormat, ImageSize, ImageUrl, ModerationInput, Prompt, Role, Stop,
20+
TimestampGranularity, VideoSize,
2121
responses::{CodeInterpreterContainer, Input, InputContent, Role as ResponsesRole},
2222
};
2323
use crate::traits::AsyncTryFrom;
@@ -146,6 +146,21 @@ impl_input!(AudioInput);
146146
impl_input!(FileInput);
147147
impl_input!(ImageInput);
148148

149+
impl Display for VideoSize {
150+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151+
write!(
152+
f,
153+
"{}",
154+
match self {
155+
Self::S720x1280 => "720x1280",
156+
Self::S1280x720 => "1280x720",
157+
Self::S1024x1792 => "1024x1792",
158+
Self::S1792x1024 => "1792x1024",
159+
}
160+
)
161+
}
162+
}
163+
149164
impl Display for ImageSize {
150165
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151166
write!(
@@ -925,6 +940,31 @@ impl AsyncTryFrom<AddUploadPartRequest> for reqwest::multipart::Form {
925940
}
926941
}
927942

943+
impl AsyncTryFrom<CreateVideoRequest> for reqwest::multipart::Form {
944+
type Error = OpenAIError;
945+
946+
async fn try_from(request: CreateVideoRequest) -> Result<Self, Self::Error> {
947+
let mut form = reqwest::multipart::Form::new().text("model", request.model);
948+
949+
form = form.text("prompt", request.prompt);
950+
951+
if request.size.is_some() {
952+
form = form.text("size", request.size.unwrap().to_string());
953+
}
954+
955+
if request.seconds.is_some() {
956+
form = form.text("seconds", request.seconds.unwrap());
957+
}
958+
959+
if request.input_reference.is_some() {
960+
let image_part = create_file_part(request.input_reference.unwrap().source).await?;
961+
form = form.part("input_reference", image_part);
962+
}
963+
964+
Ok(form)
965+
}
966+
}
967+
928968
// end: types to multipart form
929969

930970
impl Default for Input {

async-openai-wasm/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;
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-wasm/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+
Client,
3+
config::Config,
4+
error::OpenAIError,
5+
types::{
6+
CreateVideoRequest, ListVideosResponse, RemixVideoRequest, VideoJob, VideoJobMetadata,
7+
VideoVariant,
8+
},
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+
}

0 commit comments

Comments
 (0)