1use serde::{Deserialize, Serialize};
2
3use crate::client::Client;
4use crate::error::Result;
5use crate::keys::StatusResponse;
6
7#[derive(Debug, Clone, Deserialize)]
9pub struct Voice {
10 pub voice_id: String,
12
13 pub name: String,
15
16 #[serde(default)]
18 pub provider: Option<String>,
19
20 #[serde(default)]
22 pub languages: Option<Vec<String>>,
23
24 #[serde(default)]
26 pub gender: Option<String>,
27
28 #[serde(default)]
30 pub is_cloned: Option<bool>,
31
32 #[serde(default)]
34 pub preview_url: Option<String>,
35}
36
37#[derive(Debug, Clone, Deserialize)]
39pub struct VoicesResponse {
40 pub voices: Vec<Voice>,
42}
43
44#[derive(Debug, Clone, Deserialize)]
46pub struct VoiceInfo {
47 pub voice_id: String,
49
50 pub name: String,
52
53 #[serde(default)]
55 pub category: String,
56
57 #[serde(default)]
59 pub description: Option<String>,
60
61 #[serde(default)]
63 pub preview_url: Option<String>,
64}
65
66#[derive(Debug, Clone)]
68pub struct CloneVoiceFile {
69 pub filename: String,
71
72 pub data: Vec<u8>,
74
75 pub mime_type: String,
77}
78
79#[derive(Debug, Clone, Deserialize)]
81pub struct CloneVoiceResponse {
82 pub voice_id: String,
84
85 pub name: String,
87
88 #[serde(default)]
90 pub status: Option<String>,
91}
92
93#[derive(Debug, Clone, Deserialize)]
99pub struct SharedVoice {
100 pub public_owner_id: String,
102
103 pub voice_id: String,
105
106 pub name: String,
108
109 #[serde(default)]
111 pub category: Option<String>,
112
113 #[serde(default)]
115 pub description: Option<String>,
116
117 #[serde(default)]
119 pub preview_url: Option<String>,
120
121 #[serde(default)]
123 pub gender: Option<String>,
124
125 #[serde(default)]
127 pub age: Option<String>,
128
129 #[serde(default)]
131 pub accent: Option<String>,
132
133 #[serde(default)]
135 pub language: Option<String>,
136
137 #[serde(default)]
139 pub use_case: Option<String>,
140
141 #[serde(default)]
143 pub rate: Option<f64>,
144
145 #[serde(default)]
147 pub cloned_by_count: Option<i64>,
148
149 #[serde(default)]
151 pub free_users_allowed: Option<bool>,
152}
153
154#[derive(Debug, Clone, Deserialize)]
156pub struct SharedVoicesResponse {
157 pub voices: Vec<SharedVoice>,
159
160 #[serde(default)]
162 pub next_cursor: Option<String>,
163
164 #[serde(default)]
166 pub has_more: bool,
167}
168
169#[derive(Debug, Clone, Serialize, Default)]
171pub struct VoiceLibraryQuery {
172 #[serde(skip_serializing_if = "Option::is_none")]
174 pub query: Option<String>,
175
176 #[serde(skip_serializing_if = "Option::is_none")]
178 pub page_size: Option<i32>,
179
180 #[serde(skip_serializing_if = "Option::is_none")]
182 pub cursor: Option<String>,
183
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub gender: Option<String>,
187
188 #[serde(skip_serializing_if = "Option::is_none")]
190 pub language: Option<String>,
191
192 #[serde(skip_serializing_if = "Option::is_none")]
194 pub use_case: Option<String>,
195}
196
197#[derive(Debug, Clone, Serialize, Default)]
199pub struct AddVoiceFromLibraryRequest {
200 pub public_owner_id: String,
202
203 pub voice_id: String,
205
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub name: Option<String>,
209}
210
211#[derive(Debug, Clone, Serialize, Default)]
213pub struct CloneVoiceRequest {
214 pub name: String,
216
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub description: Option<String>,
220
221 pub audio_samples: Vec<String>,
223}
224
225#[derive(Debug, Clone, Deserialize)]
227pub struct AddVoiceFromLibraryResponse {
228 pub voice_id: String,
230}
231
232fn encode_query_value(s: &str) -> String {
234 let mut encoded = String::with_capacity(s.len());
235 for b in s.bytes() {
236 match b {
237 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
238 encoded.push(b as char);
239 }
240 _ => {
241 encoded.push_str(&format!("%{b:02X}"));
242 }
243 }
244 }
245 encoded
246}
247
248impl Client {
249 pub async fn list_voices(&self) -> Result<VoicesResponse> {
251 let (resp, _meta) = self
252 .get_json::<VoicesResponse>("/qai/v1/voices")
253 .await?;
254 Ok(resp)
255 }
256
257 pub async fn clone_voice(
261 &self,
262 name: &str,
263 files: Vec<CloneVoiceFile>,
264 ) -> Result<CloneVoiceResponse> {
265 let mut form = reqwest::multipart::Form::new().text("name", name.to_string());
266
267 for file in files {
268 let part = reqwest::multipart::Part::bytes(file.data)
269 .file_name(file.filename)
270 .mime_str(&file.mime_type)
271 .map_err(|e| crate::error::Error::Http(e.into()))?;
272 form = form.part("files", part);
273 }
274
275 let (resp, _meta) = self
276 .post_multipart::<CloneVoiceResponse>("/qai/v1/voices/clone", form)
277 .await?;
278 Ok(resp)
279 }
280
281 pub async fn delete_voice(&self, id: &str) -> Result<StatusResponse> {
283 let path = format!("/qai/v1/voices/{id}");
284 let (resp, _meta) = self.delete_json::<StatusResponse>(&path).await?;
285 Ok(resp)
286 }
287
288 pub async fn voice_library(
290 &self,
291 query: &VoiceLibraryQuery,
292 ) -> Result<SharedVoicesResponse> {
293 let mut params = Vec::new();
294 if let Some(ref q) = query.query {
295 params.push(format!("query={}", encode_query_value(q)));
296 }
297 if let Some(ps) = query.page_size {
298 params.push(format!("page_size={ps}"));
299 }
300 if let Some(ref c) = query.cursor {
301 params.push(format!("cursor={}", encode_query_value(c)));
302 }
303 if let Some(ref g) = query.gender {
304 params.push(format!("gender={}", encode_query_value(g)));
305 }
306 if let Some(ref l) = query.language {
307 params.push(format!("language={}", encode_query_value(l)));
308 }
309 if let Some(ref u) = query.use_case {
310 params.push(format!("use_case={}", encode_query_value(u)));
311 }
312
313 let path = if params.is_empty() {
314 "/qai/v1/voices/library".to_string()
315 } else {
316 format!("/qai/v1/voices/library?{}", params.join("&"))
317 };
318
319 let (resp, _meta) = self
320 .get_json::<SharedVoicesResponse>(&path)
321 .await?;
322 Ok(resp)
323 }
324
325 pub async fn add_voice_from_library(
327 &self,
328 public_owner_id: &str,
329 voice_id: &str,
330 name: Option<&str>,
331 ) -> Result<AddVoiceFromLibraryResponse> {
332 let mut body = serde_json::json!({
333 "public_owner_id": public_owner_id,
334 "voice_id": voice_id,
335 });
336 if let Some(n) = name {
337 body["name"] = serde_json::Value::String(n.to_string());
338 }
339 let (resp, _meta) = self
340 .post_json::<serde_json::Value, AddVoiceFromLibraryResponse>(
341 "/qai/v1/voices/library/add",
342 &body,
343 )
344 .await?;
345 Ok(resp)
346 }
347}