1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub struct TtsBackboneConfig {
14 #[serde(default = "default_backbone_n_layers")]
16 pub n_layers: usize,
17 #[serde(default = "default_backbone_dim")]
19 pub dim: usize,
20 #[serde(default = "default_backbone_n_heads")]
22 pub n_heads: usize,
23 #[serde(default = "default_backbone_n_kv_heads")]
25 pub n_kv_heads: usize,
26 #[serde(default = "default_backbone_head_dim")]
28 pub head_dim: usize,
29 #[serde(default = "default_backbone_ffn_dim")]
31 pub ffn_dim: usize,
32 #[serde(default = "default_backbone_rope_theta")]
34 pub rope_theta: f64,
35 #[serde(default = "default_vocab_size")]
37 pub vocab_size: usize,
38 #[serde(default = "default_true")]
40 pub tied_embeddings: bool,
41 #[serde(default = "default_norm_eps")]
43 pub norm_eps: f64,
44}
45
46impl Default for TtsBackboneConfig {
47 fn default() -> Self {
48 Self {
49 n_layers: default_backbone_n_layers(),
50 dim: default_backbone_dim(),
51 n_heads: default_backbone_n_heads(),
52 n_kv_heads: default_backbone_n_kv_heads(),
53 head_dim: default_backbone_head_dim(),
54 ffn_dim: default_backbone_ffn_dim(),
55 rope_theta: default_backbone_rope_theta(),
56 vocab_size: default_vocab_size(),
57 tied_embeddings: true,
58 norm_eps: default_norm_eps(),
59 }
60 }
61}
62
63impl TtsBackboneConfig {
64 pub fn gqa_groups(&self) -> usize {
66 self.n_heads / self.n_kv_heads
67 }
68
69 pub fn validate(&self) -> Result<(), ConfigError> {
71 if self.n_layers == 0 {
72 return Err(ConfigError::InvalidValue("n_layers must be > 0".into()));
73 }
74 if self.dim == 0 {
75 return Err(ConfigError::InvalidValue("dim must be > 0".into()));
76 }
77 if self.n_heads == 0 || self.n_kv_heads == 0 {
78 return Err(ConfigError::InvalidValue(
79 "n_heads and n_kv_heads must be > 0".into(),
80 ));
81 }
82 if !self.n_heads.is_multiple_of(self.n_kv_heads) {
83 return Err(ConfigError::InvalidValue(
84 "n_heads must be divisible by n_kv_heads".into(),
85 ));
86 }
87 if !self.dim.is_multiple_of(self.n_heads) {
88 return Err(ConfigError::InvalidValue(
89 "dim must be divisible by n_heads".into(),
90 ));
91 }
92 Ok(())
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
101pub struct FmTransformerConfig {
102 #[serde(default = "default_fm_n_layers")]
104 pub n_layers: usize,
105 #[serde(default = "default_backbone_dim")]
107 pub dim: usize,
108 #[serde(default = "default_backbone_n_heads")]
110 pub n_heads: usize,
111 #[serde(default = "default_backbone_n_kv_heads")]
113 pub n_kv_heads: usize,
114 #[serde(default = "default_backbone_head_dim")]
116 pub head_dim: usize,
117 #[serde(default = "default_backbone_ffn_dim")]
119 pub ffn_dim: usize,
120 #[serde(default = "default_fm_rope_theta")]
122 pub rope_theta: f64,
123 #[serde(default = "default_norm_eps")]
125 pub norm_eps: f64,
126 #[serde(default = "default_acoustic_dim")]
128 pub acoustic_dim: usize,
129 #[serde(default = "default_semantic_output_size")]
131 pub semantic_output_size: usize,
132 #[serde(default = "default_euler_steps")]
134 pub euler_steps: usize,
135 #[serde(default = "default_cfg_alpha")]
137 pub cfg_alpha: f32,
138}
139
140impl Default for FmTransformerConfig {
141 fn default() -> Self {
142 Self {
143 n_layers: default_fm_n_layers(),
144 dim: default_backbone_dim(),
145 n_heads: default_backbone_n_heads(),
146 n_kv_heads: default_backbone_n_kv_heads(),
147 head_dim: default_backbone_head_dim(),
148 ffn_dim: default_backbone_ffn_dim(),
149 rope_theta: default_fm_rope_theta(),
150 norm_eps: default_norm_eps(),
151 acoustic_dim: default_acoustic_dim(),
152 semantic_output_size: default_semantic_output_size(),
153 euler_steps: default_euler_steps(),
154 cfg_alpha: default_cfg_alpha(),
155 }
156 }
157}
158
159impl FmTransformerConfig {
160 pub fn validate(&self) -> Result<(), ConfigError> {
162 if self.n_layers == 0 {
163 return Err(ConfigError::InvalidValue("n_layers must be > 0".into()));
164 }
165 if self.dim == 0 {
166 return Err(ConfigError::InvalidValue("dim must be > 0".into()));
167 }
168 if !self.n_heads.is_multiple_of(self.n_kv_heads) {
169 return Err(ConfigError::InvalidValue(
170 "n_heads must be divisible by n_kv_heads".into(),
171 ));
172 }
173 if self.euler_steps == 0 {
174 return Err(ConfigError::InvalidValue("euler_steps must be > 0".into()));
175 }
176 Ok(())
177 }
178}
179
180#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
185pub struct CodecDecoderConfig {
186 #[serde(default = "default_codec_dim")]
188 pub dim: usize,
189 #[serde(default = "default_codec_n_heads")]
191 pub n_heads: usize,
192 #[serde(default = "default_codec_head_dim")]
194 pub head_dim: usize,
195 #[serde(default = "default_codec_ffn_dim")]
197 pub ffn_dim: usize,
198 #[serde(default = "default_codec_layers_per_block")]
200 pub layers_per_block: usize,
201 #[serde(default = "default_codec_sliding_windows")]
203 pub sliding_windows: Vec<usize>,
204 #[serde(default = "default_codec_input_channels")]
206 pub input_channels: usize,
207 #[serde(default = "default_codec_output_patch_size")]
209 pub output_patch_size: usize,
210 #[serde(default = "default_codec_sample_rate")]
212 pub sample_rate: u32,
213 #[serde(default = "default_semantic_vq_size")]
215 pub semantic_vq_size: usize,
216 #[serde(default = "default_semantic_embed_dim")]
218 pub semantic_embed_dim: usize,
219 #[serde(default = "default_acoustic_dim")]
221 pub acoustic_fsq_dims: usize,
222 #[serde(default = "default_fsq_levels")]
224 pub fsq_levels: usize,
225 #[serde(default = "default_codec_norm_eps")]
227 pub norm_eps: f64,
228 #[serde(default = "default_codec_qk_norm_eps")]
230 pub qk_norm_eps: f64,
231}
232
233impl Default for CodecDecoderConfig {
234 fn default() -> Self {
235 Self {
236 dim: default_codec_dim(),
237 n_heads: default_codec_n_heads(),
238 head_dim: default_codec_head_dim(),
239 ffn_dim: default_codec_ffn_dim(),
240 layers_per_block: default_codec_layers_per_block(),
241 sliding_windows: default_codec_sliding_windows(),
242 input_channels: default_codec_input_channels(),
243 output_patch_size: default_codec_output_patch_size(),
244 sample_rate: default_codec_sample_rate(),
245 semantic_vq_size: default_semantic_vq_size(),
246 semantic_embed_dim: default_semantic_embed_dim(),
247 acoustic_fsq_dims: default_acoustic_dim(),
248 fsq_levels: default_fsq_levels(),
249 norm_eps: default_codec_norm_eps(),
250 qk_norm_eps: default_codec_qk_norm_eps(),
251 }
252 }
253}
254
255impl CodecDecoderConfig {
256 pub fn total_transformer_layers(&self) -> usize {
258 self.sliding_windows.len() * self.layers_per_block
259 }
260
261 pub fn validate(&self) -> Result<(), ConfigError> {
263 if self.dim == 0 {
264 return Err(ConfigError::InvalidValue("dim must be > 0".into()));
265 }
266 if self.sliding_windows.is_empty() {
267 return Err(ConfigError::InvalidValue(
268 "sliding_windows must not be empty".into(),
269 ));
270 }
271 if self.n_heads == 0 {
272 return Err(ConfigError::InvalidValue("n_heads must be > 0".into()));
273 }
274 if !self.dim.is_multiple_of(self.n_heads) {
275 return Err(ConfigError::InvalidValue(
276 "dim must be divisible by n_heads".into(),
277 ));
278 }
279 Ok(())
280 }
281}
282
283#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
290pub struct TtsSpecialTokens {
291 pub bos_token_id: u32,
293 pub begin_audio_token_id: u32,
295 pub next_audio_text_token_id: u32,
297 pub repeat_audio_text_token_id: u32,
299 pub audio_token_id: u32,
301 pub empty_audio_idx: u32,
303 pub end_audio_idx: u32,
305}
306
307impl Default for TtsSpecialTokens {
308 fn default() -> Self {
309 Self {
310 bos_token_id: 1,
311 begin_audio_token_id: 25, next_audio_text_token_id: 36, repeat_audio_text_token_id: 35, audio_token_id: 24, empty_audio_idx: 0,
316 end_audio_idx: 1,
317 }
318 }
319}
320
321#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
323pub struct VoiceEmbeddingConfig {
324 #[serde(default = "default_backbone_dim")]
326 pub embed_dim: usize,
327 #[serde(default = "default_voice_presets")]
329 pub preset_names: Vec<String>,
330}
331
332impl Default for VoiceEmbeddingConfig {
333 fn default() -> Self {
334 Self {
335 embed_dim: default_backbone_dim(),
336 preset_names: default_voice_presets(),
337 }
338 }
339}
340
341#[derive(Debug, Clone, Copy, PartialEq, Eq)]
343pub struct AudioCodebookLayout {
344 pub semantic_vq_size: usize,
346 pub acoustic_codebooks: usize,
348 pub fsq_levels: usize,
350 pub specials_per_codebook: usize,
352}
353
354impl Default for AudioCodebookLayout {
355 fn default() -> Self {
356 Self {
357 semantic_vq_size: 8192,
358 acoustic_codebooks: 36,
359 fsq_levels: 21,
360 specials_per_codebook: 2,
361 }
362 }
363}
364
365impl AudioCodebookLayout {
366 pub fn acoustic_stride(&self) -> usize {
368 self.specials_per_codebook + self.fsq_levels
369 }
370
371 pub fn acoustic_region_start(&self) -> usize {
373 self.specials_per_codebook + self.semantic_vq_size
374 }
375
376 pub fn total_entries(&self) -> usize {
378 self.specials_per_codebook
380 + self.semantic_vq_size
381 + self.acoustic_codebooks * self.acoustic_stride()
382 }
383
384 pub fn semantic_global_index(&self, raw_semantic_idx: usize) -> usize {
386 raw_semantic_idx + self.specials_per_codebook
387 }
388
389 pub fn acoustic_global_index(&self, codebook: usize, level: usize) -> usize {
391 self.acoustic_region_start()
392 + codebook * self.acoustic_stride()
393 + level
394 + self.specials_per_codebook
395 }
396}
397
398#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
400pub struct TtsConfig {
401 pub backbone: TtsBackboneConfig,
402 pub fm_transformer: FmTransformerConfig,
403 pub codec_decoder: CodecDecoderConfig,
404 pub special_tokens: TtsSpecialTokens,
405 pub voice: VoiceEmbeddingConfig,
406}
407
408impl TtsConfig {
409 pub fn validate(&self) -> Result<(), ConfigError> {
411 self.backbone.validate()?;
412 self.fm_transformer.validate()?;
413 self.codec_decoder.validate()?;
414
415 if self.fm_transformer.dim != self.backbone.dim {
417 return Err(ConfigError::InvalidValue(
418 "fm_transformer.dim must match backbone.dim".into(),
419 ));
420 }
421
422 if self.voice.embed_dim != self.backbone.dim {
424 return Err(ConfigError::InvalidValue(
425 "voice.embed_dim must match backbone.dim".into(),
426 ));
427 }
428
429 Ok(())
430 }
431}
432
433#[derive(Debug, Clone, thiserror::Error)]
435pub enum ConfigError {
436 #[error("invalid config value: {0}")]
437 InvalidValue(String),
438}
439
440fn default_backbone_n_layers() -> usize {
443 26
444}
445fn default_backbone_dim() -> usize {
446 3072
447}
448fn default_backbone_n_heads() -> usize {
449 32
450}
451fn default_backbone_n_kv_heads() -> usize {
452 8
453}
454fn default_backbone_head_dim() -> usize {
455 128
456}
457fn default_backbone_ffn_dim() -> usize {
458 9216
459}
460fn default_backbone_rope_theta() -> f64 {
461 1_000_000.0
462}
463fn default_vocab_size() -> usize {
464 131_072
465}
466fn default_norm_eps() -> f64 {
467 1e-5
468}
469fn default_codec_norm_eps() -> f64 {
470 0.01 }
472fn default_codec_qk_norm_eps() -> f64 {
473 1e-6 }
475fn default_true() -> bool {
476 true
477}
478
479fn default_fm_n_layers() -> usize {
480 3
481}
482fn default_fm_rope_theta() -> f64 {
483 10_000.0
484}
485fn default_acoustic_dim() -> usize {
486 36
487}
488fn default_semantic_output_size() -> usize {
489 8320
490}
491fn default_euler_steps() -> usize {
492 5 }
494fn default_cfg_alpha() -> f32 {
495 1.2
496}
497
498fn default_codec_dim() -> usize {
499 1024
500}
501fn default_codec_n_heads() -> usize {
502 8
503}
504fn default_codec_head_dim() -> usize {
505 128
506}
507fn default_codec_ffn_dim() -> usize {
508 4096
509}
510fn default_codec_layers_per_block() -> usize {
511 2
512}
513fn default_codec_sliding_windows() -> Vec<usize> {
514 vec![2, 4, 8, 16]
515}
516fn default_codec_input_channels() -> usize {
517 292
518}
519fn default_codec_output_patch_size() -> usize {
520 240
521}
522fn default_codec_sample_rate() -> u32 {
523 24_000
524}
525fn default_semantic_vq_size() -> usize {
526 8192
527}
528fn default_semantic_embed_dim() -> usize {
529 256
530}
531fn default_fsq_levels() -> usize {
532 21
533}
534
535fn default_voice_presets() -> Vec<String> {
536 [
537 "alloy",
538 "ash",
539 "ballad",
540 "breeze",
541 "casual_female",
542 "casual_male",
543 "coral",
544 "echo",
545 "fable",
546 "nova",
547 "onyx",
548 "professional_female",
549 "professional_male",
550 "sage",
551 "shimmer",
552 "spirit",
553 "verse",
554 "warm_female",
555 "warm_male",
556 "whisper",
557 ]
558 .iter()
559 .map(|s| s.to_string())
560 .collect()
561}
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566
567 #[test]
568 fn test_backbone_defaults() {
569 let config = TtsBackboneConfig::default();
570 assert_eq!(config.n_layers, 26);
571 assert_eq!(config.dim, 3072);
572 assert_eq!(config.n_heads, 32);
573 assert_eq!(config.n_kv_heads, 8);
574 assert_eq!(config.head_dim, 128);
575 assert_eq!(config.ffn_dim, 9216);
576 assert_eq!(config.rope_theta, 1_000_000.0);
577 assert_eq!(config.vocab_size, 131_072);
578 assert!(config.tied_embeddings);
579 assert_eq!(config.gqa_groups(), 4);
580 }
581
582 #[test]
583 fn test_fm_transformer_defaults() {
584 let config = FmTransformerConfig::default();
585 assert_eq!(config.n_layers, 3);
586 assert_eq!(config.dim, 3072);
587 assert_eq!(config.n_heads, 32);
588 assert_eq!(config.n_kv_heads, 8);
589 assert_eq!(config.head_dim, 128);
590 assert_eq!(config.ffn_dim, 9216);
591 assert_eq!(config.rope_theta, 10_000.0);
592 assert_eq!(config.acoustic_dim, 36);
593 assert_eq!(config.semantic_output_size, 8320);
594 assert_eq!(config.euler_steps, 5); assert!((config.cfg_alpha - 1.2).abs() < 1e-6);
596 }
597
598 #[test]
599 fn test_codec_decoder_defaults() {
600 let config = CodecDecoderConfig::default();
601 assert_eq!(config.dim, 1024);
602 assert_eq!(config.n_heads, 8);
603 assert_eq!(config.head_dim, 128);
604 assert_eq!(config.layers_per_block, 2);
605 assert_eq!(config.sliding_windows, vec![2, 4, 8, 16]);
606 assert_eq!(config.input_channels, 292);
607 assert_eq!(config.output_patch_size, 240);
608 assert_eq!(config.sample_rate, 24_000);
609 assert_eq!(config.total_transformer_layers(), 8);
610 }
611
612 #[test]
613 fn test_special_tokens_defaults() {
614 let tokens = TtsSpecialTokens::default();
615 assert_eq!(tokens.audio_token_id, 24);
616 assert_eq!(tokens.begin_audio_token_id, 25);
617 assert_eq!(tokens.bos_token_id, 1);
618 assert_eq!(tokens.empty_audio_idx, 0);
619 assert_eq!(tokens.end_audio_idx, 1);
620 }
621
622 #[test]
623 fn test_voice_embedding_defaults() {
624 let config = VoiceEmbeddingConfig::default();
625 assert_eq!(config.embed_dim, 3072);
626 assert_eq!(config.preset_names.len(), 20);
627 assert!(config.preset_names.contains(&"casual_female".to_string()));
628 assert!(config.preset_names.contains(&"whisper".to_string()));
629 }
630
631 #[test]
632 fn test_audio_codebook_layout() {
633 let layout = AudioCodebookLayout::default();
634 assert_eq!(layout.acoustic_stride(), 23);
635 assert_eq!(layout.acoustic_region_start(), 8194);
636 assert_eq!(layout.total_entries(), 9022);
638
639 assert_eq!(layout.semantic_global_index(0), 2);
641 assert_eq!(layout.semantic_global_index(8191), 8193);
642
643 assert_eq!(layout.acoustic_global_index(0, 0), 8196);
645 assert_eq!(layout.acoustic_global_index(35, 20), 9021);
647 }
648
649 #[test]
650 fn test_tts_config_validates() {
651 let config = TtsConfig::default();
652 assert!(config.validate().is_ok());
653 }
654
655 #[test]
656 fn test_validation_rejects_zero_layers() {
657 let mut config = TtsBackboneConfig::default();
658 config.n_layers = 0;
659 assert!(config.validate().is_err());
660 }
661
662 #[test]
663 fn test_validation_rejects_mismatched_heads() {
664 let mut config = TtsBackboneConfig::default();
665 config.n_heads = 7;
666 config.n_kv_heads = 3;
667 assert!(config.validate().is_err());
668 }
669
670 #[test]
671 fn test_validation_rejects_dim_head_mismatch() {
672 let mut config = TtsBackboneConfig::default();
673 config.dim = 100;
674 config.n_heads = 32;
675 assert!(config.validate().is_err());
676 }
677
678 #[test]
679 fn test_validation_rejects_fm_zero_euler_steps() {
680 let mut config = FmTransformerConfig::default();
681 config.euler_steps = 0;
682 assert!(config.validate().is_err());
683 }
684
685 #[test]
686 fn test_validation_rejects_empty_sliding_windows() {
687 let mut config = CodecDecoderConfig::default();
688 config.sliding_windows = vec![];
689 assert!(config.validate().is_err());
690 }
691
692 #[test]
693 fn test_cross_config_validation_dim_mismatch() {
694 let mut config = TtsConfig::default();
695 config.fm_transformer.dim = 1024;
696 assert!(config.validate().is_err());
697 }
698
699 #[test]
700 fn test_cross_config_validation_voice_dim_mismatch() {
701 let mut config = TtsConfig::default();
702 config.voice.embed_dim = 512;
703 assert!(config.validate().is_err());
704 }
705
706 #[test]
707 fn test_serde_roundtrip_backbone() {
708 let config = TtsBackboneConfig::default();
709 let json = serde_json::to_string(&config).unwrap();
710 let parsed: TtsBackboneConfig = serde_json::from_str(&json).unwrap();
711 assert_eq!(config, parsed);
712 }
713
714 #[test]
715 fn test_serde_roundtrip_tts_config() {
716 let config = TtsConfig::default();
717 let json = serde_json::to_string(&config).unwrap();
718 let parsed: TtsConfig = serde_json::from_str(&json).unwrap();
719 assert_eq!(config, parsed);
720 }
721
722 #[test]
723 fn test_serde_partial_deserialize() {
724 let json = r#"{"n_layers": 12, "dim": 768}"#;
726 let config: TtsBackboneConfig = serde_json::from_str(json).unwrap();
727 assert_eq!(config.n_layers, 12);
728 assert_eq!(config.dim, 768);
729 assert_eq!(config.n_heads, 32);
731 assert_eq!(config.vocab_size, 131_072);
732 }
733}