Skip to main content

ravenrs_ai/
speech.rs

1//! Speech, from Kokoro 82M or LFM3 Audio. In LFM3 Audio, following its model card, the
2//! model writes frames of codes, a small decoder finds each frame's codes, and the codec
3//! turns them into 24 kHz sound.
4
5use std::fs::File;
6
7use candle_core::quantized::{QMatMul, gguf_file};
8use candle_core::{Device, Module, Tensor};
9use candle_nn::ops::rms_norm;
10use candle_transformers::generation::{LogitsProcessor, Sampling};
11use candle_transformers::quantized_nn::RmsNorm;
12use tokenizers::Tokenizer;
13
14use crate::engine::EngineError;
15use crate::kokoro::{self, Kokoro, Noise};
16use crate::lfm3::{self, Heads, LayerKind, LayerNames, Reach, Stack, mask, rotation};
17use crate::phonemes::phonemes_pt_br;
18use crate::spectrum::{hann, inverse_stft};
19use crate::{LocalModel, ModelFamily, ModelFolder};
20
21/// How many samples of sound a second has.
22pub const SPEECH_RATE: u32 = 24_000;
23/// The languages the model speaks, with the English name its prompt asks for.
24pub const SPEECH_LANGUAGES: &[&str] = &[
25    "Portuguese",
26    "American English",
27    "British English",
28    "Spanish",
29    "French",
30    "German",
31    "Italian",
32    "Dutch",
33    "Polish",
34    "Swedish",
35    "Danish",
36    "Finnish",
37    "Greek",
38    "Turkish",
39    "Arabic",
40    "Hebrew",
41    "Japanese",
42    "Korean",
43    "Chinese",
44];
45
46const CODEBOOKS: usize = 12;
47const CODES: usize = 2017;
48/// A code that says the speech is over.
49const END_CODE: u32 = 2016;
50/// The passes that reveal a frame's codes.
51const PASSES: [usize; 3] = [4, 4, 4];
52const DEPTH_SIZE: usize = 1024;
53const DEPTH_HEAD_SIZE: usize = 32;
54const STACKED_FRAMES: usize = 6;
55const FFT_SIZE: usize = 1280;
56const HOP: usize = 320;
57const CODEC_WINDOW: usize = 30;
58const CODEC_HEAD_SIZE: usize = 64;
59const THETA: f32 = 1_000_000.0;
60const SEED: u64 = 299_792_458;
61/// How many frames a sentence may take at most: a minute.
62const MOST_FRAMES: usize = 750;
63
64/// Which of its voices the model speaks with.
65#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
66pub enum SpeechVoice {
67    /// A female voice.
68    #[default]
69    Female,
70    /// A male voice.
71    Male,
72}
73
74/// The voices of Kokoro 82M in Brazilian Portuguese, by the names its makers gave them.
75pub const KOKORO_SPEAKERS: &[&str] = &["pf_dora", "pm_alex", "pm_santa"];
76
77/// How to speak. The default is what the model card recommends, in Portuguese.
78#[derive(Clone, Debug, PartialEq)]
79pub struct SpeechSettings {
80    /// One of [`SPEECH_LANGUAGES`]. Kokoro speaks only Portuguese.
81    pub language: String,
82    /// The voice, when no speaker is named.
83    pub voice: SpeechVoice,
84    /// One of the model's own voices, such as `pm_santa` among [`KOKORO_SPEAKERS`]. Without
85    /// one, Kokoro speaks as `pf_dora` for [`SpeechVoice::Female`] and `pm_alex` for
86    /// [`SpeechVoice::Male`]. LFM3 Audio has none and leaves it out.
87    pub speaker: Option<String>,
88    /// How free each code is to vary; 0 always takes the likeliest.
89    pub temperature: f32,
90    /// How many codes it chooses among; 0 is all of them.
91    pub top_k: usize,
92    /// How much it avoids codes it has just used; 1 does not.
93    pub repetition_penalty: f32,
94    /// How many frames back it looks to avoid them.
95    pub repetition_window: usize,
96    /// Where its choices start: the same text with the same seed sounds the same.
97    pub seed: u64,
98}
99
100impl Default for SpeechSettings {
101    fn default() -> Self {
102        Self {
103            language: "Portuguese".to_owned(),
104            voice: SpeechVoice::Female,
105            speaker: None,
106            temperature: 1.0,
107            top_k: 128,
108            repetition_penalty: 1.1,
109            repetition_window: 4096,
110            seed: SEED,
111        }
112    }
113}
114
115impl SpeechSettings {
116    fn prompt(&self, text: &str) -> String {
117        let voice = match self.voice {
118            SpeechVoice::Female => "female",
119            SpeechVoice::Male => "male",
120        };
121        format!(
122            "<|startoftext|><|im_start|>system\nPerform TTS in {} using a {voice} voice.<|im_end|>\n<|im_start|>user\n{text}<|im_end|>\n<|im_start|>assistant\n<|audio_start|>",
123            self.language
124        )
125    }
126
127    fn sampling(&self) -> Sampling {
128        let temperature = f64::from(self.temperature);
129        if self.temperature <= 0.0 {
130            Sampling::ArgMax
131        } else if self.top_k == 0 {
132            Sampling::All { temperature }
133        } else {
134            Sampling::TopK { k: self.top_k, temperature }
135        }
136    }
137}
138
139/// Finds a frame's codes from what the model holds.
140struct DepthDecoder {
141    into: QMatMul,
142    into_bias: Tensor,
143    mask_code: Tensor,
144    stack: Stack,
145    norm: RmsNorm,
146    /// One table for each codebook, of `CODES` by `DEPTH_SIZE`.
147    tables: Vec<Tensor>,
148    /// One norm weight for each codebook.
149    head_norms: Vec<Tensor>,
150    frequencies: Vec<f32>,
151    eps: f32,
152    device: Device,
153}
154
155impl DepthDecoder {
156    /// What each codebook's slot reads as, with `known` codes and the rest hidden.
157    fn slots(
158        &self,
159        start: &Tensor,
160        codes: &[u32; CODEBOOKS],
161        known: &[bool; CODEBOOKS],
162    ) -> candle_core::Result<Tensor> {
163        let mut rows = Vec::with_capacity(CODEBOOKS);
164        for index in 0..CODEBOOKS {
165            let code = if known[index] {
166                self.tables[index].narrow(0, codes[index] as usize, 1)?.squeeze(0)?
167            } else {
168                self.mask_code.clone()
169            };
170            rows.push(code);
171        }
172        let added = Tensor::stack(&rows, 0)?.unsqueeze(0)?;
173        (start + added)?.contiguous()
174    }
175
176    fn frame(
177        &mut self,
178        hidden: &Tensor,
179        sample: &mut Sampler,
180        history: &mut [Vec<u32>],
181    ) -> candle_core::Result<[u32; CODEBOOKS]> {
182        let start = (self.into.forward(hidden)? + self.into_bias.unsqueeze(0)?)?
183            .reshape((1, CODEBOOKS, DEPTH_SIZE))?;
184        let rotation = rotation(&self.frequencies, 0, CODEBOOKS, &self.device)?;
185        let reach = Reach { rotation: &rotation, mask: None };
186        let order = sample.order();
187        let mut codes = [0_u32; CODEBOOKS];
188        let mut known = [false; CODEBOOKS];
189        let mut taken = 0;
190        for pass in PASSES {
191            let slots = self.slots(&start, &codes, &known)?;
192            let out = self.stack.forward(slots, reach, reach)?;
193            let out = self.norm.forward(&out)?;
194            for &index in &order[taken..taken + pass] {
195                let row = out.narrow(1, index, 1)?.squeeze(1)?;
196                let row = rms_norm(&row, &self.head_norms[index], self.eps)?;
197                let logits = row.matmul(&self.tables[index].t()?)?.squeeze(0)?;
198                codes[index] = sample.code(&logits, &history[index])?;
199            }
200            for &index in &order[taken..taken + pass] {
201                known[index] = true;
202            }
203            taken += pass;
204        }
205        if !codes.contains(&END_CODE) {
206            for (index, code) in codes.iter().enumerate() {
207                history[index].push(*code);
208            }
209        }
210        Ok(codes)
211    }
212}
213
214/// Chooses codes as the settings ask, the same way each time.
215struct Sampler {
216    logits: LogitsProcessor,
217    penalty: f32,
218    window: usize,
219    state: u64,
220}
221
222impl Sampler {
223    fn new(settings: &SpeechSettings) -> Self {
224        Self {
225            logits: LogitsProcessor::from_sampling(settings.seed, settings.sampling()),
226            penalty: settings.repetition_penalty,
227            window: settings.repetition_window,
228            state: settings.seed | 1,
229        }
230    }
231
232    /// The codebooks in a random order, for the passes to reveal.
233    fn order(&mut self) -> [usize; CODEBOOKS] {
234        let mut order: [usize; CODEBOOKS] = std::array::from_fn(|index| index);
235        for last in (1..CODEBOOKS).rev() {
236            self.state ^= self.state << 13;
237            self.state ^= self.state >> 7;
238            self.state ^= self.state << 17;
239            order.swap(last, (self.state % (last as u64 + 1)) as usize);
240        }
241        order
242    }
243
244    fn code(&mut self, logits: &Tensor, history: &[u32]) -> candle_core::Result<u32> {
245        let mut scores = logits.to_vec1::<f32>()?;
246        if (self.penalty - 1.0).abs() > f32::EPSILON {
247            let mut seen = [false; CODES];
248            let start = history.len().saturating_sub(self.window);
249            for &code in &history[start..] {
250                seen[code as usize] = true;
251            }
252            for (score, seen) in scores.iter_mut().zip(seen) {
253                if seen {
254                    *score = if *score < 0.0 { *score * self.penalty } else { *score / self.penalty };
255                }
256            }
257        }
258        let scores = Tensor::new(scores.as_slice(), logits.device())?;
259        self.logits.sample(&scores)
260    }
261}
262
263/// Turns frames of codes into sound.
264struct Codec {
265    /// `2016` by 4: what each code means.
266    values: Vec<f32>,
267    from_values: QMatMul,
268    from_values_bias: Tensor,
269    into: QMatMul,
270    into_bias: Tensor,
271    stack: Stack,
272    norm: RmsNorm,
273    out: QMatMul,
274    out_bias: Tensor,
275    frequencies: Vec<f32>,
276    device: Device,
277}
278
279impl Codec {
280    fn sound(&mut self, frames: &[[u32; CODEBOOKS]]) -> candle_core::Result<Vec<f32>> {
281        let mut values = Vec::with_capacity(frames.len() * CODEBOOKS * 4);
282        for frame in frames {
283            for code in frame {
284                let start = *code as usize * 4;
285                values.extend_from_slice(&self.values[start..start + 4]);
286            }
287        }
288        let values = Tensor::from_vec(values, (1, frames.len(), CODEBOOKS * 4), &self.device)?;
289        let latents = (self.from_values.forward(&values)?.broadcast_add(&self.from_values_bias))?;
290        let embeds = self.into.forward(&latents)?.broadcast_add(&self.into_bias)?;
291        // Each frame of codes stands for six of the spectrum.
292        let (batch, count, size) = embeds.dims3()?;
293        let stacked = embeds.unsqueeze(2)?.repeat((1, 1, STACKED_FRAMES, 1))?.reshape((
294            batch,
295            count * STACKED_FRAMES,
296            size,
297        ))?;
298        let tokens = count * STACKED_FRAMES;
299        let rotation = rotation(&self.frequencies, 0, tokens, &self.device)?;
300        let window = mask(tokens, CODEC_WINDOW, &self.device)?;
301        let reach = Reach { rotation: &rotation, mask: Some(&window) };
302        self.stack.clear();
303        let hidden = self.stack.forward(stacked, reach, reach)?;
304        let hidden = self.norm.forward(&hidden)?;
305        let spectrum = self.out.forward(&hidden)?.broadcast_add(&self.out_bias)?;
306        let spectrum = spectrum.squeeze(0)?.to_vec2::<f32>()?;
307        Ok(from_spectrum(&spectrum))
308    }
309}
310
311/// Sound from frames of half log magnitudes and half angles, as the codec writes them.
312fn from_spectrum(frames: &[Vec<f32>]) -> Vec<f32> {
313    let bins = FFT_SIZE / 2 + 1;
314    let magnitudes: Vec<Vec<f32>> =
315        frames.iter().map(|frame| frame[..bins].iter().map(|value| value.exp()).collect()).collect();
316    let angles: Vec<Vec<f32>> = frames.iter().map(|frame| frame[bins..2 * bins].to_vec()).collect();
317    let sound = inverse_stft(&magnitudes, &angles, FFT_SIZE, HOP, &hann(FFT_SIZE));
318    let trim = (FFT_SIZE - HOP) / 2;
319    sound[trim..sound.len() - trim].to_vec()
320}
321
322/// The voice in memory: Kokoro 82M or LFM3 Audio, by the model's family. Loading takes a
323/// while and speaking blocks, so keep it away from the UI thread.
324pub struct SpeechEngine {
325    inner: Inner,
326}
327
328enum Inner {
329    Kokoro(Box<KokoroSpeech>),
330    Lfm3(Box<Lfm3Speech>),
331}
332
333impl std::fmt::Debug for SpeechEngine {
334    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
335        f.debug_struct("SpeechEngine").finish_non_exhaustive()
336    }
337}
338
339/// The most phonemes Kokoro says at once: a little under what it can, since it rushes at
340/// the end of its longest.
341const KOKORO_PIECE: usize = 400;
342/// Fewer than this and Kokoro says them weakly: short sentences go together.
343const KOKORO_SHORTEST: usize = 60;
344
345impl SpeechEngine {
346    /// Loads `model` from `folder`. Kokoro needs `model.pth`, `config.json` and its voices;
347    /// LFM3 Audio needs `model.gguf`, `projector.gguf` and `tokenizer.json`.
348    ///
349    /// # Errors
350    ///
351    /// When its files are missing or cannot be read.
352    pub fn load(folder: &ModelFolder, model: &LocalModel) -> Result<Self, EngineError> {
353        let inner = if model.family == ModelFamily::Kokoro {
354            Inner::Kokoro(Box::new(KokoroSpeech::load(folder, model)?))
355        } else {
356            Inner::Lfm3(Box::new(Lfm3Speech::load(folder, model)?))
357        };
358        Ok(Self { inner })
359    }
360
361    /// Says `text` as the settings ask. `go_on` hears how much is ready and returning
362    /// false stops it with what it has.
363    ///
364    /// # Errors
365    ///
366    /// When it stops in the middle, or Kokoro is asked for a language other than
367    /// Portuguese or a speaker it does not have.
368    pub fn speak(
369        &mut self,
370        text: &str,
371        settings: &SpeechSettings,
372        go_on: &mut dyn FnMut(usize) -> bool,
373    ) -> Result<Vec<f32>, EngineError> {
374        match &mut self.inner {
375            Inner::Kokoro(engine) => engine.speak(text, settings, go_on),
376            Inner::Lfm3(engine) => engine.speak(text, settings, go_on),
377        }
378    }
379
380    /// How `text` is best said, a piece at a time: by sentences for LFM3 Audio, and for
381    /// Kokoro sentences joined while they are short, since it says short ones weakly.
382    pub fn pieces(&self, text: &str) -> Vec<String> {
383        let sentences = crate::sentences(text);
384        if !matches!(self.inner, Inner::Kokoro(_)) {
385            return sentences;
386        }
387        let mut pieces: Vec<String> = Vec::new();
388        for sentence in sentences {
389            match pieces.last_mut() {
390                Some(last)
391                    if last.chars().count() < KOKORO_SHORTEST
392                        || last.chars().count() + sentence.chars().count() < KOKORO_PIECE / 2 =>
393                {
394                    last.push(' ');
395                    last.push_str(&sentence);
396                }
397                _ => pieces.push(sentence),
398            }
399        }
400        pieces
401    }
402}
403
404/// Kokoro 82M with its Brazilian voices.
405struct KokoroSpeech {
406    model: Kokoro,
407    voices: Vec<(&'static str, kokoro::Voice)>,
408}
409
410impl KokoroSpeech {
411    fn load(folder: &ModelFolder, model: &LocalModel) -> Result<Self, EngineError> {
412        let load = |why: String| EngineError::Load(why);
413        let named = |name: &str| {
414            let file = model.files.iter().find(|file| file.name == name);
415            file.map(|file| folder.path_of(model, file)).ok_or_else(|| load(format!("no {name}")))
416        };
417        let kokoro = Kokoro::load(&named("model.pth")?, &named("config.json")?).map_err(load)?;
418        let mut voices = Vec::new();
419        for speaker in KOKORO_SPEAKERS {
420            let voice = kokoro::Voice::load(&named(&format!("{speaker}.pt"))?).map_err(load)?;
421            voices.push((*speaker, voice));
422        }
423        Ok(Self { model: kokoro, voices })
424    }
425
426    fn speak(
427        &mut self,
428        text: &str,
429        settings: &SpeechSettings,
430        go_on: &mut dyn FnMut(usize) -> bool,
431    ) -> Result<Vec<f32>, EngineError> {
432        let run = |why: String| EngineError::Run(why);
433        if settings.language != "Portuguese" {
434            return Err(run(format!("Kokoro speaks Portuguese, not {}", settings.language)));
435        }
436        let wanted = settings.speaker.as_deref().unwrap_or(match settings.voice {
437            SpeechVoice::Female => "pf_dora",
438            SpeechVoice::Male => "pm_alex",
439        });
440        let voice = self
441            .voices
442            .iter()
443            .find(|(name, _)| *name == wanted)
444            .map(|(_, voice)| voice)
445            .ok_or_else(|| run(format!("no voice {wanted}")))?;
446        let mut noise = Noise::new(settings.seed);
447        let mut sound = Vec::new();
448        for (done, piece) in phoneme_pieces(&phonemes_pt_br(text)).iter().enumerate() {
449            if !go_on(done) {
450                break;
451            }
452            sound.extend(self.model.speak(piece, voice, 1.0, &mut noise).map_err(|e| run(e.to_string()))?);
453        }
454        Ok(sound)
455    }
456}
457
458/// Phonemes cut where a sentence or clause ends, each piece at most [`KOKORO_PIECE`].
459fn phoneme_pieces(phonemes: &str) -> Vec<String> {
460    let mut pieces = Vec::new();
461    let mut piece = String::new();
462    for word in phonemes.split(' ') {
463        let longer = piece.chars().count() + word.chars().count() + 1;
464        let pause = piece.ends_with(['.', '!', '?', ';', ':', ',']);
465        if !piece.is_empty() && (longer > kokoro::MOST_PHONEMES || (longer > KOKORO_PIECE && pause)) {
466            pieces.push(std::mem::take(&mut piece));
467        }
468        if !piece.is_empty() {
469            piece.push(' ');
470        }
471        piece.push_str(word);
472    }
473    if !piece.is_empty() {
474        pieces.push(piece);
475    }
476    pieces
477}
478
479/// LFM3 Audio in memory.
480struct Lfm3Speech {
481    model: lfm3::ModelWeights,
482    depth: DepthDecoder,
483    /// One row for each code of each codebook, for feeding a frame back.
484    frame_embeds: Tensor,
485    codec: Codec,
486    tokenizer: Tokenizer,
487    device: Device,
488}
489
490impl Lfm3Speech {
491    fn load(folder: &ModelFolder, model: &LocalModel) -> Result<Self, EngineError> {
492        let load = |why: String| EngineError::Load(why);
493        let named = |name: &str| {
494            let file = model.files.iter().find(|file| file.name == name);
495            file.map(|file| folder.path_of(model, file)).ok_or_else(|| load(format!("no {name}")))
496        };
497        let device = Device::Cpu;
498        let mut file = File::open(named("model.gguf")?).map_err(|e| load(e.to_string()))?;
499        let content = gguf_file::Content::read(&mut file).map_err(|e| load(e.to_string()))?;
500        let weights =
501            lfm3::ModelWeights::from_gguf(&content, &mut file, &device).map_err(|e| load(e.to_string()))?;
502        let mut file = File::open(named("projector.gguf")?).map_err(|e| load(e.to_string()))?;
503        let content = gguf_file::Content::read(&mut file).map_err(|e| load(e.to_string()))?;
504        let (depth, frame_embeds, codec) =
505            parts_of(&content, &mut file, &device).map_err(|e| load(e.to_string()))?;
506        let tokenizer = Tokenizer::from_file(named("tokenizer.json")?).map_err(|e| load(e.to_string()))?;
507        Ok(Self { model: weights, depth, frame_embeds, codec, tokenizer, device })
508    }
509
510    /// `go_on` hears how many frames are ready, each of 80 ms.
511    fn speak(
512        &mut self,
513        text: &str,
514        settings: &SpeechSettings,
515        go_on: &mut dyn FnMut(usize) -> bool,
516    ) -> Result<Vec<f32>, EngineError> {
517        let run = |why: String| EngineError::Run(why);
518        let encoding = self.tokenizer.encode(settings.prompt(text), false).map_err(|e| run(e.to_string()))?;
519        let prompt = encoding.get_ids();
520        self.model.clear_kv_cache();
521        let frames = self.frames(prompt, text, settings, go_on).map_err(|e| run(e.to_string()))?;
522        if frames.is_empty() {
523            return Ok(Vec::new());
524        }
525        self.codec.sound(&frames).map_err(|e| run(e.to_string()))
526    }
527
528    fn frames(
529        &mut self,
530        prompt: &[u32],
531        text: &str,
532        settings: &SpeechSettings,
533        go_on: &mut dyn FnMut(usize) -> bool,
534    ) -> candle_core::Result<Vec<[u32; CODEBOOKS]>> {
535        let input = Tensor::new(prompt, &self.device)?.unsqueeze(0)?;
536        let embeds = self.model.embed(&input)?;
537        let mut hidden = self.model.last_hidden(&embeds, 0)?;
538        let mut offset = prompt.len();
539        let most = (30 + text.chars().count() * 2).min(MOST_FRAMES);
540        let mut sampler = Sampler::new(settings);
541        let mut history = vec![Vec::new(); CODEBOOKS];
542        let mut frames = Vec::new();
543        while frames.len() < most {
544            let codes = self.depth.frame(&hidden, &mut sampler, &mut history)?;
545            if codes.contains(&END_CODE) {
546                break;
547            }
548            frames.push(codes);
549            if !go_on(frames.len()) {
550                break;
551            }
552            let rows: Vec<u32> =
553                codes.iter().enumerate().map(|(index, code)| *code + (index * CODES) as u32).collect();
554            let rows = Tensor::new(rows.as_slice(), &self.device)?;
555            let sum = self.frame_embeds.index_select(&rows, 0)?.sum_keepdim(0)?.unsqueeze(0)?;
556            hidden = self.model.last_hidden(&sum, offset)?;
557            offset += 1;
558        }
559        Ok(frames)
560    }
561}
562
563fn parts_of<R: std::io::Seek + std::io::Read>(
564    content: &gguf_file::Content,
565    reader: &mut R,
566    device: &Device,
567) -> candle_core::Result<(DepthDecoder, Tensor, Codec)> {
568    const EPS: f64 = 1e-5;
569    let frequencies = |head_size: usize| -> Vec<f32> {
570        (0..head_size).step_by(2).map(|place| 1.0 / THETA.powf(place as f32 / head_size as f32)).collect()
571    };
572    let depth_names = LayerNames {
573        attention_out: "attn_out",
574        operator_norm: "ln1",
575        after_operator_norm: None,
576        feed_norm: "ln2",
577        after_feed_norm: None,
578        conv_in: "",
579        conv_out: "",
580        conv: "",
581    };
582    let depth_sizes =
583        Heads { heads: 32, key_heads: 8, head_size: DEPTH_HEAD_SIZE, keep: 0, remembers: false, eps: EPS };
584    let depth_stack = Stack::from_gguf(
585        content,
586        reader,
587        device,
588        "a.gen.code.blk",
589        &depth_names,
590        depth_sizes,
591        &[LayerKind::Full; 3],
592    )?;
593    let codec_names = LayerNames {
594        attention_out: "attn_out",
595        operator_norm: "ln1",
596        after_operator_norm: Some("attn_post_norm"),
597        feed_norm: "ln2",
598        after_feed_norm: Some("ffn_post_norm"),
599        conv_in: "conv_pw1",
600        conv_out: "conv_pw2",
601        conv: "conv_dw",
602    };
603    let codec_sizes =
604        Heads { heads: 8, key_heads: 2, head_size: CODEC_HEAD_SIZE, keep: 0, remembers: false, eps: EPS };
605    let kinds: Vec<LayerKind> = (0..8)
606        .map(|layer| if matches!(layer, 2 | 4 | 6) { LayerKind::Window } else { LayerKind::Conv })
607        .collect();
608    let codec_stack =
609        Stack::from_gguf(content, reader, device, "a.gen.wav.tfm.blk", &codec_names, codec_sizes, &kinds)?;
610
611    let mut tensor = |name: &str| content.tensor(reader, name, device);
612    let mut plain = |name: &str| tensor(name)?.dequantize(device);
613    let tables = plain("a.gen.code.embd.weight")?;
614    let tables = (0..CODEBOOKS)
615        .map(|index| tables.narrow(0, index, 1)?.squeeze(0)?.contiguous())
616        .collect::<candle_core::Result<Vec<_>>>()?;
617    let head_norms = plain("a.gen.code.head_norm.weight")?;
618    let head_norms = (0..CODEBOOKS)
619        .map(|index| head_norms.narrow(0, index, 1)?.squeeze(0)?.contiguous())
620        .collect::<candle_core::Result<Vec<_>>>()?;
621    let mask_code = plain("a.gen.code.mask_embd")?;
622    let frame_embeds = plain("a.gen.code.out_embd.weight")?;
623    let into_bias = plain("a.gen.code.proj_in.bias")?;
624    let values = plain("a.gen.wav.quant.first.codebook.weight")?.flatten_all()?.to_vec1::<f32>()?;
625    let from_values_bias = plain("a.gen.wav.quant.first.out_proj.bias")?;
626    let codec_into_bias = plain("a.gen.wav.tfm.in_proj.bias")?;
627    let out_bias = plain("a.gen.wav.tfm.out_proj.bias")?;
628    let mut tensor = |name: &str| content.tensor(reader, name, device);
629    let depth = DepthDecoder {
630        into: QMatMul::from_qtensor(tensor("a.gen.code.proj_in.weight")?)?,
631        into_bias,
632        mask_code,
633        stack: depth_stack,
634        norm: RmsNorm::from_qtensor(tensor("a.gen.code.output_norm.weight")?, EPS)?,
635        tables,
636        head_norms,
637        frequencies: frequencies(DEPTH_HEAD_SIZE),
638        eps: EPS as f32,
639        device: device.clone(),
640    };
641    let codec = Codec {
642        values,
643        from_values: QMatMul::from_qtensor(tensor("a.gen.wav.quant.first.out_proj.weight")?)?,
644        from_values_bias,
645        into: QMatMul::from_qtensor(tensor("a.gen.wav.tfm.in_proj.weight")?)?,
646        into_bias: codec_into_bias,
647        stack: codec_stack,
648        norm: RmsNorm::from_qtensor(tensor("a.gen.wav.tfm.output_norm.weight")?, EPS)?,
649        out: QMatMul::from_qtensor(tensor("a.gen.wav.tfm.out_proj.weight")?)?,
650        out_bias,
651        frequencies: frequencies(CODEC_HEAD_SIZE),
652        device: device.clone(),
653    };
654    Ok((depth, frame_embeds, codec))
655}
656
657#[cfg(test)]
658mod tests {
659    use super::*;
660
661    #[test]
662    fn long_phonemes_are_cut_where_a_sentence_ends() {
663        let sentence = "ˈabræ ʊz ˌaʒˈusʧys. ".repeat(40);
664        let pieces = phoneme_pieces(sentence.trim());
665        assert!(pieces.len() > 1);
666        assert!(pieces.iter().all(|piece| piece.chars().count() <= kokoro::MOST_PHONEMES));
667        assert!(pieces[..pieces.len() - 1].iter().all(|piece| piece.ends_with('.')));
668        assert_eq!(pieces.join(" "), sentence.trim());
669    }
670
671    #[test]
672    fn the_prompt_is_the_one_on_the_model_card() {
673        let prompt = SpeechSettings::default().prompt("Oi");
674        assert!(prompt.contains("system\nPerform TTS in Portuguese using a female voice.<|im_end|>"));
675        assert!(prompt.ends_with("<|im_start|>assistant\n<|audio_start|>"));
676        let male = SpeechSettings { voice: SpeechVoice::Male, ..SpeechSettings::default() };
677        assert!(male.prompt("Oi").contains("using a male voice"));
678    }
679
680    #[test]
681    fn the_settings_start_as_the_model_card_says() {
682        let settings = SpeechSettings::default();
683        assert_eq!(settings.temperature, 1.0);
684        assert_eq!(settings.top_k, 128);
685        assert_eq!(settings.repetition_penalty, 1.1);
686        assert_eq!(settings.repetition_window, 4096);
687    }
688
689    #[test]
690    fn the_order_has_each_codebook_once() {
691        let mut sampler = Sampler::new(&SpeechSettings::default());
692        let mut order = sampler.order();
693        order.sort_unstable();
694        assert_eq!(order, std::array::from_fn(|index| index));
695    }
696
697    #[test]
698    fn a_silent_spectrum_is_silence_of_the_right_length() {
699        let bins = FFT_SIZE / 2 + 1;
700        let frames = vec![vec![-40.0_f32; bins * 2]; 12];
701        let sound = from_spectrum(&frames);
702        // Two frames of codes, of 1920 samples each.
703        assert_eq!(sound.len(), 3840);
704        assert!(sound.iter().all(|sample| sample.abs() < 1e-3));
705    }
706
707    #[test]
708    fn repeated_codes_lose_points() {
709        let settings = SpeechSettings { temperature: 0.0, ..SpeechSettings::default() };
710        let mut sampler = Sampler::new(&settings);
711        let logits = Tensor::new(&[1.05_f32, 1.0, 0.0], &Device::Cpu).expect("logits");
712        assert_eq!(sampler.code(&logits_padded(&logits), &[0]).expect("code"), 1);
713    }
714
715    fn logits_padded(short: &Tensor) -> Tensor {
716        let mut all = short.to_vec1::<f32>().expect("values");
717        all.resize(CODES, -10.0);
718        Tensor::new(all.as_slice(), &Device::Cpu).expect("logits")
719    }
720}