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 = 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` or `target_lufs`, which are
175/// not supported by the streaming endpoint.
176#[derive(Debug, Clone, Default, Serialize, Deserialize)]
177pub struct OutputStream {
178    /// Pitch adjustment in semitones (-12 to +12, default: 0)
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub audio_pitch: Option<i32>,
181    /// Speech speed multiplier (0.5 to 2.0, default: 1.0)
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub audio_tempo: Option<f64>,
184    /// Output audio format (wav or mp3, default: wav)
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub audio_format: Option<AudioFormat>,
187}
188
189impl OutputStream {
190    /// Create a new OutputStream with default values
191    pub fn new() -> Self {
192        Self::default()
193    }
194
195    /// Set the audio pitch (-12 to +12 semitones)
196    pub fn audio_pitch(mut self, pitch: i32) -> Self {
197        self.audio_pitch = Some(pitch.clamp(-12, 12));
198        self
199    }
200
201    /// Set the audio tempo (0.5 to 2.0)
202    pub fn audio_tempo(mut self, tempo: f64) -> Self {
203        self.audio_tempo = Some(tempo.clamp(0.5, 2.0));
204        self
205    }
206
207    /// Set the audio format
208    pub fn audio_format(mut self, format: AudioFormat) -> Self {
209        self.audio_format = Some(format);
210        self
211    }
212}
213
214/// Emotion settings for ssfm-v21 model
215#[derive(Debug, Clone, Default, Serialize, Deserialize)]
216pub struct Prompt {
217    /// Emotion preset to apply
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub emotion_preset: Option<EmotionPreset>,
220    /// Emotion intensity (0.0 to 2.0, default: 1.0)
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub emotion_intensity: Option<f64>,
223}
224
225impl Prompt {
226    /// Create a new Prompt with default values
227    pub fn new() -> Self {
228        Self::default()
229    }
230
231    /// Set the emotion preset
232    pub fn emotion_preset(mut self, preset: EmotionPreset) -> Self {
233        self.emotion_preset = Some(preset);
234        self
235    }
236
237    /// Set the emotion intensity (0.0 to 2.0)
238    pub fn emotion_intensity(mut self, intensity: f64) -> Self {
239        self.emotion_intensity = Some(intensity.clamp(0.0, 2.0));
240        self
241    }
242}
243
244/// Preset-based emotion control for ssfm-v30 model
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct PresetPrompt {
247    /// Must be "preset" for preset-based emotion control
248    pub emotion_type: String,
249    /// Emotion preset to apply
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub emotion_preset: Option<EmotionPreset>,
252    /// Emotion intensity (0.0 to 2.0, default: 1.0)
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub emotion_intensity: Option<f64>,
255}
256
257impl Default for PresetPrompt {
258    fn default() -> Self {
259        Self {
260            emotion_type: "preset".to_string(),
261            emotion_preset: None,
262            emotion_intensity: None,
263        }
264    }
265}
266
267impl PresetPrompt {
268    /// Create a new PresetPrompt
269    pub fn new() -> Self {
270        Self::default()
271    }
272
273    /// Set the emotion preset
274    pub fn emotion_preset(mut self, preset: EmotionPreset) -> Self {
275        self.emotion_preset = Some(preset);
276        self
277    }
278
279    /// Set the emotion intensity (0.0 to 2.0)
280    pub fn emotion_intensity(mut self, intensity: f64) -> Self {
281        self.emotion_intensity = Some(intensity.clamp(0.0, 2.0));
282        self
283    }
284}
285
286/// Context-aware emotion inference for ssfm-v30 model
287#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct SmartPrompt {
289    /// Must be "smart" for context-aware emotion inference
290    pub emotion_type: String,
291    /// Text that comes before the main text (max 2000 chars)
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub previous_text: Option<String>,
294    /// Text that comes after the main text (max 2000 chars)
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub next_text: Option<String>,
297}
298
299impl Default for SmartPrompt {
300    fn default() -> Self {
301        Self {
302            emotion_type: "smart".to_string(),
303            previous_text: None,
304            next_text: None,
305        }
306    }
307}
308
309impl SmartPrompt {
310    /// Create a new SmartPrompt
311    pub fn new() -> Self {
312        Self::default()
313    }
314
315    /// Set the previous text for context
316    pub fn previous_text(mut self, text: impl Into<String>) -> Self {
317        self.previous_text = Some(text.into());
318        self
319    }
320
321    /// Set the next text for context
322    pub fn next_text(mut self, text: impl Into<String>) -> Self {
323        self.next_text = Some(text.into());
324        self
325    }
326}
327
328/// Union type for all prompt types
329#[derive(Debug, Clone, Serialize, Deserialize)]
330#[serde(untagged)]
331pub enum TTSPrompt {
332    /// Basic emotion control (ssfm-v21 compatible)
333    Basic(Prompt),
334    /// Explicit preset emotion control (ssfm-v30)
335    Preset(PresetPrompt),
336    /// Context-aware emotion inference (ssfm-v30)
337    Smart(SmartPrompt),
338}
339
340impl From<Prompt> for TTSPrompt {
341    fn from(prompt: Prompt) -> Self {
342        TTSPrompt::Basic(prompt)
343    }
344}
345
346impl From<PresetPrompt> for TTSPrompt {
347    fn from(prompt: PresetPrompt) -> Self {
348        TTSPrompt::Preset(prompt)
349    }
350}
351
352impl From<SmartPrompt> for TTSPrompt {
353    fn from(prompt: SmartPrompt) -> Self {
354        TTSPrompt::Smart(prompt)
355    }
356}
357
358/// Text-to-Speech request parameters
359#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct TTSRequest {
361    /// Voice ID in format `tc_` followed by a unique identifier.
362    /// Browse available API voices at <https://typecast.ai/developers/api/voices>.
363    pub voice_id: String,
364    /// Text to convert to speech (max 2000 chars)
365    pub text: String,
366    /// TTS model to use
367    pub model: TTSModel,
368    /// Language code (ISO 639-3). Auto-detected if not provided
369    #[serde(skip_serializing_if = "Option::is_none")]
370    pub language: Option<String>,
371    /// Emotion and style settings
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub prompt: Option<TTSPrompt>,
374    /// Audio output settings
375    #[serde(skip_serializing_if = "Option::is_none")]
376    pub output: Option<Output>,
377    /// Random seed for reproducible results
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub seed: Option<i32>,
380}
381
382/// Convenience request for generating audio directly to a file.
383///
384/// `model` defaults to [`TTSModel::SsfmV30`].
385#[derive(Debug, Clone, Serialize, Deserialize)]
386pub struct GenerateToFileRequest {
387    /// Voice ID in format `tc_` followed by a unique identifier.
388    /// Browse available API voices at <https://typecast.ai/developers/api/voices>.
389    pub voice_id: String,
390    /// Text to convert to speech
391    pub text: String,
392    /// TTS model to use
393    pub model: TTSModel,
394    /// Language code (ISO 639-3). Auto-detected if not provided
395    #[serde(skip_serializing_if = "Option::is_none")]
396    pub language: Option<String>,
397    /// Emotion and style settings
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub prompt: Option<TTSPrompt>,
400    /// Audio output settings
401    #[serde(skip_serializing_if = "Option::is_none")]
402    pub output: Option<Output>,
403    /// Random seed for reproducible results
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub seed: Option<i32>,
406}
407
408impl GenerateToFileRequest {
409    /// Create a new generate-to-file request with required fields.
410    pub fn new(voice_id: impl Into<String>, text: impl Into<String>) -> Self {
411        let voice_id = voice_id.into();
412        let text = text.into();
413        assert!(!voice_id.trim().is_empty(), "voice_id is required");
414        assert!(!text.trim().is_empty(), "text is required");
415        assert!(
416            text.chars().count() <= 2000,
417            "text must not exceed 2000 characters"
418        );
419        Self {
420            voice_id,
421            text,
422            model: TTSModel::SsfmV30,
423            language: None,
424            prompt: None,
425            output: None,
426            seed: None,
427        }
428    }
429
430    pub fn model(mut self, model: TTSModel) -> Self {
431        self.model = model;
432        self
433    }
434
435    pub fn language(mut self, language: impl Into<String>) -> Self {
436        self.language = Some(language.into());
437        self
438    }
439
440    pub fn prompt(mut self, prompt: impl Into<TTSPrompt>) -> Self {
441        self.prompt = Some(prompt.into());
442        self
443    }
444
445    pub fn output(mut self, output: Output) -> Self {
446        self.output = Some(output);
447        self
448    }
449
450    pub fn seed(mut self, seed: i32) -> Self {
451        self.seed = Some(seed);
452        self
453    }
454
455    pub fn into_tts_request(self) -> TTSRequest {
456        TTSRequest {
457            voice_id: self.voice_id,
458            text: self.text,
459            model: self.model,
460            language: self.language,
461            prompt: self.prompt,
462            output: self.output,
463            seed: self.seed,
464        }
465    }
466}
467
468impl TTSRequest {
469    /// Create a new TTSRequest with required fields
470    pub fn new(voice_id: impl Into<String>, text: impl Into<String>, model: TTSModel) -> Self {
471        Self {
472            voice_id: voice_id.into(),
473            text: text.into(),
474            model,
475            language: None,
476            prompt: None,
477            output: None,
478            seed: None,
479        }
480    }
481
482    /// Set the language code (ISO 639-3)
483    pub fn language(mut self, language: impl Into<String>) -> Self {
484        self.language = Some(language.into());
485        self
486    }
487
488    /// Set the prompt (emotion settings)
489    pub fn prompt(mut self, prompt: impl Into<TTSPrompt>) -> Self {
490        self.prompt = Some(prompt.into());
491        self
492    }
493
494    /// Set the output settings
495    pub fn output(mut self, output: Output) -> Self {
496        self.output = Some(output);
497        self
498    }
499
500    /// Set the random seed for reproducible results
501    pub fn seed(mut self, seed: i32) -> Self {
502        self.seed = Some(seed);
503        self
504    }
505}
506
507/// Streaming Text-to-Speech request parameters.
508///
509/// Mirrors [`TTSRequest`] but the `output` field uses [`OutputStream`], which
510/// excludes `volume` and `target_lufs` (not supported by the streaming endpoint).
511#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct TTSRequestStream {
513    /// Voice ID in format `tc_` followed by a unique identifier.
514    /// Browse available API voices at <https://typecast.ai/developers/api/voices>.
515    pub voice_id: String,
516    /// Text to convert to speech (max 2000 chars)
517    pub text: String,
518    /// TTS model to use
519    pub model: TTSModel,
520    /// Language code (ISO 639-3). Auto-detected if not provided
521    #[serde(skip_serializing_if = "Option::is_none")]
522    pub language: Option<String>,
523    /// Emotion and style settings
524    #[serde(skip_serializing_if = "Option::is_none")]
525    pub prompt: Option<TTSPrompt>,
526    /// Audio output settings (without volume/target_lufs)
527    #[serde(skip_serializing_if = "Option::is_none")]
528    pub output: Option<OutputStream>,
529    /// Random seed for reproducible results
530    #[serde(skip_serializing_if = "Option::is_none")]
531    pub seed: Option<i32>,
532}
533
534impl TTSRequestStream {
535    /// Create a new TTSRequestStream with required fields
536    pub fn new(voice_id: impl Into<String>, text: impl Into<String>, model: TTSModel) -> Self {
537        Self {
538            voice_id: voice_id.into(),
539            text: text.into(),
540            model,
541            language: None,
542            prompt: None,
543            output: None,
544            seed: None,
545        }
546    }
547
548    /// Set the language code (ISO 639-3)
549    pub fn language(mut self, language: impl Into<String>) -> Self {
550        self.language = Some(language.into());
551        self
552    }
553
554    /// Set the prompt (emotion settings)
555    pub fn prompt(mut self, prompt: impl Into<TTSPrompt>) -> Self {
556        self.prompt = Some(prompt.into());
557        self
558    }
559
560    /// Set the output settings
561    pub fn output(mut self, output: OutputStream) -> Self {
562        self.output = Some(output);
563        self
564    }
565
566    /// Set the random seed for reproducible results
567    pub fn seed(mut self, seed: i32) -> Self {
568        self.seed = Some(seed);
569        self
570    }
571}
572
573/// Text-to-Speech response
574#[derive(Debug, Clone)]
575pub struct TTSResponse {
576    /// Generated audio data
577    pub audio_data: Vec<u8>,
578    /// Audio duration in seconds
579    pub duration: f64,
580    /// Audio format (wav or mp3)
581    pub format: AudioFormat,
582}
583
584/// Model information with supported emotions
585#[derive(Debug, Clone, Serialize, Deserialize)]
586pub struct ModelInfo {
587    /// TTS model version
588    pub version: TTSModel,
589    /// List of supported emotions for this model
590    pub emotions: Vec<String>,
591}
592
593/// Voice from V2 API with enhanced metadata
594#[derive(Debug, Clone, Serialize, Deserialize)]
595pub struct VoiceV2 {
596    /// Unique voice identifier
597    pub voice_id: String,
598    /// Human-readable name of the voice
599    pub voice_name: String,
600    /// List of supported TTS models with their emotions
601    pub models: Vec<ModelInfo>,
602    /// Voice gender classification
603    #[serde(skip_serializing_if = "Option::is_none")]
604    pub gender: Option<Gender>,
605    /// Voice age group classification
606    #[serde(skip_serializing_if = "Option::is_none")]
607    pub age: Option<Age>,
608    /// List of use case categories
609    #[serde(skip_serializing_if = "Option::is_none")]
610    pub use_cases: Option<Vec<String>>,
611}
612
613/// Filter options for V2 voices endpoint
614#[derive(Debug, Clone, Default)]
615pub struct VoicesV2Filter {
616    /// Filter by TTS model
617    pub model: Option<TTSModel>,
618    /// Filter by gender
619    pub gender: Option<Gender>,
620    /// Filter by age group
621    pub age: Option<Age>,
622    /// Filter by use case
623    pub use_cases: Option<UseCase>,
624}
625
626impl VoicesV2Filter {
627    /// Create a new empty filter
628    pub fn new() -> Self {
629        Self::default()
630    }
631
632    /// Filter by model
633    pub fn model(mut self, model: TTSModel) -> Self {
634        self.model = Some(model);
635        self
636    }
637
638    /// Filter by gender
639    pub fn gender(mut self, gender: Gender) -> Self {
640        self.gender = Some(gender);
641        self
642    }
643
644    /// Filter by age
645    pub fn age(mut self, age: Age) -> Self {
646        self.age = Some(age);
647        self
648    }
649
650    /// Filter by use case
651    pub fn use_cases(mut self, use_case: UseCase) -> Self {
652        self.use_cases = Some(use_case);
653        self
654    }
655}
656
657/// Subscription plan tier
658#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
659#[serde(rename_all = "lowercase")]
660pub enum PlanTier {
661    /// Free plan
662    Free,
663    /// Lite paid plan
664    Lite,
665    /// Plus paid plan
666    Plus,
667    /// Custom enterprise plan
668    Custom,
669}
670
671/// Credit usage information
672#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
673pub struct Credits {
674    /// Total credits provided by the plan
675    pub plan_credits: i64,
676    /// Number of credits used
677    pub used_credits: i64,
678}
679
680/// Usage limit information
681#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
682pub struct Limits {
683    /// Maximum number of concurrent requests allowed
684    pub concurrency_limit: i64,
685}
686
687/// Response from `GET /v1/users/me/subscription`
688#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
689pub struct SubscriptionResponse {
690    /// Current subscription plan tier
691    pub plan: PlanTier,
692    /// Credit usage information
693    pub credits: Credits,
694    /// Usage limit information
695    pub limits: Limits,
696}
697
698/// API error response
699#[derive(Debug, Clone, Serialize, Deserialize)]
700pub struct ErrorResponse {
701    /// Error message describing the issue
702    pub detail: String,
703}
704
705// ---------------------------------------------------------------------------
706// Voice cloning
707// ---------------------------------------------------------------------------
708
709/// Maximum audio file size for voice cloning (25 MB)
710pub const CLONING_MAX_FILE_SIZE: usize = 25 * 1024 * 1024;
711
712/// Minimum character length for a custom voice name
713pub const NAME_MIN_LENGTH: usize = 1;
714
715/// Maximum character length for a custom voice name
716pub const NAME_MAX_LENGTH: usize = 30;
717
718/// A custom (user-cloned) voice returned by the clone-voice endpoint
719#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
720pub struct CustomVoice {
721    /// Unique voice identifier assigned after cloning
722    pub voice_id: String,
723    /// Human-readable name given to this custom voice
724    pub name: String,
725    /// TTS model the voice was cloned with (e.g. `"ssfm-v30"`)
726    pub model: String,
727}