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/// Percent-encodes a query parameter value using the urlencoding crate.
233fn encode_query_value(s: &str) -> String {
234    urlencoding::encode(s).into_owned()
235}
236
237impl Client {
238    /// Lists all available TTS voices (built-in and cloned).
239    pub async fn list_voices(&self) -> Result<VoicesResponse> {
240        let (resp, _meta) = self
241            .get_json::<VoicesResponse>("/qai/v1/voices")
242            .await?;
243        Ok(resp)
244    }
245
246    /// Clones a voice from audio samples.
247    ///
248    /// Sends audio files as multipart form data along with a name for the new voice.
249    pub async fn clone_voice(
250        &self,
251        name: &str,
252        files: Vec<CloneVoiceFile>,
253    ) -> Result<CloneVoiceResponse> {
254        let mut form = reqwest::multipart::Form::new().text("name", name.to_string());
255
256        for file in files {
257            let part = reqwest::multipart::Part::bytes(file.data)
258                .file_name(file.filename)
259                .mime_str(&file.mime_type)
260                .map_err(|e| crate::error::Error::Http(e.into()))?;
261            form = form.part("files", part);
262        }
263
264        let (resp, _meta) = self
265            .post_multipart::<CloneVoiceResponse>("/qai/v1/voices/clone", form)
266            .await?;
267        Ok(resp)
268    }
269
270    /// Deletes a cloned voice by its ID.
271    pub async fn delete_voice(&self, id: &str) -> Result<StatusResponse> {
272        let path = format!("/qai/v1/voices/{id}");
273        let (resp, _meta) = self.delete_json::<StatusResponse>(&path).await?;
274        Ok(resp)
275    }
276
277    /// Browses the shared voice library with optional filters.
278    pub async fn voice_library(
279        &self,
280        query: &VoiceLibraryQuery,
281    ) -> Result<SharedVoicesResponse> {
282        let mut params = Vec::new();
283        if let Some(ref q) = query.query {
284            params.push(format!("query={}", encode_query_value(q)));
285        }
286        if let Some(ps) = query.page_size {
287            params.push(format!("page_size={ps}"));
288        }
289        if let Some(ref c) = query.cursor {
290            params.push(format!("cursor={}", encode_query_value(c)));
291        }
292        if let Some(ref g) = query.gender {
293            params.push(format!("gender={}", encode_query_value(g)));
294        }
295        if let Some(ref l) = query.language {
296            params.push(format!("language={}", encode_query_value(l)));
297        }
298        if let Some(ref u) = query.use_case {
299            params.push(format!("use_case={}", encode_query_value(u)));
300        }
301
302        let path = if params.is_empty() {
303            "/qai/v1/voices/library".to_string()
304        } else {
305            format!("/qai/v1/voices/library?{}", params.join("&"))
306        };
307
308        let (resp, _meta) = self
309            .get_json::<SharedVoicesResponse>(&path)
310            .await?;
311        Ok(resp)
312    }
313
314    /// Adds a shared voice from the library to the user's account.
315    pub async fn add_voice_from_library(
316        &self,
317        public_owner_id: &str,
318        voice_id: &str,
319        name: Option<&str>,
320    ) -> Result<AddVoiceFromLibraryResponse> {
321        let mut body = serde_json::json!({
322            "public_owner_id": public_owner_id,
323            "voice_id": voice_id,
324        });
325        if let Some(n) = name {
326            body["name"] = serde_json::Value::String(n.to_string());
327        }
328        let (resp, _meta) = self
329            .post_json::<serde_json::Value, AddVoiceFromLibraryResponse>(
330                "/qai/v1/voices/library/add",
331                &body,
332            )
333            .await?;
334        Ok(resp)
335    }
336}