Skip to main content

quantum_sdk/
voices.rs

1use serde::{Deserialize, Serialize};
2
3use crate::client::Client;
4use crate::error::Result;
5use crate::keys::StatusResponse;
6
7/// A voice available for TTS.
8#[derive(Debug, Clone, Deserialize)]
9pub struct Voice {
10    /// Voice identifier.
11    pub voice_id: String,
12
13    /// Human-readable voice name.
14    pub name: String,
15
16    /// Provider (e.g. "elevenlabs", "openai").
17    #[serde(default)]
18    pub provider: Option<String>,
19
20    /// Language/locale codes supported.
21    #[serde(default)]
22    pub languages: Option<Vec<String>>,
23
24    /// Voice gender.
25    #[serde(default)]
26    pub gender: Option<String>,
27
28    /// Whether this is a cloned voice.
29    #[serde(default)]
30    pub is_cloned: Option<bool>,
31
32    /// Preview audio URL.
33    #[serde(default)]
34    pub preview_url: Option<String>,
35}
36
37/// Response from listing voices.
38#[derive(Debug, Clone, Deserialize)]
39pub struct VoicesResponse {
40    /// Available voices.
41    pub voices: Vec<Voice>,
42}
43
44/// Describes an available voice with detail info (sdk-graph canonical name).
45#[derive(Debug, Clone, Deserialize)]
46pub struct VoiceInfo {
47    /// Voice identifier.
48    pub voice_id: String,
49
50    /// Human-readable voice name.
51    pub name: String,
52
53    /// Voice category (e.g. "premade", "cloned").
54    #[serde(default)]
55    pub category: String,
56
57    /// Voice description.
58    #[serde(default)]
59    pub description: Option<String>,
60
61    /// Preview audio URL.
62    #[serde(default)]
63    pub preview_url: Option<String>,
64}
65
66/// A file to include in a voice clone request.
67#[derive(Debug, Clone)]
68pub struct CloneVoiceFile {
69    /// Original filename (e.g. "sample.mp3").
70    pub filename: String,
71
72    /// Raw file bytes.
73    pub data: Vec<u8>,
74
75    /// MIME type (e.g. "audio/mpeg").
76    pub mime_type: String,
77}
78
79/// Response from cloning a voice.
80#[derive(Debug, Clone, Deserialize)]
81pub struct CloneVoiceResponse {
82    /// The new voice identifier.
83    pub voice_id: String,
84
85    /// The name assigned to the cloned voice.
86    pub name: String,
87
88    /// Status message.
89    #[serde(default)]
90    pub status: Option<String>,
91}
92
93// ---------------------------------------------------------------------------
94// Voice Library (shared/community voices)
95// ---------------------------------------------------------------------------
96
97/// A shared voice from the voice library.
98#[derive(Debug, Clone, Deserialize)]
99pub struct SharedVoice {
100    /// Owner's public identifier.
101    pub public_owner_id: String,
102
103    /// Voice identifier.
104    pub voice_id: String,
105
106    /// Voice display name.
107    pub name: String,
108
109    /// Voice category (e.g. "professional", "generated").
110    #[serde(default)]
111    pub category: Option<String>,
112
113    /// Voice description.
114    #[serde(default)]
115    pub description: Option<String>,
116
117    /// Preview audio URL.
118    #[serde(default)]
119    pub preview_url: Option<String>,
120
121    /// Voice gender.
122    #[serde(default)]
123    pub gender: Option<String>,
124
125    /// Perceived age range.
126    #[serde(default)]
127    pub age: Option<String>,
128
129    /// Accent (e.g. "british", "american").
130    #[serde(default)]
131    pub accent: Option<String>,
132
133    /// Primary language.
134    #[serde(default)]
135    pub language: Option<String>,
136
137    /// Intended use case (e.g. "narration", "conversational").
138    #[serde(default)]
139    pub use_case: Option<String>,
140
141    /// Average rating.
142    #[serde(default)]
143    pub rate: Option<f64>,
144
145    /// Number of times this voice has been cloned.
146    #[serde(default)]
147    pub cloned_by_count: Option<i64>,
148
149    /// Whether free-tier users can use this voice.
150    #[serde(default)]
151    pub free_users_allowed: Option<bool>,
152}
153
154/// Response from browsing the voice library.
155#[derive(Debug, Clone, Deserialize)]
156pub struct SharedVoicesResponse {
157    /// Shared voices matching the query.
158    pub voices: Vec<SharedVoice>,
159
160    /// Cursor for pagination (pass as `cursor` in next request).
161    #[serde(default)]
162    pub next_cursor: Option<String>,
163
164    /// Whether more results are available.
165    #[serde(default)]
166    pub has_more: bool,
167}
168
169/// Request parameters for browsing the voice library.
170#[derive(Debug, Clone, Serialize, Default)]
171pub struct VoiceLibraryQuery {
172    /// Search query string.
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub query: Option<String>,
175
176    /// Maximum number of results per page.
177    #[serde(skip_serializing_if = "Option::is_none")]
178    pub page_size: Option<i32>,
179
180    /// Pagination cursor from a previous response.
181    #[serde(skip_serializing_if = "Option::is_none")]
182    pub cursor: Option<String>,
183
184    /// Filter by gender.
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub gender: Option<String>,
187
188    /// Filter by language.
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub language: Option<String>,
191
192    /// Filter by use case.
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub use_case: Option<String>,
195}
196
197/// Request body for adding a shared voice from the library.
198#[derive(Debug, Clone, Serialize, Default)]
199pub struct AddVoiceFromLibraryRequest {
200    /// Public owner identifier.
201    pub public_owner_id: String,
202
203    /// Voice identifier in the library.
204    pub voice_id: String,
205
206    /// Optional display name (defaults to the library name).
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub name: Option<String>,
209}
210
211/// Request body for instant voice cloning from audio samples (JSON path).
212#[derive(Debug, Clone, Serialize, Default)]
213pub struct CloneVoiceRequest {
214    /// Display name for the cloned voice.
215    pub name: String,
216
217    /// Description of the voice.
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub description: Option<String>,
220
221    /// Base64-encoded audio files for cloning.
222    pub audio_samples: Vec<String>,
223}
224
225/// Response from adding a voice from the library.
226#[derive(Debug, Clone, Deserialize)]
227pub struct AddVoiceFromLibraryResponse {
228    /// The voice ID added to the user's account.
229    pub voice_id: String,
230}
231
232/// Simple percent-encoding for query parameter values.
233fn 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    /// Lists all available TTS voices (built-in and cloned).
250    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    /// Clones a voice from audio samples.
258    ///
259    /// Sends audio files as multipart form data along with a name for the new voice.
260    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    /// Deletes a cloned voice by its ID.
282    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    /// Browses the shared voice library with optional filters.
289    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    /// Adds a shared voice from the library to the user's account.
326    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}