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, `VoiceprintTracker` judges who holds each
9//! turn as a recording is heard, and [`PyannoteVoices`] settles who spoke
10//! when from the audio itself, afterwards.
11
12#[cfg(all(feature = "silero", not(target_family = "wasm")))]
13mod changes;
14mod diarizer;
15#[cfg(not(target_family = "wasm"))]
16mod enrollment;
17mod label;
18mod profile;
19#[cfg(not(target_family = "wasm"))]
20mod store;
21#[cfg(all(feature = "silero", not(target_family = "wasm")))]
22mod turns;
23mod voices;
24
25use std::path::PathBuf;
26
27use crate::backend::{BackendError, ModelDownload, ModelError};
28
29#[cfg(all(
30    any(test, feature = "test-support"),
31    feature = "silero",
32    not(target_family = "wasm")
33))]
34pub use changes::LevelSegmenter;
35pub use diarizer::{CLUSTER_THRESHOLD, Diarizer, ENROLLED_THRESHOLD, OWN_VOICE_THRESHOLD, settle};
36#[cfg(not(target_family = "wasm"))]
37pub use enrollment::{Enrolled, Enrollment, EnrollmentError, MIN_SAMPLES, centroid, consistency};
38pub(crate) use label::AnonymousNumbering;
39pub use label::Label;
40#[cfg(any(test, feature = "test-support"))]
41pub use profile::enrolled_voice;
42pub use profile::{Profile, Role, SpeakerName, SpeakerNameError};
43#[cfg(not(target_family = "wasm"))]
44pub use store::{ProfileError, ProfileStore, Replace, StoredProfiles};
45#[cfg(all(feature = "silero", not(target_family = "wasm")))]
46pub use turns::{
47    Admission, Ahead, Decision, Gate, NoSegmenter, OwnerTrackingError, Released, Speaker,
48    SpeakerTracker, Timeline, TurnFeed, VoiceprintTracker,
49};
50pub use voices::{
51    Chunk, Grouping, Merged, PyannoteVoices, Revision, Spoken, Standing, Voiceprint, Voices,
52    attribute_spans,
53};
54
55/// Turns audio into a comparable voice vector.
56///
57/// Implementations map their own failures through
58/// [`BackendError::engine`](crate::backend::BackendError::engine) and name
59/// themselves with [`Engine::NAME`].
60pub trait Embedder {
61    /// A unit-norm embedding for one window of 16 kHz samples.
62    ///
63    /// `Ok(None)` means the window carried no voice worth embedding — too
64    /// short, or too quiet. That is an ordinary answer, not a failure, and a
65    /// caller usually keeps whatever label it already had. `Err` means the
66    /// model itself broke.
67    fn embed(&mut self, window: &[f32]) -> Result<Option<Vec<f32>>, SpeakerError>;
68
69    /// The width of every embedding this engine produces. A profile of any
70    /// other width was made by a different engine and cannot be compared.
71    fn dimension(&self) -> usize;
72}
73
74/// How an engine is constructed from its own configuration. `NAME` names the
75/// engine in stored profiles and errors; `Config` carries whatever it needs
76/// to load.
77pub trait Engine: Embedder + Sized {
78    const NAME: &'static str;
79
80    /// The files to download for this engine, all into one folder. Empty
81    /// when nothing is downloaded for it. An engine that reads a second
82    /// folder lists that folder's files in a const of its own.
83    const MODEL: &'static [ModelDownload];
84
85    type Config;
86
87    /// Resolves what a load would read, without reading it.
88    fn check_from(config: &Self::Config) -> Result<PathBuf, SpeakerError>;
89
90    fn load_from(config: Self::Config) -> Result<Self, SpeakerError>;
91}
92
93/// Scales a vector to unit length, so cosines are plain dot products.
94/// `None` when the vector has no direction to keep.
95pub fn normalized(mut values: Vec<f32>) -> Option<Vec<f32>> {
96    let norm = values.iter().map(|value| value * value).sum::<f32>().sqrt();
97    if !norm.is_normal() {
98        return None;
99    }
100    for value in &mut values {
101        *value /= norm;
102    }
103    Some(values)
104}
105
106/// [`crate::backend::validate_model_file`], failing as a [`SpeakerError`].
107#[cfg(not(target_family = "wasm"))]
108pub fn validate_model_file(path: PathBuf, backend: &'static str) -> Result<PathBuf, SpeakerError> {
109    crate::backend::validate_model_file(path)
110        .map_err(|source| SpeakerError::Model { backend, source })
111}
112
113#[derive(Debug, thiserror::Error)]
114pub enum SpeakerError {
115    #[error("the {backend} model cannot be used")]
116    Model {
117        backend: &'static str,
118        #[source]
119        source: ModelError,
120    },
121
122    #[error("failed to load the {backend} model {model}")]
123    Load {
124        backend: &'static str,
125        model: String,
126        #[source]
127        source: BackendError,
128    },
129
130    #[error("{backend} inference failed")]
131    Inference {
132        backend: &'static str,
133        #[source]
134        source: BackendError,
135    },
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn normalizing_leaves_a_unit_vector_pointing_the_same_way() {
144        let unit = normalized(vec![3.0, 4.0]).expect("the vector has a direction");
145        assert!((unit[0] - 0.6).abs() < 1e-6);
146        assert!((unit[1] - 0.8).abs() < 1e-6);
147    }
148
149    #[test]
150    fn a_vector_without_direction_cannot_be_normalized() {
151        assert!(normalized(vec![0.0, 0.0]).is_none());
152    }
153}