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)]
46pub struct CloneVoiceFile {
47 pub filename: String,
49
50 pub data: Vec<u8>,
52
53 pub mime_type: String,
55}
56
57#[derive(Debug, Clone, Deserialize)]
59pub struct CloneVoiceResponse {
60 pub voice_id: String,
62
63 pub name: String,
65
66 #[serde(default)]
68 pub status: Option<String>,
69}
70
71#[derive(Debug, Clone, Deserialize)]
77pub struct SharedVoice {
78 pub public_owner_id: String,
80
81 pub voice_id: String,
83
84 pub name: String,
86
87 #[serde(default)]
89 pub category: Option<String>,
90
91 #[serde(default)]
93 pub description: Option<String>,
94
95 #[serde(default)]
97 pub preview_url: Option<String>,
98
99 #[serde(default)]
101 pub gender: Option<String>,
102
103 #[serde(default)]
105 pub age: Option<String>,
106
107 #[serde(default)]
109 pub accent: Option<String>,
110
111 #[serde(default)]
113 pub language: Option<String>,
114
115 #[serde(default)]
117 pub use_case: Option<String>,
118
119 #[serde(default)]
121 pub rate: Option<f64>,
122
123 #[serde(default)]
125 pub cloned_by_count: Option<i64>,
126
127 #[serde(default)]
129 pub free_users_allowed: Option<bool>,
130}
131
132#[derive(Debug, Clone, Deserialize)]
134pub struct SharedVoicesResponse {
135 pub voices: Vec<SharedVoice>,
137
138 #[serde(default)]
140 pub next_cursor: Option<String>,
141
142 #[serde(default)]
144 pub has_more: bool,
145}
146
147#[derive(Debug, Clone, Serialize, Default)]
149pub struct VoiceLibraryQuery {
150 #[serde(skip_serializing_if = "Option::is_none")]
152 pub query: Option<String>,
153
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub page_size: Option<i32>,
157
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub cursor: Option<String>,
161
162 #[serde(skip_serializing_if = "Option::is_none")]
164 pub gender: Option<String>,
165
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub language: Option<String>,
169
170 #[serde(skip_serializing_if = "Option::is_none")]
172 pub use_case: Option<String>,
173}
174
175#[derive(Debug, Clone, Deserialize)]
177pub struct AddVoiceFromLibraryResponse {
178 pub voice_id: String,
180}
181
182fn encode_query_value(s: &str) -> String {
184 let mut encoded = String::with_capacity(s.len());
185 for b in s.bytes() {
186 match b {
187 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
188 encoded.push(b as char);
189 }
190 _ => {
191 encoded.push_str(&format!("%{b:02X}"));
192 }
193 }
194 }
195 encoded
196}
197
198impl Client {
199 pub async fn list_voices(&self) -> Result<VoicesResponse> {
201 let (resp, _meta) = self
202 .get_json::<VoicesResponse>("/qai/v1/voices")
203 .await?;
204 Ok(resp)
205 }
206
207 pub async fn clone_voice(
211 &self,
212 name: &str,
213 files: Vec<CloneVoiceFile>,
214 ) -> Result<CloneVoiceResponse> {
215 let mut form = reqwest::multipart::Form::new().text("name", name.to_string());
216
217 for file in files {
218 let part = reqwest::multipart::Part::bytes(file.data)
219 .file_name(file.filename)
220 .mime_str(&file.mime_type)
221 .map_err(|e| crate::error::Error::Http(e.into()))?;
222 form = form.part("files", part);
223 }
224
225 let (resp, _meta) = self
226 .post_multipart::<CloneVoiceResponse>("/qai/v1/voices/clone", form)
227 .await?;
228 Ok(resp)
229 }
230
231 pub async fn delete_voice(&self, id: &str) -> Result<StatusResponse> {
233 let path = format!("/qai/v1/voices/{id}");
234 let (resp, _meta) = self.delete_json::<StatusResponse>(&path).await?;
235 Ok(resp)
236 }
237
238 pub async fn voice_library(
240 &self,
241 query: &VoiceLibraryQuery,
242 ) -> Result<SharedVoicesResponse> {
243 let mut params = Vec::new();
244 if let Some(ref q) = query.query {
245 params.push(format!("query={}", encode_query_value(q)));
246 }
247 if let Some(ps) = query.page_size {
248 params.push(format!("page_size={ps}"));
249 }
250 if let Some(ref c) = query.cursor {
251 params.push(format!("cursor={}", encode_query_value(c)));
252 }
253 if let Some(ref g) = query.gender {
254 params.push(format!("gender={}", encode_query_value(g)));
255 }
256 if let Some(ref l) = query.language {
257 params.push(format!("language={}", encode_query_value(l)));
258 }
259 if let Some(ref u) = query.use_case {
260 params.push(format!("use_case={}", encode_query_value(u)));
261 }
262
263 let path = if params.is_empty() {
264 "/qai/v1/voices/library".to_string()
265 } else {
266 format!("/qai/v1/voices/library?{}", params.join("&"))
267 };
268
269 let (resp, _meta) = self
270 .get_json::<SharedVoicesResponse>(&path)
271 .await?;
272 Ok(resp)
273 }
274
275 pub async fn add_voice_from_library(
277 &self,
278 public_owner_id: &str,
279 voice_id: &str,
280 name: Option<&str>,
281 ) -> Result<AddVoiceFromLibraryResponse> {
282 let mut body = serde_json::json!({
283 "public_owner_id": public_owner_id,
284 "voice_id": voice_id,
285 });
286 if let Some(n) = name {
287 body["name"] = serde_json::Value::String(n.to_string());
288 }
289 let (resp, _meta) = self
290 .post_json::<serde_json::Value, AddVoiceFromLibraryResponse>(
291 "/qai/v1/voices/library/add",
292 &body,
293 )
294 .await?;
295 Ok(resp)
296 }
297}