1use crate::audio::WHISPER_SAMPLE_RATE;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct PartialWindowPolicy {
17 pub min_partial_samples: usize,
19 pub window_samples: usize,
21 pub interval_nanos: u64,
23 pub min_rms_bits: u32, }
26
27impl PartialWindowPolicy {
28 pub fn dictation() -> Self {
30 Self {
31 min_partial_samples: WHISPER_SAMPLE_RATE as usize, window_samples: WHISPER_SAMPLE_RATE as usize * 15, interval_nanos: 1_200_000_000, min_rms_bits: 500, }
36 }
37
38 pub fn min_rms(&self) -> f32 {
39 self.min_rms_bits as f32 / 1_000_000.0
40 }
41
42 pub fn with_min_rms(mut self, rms: f32) -> Self {
43 self.min_rms_bits = (rms.clamp(0.0, 1.0) * 1_000_000.0).round() as u32;
44 self
45 }
46
47 pub fn min_partial_secs(&self) -> f64 {
48 self.min_partial_samples as f64 / f64::from(WHISPER_SAMPLE_RATE)
49 }
50
51 pub fn window_secs(&self) -> f64 {
52 self.window_samples as f64 / f64::from(WHISPER_SAMPLE_RATE)
53 }
54
55 pub fn can_run_partial(&self, sample_count: usize) -> bool {
57 sample_count >= self.min_partial_samples
58 }
59
60 pub fn slice_for_partial<'a>(&self, samples: &'a [f32]) -> &'a [f32] {
62 if samples.len() > self.window_samples {
63 &samples[samples.len() - self.window_samples..]
64 } else {
65 samples
66 }
67 }
68
69 pub fn passes_energy_gate(&self, samples: &[f32]) -> bool {
71 if samples.is_empty() {
72 return false;
73 }
74 let sum: f32 = samples.iter().map(|s| s * s).sum();
75 let rms = (sum / samples.len() as f32).sqrt();
76 rms >= self.min_rms()
77 }
78
79 pub fn should_run_partial(&self, samples: &[f32]) -> bool {
81 self.can_run_partial(samples.len()) && self.passes_energy_gate(samples)
82 }
83}
84
85#[derive(Debug, Clone)]
87pub struct PartialClock {
88 policy: PartialWindowPolicy,
89 last_partial_at: Option<std::time::Instant>,
90}
91
92impl PartialClock {
93 pub fn new(policy: PartialWindowPolicy) -> Self {
94 Self {
95 policy,
96 last_partial_at: None,
97 }
98 }
99
100 pub fn policy(&self) -> &PartialWindowPolicy {
101 &self.policy
102 }
103
104 pub fn interval_elapsed(&self) -> bool {
106 match self.last_partial_at {
107 None => true,
108 Some(t) => t.elapsed().as_nanos() as u64 >= self.policy.interval_nanos,
109 }
110 }
111
112 pub fn mark(&mut self) {
114 self.last_partial_at = Some(std::time::Instant::now());
115 }
116
117 pub fn ready(&self, samples: &[f32]) -> bool {
120 if !self.interval_elapsed() || !self.policy.can_run_partial(samples.len()) {
121 return false;
122 }
123 let window = self.policy.slice_for_partial(samples);
124 self.policy.passes_energy_gate(window)
125 }
126
127 pub fn take_partial_slice<'a>(&mut self, samples: &'a [f32]) -> Option<&'a [f32]> {
129 if !self.ready(samples) {
130 return None;
131 }
132 self.mark();
133 Some(self.policy.slice_for_partial(samples))
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn slice_takes_tail() {
143 let p = PartialWindowPolicy {
144 window_samples: 4,
145 min_partial_samples: 2,
146 interval_nanos: 0,
147 min_rms_bits: 0,
148 };
149 let s = [1., 2., 3., 4., 5., 6.];
150 assert_eq!(p.slice_for_partial(&s), &[3., 4., 5., 6.]);
151 }
152
153 #[test]
154 fn energy_gate() {
155 let p = PartialWindowPolicy::dictation().with_min_rms(0.01);
156 assert!(!p.passes_energy_gate(&[0.0; 1600]));
157 assert!(p.passes_energy_gate(&[0.5; 1600]));
158 }
159
160 #[test]
161 fn clock_interval() {
162 let mut c = PartialClock::new(PartialWindowPolicy {
163 min_partial_samples: 1,
164 window_samples: 100,
165 interval_nanos: 10_000_000_000, min_rms_bits: 0,
167 });
168 let s = [0.1; 10];
169 assert!(c.ready(&s));
170 c.mark();
171 assert!(!c.ready(&s));
172 }
173}