Skip to main content

quantum_sdk/
audio.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::client::Client;
6use crate::error::Result;
7
8/// Request body for text-to-speech.
9#[derive(Debug, Clone, Serialize, Default)]
10pub struct TextToSpeechRequest {
11    /// TTS model (e.g. "tts-1", "eleven_multilingual_v2", "grok-3-tts").
12    pub model: String,
13
14    /// Text to synthesise into speech.
15    pub text: String,
16
17    /// Voice to use (e.g. "alloy", "echo", "nova", "Rachel").
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub voice: Option<String>,
20
21    /// Audio format (e.g. "mp3", "wav", "opus"). Default: "mp3".
22    #[serde(rename = "format", skip_serializing_if = "Option::is_none")]
23    pub output_format: Option<String>,
24
25    /// Speech rate (provider-dependent).
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub speed: Option<f64>,
28}
29
30/// Backwards-compatible alias.
31pub type TtsRequest = TextToSpeechRequest;
32
33/// Response from text-to-speech.
34#[derive(Debug, Clone, Deserialize)]
35pub struct TextToSpeechResponse {
36    /// Base64-encoded audio data.
37    pub audio_base64: String,
38
39    /// Audio format (e.g. "mp3").
40    pub format: String,
41
42    /// Audio file size.
43    pub size_bytes: i64,
44
45    /// Model that generated the audio.
46    pub model: String,
47
48    /// Total cost in ticks.
49    #[serde(default)]
50    pub cost_ticks: i64,
51
52    /// Post-deduction credit balance in ticks (Receipt Pattern).
53    #[serde(default)]
54    pub balance_after: i64,
55
56    /// Unique request identifier.
57    #[serde(default)]
58    pub request_id: String,
59}
60
61/// Backwards-compatible alias.
62pub type TtsResponse = TextToSpeechResponse;
63
64/// Request body for speech-to-text.
65#[derive(Debug, Clone, Serialize, Default)]
66pub struct SpeechToTextRequest {
67    /// STT model (e.g. "whisper-1", "scribe_v2").
68    pub model: String,
69
70    /// Base64-encoded audio data.
71    pub audio_base64: String,
72
73    /// Original filename (helps with format detection). Default: "audio.mp3".
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub filename: Option<String>,
76
77    /// BCP-47 language code hint (e.g. "en", "de").
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub language: Option<String>,
80}
81
82/// Backwards-compatible alias.
83pub type SttRequest = SpeechToTextRequest;
84
85/// Response from speech-to-text.
86#[derive(Debug, Clone, Deserialize)]
87pub struct SpeechToTextResponse {
88    /// Transcribed text.
89    pub text: String,
90
91    /// Model that performed transcription.
92    pub model: String,
93
94    /// Total cost in ticks.
95    #[serde(default)]
96    pub cost_ticks: i64,
97
98    /// Post-deduction credit balance in ticks (Receipt Pattern).
99    #[serde(default)]
100    pub balance_after: i64,
101
102    /// Unique request identifier.
103    #[serde(default)]
104    pub request_id: String,
105}
106
107/// Backwards-compatible alias.
108pub type SttResponse = SpeechToTextResponse;
109
110/// Request body for music generation.
111#[derive(Debug, Clone, Serialize, Default)]
112pub struct MusicRequest {
113    /// Music generation model (e.g. "lyria").
114    pub model: String,
115
116    /// Describes the music to generate.
117    pub prompt: String,
118
119    /// Target duration in seconds (default 30).
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub duration_seconds: Option<i32>,
122}
123
124/// Response from music generation.
125#[derive(Debug, Clone, Deserialize)]
126pub struct MusicResponse {
127    /// Generated music clips.
128    #[serde(default)]
129    pub audio_clips: Vec<MusicClip>,
130
131    /// Model that generated the music.
132    #[serde(default)]
133    pub model: String,
134
135    /// Total cost in ticks.
136    #[serde(default)]
137    pub cost_ticks: i64,
138
139    /// Post-deduction credit balance in ticks (Receipt Pattern).
140    #[serde(default)]
141    pub balance_after: i64,
142
143    /// Unique request identifier.
144    #[serde(default)]
145    pub request_id: String,
146}
147
148/// A single generated music clip.
149#[derive(Debug, Clone, Deserialize)]
150pub struct MusicClip {
151    /// Base64-encoded audio data.
152    pub base64: String,
153
154    /// Audio format (e.g. "mp3", "wav").
155    #[serde(default)]
156    pub format: String,
157
158    /// Audio file size.
159    #[serde(default)]
160    pub size_bytes: i64,
161
162    /// Clip index within the batch.
163    #[serde(default)]
164    pub index: i32,
165}
166
167/// Request body for sound effects generation.
168#[derive(Debug, Clone, Serialize, Default)]
169pub struct SoundEffectRequest {
170    /// Text prompt describing the sound effect.
171    pub prompt: String,
172
173    /// Optional duration in seconds.
174    #[serde(skip_serializing_if = "Option::is_none")]
175    pub duration_seconds: Option<f64>,
176}
177
178/// Response from sound effects generation.
179#[derive(Debug, Clone, Deserialize)]
180pub struct SoundEffectResponse {
181    /// Base64-encoded audio data.
182    pub audio_base64: String,
183
184    /// Audio format (e.g. "mp3").
185    pub format: String,
186
187    /// File size in bytes.
188    #[serde(default)]
189    pub size_bytes: i64,
190
191    /// Model used.
192    #[serde(default)]
193    pub model: String,
194
195    /// Total cost in ticks.
196    #[serde(default)]
197    pub cost_ticks: i64,
198
199    /// Unique request identifier.
200    #[serde(default)]
201    pub request_id: String,
202}
203
204// ---------------------------------------------------------------------------
205// Advanced Audio Types
206// ---------------------------------------------------------------------------
207
208/// Generic audio response used by multiple advanced audio endpoints.
209#[derive(Debug, Clone, Deserialize)]
210pub struct AudioResponse {
211    /// Base64-encoded audio data.
212    #[serde(default)]
213    pub audio_base64: Option<String>,
214
215    /// Audio format (e.g. "mp3", "wav").
216    #[serde(default)]
217    pub format: Option<String>,
218
219    /// File size in bytes.
220    #[serde(default)]
221    pub size_bytes: Option<i64>,
222
223    /// Model used.
224    #[serde(default)]
225    pub model: Option<String>,
226
227    /// Total cost in ticks.
228    #[serde(default)]
229    pub cost_ticks: i64,
230
231    /// Unique request identifier.
232    #[serde(default)]
233    pub request_id: String,
234
235    /// Additional response fields.
236    #[serde(flatten)]
237    pub extra: HashMap<String, serde_json::Value>,
238}
239
240/// A single dialogue turn (used for building the request — converted to text + voices).
241#[derive(Debug, Clone, Serialize, Deserialize, Default)]
242pub struct DialogueTurn {
243    /// Speaker name or identifier.
244    pub speaker: String,
245
246    /// Text for this speaker to say.
247    pub text: String,
248
249    /// Voice ID to use for this speaker.
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub voice: Option<String>,
252}
253
254/// Voice mapping for ElevenLabs dialogue.
255#[derive(Debug, Clone, Serialize)]
256pub struct DialogueVoice {
257    pub voice_id: String,
258    pub name: String,
259}
260
261/// Request body sent to the QAI proxy for dialogue generation.
262/// The proxy expects `text` (full script) + `voices` (speaker-to-voice mapping).
263#[derive(Debug, Clone, Serialize, Default)]
264pub struct DialogueRequest {
265    /// Full dialogue script (e.g. "Speaker1: Hello!\nSpeaker2: Hi there!").
266    pub text: String,
267
268    /// Voice mappings — each speaker name mapped to a voice_id.
269    pub voices: Vec<DialogueVoice>,
270
271    /// Dialogue model.
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub model: Option<String>,
274
275    /// Output audio format.
276    #[serde(rename = "output_format", skip_serializing_if = "Option::is_none")]
277    pub output_format: Option<String>,
278
279    /// Seed for reproducible generation.
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub seed: Option<i32>,
282}
283
284impl DialogueRequest {
285    /// Build a DialogueRequest from individual turns.
286    /// Converts turns into the text + voices format the API expects.
287    pub fn from_turns(turns: Vec<DialogueTurn>, model: Option<String>) -> Self {
288        // Build the script text: "Speaker: text\n..."
289        let text = turns.iter()
290            .map(|t| format!("{}: {}", t.speaker, t.text))
291            .collect::<Vec<_>>()
292            .join("\n");
293
294        // Deduplicate voices — one entry per unique speaker
295        let mut seen = std::collections::HashSet::new();
296        let voices: Vec<DialogueVoice> = turns.iter()
297            .filter(|t| t.voice.is_some() && seen.insert(t.speaker.clone()))
298            .map(|t| DialogueVoice {
299                voice_id: t.voice.clone().unwrap_or_default(),
300                name: t.speaker.clone(),
301            })
302            .collect();
303
304        Self {
305            text,
306            voices,
307            model,
308            ..Default::default()
309        }
310    }
311}
312
313/// Request body for speech-to-speech conversion.
314#[derive(Debug, Clone, Serialize, Default)]
315pub struct SpeechToSpeechRequest {
316    /// Model for conversion.
317    #[serde(skip_serializing_if = "Option::is_none")]
318    pub model: Option<String>,
319
320    /// Base64-encoded source audio.
321    pub audio_base64: String,
322
323    /// Target voice identifier.
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub voice_id: Option<String>,
326
327    /// Target voice name (alternative to voice_id).
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub voice: Option<String>,
330
331    /// Output audio format.
332    #[serde(rename = "format", skip_serializing_if = "Option::is_none")]
333    pub output_format: Option<String>,
334}
335
336/// Request body for voice isolation.
337#[derive(Debug, Clone, Serialize, Default)]
338pub struct IsolateVoiceRequest {
339    /// Base64-encoded audio to isolate voice from.
340    pub audio_base64: String,
341
342    /// Output audio format.
343    #[serde(rename = "format", skip_serializing_if = "Option::is_none")]
344    pub output_format: Option<String>,
345}
346
347/// Backwards-compatible alias.
348pub type IsolateRequest = IsolateVoiceRequest;
349
350/// Request body for voice remixing.
351#[derive(Debug, Clone, Serialize, Default)]
352pub struct RemixVoiceRequest {
353    /// Base64-encoded source audio.
354    pub audio_base64: String,
355
356    /// Target voice for the remix.
357    #[serde(skip_serializing_if = "Option::is_none")]
358    pub voice: Option<String>,
359
360    /// Model for remixing.
361    #[serde(skip_serializing_if = "Option::is_none")]
362    pub model: Option<String>,
363
364    /// Output audio format.
365    #[serde(rename = "format", skip_serializing_if = "Option::is_none")]
366    pub output_format: Option<String>,
367}
368
369/// Backwards-compatible alias.
370pub type RemixRequest = RemixVoiceRequest;
371
372/// Request body for audio dubbing.
373#[derive(Debug, Clone, Serialize, Default)]
374pub struct DubRequest {
375    /// Base64-encoded source audio or video.
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub audio_base64: Option<String>,
378
379    /// Original filename (helps detect format).
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub filename: Option<String>,
382
383    /// URL to source media (alternative to audio_base64).
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub source_url: Option<String>,
386
387    /// Target language (BCP-47 code, e.g. "es", "de").
388    pub target_lang: String,
389
390    /// Source language (auto-detected if omitted).
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub source_lang: Option<String>,
393
394    /// Number of speakers (optional).
395    #[serde(skip_serializing_if = "Option::is_none")]
396    pub num_speakers: Option<i32>,
397
398    /// Enable highest quality processing.
399    #[serde(skip_serializing_if = "Option::is_none")]
400    pub highest_resolution: Option<bool>,
401}
402
403/// Request body for audio alignment / forced alignment.
404#[derive(Debug, Clone, Serialize, Default)]
405pub struct AlignRequest {
406    /// Base64-encoded audio data.
407    pub audio_base64: String,
408
409    /// Transcript text to align against the audio.
410    pub text: String,
411
412    /// Language code.
413    #[serde(skip_serializing_if = "Option::is_none")]
414    pub language: Option<String>,
415}
416
417/// A single alignment segment.
418#[derive(Debug, Clone, Deserialize)]
419pub struct AlignmentSegment {
420    /// Aligned text.
421    pub text: String,
422
423    /// Start time in seconds.
424    pub start: f64,
425
426    /// End time in seconds.
427    pub end: f64,
428}
429
430/// A single word with timing information from forced alignment.
431#[derive(Debug, Clone, Deserialize)]
432pub struct AlignedWord {
433    /// Word text.
434    pub text: String,
435
436    /// Start time in seconds.
437    pub start_time: f64,
438
439    /// End time in seconds.
440    pub end_time: f64,
441
442    /// Alignment confidence score.
443    #[serde(default)]
444    pub confidence: f64,
445}
446
447/// Response from audio alignment.
448#[derive(Debug, Clone, Deserialize)]
449pub struct AlignResponse {
450    /// Aligned segments.
451    #[serde(default)]
452    pub segments: Vec<AlignmentSegment>,
453
454    /// Word-level alignment.
455    #[serde(default)]
456    pub alignment: Vec<AlignedWord>,
457
458    /// Model used.
459    #[serde(default)]
460    pub model: String,
461
462    /// Total cost in ticks.
463    #[serde(default)]
464    pub cost_ticks: i64,
465
466    /// Unique request identifier.
467    #[serde(default)]
468    pub request_id: String,
469}
470
471// ---------------------------------------------------------------------------
472// Typed response structs (parity with Go SDK)
473// ---------------------------------------------------------------------------
474
475/// Response from dialogue generation.
476#[derive(Debug, Clone, Deserialize)]
477pub struct DialogueResponse {
478    pub audio_base64: String,
479    pub format: String,
480    #[serde(default)]
481    pub size_bytes: i64,
482    #[serde(default)]
483    pub model: String,
484    #[serde(default)]
485    pub cost_ticks: i64,
486    #[serde(default)]
487    pub request_id: String,
488}
489
490/// Response from speech-to-speech conversion.
491#[derive(Debug, Clone, Deserialize)]
492pub struct SpeechToSpeechResponse {
493    pub audio_base64: String,
494    pub format: String,
495    #[serde(default)]
496    pub size_bytes: i64,
497    #[serde(default)]
498    pub model: String,
499    #[serde(default)]
500    pub cost_ticks: i64,
501    #[serde(default)]
502    pub request_id: String,
503}
504
505/// Response from voice isolation.
506#[derive(Debug, Clone, Deserialize)]
507pub struct IsolateVoiceResponse {
508    pub audio_base64: String,
509    pub format: String,
510    #[serde(default)]
511    pub size_bytes: i64,
512    #[serde(default)]
513    pub cost_ticks: i64,
514    #[serde(default)]
515    pub request_id: String,
516}
517
518/// Response from voice remixing.
519#[derive(Debug, Clone, Deserialize)]
520pub struct RemixVoiceResponse {
521    #[serde(default)]
522    pub audio_base64: Option<String>,
523    pub format: String,
524    #[serde(default)]
525    pub size_bytes: i64,
526    #[serde(default)]
527    pub voice_id: Option<String>,
528    #[serde(default)]
529    pub cost_ticks: i64,
530    #[serde(default)]
531    pub request_id: String,
532}
533
534/// Response from dubbing.
535#[derive(Debug, Clone, Deserialize)]
536pub struct DubResponse {
537    pub dubbing_id: String,
538    pub audio_base64: String,
539    pub format: String,
540    #[serde(default)]
541    pub target_lang: String,
542    #[serde(default)]
543    pub status: String,
544    #[serde(default)]
545    pub processing_time_seconds: f64,
546    #[serde(default)]
547    pub cost_ticks: i64,
548    #[serde(default)]
549    pub request_id: String,
550}
551
552/// Response from voice design.
553#[derive(Debug, Clone, Deserialize)]
554pub struct VoiceDesignResponse {
555    pub previews: Vec<VoicePreview>,
556    #[serde(default)]
557    pub cost_ticks: i64,
558    #[serde(default)]
559    pub request_id: String,
560}
561
562/// A single voice preview from voice design.
563#[derive(Debug, Clone, Deserialize)]
564pub struct VoicePreview {
565    pub generated_voice_id: String,
566    pub audio_base64: String,
567    pub format: String,
568}
569
570/// Response from Starfish TTS.
571#[derive(Debug, Clone, Deserialize)]
572pub struct StarfishTTSResponse {
573    #[serde(default)]
574    pub audio_base64: Option<String>,
575    #[serde(default)]
576    pub url: Option<String>,
577    pub format: String,
578    #[serde(default)]
579    pub size_bytes: i64,
580    #[serde(default)]
581    pub duration: f64,
582    #[serde(default)]
583    pub model: String,
584    #[serde(default)]
585    pub cost_ticks: i64,
586    #[serde(default)]
587    pub request_id: String,
588}
589
590/// Advanced music generation request.
591#[derive(Debug, Clone, Serialize, Default)]
592pub struct MusicAdvancedRequest {
593    pub prompt: String,
594    #[serde(skip_serializing_if = "Option::is_none")]
595    pub duration_seconds: Option<i32>,
596    #[serde(skip_serializing_if = "Option::is_none")]
597    pub model: Option<String>,
598    #[serde(skip_serializing_if = "Option::is_none")]
599    pub finetune_id: Option<String>,
600}
601
602/// A single clip from advanced music generation.
603#[derive(Debug, Clone, Deserialize)]
604pub struct MusicAdvancedClip {
605    #[serde(default)]
606    pub base64: String,
607    #[serde(default)]
608    pub format: String,
609    #[serde(default)]
610    pub size: i64,
611}
612
613/// Response from advanced music generation.
614#[derive(Debug, Clone, Deserialize)]
615pub struct MusicAdvancedResponse {
616    #[serde(default)]
617    pub clips: Vec<MusicAdvancedClip>,
618    #[serde(default)]
619    pub model: String,
620    #[serde(default)]
621    pub cost_ticks: i64,
622    #[serde(default)]
623    pub request_id: String,
624}
625
626/// Music finetune info.
627#[derive(Debug, Clone, Serialize, Deserialize)]
628pub struct MusicFinetuneInfo {
629    pub finetune_id: String,
630    pub name: String,
631    #[serde(default)]
632    pub description: Option<String>,
633    #[serde(default)]
634    pub status: String,
635    #[serde(default)]
636    pub model_id: Option<String>,
637    #[serde(default)]
638    pub created_at: Option<String>,
639}
640
641/// Response from listing music finetunes.
642#[derive(Debug, Clone, Deserialize)]
643pub struct MusicFinetuneListResponse {
644    pub finetunes: Vec<MusicFinetuneInfo>,
645}
646
647/// Request to create a music finetune.
648#[derive(Debug, Clone, Serialize)]
649pub struct MusicFinetuneCreateRequest {
650    pub name: String,
651    #[serde(skip_serializing_if = "Option::is_none")]
652    pub description: Option<String>,
653    pub samples: Vec<String>,
654}
655
656/// Request body for voice design (generating a voice from a description).
657#[derive(Debug, Clone, Serialize, Default)]
658pub struct VoiceDesignRequest {
659    /// Text description of the desired voice.
660    #[serde(rename = "voice_description")]
661    pub description: String,
662
663    /// Sample text to speak with the designed voice.
664    #[serde(rename = "sample_text")]
665    pub text: String,
666
667    /// Output audio format.
668    #[serde(rename = "format", skip_serializing_if = "Option::is_none")]
669    pub output_format: Option<String>,
670}
671
672/// Request body for Starfish TTS.
673#[derive(Debug, Clone, Serialize, Default)]
674pub struct StarfishTTSRequest {
675    /// Text to synthesise.
676    pub text: String,
677
678    /// HeyGen voice identifier.
679    #[serde(skip_serializing_if = "Option::is_none")]
680    pub voice_id: Option<String>,
681
682    /// Voice name (alternative to voice_id).
683    #[serde(skip_serializing_if = "Option::is_none")]
684    pub voice: Option<String>,
685
686    /// Output audio format.
687    #[serde(rename = "format", skip_serializing_if = "Option::is_none")]
688    pub output_format: Option<String>,
689
690    /// Input type (e.g. "text", "ssml").
691    #[serde(skip_serializing_if = "Option::is_none")]
692    pub input_type: Option<String>,
693
694    /// Speech speed multiplier.
695    #[serde(skip_serializing_if = "Option::is_none")]
696    pub speed: Option<f64>,
697
698    /// BCP-47 language code.
699    #[serde(skip_serializing_if = "Option::is_none")]
700    pub language: Option<String>,
701
702    /// Locale code.
703    #[serde(skip_serializing_if = "Option::is_none")]
704    pub locale: Option<String>,
705}
706
707// ---------------------------------------------------------------------------
708// Eleven Music (advanced music generation with sections, finetunes, etc.)
709// ---------------------------------------------------------------------------
710
711/// A section within an Eleven Music generation request.
712#[derive(Debug, Clone, Serialize, Deserialize, Default)]
713pub struct MusicSection {
714    pub section_type: String,
715    #[serde(skip_serializing_if = "Option::is_none")]
716    pub lyrics: Option<String>,
717    #[serde(skip_serializing_if = "Option::is_none")]
718    pub style: Option<String>,
719    #[serde(skip_serializing_if = "Option::is_none")]
720    pub style_exclude: Option<String>,
721}
722
723/// Request body for advanced music generation (ElevenLabs Eleven Music).
724#[derive(Debug, Clone, Serialize, Default)]
725pub struct ElevenMusicRequest {
726    pub model: String,
727    pub prompt: String,
728    #[serde(skip_serializing_if = "Option::is_none")]
729    pub sections: Option<Vec<MusicSection>>,
730    #[serde(skip_serializing_if = "Option::is_none")]
731    pub duration_seconds: Option<i32>,
732    #[serde(skip_serializing_if = "Option::is_none")]
733    pub language: Option<String>,
734    #[serde(skip_serializing_if = "Option::is_none")]
735    pub vocals: Option<bool>,
736    #[serde(skip_serializing_if = "Option::is_none")]
737    pub style: Option<String>,
738    #[serde(skip_serializing_if = "Option::is_none")]
739    pub style_exclude: Option<String>,
740    #[serde(skip_serializing_if = "Option::is_none")]
741    pub finetune_id: Option<String>,
742    #[serde(skip_serializing_if = "Option::is_none")]
743    pub edit_reference_id: Option<String>,
744    #[serde(skip_serializing_if = "Option::is_none")]
745    pub edit_instruction: Option<String>,
746}
747
748/// A single music clip from advanced generation.
749#[derive(Debug, Clone, Deserialize)]
750pub struct ElevenMusicClip {
751    /// Base64-encoded audio data.
752    #[serde(default)]
753    pub base64: String,
754    /// Audio format (e.g. "mp3").
755    #[serde(default)]
756    pub format: String,
757    /// File size in bytes.
758    #[serde(default)]
759    pub size: i64,
760}
761
762/// Response from advanced music generation.
763/// Backend returns: { clips: [...], model, cost_ticks, request_id }
764#[derive(Debug, Clone, Deserialize)]
765pub struct ElevenMusicResponse {
766    /// Generated music clips.
767    #[serde(default)]
768    pub clips: Vec<ElevenMusicClip>,
769    /// Model used.
770    #[serde(default)]
771    pub model: String,
772    /// Total cost in ticks.
773    #[serde(default)]
774    pub cost_ticks: i64,
775    /// Unique request identifier.
776    #[serde(default)]
777    pub request_id: String,
778}
779
780/// Info about a music finetune.
781#[derive(Debug, Clone, Serialize, Deserialize)]
782pub struct FinetuneInfo {
783    pub finetune_id: String,
784    pub name: String,
785    #[serde(default)]
786    pub status: String,
787    #[serde(default)]
788    pub created_at: Option<String>,
789}
790
791/// Response from listing finetunes.
792#[derive(Debug, Clone, Deserialize)]
793pub struct ListFinetunesResponse {
794    pub finetunes: Vec<FinetuneInfo>,
795}
796
797// ---------------------------------------------------------------------------
798// Client impl
799// ---------------------------------------------------------------------------
800
801impl Client {
802    /// Generates speech from text.
803    pub async fn speak(&self, req: &TextToSpeechRequest) -> Result<TextToSpeechResponse> {
804        let (mut resp, meta) = self
805            .post_json::<TextToSpeechRequest, TextToSpeechResponse>("/qai/v1/audio/tts", req)
806            .await?;
807        if resp.cost_ticks == 0 {
808            resp.cost_ticks = meta.cost_ticks;
809        }
810        if resp.request_id.is_empty() {
811            resp.request_id = meta.request_id;
812        }
813        Ok(resp)
814    }
815
816    /// Converts speech to text.
817    pub async fn transcribe(&self, req: &SpeechToTextRequest) -> Result<SpeechToTextResponse> {
818        let (mut resp, meta) = self
819            .post_json::<SpeechToTextRequest, SpeechToTextResponse>("/qai/v1/audio/stt", req)
820            .await?;
821        if resp.cost_ticks == 0 {
822            resp.cost_ticks = meta.cost_ticks;
823        }
824        if resp.request_id.is_empty() {
825            resp.request_id = meta.request_id;
826        }
827        Ok(resp)
828    }
829
830    /// Generates sound effects from a text prompt (ElevenLabs).
831    pub async fn sound_effects(&self, req: &SoundEffectRequest) -> Result<SoundEffectResponse> {
832        let (mut resp, meta) = self
833            .post_json::<SoundEffectRequest, SoundEffectResponse>(
834                "/qai/v1/audio/sound-effects",
835                req,
836            )
837            .await?;
838        if resp.cost_ticks == 0 {
839            resp.cost_ticks = meta.cost_ticks;
840        }
841        if resp.request_id.is_empty() {
842            resp.request_id = meta.request_id;
843        }
844        Ok(resp)
845    }
846
847    /// Generates music from a text prompt.
848    pub async fn generate_music(&self, req: &MusicRequest) -> Result<MusicResponse> {
849        let (mut resp, meta) = self
850            .post_json::<MusicRequest, MusicResponse>("/qai/v1/audio/music", req)
851            .await?;
852        if resp.cost_ticks == 0 {
853            resp.cost_ticks = meta.cost_ticks;
854        }
855        if resp.request_id.is_empty() {
856            resp.request_id = meta.request_id;
857        }
858        Ok(resp)
859    }
860
861    /// Generates multi-speaker dialogue audio.
862    pub async fn dialogue(&self, req: &DialogueRequest) -> Result<AudioResponse> {
863        let (mut resp, meta) = self
864            .post_json::<DialogueRequest, AudioResponse>("/qai/v1/audio/dialogue", req)
865            .await?;
866        if resp.cost_ticks == 0 {
867            resp.cost_ticks = meta.cost_ticks;
868        }
869        if resp.request_id.is_empty() {
870            resp.request_id = meta.request_id;
871        }
872        Ok(resp)
873    }
874
875    /// Converts speech to a different voice.
876    pub async fn speech_to_speech(
877        &self,
878        req: &SpeechToSpeechRequest,
879    ) -> Result<AudioResponse> {
880        let (mut resp, meta) = self
881            .post_json::<SpeechToSpeechRequest, AudioResponse>(
882                "/qai/v1/audio/speech-to-speech",
883                req,
884            )
885            .await?;
886        if resp.cost_ticks == 0 {
887            resp.cost_ticks = meta.cost_ticks;
888        }
889        if resp.request_id.is_empty() {
890            resp.request_id = meta.request_id;
891        }
892        Ok(resp)
893    }
894
895    /// Isolates voice from background noise and music.
896    pub async fn isolate_voice(&self, req: &IsolateVoiceRequest) -> Result<AudioResponse> {
897        let (mut resp, meta) = self
898            .post_json::<IsolateVoiceRequest, AudioResponse>("/qai/v1/audio/isolate", req)
899            .await?;
900        if resp.cost_ticks == 0 {
901            resp.cost_ticks = meta.cost_ticks;
902        }
903        if resp.request_id.is_empty() {
904            resp.request_id = meta.request_id;
905        }
906        Ok(resp)
907    }
908
909    /// Remixes audio with a different voice.
910    pub async fn remix_voice(&self, req: &RemixVoiceRequest) -> Result<AudioResponse> {
911        let (mut resp, meta) = self
912            .post_json::<RemixVoiceRequest, AudioResponse>("/qai/v1/audio/remix", req)
913            .await?;
914        if resp.cost_ticks == 0 {
915            resp.cost_ticks = meta.cost_ticks;
916        }
917        if resp.request_id.is_empty() {
918            resp.request_id = meta.request_id;
919        }
920        Ok(resp)
921    }
922
923    /// Dubs audio or video into a target language.
924    pub async fn dub(&self, req: &DubRequest) -> Result<AudioResponse> {
925        let (mut resp, meta) = self
926            .post_json::<DubRequest, AudioResponse>("/qai/v1/audio/dub", req)
927            .await?;
928        if resp.cost_ticks == 0 {
929            resp.cost_ticks = meta.cost_ticks;
930        }
931        if resp.request_id.is_empty() {
932            resp.request_id = meta.request_id;
933        }
934        Ok(resp)
935    }
936
937    /// Performs forced alignment of text against audio.
938    pub async fn align(&self, req: &AlignRequest) -> Result<AlignResponse> {
939        let (mut resp, meta) = self
940            .post_json::<AlignRequest, AlignResponse>("/qai/v1/audio/align", req)
941            .await?;
942        if resp.cost_ticks == 0 {
943            resp.cost_ticks = meta.cost_ticks;
944        }
945        if resp.request_id.is_empty() {
946            resp.request_id = meta.request_id;
947        }
948        Ok(resp)
949    }
950
951    /// Designs a new voice from a text description and generates sample audio.
952    pub async fn voice_design(&self, req: &VoiceDesignRequest) -> Result<AudioResponse> {
953        let (mut resp, meta) = self
954            .post_json::<VoiceDesignRequest, AudioResponse>("/qai/v1/audio/voice-design", req)
955            .await?;
956        if resp.cost_ticks == 0 {
957            resp.cost_ticks = meta.cost_ticks;
958        }
959        if resp.request_id.is_empty() {
960            resp.request_id = meta.request_id;
961        }
962        Ok(resp)
963    }
964
965    /// Generates speech using Starfish TTS (HeyGen).
966    pub async fn starfish_tts(&self, req: &StarfishTTSRequest) -> Result<AudioResponse> {
967        let (mut resp, meta) = self
968            .post_json::<StarfishTTSRequest, AudioResponse>("/qai/v1/audio/starfish-tts", req)
969            .await?;
970        if resp.cost_ticks == 0 {
971            resp.cost_ticks = meta.cost_ticks;
972        }
973        if resp.request_id.is_empty() {
974            resp.request_id = meta.request_id;
975        }
976        Ok(resp)
977    }
978
979    /// Generates music via ElevenLabs Eleven Music (advanced: sections, finetunes, edits).
980    pub async fn generate_music_advanced(
981        &self,
982        req: &ElevenMusicRequest,
983    ) -> Result<ElevenMusicResponse> {
984        let (mut resp, meta) = self
985            .post_json::<ElevenMusicRequest, ElevenMusicResponse>(
986                "/qai/v1/audio/music/advanced",
987                req,
988            )
989            .await?;
990        if resp.cost_ticks == 0 {
991            resp.cost_ticks = meta.cost_ticks;
992        }
993        if resp.request_id.is_empty() {
994            resp.request_id = meta.request_id;
995        }
996        Ok(resp)
997    }
998
999    /// Lists all music finetunes for the authenticated user.
1000    pub async fn list_finetunes(&self) -> Result<ListFinetunesResponse> {
1001        let (resp, _) = self
1002            .get_json::<ListFinetunesResponse>("/qai/v1/audio/finetunes")
1003            .await?;
1004        Ok(resp)
1005    }
1006
1007    /// Creates a new music finetune from audio sample files.
1008    pub async fn create_finetune(
1009        &self,
1010        name: &str,
1011        files: Vec<crate::voices::CloneVoiceFile>,
1012    ) -> Result<FinetuneInfo> {
1013        let mut form = reqwest::multipart::Form::new().text("name", name.to_string());
1014
1015        for file in files {
1016            let part = reqwest::multipart::Part::bytes(file.data)
1017                .file_name(file.filename)
1018                .mime_str(&file.mime_type)
1019                .map_err(|e| crate::error::Error::Http(e.into()))?;
1020            form = form.part("files", part);
1021        }
1022
1023        let (resp, _) = self
1024            .post_multipart::<FinetuneInfo>("/qai/v1/audio/finetunes", form)
1025            .await?;
1026        Ok(resp)
1027    }
1028
1029    /// Deletes a music finetune by ID.
1030    pub async fn delete_finetune(&self, id: &str) -> Result<serde_json::Value> {
1031        let path = format!("/qai/v1/audio/finetunes/{id}");
1032        let (resp, _) = self.delete_json::<serde_json::Value>(&path).await?;
1033        Ok(resp)
1034    }
1035}