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