rig_core/providers/minimax/
voice_clone.rs1use serde::{Deserialize, Serialize};
22use serde_json::Value;
23
24use crate::driver::Model;
25use crate::error::{EncodeError, ProviderError};
26use crate::operation::Whole;
27use crate::providers::openai::OpenAI;
28use crate::providers::openai::wire::OpenAIConfig;
29use crate::wire::{
30 Body, Call, Decoder, Descriptor, Encoded, Flow, Free, Mode, Operation, Out, Wire, WireEvent,
31 WireFrame,
32};
33
34const FILES_UPLOAD_PATH: &str = "/files/upload";
35const VOICE_CLONE_PATH: &str = "/voice_clone";
36
37pub fn voice_cloning(client: &OpenAI) -> Model<VoiceClones> {
40 client.model(VoiceClones::new(client.config().clone()))
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum AudioPurpose {
46 VoiceClone,
48 PromptAudio,
50}
51
52impl AudioPurpose {
53 pub fn as_str(self) -> &'static str {
55 match self {
56 Self::VoiceClone => "voice_clone",
57 Self::PromptAudio => "prompt_audio",
58 }
59 }
60}
61
62#[derive(Clone, Debug, PartialEq, Eq)]
65pub struct CloneAudio {
66 pub purpose: AudioPurpose,
68 pub filename: String,
70 pub data: Vec<u8>,
72}
73
74impl CloneAudio {
75 pub fn sample(filename: impl Into<String>, data: impl Into<Vec<u8>>) -> Self {
77 Self {
78 purpose: AudioPurpose::VoiceClone,
79 filename: filename.into(),
80 data: data.into(),
81 }
82 }
83
84 pub fn prompt(filename: impl Into<String>, data: impl Into<Vec<u8>>) -> Self {
86 Self {
87 purpose: AudioPurpose::PromptAudio,
88 filename: filename.into(),
89 data: data.into(),
90 }
91 }
92}
93
94#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
96pub struct UploadedFile {
97 pub file_id: i64,
99 #[serde(default)]
101 pub bytes: Option<u64>,
102 #[serde(default)]
104 pub created_at: Option<u64>,
105 #[serde(default)]
107 pub filename: Option<String>,
108 #[serde(default)]
110 pub purpose: Option<String>,
111}
112
113#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
116pub struct ClonePrompt {
117 pub prompt_audio: i64,
119 pub prompt_text: String,
121}
122
123#[derive(Clone, Debug, PartialEq, Serialize)]
125pub struct VoiceClone {
126 pub file_id: i64,
128 pub voice_id: String,
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub clone_prompt: Option<ClonePrompt>,
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub text: Option<String>,
137 #[serde(skip_serializing_if = "Option::is_none")]
139 pub model: Option<String>,
140 #[serde(skip)]
143 pub additional_params: Option<Value>,
144}
145
146impl VoiceClone {
147 pub fn new(file_id: i64, voice_id: impl Into<String>) -> Self {
149 Self {
150 file_id,
151 voice_id: voice_id.into(),
152 clone_prompt: None,
153 text: None,
154 model: None,
155 additional_params: None,
156 }
157 }
158
159 pub fn prompt(mut self, prompt_audio: i64, prompt_text: impl Into<String>) -> Self {
162 self.clone_prompt = Some(ClonePrompt {
163 prompt_audio,
164 prompt_text: prompt_text.into(),
165 });
166 self
167 }
168
169 pub fn preview(mut self, text: impl Into<String>, model: impl Into<String>) -> Self {
171 self.text = Some(text.into());
172 self.model = Some(model.into());
173 self
174 }
175
176 pub fn created_voice_id(&self) -> &str {
179 self.additional_params
180 .as_ref()
181 .and_then(|params| params.get("voice_id"))
182 .and_then(Value::as_str)
183 .unwrap_or(&self.voice_id)
184 }
185
186 pub fn additional_params(mut self, params: impl Into<Option<Value>>) -> Self {
189 self.additional_params =
190 crate::json_utils::merge_params(self.additional_params.take(), params.into());
191 self
192 }
193}
194
195#[derive(Clone, Debug, PartialEq)]
197pub struct ClonedVoice {
198 pub voice_id: String,
200 pub demo_audio: Option<String>,
202 pub extra_info: Option<Value>,
204}
205
206#[derive(Clone, Debug, PartialEq)]
208pub enum VoiceCloneRequest {
209 Upload(CloneAudio),
211 Clone(VoiceClone),
213}
214
215#[derive(Clone, Debug, Default, Deserialize, Serialize)]
218pub struct VoiceCloneReply {
219 #[serde(default)]
221 pub file: Option<UploadedFile>,
222 #[serde(default)]
224 pub demo_audio: Option<String>,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
227 pub extra_info: Option<Value>,
228}
229
230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
232pub struct VoiceCloning;
233
234impl Operation for VoiceCloning {
235 type Request = VoiceCloneRequest;
236 type Event = std::convert::Infallible;
237 type End = VoiceCloneReply;
238 type Response = VoiceCloneReply;
239 type Fold = Whole<Self>;
240 type Emit = Free;
241
242 fn fold(_request: &Self::Request, _call: &mut Call<'_>) -> Self::Fold {
243 Whole::new()
244 }
245}
246
247#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
250pub struct VoiceClones {
251 pub provider: OpenAIConfig,
253}
254
255impl VoiceClones {
256 pub fn new(provider: OpenAIConfig) -> Self {
258 Self { provider }
259 }
260}
261
262impl Wire for VoiceClones {
263 type Op = VoiceCloning;
264 type Event = Vec<u8>;
265 type Payload = Encoded;
266 type Frame = WireFrame;
267 type Decoder<'id> = VoiceClonesDecoder;
268 type Reassembler = crate::wire::document::Unreassembled;
269
270 fn describe(&self) -> Descriptor<'_> {
271 Descriptor::new(self.provider.dialect.name)
272 }
273
274 fn encode(&self, request: VoiceCloneRequest, _mode: Mode) -> Result<Encoded, EncodeError> {
275 if self.provider.dialect.name != super::PROVIDER_NAME {
276 return Err(EncodeError::request(format!(
277 "voice cloning is a MiniMax endpoint, not one {} serves",
278 self.provider.dialect.name
279 )));
280 }
281 let provider = &self.provider;
282 match request {
283 VoiceCloneRequest::Upload(audio) => {
284 use crate::http_client::MultipartForm;
285 use crate::http_client::multipart::Part;
286
287 let format = std::path::Path::new(&audio.filename)
288 .extension()
289 .and_then(std::ffi::OsStr::to_str)
290 .map(str::to_ascii_lowercase);
291 if !matches!(format.as_deref(), Some("mp3" | "m4a" | "wav")) {
292 return Err(EncodeError::request(format!(
293 "MiniMax clones voices from mp3, m4a or wav files, not `{}`",
294 audio.filename
295 )));
296 }
297
298 let form = MultipartForm::new()
299 .text("purpose", audio.purpose.as_str())
300 .part(Part::bytes("file", audio.data).filename(audio.filename));
301 let builder = http::Request::post(provider.uri(FILES_UPLOAD_PATH, None));
302 crate::providers::openai::wire::encoded(provider, builder, Body::Multipart(form))
303 }
304 VoiceCloneRequest::Clone(mut clone) => {
305 let additional_params = clone.additional_params.take();
306 let mut body = serde_json::to_value(clone)?;
307 if let Some(additional_params) = additional_params {
308 crate::json_utils::merge_inplace(&mut body, additional_params);
309 }
310 crate::providers::openai::wire::json_post(provider, VOICE_CLONE_PATH, None, &body)
311 }
312 }
313 }
314
315 fn decoder<'id>(&self) -> Self::Decoder<'id> {
316 VoiceClonesDecoder
317 }
318}
319
320pub struct VoiceClonesDecoder;
322
323impl<'id> Decoder<'id, VoiceCloning> for VoiceClonesDecoder {
324 type Event = Vec<u8>;
325
326 fn classify(&self, frame: WireFrame) -> WireEvent<Self::Event> {
327 WireEvent::Known(match frame {
328 WireFrame::Text(text) => text.into_bytes(),
329 WireFrame::Bytes(bytes) => bytes,
330 })
331 }
332
333 fn decode(
334 &mut self,
335 body: Vec<u8>,
336 out: Out<'id, VoiceCloning>,
337 ) -> Result<Flow, ProviderError> {
338 Ok(out.end(super::decode_reply(&body)?))
339 }
340}