Skip to main content

voxtral_micro/tts/codec/
mod.rs

1//! Codec decoder for TTS waveform synthesis.
2//!
3//! Converts quantized audio tokens (semantic VQ + acoustic FSQ) into 24 kHz waveform
4//! via transformer blocks with ALiBi attention and causal convolution upsampling.
5//!
6//! Pipeline:
7//! ```text
8//! quantized tokens (292-dim: 256 semantic + 36 acoustic)
9//!   → Conv1d [292→1024, k=3, s=1]          (block 0: input projection)
10//!   → 2× Transformer(SW=2) + ConvT(2×up)   (blocks 1,2)
11//!   → 2× Transformer(SW=4) + ConvT(2×up)   (blocks 3,4)
12//!   → 2× Transformer(SW=8) + ConvT(2×up)   (blocks 5,6)
13//!   → 2× Transformer(SW=16)                (block 7)
14//!   → Conv1d [1024→240, k=7]               (output projection)
15//!   → reshape patches to waveform
16//! ```
17
18pub mod alibi;
19pub mod block;
20pub mod conv;
21pub mod layer_scale;
22pub mod qk_norm;
23pub mod quantizer;
24
25use anyhow::{Context, Result};
26use burn::tensor::backend::Backend;
27use burn::tensor::Tensor;
28use safetensors::SafeTensors;
29
30use crate::tts::config::CodecDecoderConfig;
31use block::CodecTransformerLayer;
32use conv::{CausalConv1d, CausalConvTranspose1d};
33use quantizer::{Fsq, VqCodebook};
34
35/// A transformer block group: 2 transformer layers sharing the same sliding window.
36struct TransformerGroup<B: Backend> {
37    layers: Vec<CodecTransformerLayer<B>>,
38}
39
40impl<B: Backend> TransformerGroup<B> {
41    fn forward(&self, mut x: Tensor<B, 3>) -> Tensor<B, 3> {
42        for layer in &self.layers {
43            x = layer.forward(x);
44        }
45        x
46    }
47}
48
49/// Full codec decoder: conv-transformer autoencoder producing 24 kHz audio.
50///
51/// Loads 117 tensors from SafeTensors with weight norm fusion.
52/// The decoder_blocks layout is:
53/// - Even blocks (0, 2, 4, 6): convolution (input proj or upsample)
54/// - Odd blocks (1, 3, 5, 7): transformer groups (2 layers each)
55/// - Output: weight-normed Conv1d [1024→240, k=7]
56pub struct CodecDecoder<B: Backend> {
57    /// Input Conv1d [292→1024, k=3, s=1] (block 0).
58    input_conv: CausalConv1d<B>,
59    /// 4 transformer groups (blocks 1, 3, 5, 7), each with 2 layers.
60    transformer_groups: Vec<TransformerGroup<B>>,
61    /// 3 upsample ConvTranspose1d [1024→1024, k=4, s=2] (blocks 2, 4, 6).
62    upsample_convs: Vec<CausalConvTranspose1d<B>>,
63    /// Output Conv1d [1024→240, k=7, s=1].
64    output_conv: CausalConv1d<B>,
65    /// VQ semantic codebook for dequantizing semantic tokens.
66    vq_codebook: VqCodebook<B>,
67}
68
69impl<B: Backend> CodecDecoder<B> {
70    /// Create a codec decoder from pre-loaded components (for GGUF loading).
71    ///
72    /// `transformer_group_layers` is a Vec of 4 groups, each a Vec of layers.
73    pub fn from_components(
74        input_conv: CausalConv1d<B>,
75        transformer_group_layers: Vec<Vec<CodecTransformerLayer<B>>>,
76        upsample_convs: Vec<CausalConvTranspose1d<B>>,
77        output_conv: CausalConv1d<B>,
78        vq_codebook: VqCodebook<B>,
79    ) -> Self {
80        let transformer_groups = transformer_group_layers
81            .into_iter()
82            .map(|layers| TransformerGroup { layers })
83            .collect();
84
85        Self {
86            input_conv,
87            transformer_groups,
88            upsample_convs,
89            output_conv,
90            vq_codebook,
91        }
92    }
93
94    /// Load the full codec decoder from SafeTensors.
95    ///
96    /// # Arguments
97    /// * `safetensors` - SafeTensors data containing all codec weights
98    /// * `config` - Codec decoder configuration
99    /// * `device` - Device for tensor allocation
100    pub fn from_safetensors(
101        safetensors: &SafeTensors,
102        config: &CodecDecoderConfig,
103        device: &B::Device,
104    ) -> Result<Self> {
105        let prefix = "audio_tokenizer";
106
107        // Block 0: Input Conv1d [292→1024, k=3, s=1]
108        let input_conv = CausalConv1d::from_safetensors(
109            safetensors,
110            &format!("{prefix}.decoder_blocks.0.conv"),
111            1, // stride
112            device,
113        )
114        .context("Loading input conv (block 0)")?;
115
116        // Blocks 1,3,5,7: Transformer groups
117        let transformer_block_indices = [1, 3, 5, 7];
118        let mut transformer_groups = Vec::with_capacity(4);
119        for (group_idx, &block_idx) in transformer_block_indices.iter().enumerate() {
120            let sliding_window = config.sliding_windows[group_idx];
121            let mut layers = Vec::with_capacity(config.layers_per_block);
122            for layer_idx in 0..config.layers_per_block {
123                let layer_prefix =
124                    format!("{prefix}.decoder_blocks.{block_idx}.layers.{layer_idx}");
125                let layer = CodecTransformerLayer::from_safetensors(
126                    safetensors,
127                    &layer_prefix,
128                    config.n_heads,
129                    config.head_dim,
130                    sliding_window,
131                    config.norm_eps,
132                    device,
133                )
134                .with_context(|| {
135                    format!("Loading transformer block {block_idx} layer {layer_idx}")
136                })?;
137                layers.push(layer);
138            }
139            transformer_groups.push(TransformerGroup { layers });
140        }
141
142        // Blocks 2,4,6: Upsample ConvTranspose1d [1024→1024, k=4, s=2]
143        let upsample_block_indices = [2, 4, 6];
144        let mut upsample_convs = Vec::with_capacity(3);
145        for &block_idx in &upsample_block_indices {
146            let conv_t = CausalConvTranspose1d::from_safetensors(
147                safetensors,
148                &format!("{prefix}.decoder_blocks.{block_idx}.conv"),
149                2, // stride (2x upsample)
150                device,
151            )
152            .with_context(|| format!("Loading upsample conv (block {block_idx})"))?;
153            upsample_convs.push(conv_t);
154        }
155
156        // Output Conv1d [1024→240, k=7, s=1]
157        let output_conv = CausalConv1d::from_safetensors(
158            safetensors,
159            &format!("{prefix}.output_proj.conv"),
160            1, // stride
161            device,
162        )
163        .context("Loading output conv")?;
164
165        // VQ codebook
166        let vq_codebook =
167            VqCodebook::from_safetensors(safetensors, device).context("Loading VQ codebook")?;
168
169        Ok(Self {
170            input_conv,
171            transformer_groups,
172            upsample_convs,
173            output_conv,
174            vq_codebook,
175        })
176    }
177
178    /// Decode quantized tokens into a 24 kHz waveform.
179    ///
180    /// # Arguments
181    /// * `semantic_indices` - Semantic token indices per frame, each in [0, 8191]. Shape: [N]
182    /// * `acoustic_indices` - Acoustic FSQ indices per frame [N, 36], each in [0, 20] as f32
183    ///
184    /// # Returns
185    /// Audio samples [1, total_samples] at 24 kHz.
186    pub fn decode(
187        &self,
188        semantic_indices: &[usize],
189        acoustic_indices: Tensor<B, 2>,
190    ) -> Tensor<B, 2> {
191        let n_frames = semantic_indices.len();
192
193        // Step 1: Dequantize tokens to continuous features
194        // Semantic: VQ codebook lookup → [N, 256]
195        let semantic_embeds = self.vq_codebook.dequantize(semantic_indices);
196
197        // Acoustic: FSQ dequantize indices → continuous values [N, 36]
198        let acoustic_values = Fsq::dequantize(acoustic_indices);
199
200        // Step 2: Concatenate semantic + acoustic → [N, input_channels]
201        let features = Tensor::cat(vec![semantic_embeds, acoustic_values], 1);
202        let input_channels = features.dims()[1];
203
204        // Step 3: Reshape to conv format [1, input_channels, N] (batch, channels, time)
205        let x = features
206            .reshape([1, n_frames, input_channels])
207            .swap_dims(1, 2);
208
209        // Step 4: Input projection [1, 292, N] → [1, 1024, N]
210        let x = self.input_conv.forward(x);
211
212        // Step 5: 4 rounds of (transformer group + upsample)
213        // Group 0 (SW=2) → upsample 2x
214        // Group 1 (SW=4) → upsample 2x
215        // Group 2 (SW=8) → upsample 2x
216        // Group 3 (SW=16) → no upsample (last group)
217        let mut x = x;
218        for (i, group) in self.transformer_groups.iter().enumerate() {
219            // Transformer expects [batch, seq, dim], conv uses [batch, dim, time]
220            let x_seq = x.swap_dims(1, 2); // [batch, time, channels]
221            let x_seq = group.forward(x_seq);
222            x = x_seq.swap_dims(1, 2); // back to [batch, channels, time]
223
224            // Upsample (only for first 3 groups)
225            if i < self.upsample_convs.len() {
226                x = self.upsample_convs[i].forward(x);
227            }
228        }
229
230        // Step 6: Output projection [1, 1024, T] → [1, 240, T]
231        let x = self.output_conv.forward(x);
232
233        // Step 7: Reshape patches to waveform
234        // x: [1, 240, T_patches] → [1, 240 * T_patches]
235        let [_batch, patch_size, n_patches] = x.dims();
236        let x = x.swap_dims(1, 2); // [1, T_patches, 240]
237        x.reshape([1, n_patches * patch_size])
238    }
239
240    /// Access the VQ codebook (for external use).
241    pub fn vq_codebook(&self) -> &VqCodebook<B> {
242        &self.vq_codebook
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use burn::backend::Wgpu;
250    use burn::tensor::TensorData;
251
252    type TestBackend = Wgpu;
253
254    /// Helper to create a small codec decoder for testing (no safetensors needed).
255    fn make_test_decoder(device: &<TestBackend as Backend>::Device) -> CodecDecoder<TestBackend> {
256        // Use tiny dimensions for fast testing
257        let input_channels = 6; // instead of 292
258        let dim = 8; // instead of 1024
259        let n_heads = 2;
260        let head_dim = 4; // dim = n_heads * head_dim
261        let ffn_dim = 32;
262        let output_patch_size = 4; // instead of 240
263
264        // Input conv: [input_channels → dim, k=3, s=1]
265        let g = Tensor::<TestBackend, 3>::ones([dim, 1, 1], device);
266        let v = Tensor::<TestBackend, 3>::ones([dim, input_channels, 3], device);
267        let input_conv = CausalConv1d::from_weight_norm(g, v, 1, device);
268
269        // 4 transformer groups
270        let windows = [2, 4, 8, 16];
271        let mut transformer_groups = Vec::new();
272        for &window in &windows {
273            let mut layers = Vec::new();
274            for _ in 0..2 {
275                use block::CodecAttention;
276                use burn::nn::LinearConfig;
277
278                let wq = LinearConfig::new(dim, dim).with_bias(false).init(device);
279                let wk = LinearConfig::new(dim, dim).with_bias(false).init(device);
280                let wv = LinearConfig::new(dim, dim).with_bias(false).init(device);
281                let wo = LinearConfig::new(dim, dim).with_bias(false).init(device);
282
283                let q_weight = Tensor::<TestBackend, 1>::ones([dim], device);
284                let k_weight = Tensor::<TestBackend, 1>::ones([dim], device);
285                let qk_norm = qk_norm::QkNorm::new(q_weight, k_weight, n_heads, head_dim);
286
287                let attention =
288                    CodecAttention::new(wq, wk, wv, wo, qk_norm, n_heads, head_dim, window);
289
290                let attn_scale = layer_scale::LayerScale::new(Tensor::ones([dim], device) * 0.01);
291                let ffn_scale = layer_scale::LayerScale::new(Tensor::ones([dim], device) * 0.01);
292
293                use crate::models::layers::{RmsNorm, SwiGLUConfig};
294                use burn::module::{Param, ParamId};
295
296                let attention_norm = RmsNorm {
297                    weight: burn::nn::RmsNorm {
298                        gamma: Param::initialized(
299                            ParamId::new(),
300                            Tensor::<TestBackend, 1>::ones([dim], device),
301                        ),
302                        epsilon: 1e-5,
303                    },
304                };
305                let ffn_norm = RmsNorm {
306                    weight: burn::nn::RmsNorm {
307                        gamma: Param::initialized(
308                            ParamId::new(),
309                            Tensor::<TestBackend, 1>::ones([dim], device),
310                        ),
311                        epsilon: 1e-5,
312                    },
313                };
314                let ffn = SwiGLUConfig::new(dim, ffn_dim)
315                    .with_bias(false)
316                    .init(device);
317
318                let layer = CodecTransformerLayer::new(
319                    attention_norm,
320                    attention,
321                    attn_scale,
322                    ffn_norm,
323                    ffn,
324                    ffn_scale,
325                );
326                layers.push(layer);
327            }
328            transformer_groups.push(TransformerGroup { layers });
329        }
330
331        // 3 upsample convs: [dim → dim, k=4, s=2]
332        let mut upsample_convs = Vec::new();
333        for _ in 0..3 {
334            let g = Tensor::<TestBackend, 3>::ones([dim, 1, 1], device);
335            let v = Tensor::<TestBackend, 3>::ones([dim, dim, 4], device);
336            upsample_convs.push(CausalConvTranspose1d::from_weight_norm(g, v, 2, device));
337        }
338
339        // Output conv: [dim → output_patch_size, k=7, s=1]
340        let g = Tensor::<TestBackend, 3>::ones([output_patch_size, 1, 1], device);
341        let v = Tensor::<TestBackend, 3>::ones([output_patch_size, dim, 7], device);
342        let output_conv = CausalConv1d::from_weight_norm(g, v, 1, device);
343
344        // VQ codebook: tiny
345        let embed_sum = Tensor::<TestBackend, 2>::ones([16, 4], device);
346        let usage = Tensor::<TestBackend, 1>::ones([16], device);
347        let cpu_norm =
348            VqCodebook::<TestBackend>::precompute_normalized(&vec![1.0; 64], &vec![1.0; 16], 16, 4);
349        let vq_codebook = VqCodebook::new(embed_sum, usage, cpu_norm);
350
351        CodecDecoder {
352            input_conv,
353            transformer_groups,
354            upsample_convs,
355            output_conv,
356            vq_codebook,
357        }
358    }
359
360    #[test]
361    fn test_codec_decoder_output_shape() {
362        let device = Default::default();
363        let decoder = make_test_decoder(&device);
364
365        let n_frames = 4;
366        let semantic_indices = vec![0usize; n_frames];
367        // acoustic indices: [N, 36] — but our test decoder uses input_channels=6,
368        // and semantic embed is 4, so acoustic is 6-4=2
369        let acoustic_indices = Tensor::<TestBackend, 2>::zeros([n_frames, 2], &device);
370
371        let output = decoder.decode(&semantic_indices, acoustic_indices);
372
373        // n_frames=4 → after 3 upsample (2x each) = 4*8 = 32 time patches
374        // output_patch_size=4, so total samples = 32 * 4 = 128
375        assert_eq!(output.dims()[0], 1);
376        assert_eq!(output.dims()[1], 32 * 4);
377    }
378
379    #[test]
380    fn test_codec_decoder_single_frame() {
381        let device = Default::default();
382        let decoder = make_test_decoder(&device);
383
384        let semantic_indices = vec![0usize; 1];
385        let acoustic_indices = Tensor::<TestBackend, 2>::zeros([1, 2], &device);
386
387        let output = decoder.decode(&semantic_indices, acoustic_indices);
388
389        // 1 frame → 1*8 = 8 time patches → 8*4 = 32 samples
390        assert_eq!(output.dims(), [1, 32]);
391    }
392
393    #[test]
394    fn test_codec_decoder_output_not_all_zeros() {
395        // Verify that the decoder produces non-trivial output
396        let device = Default::default();
397        let decoder = make_test_decoder(&device);
398
399        let n_frames = 2;
400        let semantic_indices = vec![0usize; n_frames];
401        let acoustic_indices = Tensor::<TestBackend, 2>::ones([n_frames, 2], &device) * 10.0; // mid-range FSQ
402
403        let output = decoder.decode(&semantic_indices, acoustic_indices);
404
405        let data = output.to_data();
406        let vals = data.as_slice::<f32>().unwrap();
407
408        // At least some samples should be non-zero
409        let has_nonzero = vals.iter().any(|&v| v.abs() > 1e-6);
410        assert!(has_nonzero, "Decoder output should not be all zeros");
411    }
412
413    #[test]
414    fn test_codec_decoder_upsampling_ratio() {
415        // Verify 8x total upsampling (3 stages of 2x each)
416        let device = Default::default();
417        let decoder = make_test_decoder(&device);
418
419        for n_frames in [2, 5, 10] {
420            let semantic_indices = vec![0usize; n_frames];
421            let acoustic_indices = Tensor::<TestBackend, 2>::zeros([n_frames, 2], &device);
422
423            let output = decoder.decode(&semantic_indices, acoustic_indices);
424            let total_samples = output.dims()[1];
425            let expected_patches = n_frames * 8; // 3x 2x upsample
426            let expected_samples = expected_patches * 4; // output_patch_size = 4
427
428            assert_eq!(
429                total_samples, expected_samples,
430                "For {} frames: expected {} samples, got {}",
431                n_frames, expected_samples, total_samples
432            );
433        }
434    }
435
436    #[test]
437    fn test_fsq_dequantize_integration() {
438        // Verify that FSQ dequantize produces expected range for codec input
439        let device: <TestBackend as Backend>::Device = Default::default();
440
441        // Indices at boundaries
442        let indices = Tensor::<TestBackend, 2>::from_data(
443            TensorData::new(vec![0.0f32, 10.0, 20.0, 0.0, 10.0, 20.0], [2, 3]),
444            &device,
445        );
446        let values = Fsq::dequantize(indices);
447
448        let data = values.to_data();
449        let vals = data.as_slice::<f32>().unwrap();
450
451        // idx 0 → -1.0, idx 10 → 0.0, idx 20 → 1.0
452        assert!((vals[0] - (-1.0)).abs() < 1e-5);
453        assert!((vals[1] - 0.0).abs() < 1e-5);
454        assert!((vals[2] - 1.0).abs() < 1e-5);
455    }
456}