use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::driver::Model;
use crate::error::{EncodeError, ProviderError};
use crate::operation::Whole;
use crate::providers::openai::OpenAI;
use crate::providers::openai::wire::OpenAIConfig;
use crate::wire::{
Body, Call, Decoder, Descriptor, Encoded, Flow, Free, Mode, Operation, Out, Wire, WireEvent,
WireFrame,
};
const FILES_UPLOAD_PATH: &str = "/files/upload";
const VOICE_CLONE_PATH: &str = "/voice_clone";
pub fn voice_cloning(client: &OpenAI) -> Model<VoiceClones> {
client.model(VoiceClones::new(client.config().clone()))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AudioPurpose {
VoiceClone,
PromptAudio,
}
impl AudioPurpose {
pub fn as_str(self) -> &'static str {
match self {
Self::VoiceClone => "voice_clone",
Self::PromptAudio => "prompt_audio",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CloneAudio {
pub purpose: AudioPurpose,
pub filename: String,
pub data: Vec<u8>,
}
impl CloneAudio {
pub fn sample(filename: impl Into<String>, data: impl Into<Vec<u8>>) -> Self {
Self {
purpose: AudioPurpose::VoiceClone,
filename: filename.into(),
data: data.into(),
}
}
pub fn prompt(filename: impl Into<String>, data: impl Into<Vec<u8>>) -> Self {
Self {
purpose: AudioPurpose::PromptAudio,
filename: filename.into(),
data: data.into(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct UploadedFile {
pub file_id: i64,
#[serde(default)]
pub bytes: Option<u64>,
#[serde(default)]
pub created_at: Option<u64>,
#[serde(default)]
pub filename: Option<String>,
#[serde(default)]
pub purpose: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct ClonePrompt {
pub prompt_audio: i64,
pub prompt_text: String,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct VoiceClone {
pub file_id: i64,
pub voice_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub clone_prompt: Option<ClonePrompt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip)]
pub additional_params: Option<Value>,
}
impl VoiceClone {
pub fn new(file_id: i64, voice_id: impl Into<String>) -> Self {
Self {
file_id,
voice_id: voice_id.into(),
clone_prompt: None,
text: None,
model: None,
additional_params: None,
}
}
pub fn prompt(mut self, prompt_audio: i64, prompt_text: impl Into<String>) -> Self {
self.clone_prompt = Some(ClonePrompt {
prompt_audio,
prompt_text: prompt_text.into(),
});
self
}
pub fn preview(mut self, text: impl Into<String>, model: impl Into<String>) -> Self {
self.text = Some(text.into());
self.model = Some(model.into());
self
}
pub fn created_voice_id(&self) -> &str {
self.additional_params
.as_ref()
.and_then(|params| params.get("voice_id"))
.and_then(Value::as_str)
.unwrap_or(&self.voice_id)
}
pub fn additional_params(mut self, params: impl Into<Option<Value>>) -> Self {
self.additional_params =
crate::json_utils::merge_params(self.additional_params.take(), params.into());
self
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ClonedVoice {
pub voice_id: String,
pub demo_audio: Option<String>,
pub extra_info: Option<Value>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum VoiceCloneRequest {
Upload(CloneAudio),
Clone(VoiceClone),
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct VoiceCloneReply {
#[serde(default)]
pub file: Option<UploadedFile>,
#[serde(default)]
pub demo_audio: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extra_info: Option<Value>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VoiceCloning;
impl Operation for VoiceCloning {
type Request = VoiceCloneRequest;
type Event = std::convert::Infallible;
type End = VoiceCloneReply;
type Response = VoiceCloneReply;
type Fold = Whole<Self>;
type Emit = Free;
fn fold(_request: &Self::Request, _call: &mut Call<'_>) -> Self::Fold {
Whole::new()
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct VoiceClones {
pub provider: OpenAIConfig,
}
impl VoiceClones {
pub fn new(provider: OpenAIConfig) -> Self {
Self { provider }
}
}
impl Wire for VoiceClones {
type Op = VoiceCloning;
type Event = Vec<u8>;
type Payload = Encoded;
type Frame = WireFrame;
type Decoder<'id> = VoiceClonesDecoder;
type Reassembler = crate::wire::document::Unreassembled;
fn describe(&self) -> Descriptor<'_> {
Descriptor::new(self.provider.dialect.name)
}
fn encode(&self, request: VoiceCloneRequest, _mode: Mode) -> Result<Encoded, EncodeError> {
if self.provider.dialect.name != super::PROVIDER_NAME {
return Err(EncodeError::request(format!(
"voice cloning is a MiniMax endpoint, not one {} serves",
self.provider.dialect.name
)));
}
let provider = &self.provider;
match request {
VoiceCloneRequest::Upload(audio) => {
use crate::http_client::MultipartForm;
use crate::http_client::multipart::Part;
let format = std::path::Path::new(&audio.filename)
.extension()
.and_then(std::ffi::OsStr::to_str)
.map(str::to_ascii_lowercase);
if !matches!(format.as_deref(), Some("mp3" | "m4a" | "wav")) {
return Err(EncodeError::request(format!(
"MiniMax clones voices from mp3, m4a or wav files, not `{}`",
audio.filename
)));
}
let form = MultipartForm::new()
.text("purpose", audio.purpose.as_str())
.part(Part::bytes("file", audio.data).filename(audio.filename));
let builder = http::Request::post(provider.uri(FILES_UPLOAD_PATH, None));
crate::providers::openai::wire::encoded(provider, builder, Body::Multipart(form))
}
VoiceCloneRequest::Clone(mut clone) => {
let additional_params = clone.additional_params.take();
let mut body = serde_json::to_value(clone)?;
if let Some(additional_params) = additional_params {
crate::json_utils::merge_inplace(&mut body, additional_params);
}
crate::providers::openai::wire::json_post(provider, VOICE_CLONE_PATH, None, &body)
}
}
}
fn decoder<'id>(&self) -> Self::Decoder<'id> {
VoiceClonesDecoder
}
}
pub struct VoiceClonesDecoder;
impl<'id> Decoder<'id, VoiceCloning> for VoiceClonesDecoder {
type Event = Vec<u8>;
fn classify(&self, frame: WireFrame) -> WireEvent<Self::Event> {
WireEvent::Known(match frame {
WireFrame::Text(text) => text.into_bytes(),
WireFrame::Bytes(bytes) => bytes,
})
}
fn decode(
&mut self,
body: Vec<u8>,
out: Out<'id, VoiceCloning>,
) -> Result<Flow, ProviderError> {
Ok(out.end(super::decode_reply(&body)?))
}
}