pub const SAMPLE_RATE: u32 = 24_000;
pub trait Synthesizer: Send {
fn phonemize(&mut self, text: &str, voice: &str) -> String;
fn fits(&mut self, phonemes: &str) -> bool;
fn synth(&mut self, phonemes: &str, voice: &str, speed: f32) -> Result<Vec<f32>, String>;
fn sample_rate(&self) -> u32 {
SAMPLE_RATE
}
fn unload(&mut self);
fn is_loaded(&self) -> bool;
}
pub struct StubSynthesizer {
token_budget: usize,
loaded: bool,
pub synth_calls: usize,
pub unload_calls: usize,
}
impl StubSynthesizer {
pub fn new() -> Self {
Self::with_token_budget(509)
}
pub fn with_token_budget(token_budget: usize) -> Self {
StubSynthesizer { token_budget, loaded: false, synth_calls: 0, unload_calls: 0 }
}
}
impl Default for StubSynthesizer {
fn default() -> Self {
Self::new()
}
}
impl Synthesizer for StubSynthesizer {
fn phonemize(&mut self, text: &str, _voice: &str) -> String {
text.to_lowercase()
}
fn fits(&mut self, phonemes: &str) -> bool {
phonemes.chars().count() <= self.token_budget
}
fn synth(&mut self, phonemes: &str, _voice: &str, speed: f32) -> Result<Vec<f32>, String> {
self.loaded = true;
self.synth_calls += 1;
let per_char = SAMPLE_RATE as f32 * 0.08;
let n = ((phonemes.chars().count() as f32 * per_char) / speed.max(0.1)) as usize;
Ok(vec![0.0; n])
}
fn unload(&mut self) {
self.loaded = false;
self.unload_calls += 1;
}
fn is_loaded(&self) -> bool {
self.loaded
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stub_produces_silence_proportional_to_length() {
let mut s = StubSynthesizer::new();
let short = s.synth("ab", "af_heart", 1.0).expect("synth");
let long = s.synth("abcdefgh", "af_heart", 1.0).expect("synth");
assert!(long.len() > short.len());
assert!(short.iter().all(|x| *x == 0.0), "stub must emit silence");
}
#[test]
fn stub_speed_shortens_output() {
let mut s = StubSynthesizer::new();
let normal = s.synth("abcdefgh", "af_heart", 1.0).expect("synth");
let fast = s.synth("abcdefgh", "af_heart", 2.0).expect("synth");
assert!(fast.len() < normal.len());
}
#[test]
fn stub_counts_calls() {
let mut s = StubSynthesizer::new();
let _ = s.synth("a", "af_heart", 1.0);
let _ = s.synth("b", "af_heart", 1.0);
assert_eq!(s.synth_calls, 2);
}
#[test]
fn stub_tracks_load_state() {
let mut s = StubSynthesizer::new();
assert!(!s.is_loaded(), "nothing is loaded before the first synth");
let _ = s.synth("a", "af_heart", 1.0);
assert!(s.is_loaded());
s.unload();
assert!(!s.is_loaded());
assert_eq!(s.unload_calls, 1);
}
#[test]
fn stub_fits_respects_the_configured_budget() {
let mut s = StubSynthesizer::with_token_budget(4);
assert!(s.fits("abcd"));
assert!(!s.fits("abcde"));
}
#[test]
fn stub_phonemize_is_identity_lowercased() {
let mut s = StubSynthesizer::new();
assert_eq!(s.phonemize("Hello There", "af_heart"), "hello there");
}
}