Skip to main content

aria_kit/speaker/
mod.rs

1//! Who is speaking, as a vector: the contract every speaker-embedding engine
2//! implements.
3//!
4//! An engine turns a window of 16 kHz samples into a unit-norm embedding.
5//! Two embeddings of one voice point the same way, so their cosine says
6//! whether two stretches of speech came from the same person. What to do
7//! with that number is policy, kept out of every engine: [`Diarizer`] groups
8//! voices and claims enrolled names, and `SegmentReader` applies it to the
9//! segments a transcription session emits.
10
11mod diarizer;
12#[cfg(not(target_family = "wasm"))]
13mod enrollment;
14#[cfg(all(feature = "silero", not(target_family = "wasm")))]
15mod labeler;
16mod profile;
17#[cfg(not(target_family = "wasm"))]
18mod store;
19
20use std::path::PathBuf;
21
22use crate::backend::{BackendError, ModelError};
23
24pub use diarizer::{CLUSTER_THRESHOLD, Diarizer, ENROLLED_THRESHOLD, settle};
25#[cfg(not(target_family = "wasm"))]
26pub use enrollment::{Enrolled, Enrollment, EnrollmentError, MIN_SAMPLES, centroid, consistency};
27#[cfg(all(feature = "silero", not(target_family = "wasm")))]
28pub use labeler::{
29    Heard, HeardUpdate, OWN_VOICE_THRESHOLD, SegmentLabeler, SegmentReader, Verdict,
30};
31#[cfg(any(test, feature = "test-support"))]
32pub use profile::enrolled_voice;
33pub use profile::{Profile, Role, SpeakerName, SpeakerNameError};
34#[cfg(not(target_family = "wasm"))]
35pub use store::{ProfileError, ProfileStore, Replace, StoredProfiles};
36
37/// Turns audio into a comparable voice vector.
38///
39/// Implementations map their own failures through
40/// [`BackendError::engine`](crate::backend::BackendError::engine) and name
41/// themselves with [`Engine::NAME`].
42pub trait Embedder {
43    /// A unit-norm embedding for one window of 16 kHz samples.
44    ///
45    /// `Ok(None)` means the window carried no voice worth embedding — too
46    /// short, or too quiet. That is an ordinary answer, not a failure, and a
47    /// caller usually keeps whatever label it already had. `Err` means the
48    /// model itself broke.
49    fn embed(&mut self, window: &[f32]) -> Result<Option<Vec<f32>>, SpeakerError>;
50
51    /// The width of every embedding this engine produces. A profile of any
52    /// other width was made by a different engine and cannot be compared.
53    fn dimension(&self) -> usize;
54}
55
56/// How an engine is constructed from its own configuration. `NAME` names the
57/// engine in stored profiles and errors; `Config` carries whatever it needs
58/// to load.
59pub trait Engine: Embedder + Sized {
60    const NAME: &'static str;
61
62    type Config;
63
64    /// Resolves what a load would read, without reading it.
65    fn check_from(config: &Self::Config) -> Result<PathBuf, SpeakerError>;
66
67    fn load_from(config: Self::Config) -> Result<Self, SpeakerError>;
68}
69
70/// Scales a vector to unit length, so cosines are plain dot products.
71/// `None` when the vector has no direction to keep.
72pub fn normalized(mut values: Vec<f32>) -> Option<Vec<f32>> {
73    let norm = values.iter().map(|value| value * value).sum::<f32>().sqrt();
74    if !norm.is_normal() {
75        return None;
76    }
77    for value in &mut values {
78        *value /= norm;
79    }
80    Some(values)
81}
82
83/// [`crate::backend::validate_model_file`], failing as a [`SpeakerError`].
84#[cfg(not(target_family = "wasm"))]
85pub fn validate_model_file(path: PathBuf, backend: &'static str) -> Result<PathBuf, SpeakerError> {
86    crate::backend::validate_model_file(path)
87        .map_err(|source| SpeakerError::Model { backend, source })
88}
89
90#[derive(Debug, thiserror::Error)]
91pub enum SpeakerError {
92    #[error("the {backend} model cannot be used")]
93    Model {
94        backend: &'static str,
95        #[source]
96        source: ModelError,
97    },
98
99    #[error("failed to load the {backend} model {model}")]
100    Load {
101        backend: &'static str,
102        model: String,
103        #[source]
104        source: BackendError,
105    },
106
107    #[error("{backend} inference failed")]
108    Inference {
109        backend: &'static str,
110        #[source]
111        source: BackendError,
112    },
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn normalizing_leaves_a_unit_vector_pointing_the_same_way() {
121        let unit = normalized(vec![3.0, 4.0]).expect("the vector has a direction");
122        assert!((unit[0] - 0.6).abs() < 1e-6);
123        assert!((unit[1] - 0.8).abs() < 1e-6);
124    }
125
126    #[test]
127    fn a_vector_without_direction_cannot_be_normalized() {
128        assert!(normalized(vec![0.0, 0.0]).is_none());
129    }
130}