Skip to main content

aria_kit/segmentation/
mod.rs

1//! Who is speaking *when*: the contract every speech-segmentation engine
2//! implements.
3//!
4//! An embedder answers "whose voice is this window" and needs the window to
5//! hold one person. A segmenter answers the question underneath: given a
6//! fixed stretch of audio, which voices are active in each frame of it, and
7//! where does one stop and the next begin. Two people talking at once is an
8//! ordinary answer here rather than a window nobody can embed.
9//!
10//! What a caller does with those frames — masking audio per voice, grouping
11//! the voices across a recording, naming them — is policy, not an engine's
12//! business.
13
14use std::path::PathBuf;
15
16use crate::backend::{BackendError, ModelDownload, ModelError};
17
18/// Speakers active in each frame of one fixed-width chunk of audio.
19///
20/// Implementations map their own failures through
21/// [`BackendError::engine`](crate::backend::BackendError::engine) and name
22/// themselves with [`Engine::NAME`].
23pub trait Segmenter {
24    /// The chunk width this engine reads and the frame grid it answers on.
25    /// Fixed for the life of the engine — it is a property of the graph.
26    fn shape(&self) -> ChunkShape;
27
28    /// Who speaks in each frame of `chunk`, which must be exactly
29    /// [`ChunkShape::samples`] long. A caller with less audio than that pads
30    /// it; a caller with more cuts it.
31    fn segment(&mut self, chunk: &[f32]) -> Result<Vec<Activity>, SegmentationError>;
32}
33
34impl<S: Segmenter> Segmenter for &mut S {
35    fn shape(&self) -> ChunkShape {
36        (**self).shape()
37    }
38
39    fn segment(&mut self, chunk: &[f32]) -> Result<Vec<Activity>, SegmentationError> {
40        (**self).segment(chunk)
41    }
42}
43
44/// How an engine is constructed from its own configuration. Implementing
45/// this beside [`Segmenter`] is the whole checklist for a new engine.
46pub trait Engine: Segmenter + Sized {
47    const NAME: &'static str;
48
49    /// The files to download for this engine, all into one folder. Empty
50    /// when nothing is downloaded for it. An engine that reads a second
51    /// folder lists that folder's files in a const of its own.
52    const MODEL: &'static [ModelDownload];
53
54    type Config;
55
56    /// Resolves what a load would read, without reading it.
57    fn check_from(config: &Self::Config) -> Result<PathBuf, SegmentationError>;
58
59    fn load_from(config: Self::Config) -> Result<Self, SegmentationError>;
60}
61
62/// The grid one engine answers on.
63///
64/// `frame_size` is wider than `frame_shift` — frames overlap — so a frame's
65/// time is the centre of the audio behind it rather than its leading edge.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct ChunkShape {
68    /// Samples in one chunk, at 16 kHz.
69    pub samples: usize,
70    /// Samples from one frame to the next.
71    pub frame_shift: usize,
72    /// Samples one frame answers for.
73    pub frame_size: usize,
74    /// Voices the engine can tell apart inside a single chunk. They are
75    /// local to the chunk: voice 0 of one chunk and voice 0 of the next are
76    /// not the same person.
77    pub speakers: usize,
78}
79
80/// Which of a chunk's local voices speak in one frame.
81///
82/// A bitset rather than a `Vec<bool>`: there is one of these per frame and a
83/// ten-second chunk holds hundreds, so the whole answer stays in cache.
84#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
85pub struct Activity(u8);
86
87impl Activity {
88    /// Nobody speaking.
89    pub const SILENT: Self = Self(0);
90
91    /// The most local voices any engine may report. Beyond this an activity
92    /// would not fit its bitset, and no published segmenter comes close.
93    pub const MAX_SPEAKERS: usize = 8;
94
95    /// Marks `local` as speaking. Positions at or past [`Self::MAX_SPEAKERS`]
96    /// are ignored — an engine claiming more voices than the set holds is a
97    /// bug in that engine, and dropping the overflow keeps the frames it did
98    /// get right.
99    pub fn speaking(mut self, local: usize) -> Self {
100        if local < Self::MAX_SPEAKERS {
101            self.0 |= 1 << local;
102        }
103        self
104    }
105
106    pub fn speaks(self, local: usize) -> bool {
107        local < Self::MAX_SPEAKERS && self.0 & (1 << local) != 0
108    }
109
110    /// How many voices are speaking at once.
111    pub fn voices(self) -> usize {
112        self.0.count_ones() as usize
113    }
114}
115
116/// The shared model validator, answering in this capability's vocabulary.
117#[cfg(not(target_family = "wasm"))]
118pub fn validate_model_file(
119    path: PathBuf,
120    backend: &'static str,
121) -> Result<PathBuf, SegmentationError> {
122    crate::backend::validate_model_file(path)
123        .map_err(|source| SegmentationError::Model { backend, source })
124}
125
126#[derive(Debug, thiserror::Error)]
127pub enum SegmentationError {
128    #[error("the {backend} model cannot be used")]
129    Model {
130        backend: &'static str,
131        #[source]
132        source: ModelError,
133    },
134
135    #[error("failed to load the {backend} model {model}")]
136    Load {
137        backend: &'static str,
138        model: String,
139        #[source]
140        source: BackendError,
141    },
142
143    #[error("{backend} inference failed")]
144    Inference {
145        backend: &'static str,
146        #[source]
147        source: BackendError,
148    },
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn silence_names_nobody() {
157        assert_eq!(Activity::SILENT.voices(), 0);
158        assert!(!Activity::SILENT.speaks(0));
159    }
160
161    #[test]
162    fn two_voices_at_once_is_an_ordinary_answer() {
163        let both = Activity::SILENT.speaking(0).speaking(2);
164        assert_eq!(both.voices(), 2);
165        assert!(both.speaks(0) && both.speaks(2));
166        assert!(!both.speaks(1));
167    }
168
169    #[test]
170    fn a_voice_the_set_cannot_hold_is_dropped_not_folded_onto_another() {
171        let overflowed = Activity::SILENT.speaking(Activity::MAX_SPEAKERS);
172        assert_eq!(overflowed.voices(), 0);
173        assert!(!overflowed.speaks(0));
174    }
175}