aria_kit/speaker/
enrollment.rs1use super::{Engine, Profile, Role, SpeakerError, SpeakerName, normalized};
7
8pub const MIN_SAMPLES: usize = 2;
11const MIN_CONSISTENCY: f32 = 0.40;
16
17pub struct Enrollment<E> {
19 embedder: E,
20 samples: Vec<Vec<f32>>,
21}
22
23impl<E: Engine> Enrollment<E> {
24 pub fn new(embedder: E) -> Self {
25 Self {
26 embedder,
27 samples: Vec::new(),
28 }
29 }
30
31 pub fn add(&mut self, window: &[f32]) -> Result<(), EnrollmentError> {
37 match self.embedder.embed(window)? {
38 Some(embedding) => {
39 self.samples.push(embedding);
40 Ok(())
41 }
42 None => Err(EnrollmentError::NoVoice),
43 }
44 }
45
46 pub fn finish(self, name: SpeakerName, role: Role) -> Result<Enrolled, EnrollmentError> {
48 if self.samples.len() < MIN_SAMPLES {
49 return Err(EnrollmentError::TooFewSamples {
50 samples: self.samples.len(),
51 minimum: MIN_SAMPLES,
52 });
53 }
54
55 let consistency = consistency(&self.samples);
56 if consistency < MIN_CONSISTENCY {
57 return Err(EnrollmentError::Inconsistent {
58 consistency,
59 minimum: MIN_CONSISTENCY,
60 });
61 }
62
63 let vector = centroid(&self.samples).ok_or(EnrollmentError::NoDirection)?;
64 Ok(Enrolled {
65 profile: Profile {
66 name,
67 role,
68 engine: E::NAME.to_owned(),
69 enrolled_at: std::time::SystemTime::now(),
70 vector,
71 },
72 consistency,
73 })
74 }
75}
76
77pub struct Enrolled {
80 pub profile: Profile,
81 pub consistency: f32,
83}
84
85pub fn centroid(samples: &[Vec<f32>]) -> Option<Vec<f32>> {
88 let width = samples.first()?.len();
89 let mut sum = vec![0.0; width];
90 for sample in samples {
91 for (total, value) in sum.iter_mut().zip(sample) {
92 *total += value;
93 }
94 }
95 for total in &mut sum {
96 *total /= samples.len() as f32;
97 }
98 normalized(sum)
99}
100
101pub fn consistency(samples: &[Vec<f32>]) -> f32 {
103 let mut total = 0.0;
104 let mut pairs = 0;
105 for (at, sample) in samples.iter().enumerate() {
106 for other in &samples[at + 1..] {
107 total += sample.iter().zip(other).map(|(a, b)| a * b).sum::<f32>();
108 pairs += 1;
109 }
110 }
111 total / pairs as f32
112}
113
114#[derive(Debug, thiserror::Error)]
115pub enum EnrollmentError {
116 #[error("that sample carried no voice")]
117 NoVoice,
118
119 #[error("enrolling needs {minimum} samples; {samples} were recorded")]
120 TooFewSamples { samples: usize, minimum: usize },
121
122 #[error(
123 "the samples sound like different voices (consistency {consistency:.2}, needs {minimum:.2})"
124 )]
125 Inconsistent { consistency: f32, minimum: f32 },
126
127 #[error("the samples average to nothing")]
128 NoDirection,
129
130 #[error(transparent)]
131 Engine(#[from] SpeakerError),
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::speaker::Embedder;
138 use std::path::PathBuf;
139
140 struct Replay {
142 embeddings: Vec<Option<Vec<f32>>>,
143 }
144
145 impl Embedder for Replay {
146 fn embed(&mut self, _window: &[f32]) -> Result<Option<Vec<f32>>, SpeakerError> {
147 Ok(self.embeddings.remove(0))
148 }
149
150 fn dimension(&self) -> usize {
151 2
152 }
153 }
154
155 impl Engine for Replay {
156 const NAME: &'static str = "replay";
157
158 type Config = ();
159
160 fn check_from(_config: &Self::Config) -> Result<PathBuf, SpeakerError> {
161 Ok(PathBuf::new())
162 }
163
164 fn load_from(_config: Self::Config) -> Result<Self, SpeakerError> {
165 Ok(Self {
166 embeddings: Vec::new(),
167 })
168 }
169 }
170
171 fn enrolling(embeddings: Vec<Option<Vec<f32>>>) -> Enrollment<Replay> {
172 Enrollment::new(Replay { embeddings })
173 }
174
175 fn ada() -> SpeakerName {
176 SpeakerName::new("ada").expect("the name is usable")
177 }
178
179 #[test]
180 fn one_voice_averages_to_itself_and_scores_perfectly() {
181 let voice = vec![0.6, 0.8];
182 let mut enrollment = enrolling(vec![Some(voice.clone()), Some(voice.clone())]);
183 enrollment.add(&[]).expect("the sample carries a voice");
184 enrollment.add(&[]).expect("the sample carries a voice");
185
186 let enrolled = enrollment
187 .finish(ada(), Role::Owner)
188 .expect("the samples agree");
189 assert!((enrolled.consistency - 1.0).abs() < 1e-6);
190 assert!((enrolled.profile.vector[0] - voice[0]).abs() < 1e-6);
191 assert_eq!(enrolled.profile.engine, "replay");
192 }
193
194 #[test]
195 fn the_profile_is_unit_norm() {
196 let mut enrollment = enrolling(vec![Some(vec![1.0, 0.0]), Some(vec![0.8, 0.6])]);
197 enrollment.add(&[]).expect("the sample carries a voice");
198 enrollment.add(&[]).expect("the sample carries a voice");
199
200 let enrolled = enrollment
201 .finish(ada(), Role::Guest)
202 .expect("the samples agree");
203 let norm: f32 = enrolled.profile.vector.iter().map(|v| v * v).sum();
204 assert!((norm - 1.0).abs() < 1e-6);
205 }
206
207 #[test]
208 fn a_silent_sample_is_reported_as_one_sample() {
209 let mut enrollment = enrolling(vec![None]);
210 assert!(matches!(enrollment.add(&[]), Err(EnrollmentError::NoVoice)));
211 assert!(enrollment.samples.is_empty());
212 }
213
214 #[test]
215 fn two_different_voices_do_not_enroll() {
216 let mut enrollment = enrolling(vec![Some(vec![1.0, 0.0]), Some(vec![0.0, 1.0])]);
217 enrollment.add(&[]).expect("the sample carries a voice");
218 enrollment.add(&[]).expect("the sample carries a voice");
219
220 assert!(matches!(
221 enrollment.finish(ada(), Role::Owner),
222 Err(EnrollmentError::Inconsistent { .. })
223 ));
224 }
225
226 #[test]
227 fn one_sample_is_not_an_enrollment() {
228 let mut enrollment = enrolling(vec![Some(vec![1.0, 0.0])]);
229 enrollment.add(&[]).expect("the sample carries a voice");
230
231 assert!(matches!(
232 enrollment.finish(ada(), Role::Owner),
233 Err(EnrollmentError::TooFewSamples { samples: 1, .. })
234 ));
235 }
236}