use crate::common::auth::{AuthProvider, OpenAIAuth};
use crate::common::client::create_http_client;
use crate::common::errors::{ErrorResponse, OpenAIToolError, Result};
use crate::videos::response::{DeleteVideoResponse, Video, VideoListResponse};
use serde::{Deserialize, Serialize};
use std::time::Duration;
const VIDEOS_PATH: &str = "videos";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[non_exhaustive]
pub enum VideoModel {
#[serde(rename = "sora-2")]
#[default]
Sora2,
#[serde(rename = "sora-2-pro")]
Sora2Pro,
}
impl VideoModel {
pub fn as_str(&self) -> &'static str {
match self {
Self::Sora2 => "sora-2",
Self::Sora2Pro => "sora-2-pro",
}
}
}
impl std::fmt::Display for VideoModel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[non_exhaustive]
pub enum VideoSize {
#[serde(rename = "720x1280")]
#[default]
Size720x1280,
#[serde(rename = "1280x720")]
Size1280x720,
#[serde(rename = "1024x1792")]
Size1024x1792,
#[serde(rename = "1792x1024")]
Size1792x1024,
}
impl VideoSize {
pub fn as_str(&self) -> &'static str {
match self {
Self::Size720x1280 => "720x1280",
Self::Size1280x720 => "1280x720",
Self::Size1024x1792 => "1024x1792",
Self::Size1792x1024 => "1792x1024",
}
}
}
impl std::fmt::Display for VideoSize {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[non_exhaustive]
pub enum VideoSeconds {
#[serde(rename = "4")]
#[default]
Four,
#[serde(rename = "8")]
Eight,
#[serde(rename = "12")]
Twelve,
}
impl VideoSeconds {
pub fn as_str(&self) -> &'static str {
match self {
Self::Four => "4",
Self::Eight => "8",
Self::Twelve => "12",
}
}
}
impl std::fmt::Display for VideoSeconds {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum VideoVariant {
#[default]
Video,
Thumbnail,
Spritesheet,
}
impl VideoVariant {
pub fn as_str(&self) -> &'static str {
match self {
Self::Video => "video",
Self::Thumbnail => "thumbnail",
Self::Spritesheet => "spritesheet",
}
}
}
impl std::fmt::Display for VideoVariant {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct InputReference {
#[serde(skip_serializing_if = "Option::is_none")]
pub file_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image_url: Option<String>,
}
impl InputReference {
pub fn file_id<S: Into<String>>(file_id: S) -> Self {
Self { file_id: Some(file_id.into()), image_url: None }
}
pub fn image_url<S: Into<String>>(image_url: S) -> Self {
Self { file_id: None, image_url: Some(image_url.into()) }
}
}
#[derive(Debug, Clone, Default)]
pub struct CreateVideoOptions {
pub model: Option<VideoModel>,
pub seconds: Option<VideoSeconds>,
pub size: Option<VideoSize>,
pub input_reference: Option<InputReference>,
}
impl CreateVideoOptions {
pub fn into_request<S: Into<String>>(self, prompt: S) -> CreateVideoRequest {
CreateVideoRequest { prompt: prompt.into(), model: self.model, seconds: self.seconds, size: self.size, input_reference: self.input_reference }
}
}
#[derive(Debug, Clone, Serialize)]
pub struct CreateVideoRequest {
pub prompt: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<VideoModel>,
#[serde(skip_serializing_if = "Option::is_none")]
pub seconds: Option<VideoSeconds>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<VideoSize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub input_reference: Option<InputReference>,
}
#[derive(Debug, Clone, Serialize)]
struct RemixVideoRequest {
prompt: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SortOrder {
Asc,
#[default]
Desc,
}
impl SortOrder {
pub fn as_str(&self) -> &'static str {
match self {
Self::Asc => "asc",
Self::Desc => "desc",
}
}
}
pub struct Videos {
auth: AuthProvider,
timeout: Option<Duration>,
}
impl Videos {
pub fn new() -> Result<Self> {
let auth = AuthProvider::openai_from_env()?;
Ok(Self { auth, timeout: None })
}
pub fn with_auth(auth: AuthProvider) -> Self {
Self { auth, timeout: None }
}
pub fn azure() -> Result<Self> {
let auth = AuthProvider::azure_from_env()?;
Ok(Self { auth, timeout: None })
}
pub fn detect_provider() -> Result<Self> {
let auth = AuthProvider::from_env()?;
Ok(Self { auth, timeout: None })
}
pub fn with_url<S: Into<String>>(base_url: S, api_key: S) -> Self {
let auth = AuthProvider::from_url_with_key(base_url, api_key);
Self { auth, timeout: None }
}
pub fn from_url<S: Into<String>>(url: S) -> Result<Self> {
let auth = AuthProvider::from_url(url)?;
Ok(Self { auth, timeout: None })
}
pub fn auth(&self) -> &AuthProvider {
&self.auth
}
pub fn base_url<T: AsRef<str>>(&mut self, url: T) -> &mut Self {
if let AuthProvider::OpenAI(ref openai_auth) = self.auth {
let new_auth = OpenAIAuth::new(openai_auth.api_key()).with_base_url(url.as_ref());
self.auth = AuthProvider::OpenAI(new_auth);
} else {
tracing::warn!("base_url() is only supported for OpenAI provider. Use azure() or with_auth() for Azure.");
}
self
}
pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
self.timeout = Some(timeout);
self
}
fn create_client(&self) -> Result<(request::Client, request::header::HeaderMap)> {
let client = create_http_client(self.timeout)?;
let mut headers = request::header::HeaderMap::new();
self.auth.apply_headers(&mut headers)?;
headers.insert("Content-Type", request::header::HeaderValue::from_static("application/json"));
headers.insert("User-Agent", request::header::HeaderValue::from_static("openai-tools-rust"));
Ok((client, headers))
}
async fn read_json<T: serde::de::DeserializeOwned>(response: request::Response) -> Result<T> {
let status = response.status();
let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
if cfg!(test) {
tracing::info!("Response content: {}", content);
}
if !status.is_success() {
if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
}
return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
}
serde_json::from_str::<T>(&content).map_err(OpenAIToolError::SerdeJsonError)
}
pub async fn create<S: Into<String>>(&self, prompt: S, options: CreateVideoOptions) -> Result<Video> {
let (client, headers) = self.create_client()?;
let url = self.auth.endpoint(VIDEOS_PATH);
let body = serde_json::to_string(&options.into_request(prompt))?;
let response = client.post(&url).headers(headers).body(body).send().await.map_err(OpenAIToolError::RequestError)?;
Self::read_json(response).await
}
pub async fn retrieve(&self, video_id: &str) -> Result<Video> {
let (client, headers) = self.create_client()?;
let url = format!("{}/{}", self.auth.endpoint(VIDEOS_PATH), video_id);
let response = client.get(&url).headers(headers).send().await.map_err(OpenAIToolError::RequestError)?;
Self::read_json(response).await
}
pub async fn list(&self, limit: Option<u32>, after: Option<&str>, order: Option<SortOrder>) -> Result<VideoListResponse> {
let (client, headers) = self.create_client()?;
let mut query: Vec<String> = Vec::new();
if let Some(limit) = limit {
query.push(format!("limit={}", limit));
}
if let Some(after) = after {
query.push(format!("after={}", after));
}
if let Some(order) = order {
query.push(format!("order={}", order.as_str()));
}
let endpoint = self.auth.endpoint(VIDEOS_PATH);
let url = if query.is_empty() { endpoint } else { format!("{}?{}", endpoint, query.join("&")) };
let response = client.get(&url).headers(headers).send().await.map_err(OpenAIToolError::RequestError)?;
Self::read_json(response).await
}
pub async fn delete(&self, video_id: &str) -> Result<DeleteVideoResponse> {
let (client, headers) = self.create_client()?;
let url = format!("{}/{}", self.auth.endpoint(VIDEOS_PATH), video_id);
let response = client.delete(&url).headers(headers).send().await.map_err(OpenAIToolError::RequestError)?;
Self::read_json(response).await
}
pub async fn content(&self, video_id: &str, variant: Option<VideoVariant>) -> Result<Vec<u8>> {
let (client, headers) = self.create_client()?;
let url = match variant {
Some(variant) => format!("{}/{}/content?variant={}", self.auth.endpoint(VIDEOS_PATH), video_id, variant.as_str()),
None => format!("{}/{}/content", self.auth.endpoint(VIDEOS_PATH), video_id),
};
let response = client.get(&url).headers(headers).send().await.map_err(OpenAIToolError::RequestError)?;
let status = response.status();
if !status.is_success() {
let content = response.text().await.map_err(OpenAIToolError::RequestError)?;
if let Ok(error_resp) = serde_json::from_str::<ErrorResponse>(&content) {
return Err(OpenAIToolError::Error(error_resp.error.message.unwrap_or_default()));
}
return Err(OpenAIToolError::Error(format!("API error ({}): {}", status, content)));
}
let bytes = response.bytes().await.map_err(OpenAIToolError::RequestError)?;
Ok(bytes.to_vec())
}
pub async fn remix<S: Into<String>>(&self, video_id: &str, prompt: S) -> Result<Video> {
let (client, headers) = self.create_client()?;
let url = format!("{}/{}/remix", self.auth.endpoint(VIDEOS_PATH), video_id);
let body = serde_json::to_string(&RemixVideoRequest { prompt: prompt.into() })?;
let response = client.post(&url).headers(headers).body(body).send().await.map_err(OpenAIToolError::RequestError)?;
Self::read_json(response).await
}
}