1use 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
21pub const SPEECH_RATE: u32 = 24_000;
23pub 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;
48const END_CODE: u32 = 2016;
50const 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;
61const MOST_FRAMES: usize = 750;
63
64#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
66pub enum SpeechVoice {
67 #[default]
69 Female,
70 Male,
72}
73
74pub const KOKORO_SPEAKERS: &[&str] = &["pf_dora", "pm_alex", "pm_santa"];
76
77#[derive(Clone, Debug, PartialEq)]
79pub struct SpeechSettings {
80 pub language: String,
82 pub voice: SpeechVoice,
84 pub speaker: Option<String>,
88 pub temperature: f32,
90 pub top_k: usize,
92 pub repetition_penalty: f32,
94 pub repetition_window: usize,
96 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
139struct DepthDecoder {
141 into: QMatMul,
142 into_bias: Tensor,
143 mask_code: Tensor,
144 stack: Stack,
145 norm: RmsNorm,
146 tables: Vec<Tensor>,
148 head_norms: Vec<Tensor>,
150 frequencies: Vec<f32>,
151 eps: f32,
152 device: Device,
153}
154
155impl DepthDecoder {
156 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
214struct 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 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
263struct Codec {
265 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 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
311fn 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
322pub 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
339const KOKORO_PIECE: usize = 400;
342const KOKORO_SHORTEST: usize = 60;
344
345impl SpeechEngine {
346 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 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 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
404struct 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
458fn 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
479struct Lfm3Speech {
481 model: lfm3::ModelWeights,
482 depth: DepthDecoder,
483 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 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 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}