1pub const SAMPLE_RATE: u32 = 24_000;
10
11pub trait Synthesizer: Send {
12 fn phonemize(&mut self, text: &str, voice: &str) -> String;
14
15 fn fits(&mut self, phonemes: &str) -> bool;
17
18 fn synth(&mut self, phonemes: &str, voice: &str, speed: f32) -> Result<Vec<f32>, String>;
20
21 fn sample_rate(&self) -> u32 {
22 SAMPLE_RATE
23 }
24
25 fn unload(&mut self);
27
28 fn is_loaded(&self) -> bool;
29}
30
31pub struct StubSynthesizer {
34 token_budget: usize,
35 loaded: bool,
36 pub synth_calls: usize,
37 pub unload_calls: usize,
38}
39
40impl StubSynthesizer {
41 pub fn new() -> Self {
42 Self::with_token_budget(509)
43 }
44
45 pub fn with_token_budget(token_budget: usize) -> Self {
46 StubSynthesizer { token_budget, loaded: false, synth_calls: 0, unload_calls: 0 }
47 }
48}
49
50impl Default for StubSynthesizer {
51 fn default() -> Self {
52 Self::new()
53 }
54}
55
56impl Synthesizer for StubSynthesizer {
57 fn phonemize(&mut self, text: &str, _voice: &str) -> String {
58 text.to_lowercase()
59 }
60
61 fn fits(&mut self, phonemes: &str) -> bool {
62 phonemes.chars().count() <= self.token_budget
63 }
64
65 fn synth(&mut self, phonemes: &str, _voice: &str, speed: f32) -> Result<Vec<f32>, String> {
66 self.loaded = true;
67 self.synth_calls += 1;
68 let per_char = SAMPLE_RATE as f32 * 0.08;
70 let n = ((phonemes.chars().count() as f32 * per_char) / speed.max(0.1)) as usize;
71 Ok(vec![0.0; n])
72 }
73
74 fn unload(&mut self) {
75 self.loaded = false;
76 self.unload_calls += 1;
77 }
78
79 fn is_loaded(&self) -> bool {
80 self.loaded
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn stub_produces_silence_proportional_to_length() {
90 let mut s = StubSynthesizer::new();
91 let short = s.synth("ab", "af_heart", 1.0).expect("synth");
92 let long = s.synth("abcdefgh", "af_heart", 1.0).expect("synth");
93 assert!(long.len() > short.len());
94 assert!(short.iter().all(|x| *x == 0.0), "stub must emit silence");
95 }
96
97 #[test]
98 fn stub_speed_shortens_output() {
99 let mut s = StubSynthesizer::new();
100 let normal = s.synth("abcdefgh", "af_heart", 1.0).expect("synth");
101 let fast = s.synth("abcdefgh", "af_heart", 2.0).expect("synth");
102 assert!(fast.len() < normal.len());
103 }
104
105 #[test]
106 fn stub_counts_calls() {
107 let mut s = StubSynthesizer::new();
108 let _ = s.synth("a", "af_heart", 1.0);
109 let _ = s.synth("b", "af_heart", 1.0);
110 assert_eq!(s.synth_calls, 2);
111 }
112
113 #[test]
114 fn stub_tracks_load_state() {
115 let mut s = StubSynthesizer::new();
116 assert!(!s.is_loaded(), "nothing is loaded before the first synth");
117 let _ = s.synth("a", "af_heart", 1.0);
118 assert!(s.is_loaded());
119 s.unload();
120 assert!(!s.is_loaded());
121 assert_eq!(s.unload_calls, 1);
122 }
123
124 #[test]
125 fn stub_fits_respects_the_configured_budget() {
126 let mut s = StubSynthesizer::with_token_budget(4);
127 assert!(s.fits("abcd"));
128 assert!(!s.fits("abcde"));
129 }
130
131 #[test]
132 fn stub_phonemize_is_identity_lowercased() {
133 let mut s = StubSynthesizer::new();
134 assert_eq!(s.phonemize("Hello There", "af_heart"), "hello there");
135 }
136}