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 const MODEL: &'static [crate::backend::ModelDownload] = &[];
158
159 type Config = ();
160
161 fn check_from(_config: &Self::Config) -> Result<PathBuf, SpeakerError> {
162 Ok(PathBuf::new())
163 }
164
165 fn load_from(_config: Self::Config) -> Result<Self, SpeakerError> {
166 Ok(Self {
167 embeddings: Vec::new(),
168 })
169 }
170 }
171
172 fn enrolling(embeddings: Vec<Option<Vec<f32>>>) -> Enrollment<Replay> {
173 Enrollment::new(Replay { embeddings })
174 }
175
176 fn ada() -> SpeakerName {
177 SpeakerName::new("ada").expect("the name is usable")
178 }
179
180 #[test]
181 fn one_voice_averages_to_itself_and_scores_perfectly() {
182 let voice = vec![0.6, 0.8];
183 let mut enrollment = enrolling(vec![Some(voice.clone()), Some(voice.clone())]);
184 enrollment.add(&[]).expect("the sample carries a voice");
185 enrollment.add(&[]).expect("the sample carries a voice");
186
187 let enrolled = enrollment
188 .finish(ada(), Role::Owner)
189 .expect("the samples agree");
190 assert!((enrolled.consistency - 1.0).abs() < 1e-6);
191 assert!((enrolled.profile.vector[0] - voice[0]).abs() < 1e-6);
192 assert_eq!(enrolled.profile.engine, "replay");
193 }
194
195 #[test]
196 fn the_profile_is_unit_norm() {
197 let mut enrollment = enrolling(vec![Some(vec![1.0, 0.0]), Some(vec![0.8, 0.6])]);
198 enrollment.add(&[]).expect("the sample carries a voice");
199 enrollment.add(&[]).expect("the sample carries a voice");
200
201 let enrolled = enrollment
202 .finish(ada(), Role::Guest)
203 .expect("the samples agree");
204 let norm: f32 = enrolled.profile.vector.iter().map(|v| v * v).sum();
205 assert!((norm - 1.0).abs() < 1e-6);
206 }
207
208 #[test]
209 fn a_silent_sample_is_reported_as_one_sample() {
210 let mut enrollment = enrolling(vec![None]);
211 assert!(matches!(enrollment.add(&[]), Err(EnrollmentError::NoVoice)));
212 assert!(enrollment.samples.is_empty());
213 }
214
215 #[test]
216 fn two_different_voices_do_not_enroll() {
217 let mut enrollment = enrolling(vec![Some(vec![1.0, 0.0]), Some(vec![0.0, 1.0])]);
218 enrollment.add(&[]).expect("the sample carries a voice");
219 enrollment.add(&[]).expect("the sample carries a voice");
220
221 assert!(matches!(
222 enrollment.finish(ada(), Role::Owner),
223 Err(EnrollmentError::Inconsistent { .. })
224 ));
225 }
226
227 #[test]
228 fn one_sample_is_not_an_enrollment() {
229 let mut enrollment = enrolling(vec![Some(vec![1.0, 0.0])]);
230 enrollment.add(&[]).expect("the sample carries a voice");
231
232 assert!(matches!(
233 enrollment.finish(ada(), Role::Owner),
234 Err(EnrollmentError::TooFewSamples { samples: 1, .. })
235 ));
236 }
237}