Skip to main content

typecast_rust/
models.rs

1//! Data models for the Typecast API
2//!
3//! This module contains all the data structures used for API requests and responses.
4
5use serde::{Deserialize, Serialize};
6
7/// TTS model version to use for speech synthesis
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9pub enum TTSModel {
10    /// Latest model with improved prosody and additional emotion presets (recommended)
11    #[serde(rename = "ssfm-v30")]
12    SsfmV30,
13    /// Stable production model with proven reliability and consistent quality
14    #[serde(rename = "ssfm-v21")]
15    SsfmV21,
16}
17
18impl Default for TTSModel {
19    fn default() -> Self {
20        TTSModel::SsfmV30
21    }
22}
23
24/// Emotion preset types for speech synthesis
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "lowercase")]
27pub enum EmotionPreset {
28    /// Neutral, balanced tone
29    Normal,
30    /// Bright, cheerful expression
31    Happy,
32    /// Melancholic, subdued tone
33    Sad,
34    /// Strong, intense delivery
35    Angry,
36    /// Soft, quiet speech (ssfm-v30 only)
37    Whisper,
38    /// Higher tonal emphasis (ssfm-v30 only)
39    #[serde(rename = "toneup")]
40    ToneUp,
41    /// Lower tonal emphasis (ssfm-v30 only)
42    #[serde(rename = "tonedown")]
43    ToneDown,
44}
45
46impl Default for EmotionPreset {
47    fn default() -> Self {
48        EmotionPreset::Normal
49    }
50}
51
52/// Audio output format
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(rename_all = "lowercase")]
55pub enum AudioFormat {
56    /// Uncompressed PCM audio (16-bit depth, mono, 44100 Hz)
57    Wav,
58    /// Compressed MPEG Layer III audio (320 kbps, 44100 Hz)
59    Mp3,
60}
61
62impl Default for AudioFormat {
63    fn default() -> Self {
64        AudioFormat::Wav
65    }
66}
67
68/// Gender classification for voices
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
70#[serde(rename_all = "lowercase")]
71pub enum Gender {
72    Male,
73    Female,
74}
75
76/// Age group classification for voices
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78#[serde(rename_all = "snake_case")]
79pub enum Age {
80    /// Child voice (under 12 years old)
81    Child,
82    /// Teenage voice (13-19 years old)
83    Teenager,
84    /// Young adult voice (20-35 years old)
85    YoungAdult,
86    /// Middle-aged voice (36-60 years old)
87    MiddleAge,
88    /// Elder voice (over 60 years old)
89    Elder,
90}
91
92/// Voice use case categories
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
94pub enum UseCase {
95    Announcer,
96    Anime,
97    Audiobook,
98    Conversational,
99    Documentary,
100    #[serde(rename = "E-learning")]
101    ELearning,
102    Rapper,
103    Game,
104    #[serde(rename = "Tiktok/Reels")]
105    TikTokReels,
106    News,
107    Podcast,
108    Voicemail,
109    Ads,
110}
111
112/// Audio output settings
113#[derive(Debug, Clone, Default, Serialize, Deserialize)]
114pub struct Output {
115    /// Volume level (0-200, default: 100).
116    /// Cannot be used simultaneously with target_lufs.
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub volume: Option<i32>,
119    /// Target loudness in LUFS for absolute loudness normalization (-70 to 0).
120    /// Cannot be used simultaneously with volume.
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub target_lufs: Option<f64>,
123    /// Pitch adjustment in semitones (-12 to +12, default: 0)
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub audio_pitch: Option<i32>,
126    /// Speech speed multiplier (0.5 to 2.0, default: 1.0)
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub audio_tempo: Option<f64>,
129    /// Output audio format (wav or mp3, default: wav)
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub audio_format: Option<AudioFormat>,
132}
133
134impl Output {
135    /// Create a new Output with default values
136    pub fn new() -> Self {
137        Self::default()
138    }
139
140    /// Set the volume (0-200).
141    /// Cannot be used simultaneously with target_lufs.
142    pub fn volume(mut self, volume: i32) -> Self {
143        self.volume = Some(volume.clamp(0, 200));
144        self
145    }
146
147    /// Set the target LUFS (-70 to 0)
148    pub fn target_lufs(mut self, lufs: f64) -> Self {
149        self.target_lufs = lufs.is_finite().then_some(lufs.clamp(-70.0, 0.0));
150        self
151    }
152
153    /// Set the audio pitch (-12 to +12 semitones)
154    pub fn audio_pitch(mut self, pitch: i32) -> Self {
155        self.audio_pitch = Some(pitch.clamp(-12, 12));
156        self
157    }
158
159    /// Set the audio tempo (0.5 to 2.0)
160    pub fn audio_tempo(mut self, tempo: f64) -> Self {
161        self.audio_tempo = Some(tempo.clamp(0.5, 2.0));
162        self
163    }
164
165    /// Set the audio format
166    pub fn audio_format(mut self, format: AudioFormat) -> Self {
167        self.audio_format = Some(format);
168        self
169    }
170}
171
172/// Audio output settings for streaming TTS requests.
173///
174/// Identical to [`Output`] but without `volume`, which is not supported by the
175/// streaming endpoint. Streaming supports `target_lufs`.
176#[derive(Debug, Clone, Default, Serialize, Deserialize)]
177pub struct OutputStream {
178    /// Target loudness in LUFS (-70 to 0)
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub target_lufs: Option<f64>,
181    /// Pitch adjustment in semitones (-12 to +12, default: 0)
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub audio_pitch: Option<i32>,
184    /// Speech speed multiplier (0.5 to 2.0, default: 1.0)
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub audio_tempo: Option<f64>,
187    /// Output audio format (wav or mp3, default: wav)
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub audio_format: Option<AudioFormat>,
190}
191
192impl OutputStream {
193    /// Create a new OutputStream with default values
194    pub fn new() -> Self {
195        Self::default()
196    }
197
198    /// Set the audio pitch (-12 to +12 semitones)
199    pub fn audio_pitch(mut self, pitch: i32) -> Self {
200        self.audio_pitch = Some(pitch.clamp(-12, 12));
201        self
202    }
203
204    /// Set the audio tempo (0.5 to 2.0)
205    pub fn audio_tempo(mut self, tempo: f64) -> Self {
206        self.audio_tempo = Some(tempo.clamp(0.5, 2.0));
207        self
208    }
209
210    /// Set the audio format
211    pub fn audio_format(mut self, format: AudioFormat) -> Self {
212        self.audio_format = Some(format);
213        self
214    }
215
216    /// Set the target LUFS (-70 to 0)
217    pub fn target_lufs(mut self, lufs: f64) -> Self {
218        self.target_lufs = Some(lufs.clamp(-70.0, 0.0));
219        self
220    }
221}
222
223/// Emotion settings for ssfm-v21 model
224#[derive(Debug, Clone, Default, Serialize, Deserialize)]
225pub struct Prompt {
226    /// Emotion preset to apply
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub emotion_preset: Option<EmotionPreset>,
229    /// Emotion intensity (0.0 to 2.0, default: 1.0)
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub emotion_intensity: Option<f64>,
232}
233
234impl Prompt {
235    /// Create a new Prompt with default values
236    pub fn new() -> Self {
237        Self::default()
238    }
239
240    /// Set the emotion preset
241    pub fn emotion_preset(mut self, preset: EmotionPreset) -> Self {
242        self.emotion_preset = Some(preset);
243        self
244    }
245
246    /// Set the emotion intensity (0.0 to 2.0)
247    pub fn emotion_intensity(mut self, intensity: f64) -> Self {
248        self.emotion_intensity = Some(intensity.clamp(0.0, 2.0));
249        self
250    }
251}
252
253/// Preset-based emotion control for ssfm-v30 model
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct PresetPrompt {
256    /// Must be "preset" for preset-based emotion control
257    pub emotion_type: String,
258    /// Emotion preset to apply
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub emotion_preset: Option<EmotionPreset>,
261    /// Emotion intensity (0.0 to 2.0, default: 1.0)
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub emotion_intensity: Option<f64>,
264}
265
266impl Default for PresetPrompt {
267    fn default() -> Self {
268        Self {
269            emotion_type: "preset".to_string(),
270            emotion_preset: None,
271            emotion_intensity: None,
272        }
273    }
274}
275
276impl PresetPrompt {
277    /// Create a new PresetPrompt
278    pub fn new() -> Self {
279        Self::default()
280    }
281
282    /// Set the emotion preset
283    pub fn emotion_preset(mut self, preset: EmotionPreset) -> Self {
284        self.emotion_preset = Some(preset);
285        self
286    }
287
288    /// Set the emotion intensity (0.0 to 2.0)
289    pub fn emotion_intensity(mut self, intensity: f64) -> Self {
290        self.emotion_intensity = Some(intensity.clamp(0.0, 2.0));
291        self
292    }
293}
294
295/// Context-aware emotion inference for ssfm-v30 model
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct SmartPrompt {
298    /// Must be "smart" for context-aware emotion inference
299    pub emotion_type: String,
300    /// Text that comes before the main text (max 2000 chars)
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub previous_text: Option<String>,
303    /// Text that comes after the main text (max 2000 chars)
304    #[serde(skip_serializing_if = "Option::is_none")]
305    pub next_text: Option<String>,
306}
307
308impl Default for SmartPrompt {
309    fn default() -> Self {
310        Self {
311            emotion_type: "smart".to_string(),
312            previous_text: None,
313            next_text: None,
314        }
315    }
316}
317
318impl SmartPrompt {
319    /// Create a new SmartPrompt
320    pub fn new() -> Self {
321        Self::default()
322    }
323
324    /// Set the previous text for context
325    pub fn previous_text(mut self, text: impl Into<String>) -> Self {
326        self.previous_text = Some(text.into());
327        self
328    }
329
330    /// Set the next text for context
331    pub fn next_text(mut self, text: impl Into<String>) -> Self {
332        self.next_text = Some(text.into());
333        self
334    }
335}
336
337/// Union type for all prompt types
338#[derive(Debug, Clone, Serialize, Deserialize)]
339#[serde(untagged)]
340pub enum TTSPrompt {
341    /// Basic emotion control (ssfm-v21 compatible)
342    Basic(Prompt),
343    /// Explicit preset emotion control (ssfm-v30)
344    Preset(PresetPrompt),
345    /// Context-aware emotion inference (ssfm-v30)
346    Smart(SmartPrompt),
347}
348
349impl From<Prompt> for TTSPrompt {
350    fn from(prompt: Prompt) -> Self {
351        TTSPrompt::Basic(prompt)
352    }
353}
354
355impl From<PresetPrompt> for TTSPrompt {
356    fn from(prompt: PresetPrompt) -> Self {
357        TTSPrompt::Preset(prompt)
358    }
359}
360
361impl From<SmartPrompt> for TTSPrompt {
362    fn from(prompt: SmartPrompt) -> Self {
363        TTSPrompt::Smart(prompt)
364    }
365}
366
367/// Text-to-Speech request parameters
368#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct TTSRequest {
370    /// Voice ID in format `tc_` followed by a unique identifier.
371    /// Browse available API voices at <https://typecast.ai/developers/api/voices>.
372    pub voice_id: String,
373    /// Text to convert to speech (max 2000 chars)
374    pub text: String,
375    /// TTS model to use
376    pub model: TTSModel,
377    /// Language code (ISO 639-3). Auto-detected if not provided
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub language: Option<String>,
380    /// Emotion and style settings
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub prompt: Option<TTSPrompt>,
383    /// Audio output settings
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub output: Option<Output>,
386    /// Random seed for reproducible results
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub seed: Option<i32>,
389}
390
391/// Convenience request for generating audio directly to a file.
392///
393/// `model` defaults to [`TTSModel::SsfmV30`].
394#[derive(Debug, Clone, Serialize, Deserialize)]
395pub struct GenerateToFileRequest {
396    /// Voice ID in format `tc_` followed by a unique identifier.
397    /// Browse available API voices at <https://typecast.ai/developers/api/voices>.
398    pub voice_id: String,
399    /// Text to convert to speech
400    pub text: String,
401    /// TTS model to use
402    pub model: TTSModel,
403    /// Language code (ISO 639-3). Auto-detected if not provided
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub language: Option<String>,
406    /// Emotion and style settings
407    #[serde(skip_serializing_if = "Option::is_none")]
408    pub prompt: Option<TTSPrompt>,
409    /// Audio output settings
410    #[serde(skip_serializing_if = "Option::is_none")]
411    pub output: Option<Output>,
412    /// Random seed for reproducible results
413    #[serde(skip_serializing_if = "Option::is_none")]
414    pub seed: Option<i32>,
415}
416
417impl GenerateToFileRequest {
418    /// Create a new generate-to-file request with required fields.
419    pub fn new(voice_id: impl Into<String>, text: impl Into<String>) -> Self {
420        let voice_id = voice_id.into();
421        let text = text.into();
422        assert!(!voice_id.trim().is_empty(), "voice_id is required");
423        assert!(!text.trim().is_empty(), "text is required");
424        assert!(
425            text.chars().count() <= 2000,
426            "text must not exceed 2000 characters"
427        );
428        Self {
429            voice_id,
430            text,
431            model: TTSModel::SsfmV30,
432            language: None,
433            prompt: None,
434            output: None,
435            seed: None,
436        }
437    }
438
439    pub fn model(mut self, model: TTSModel) -> Self {
440        self.model = model;
441        self
442    }
443
444    pub fn language(mut self, language: impl Into<String>) -> Self {
445        self.language = Some(language.into());
446        self
447    }
448
449    pub fn prompt(mut self, prompt: impl Into<TTSPrompt>) -> Self {
450        self.prompt = Some(prompt.into());
451        self
452    }
453
454    pub fn output(mut self, output: Output) -> Self {
455        self.output = Some(output);
456        self
457    }
458
459    pub fn seed(mut self, seed: i32) -> Self {
460        self.seed = Some(seed);
461        self
462    }
463
464    pub fn into_tts_request(self) -> TTSRequest {
465        TTSRequest {
466            voice_id: self.voice_id,
467            text: self.text,
468            model: self.model,
469            language: self.language,
470            prompt: self.prompt,
471            output: self.output,
472            seed: self.seed,
473        }
474    }
475}
476
477impl TTSRequest {
478    /// Create a new TTSRequest with required fields
479    pub fn new(voice_id: impl Into<String>, text: impl Into<String>, model: TTSModel) -> Self {
480        Self {
481            voice_id: voice_id.into(),
482            text: text.into(),
483            model,
484            language: None,
485            prompt: None,
486            output: None,
487            seed: None,
488        }
489    }
490
491    /// Set the language code (ISO 639-3)
492    pub fn language(mut self, language: impl Into<String>) -> Self {
493        self.language = Some(language.into());
494        self
495    }
496
497    /// Set the prompt (emotion settings)
498    pub fn prompt(mut self, prompt: impl Into<TTSPrompt>) -> Self {
499        self.prompt = Some(prompt.into());
500        self
501    }
502
503    /// Set the output settings
504    pub fn output(mut self, output: Output) -> Self {
505        self.output = Some(output);
506        self
507    }
508
509    /// Set the random seed for reproducible results
510    pub fn seed(mut self, seed: i32) -> Self {
511        self.seed = Some(seed);
512        self
513    }
514}
515
516/// Streaming Text-to-Speech request parameters.
517///
518/// Mirrors [`TTSRequest`] but the `output` field uses [`OutputStream`], which
519/// excludes `volume` (not supported by the streaming endpoint).
520#[derive(Debug, Clone, Serialize, Deserialize)]
521pub struct TTSRequestStream {
522    /// Voice ID in format `tc_` followed by a unique identifier.
523    /// Browse available API voices at <https://typecast.ai/developers/api/voices>.
524    pub voice_id: String,
525    /// Text to convert to speech (max 2000 chars)
526    pub text: String,
527    /// TTS model to use
528    pub model: TTSModel,
529    /// Language code (ISO 639-3). Auto-detected if not provided
530    #[serde(skip_serializing_if = "Option::is_none")]
531    pub language: Option<String>,
532    /// Emotion and style settings
533    #[serde(skip_serializing_if = "Option::is_none")]
534    pub prompt: Option<TTSPrompt>,
535    /// Audio output settings (without volume)
536    #[serde(skip_serializing_if = "Option::is_none")]
537    pub output: Option<OutputStream>,
538    /// Random seed for reproducible results
539    #[serde(skip_serializing_if = "Option::is_none")]
540    pub seed: Option<i32>,
541}
542
543impl TTSRequestStream {
544    /// Create a new TTSRequestStream with required fields
545    pub fn new(voice_id: impl Into<String>, text: impl Into<String>, model: TTSModel) -> Self {
546        Self {
547            voice_id: voice_id.into(),
548            text: text.into(),
549            model,
550            language: None,
551            prompt: None,
552            output: None,
553            seed: None,
554        }
555    }
556
557    /// Set the language code (ISO 639-3)
558    pub fn language(mut self, language: impl Into<String>) -> Self {
559        self.language = Some(language.into());
560        self
561    }
562
563    /// Set the prompt (emotion settings)
564    pub fn prompt(mut self, prompt: impl Into<TTSPrompt>) -> Self {
565        self.prompt = Some(prompt.into());
566        self
567    }
568
569    /// Set the output settings
570    pub fn output(mut self, output: OutputStream) -> Self {
571        self.output = Some(output);
572        self
573    }
574
575    /// Set the random seed for reproducible results
576    pub fn seed(mut self, seed: i32) -> Self {
577        self.seed = Some(seed);
578        self
579    }
580}
581
582/// Text-to-Speech response
583#[derive(Debug, Clone)]
584pub struct TTSResponse {
585    /// Generated audio data
586    pub audio_data: Vec<u8>,
587    /// Audio duration in seconds
588    pub duration: f64,
589    /// Audio format (wav or mp3)
590    pub format: AudioFormat,
591}
592
593/// Model information with supported emotions
594#[derive(Debug, Clone, Serialize, Deserialize)]
595pub struct ModelInfo {
596    /// TTS model version
597    pub version: TTSModel,
598    /// List of supported emotions for this model
599    pub emotions: Vec<String>,
600}
601
602/// Voice from V2 API with enhanced metadata
603#[derive(Debug, Clone, Serialize, Deserialize)]
604pub struct VoiceV2 {
605    /// Unique voice identifier
606    pub voice_id: String,
607    /// Human-readable name of the voice
608    pub voice_name: String,
609    /// List of supported TTS models with their emotions
610    pub models: Vec<ModelInfo>,
611    /// Voice gender classification
612    #[serde(skip_serializing_if = "Option::is_none")]
613    pub gender: Option<Gender>,
614    /// Voice age group classification
615    #[serde(skip_serializing_if = "Option::is_none")]
616    pub age: Option<Age>,
617    /// List of use case categories
618    #[serde(skip_serializing_if = "Option::is_none")]
619    pub use_cases: Option<Vec<String>>,
620}
621
622/// Filter options for V2 voices endpoint
623#[derive(Debug, Clone, Default)]
624pub struct VoicesV2Filter {
625    /// Filter by TTS model
626    pub model: Option<TTSModel>,
627    /// Filter by gender
628    pub gender: Option<Gender>,
629    /// Filter by age group
630    pub age: Option<Age>,
631    /// Filter by use case
632    pub use_cases: Option<UseCase>,
633}
634
635impl VoicesV2Filter {
636    /// Create a new empty filter
637    pub fn new() -> Self {
638        Self::default()
639    }
640
641    /// Filter by model
642    pub fn model(mut self, model: TTSModel) -> Self {
643        self.model = Some(model);
644        self
645    }
646
647    /// Filter by gender
648    pub fn gender(mut self, gender: Gender) -> Self {
649        self.gender = Some(gender);
650        self
651    }
652
653    /// Filter by age
654    pub fn age(mut self, age: Age) -> Self {
655        self.age = Some(age);
656        self
657    }
658
659    /// Filter by use case
660    pub fn use_cases(mut self, use_case: UseCase) -> Self {
661        self.use_cases = Some(use_case);
662        self
663    }
664}
665
666/// Subscription plan tier
667#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
668#[serde(rename_all = "lowercase")]
669pub enum PlanTier {
670    /// Free plan
671    Free,
672    /// Lite paid plan
673    Lite,
674    /// Plus paid plan
675    Plus,
676    /// Custom enterprise plan
677    Custom,
678}
679
680/// Credit usage information
681#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
682pub struct Credits {
683    /// Total credits provided by the plan
684    pub plan_credits: i64,
685    /// Number of credits used
686    pub used_credits: i64,
687}
688
689/// Usage limit information
690#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
691pub struct Limits {
692    /// Maximum number of concurrent requests allowed
693    pub concurrency_limit: i64,
694}
695
696/// Response from `GET /v1/users/me/subscription`
697#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
698pub struct SubscriptionResponse {
699    /// Current subscription plan tier
700    pub plan: PlanTier,
701    /// Credit usage information
702    pub credits: Credits,
703    /// Usage limit information
704    pub limits: Limits,
705}
706
707/// API error response
708#[derive(Debug, Clone, Serialize, Deserialize)]
709pub struct ErrorResponse {
710    /// Error message describing the issue
711    pub detail: String,
712}
713
714// ---------------------------------------------------------------------------
715// Voice cloning
716// ---------------------------------------------------------------------------
717
718/// Maximum audio file size for voice cloning (25 MB)
719pub const CLONING_MAX_FILE_SIZE: usize = 25 * 1024 * 1024;
720
721/// Minimum character length for a custom voice name
722pub const NAME_MIN_LENGTH: usize = 1;
723
724/// Maximum character length for a custom voice name
725pub const NAME_MAX_LENGTH: usize = 30;
726
727/// A custom (user-cloned) voice returned by the clone-voice endpoint
728#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
729pub struct CustomVoice {
730    /// Unique voice identifier assigned after cloning
731    pub voice_id: String,
732    /// Human-readable name given to this custom voice
733    pub name: String,
734    /// TTS model the voice was cloned with (e.g. `"ssfm-v30"`)
735    pub model: String,
736}