Skip to main content

aria_kit/speaker/
voices.rs

1//! Who spoke when, read from the audio rather than from the transcript.
2//!
3//! Per-segment labels depend on transcription boundaries, which can contain
4//! too little speech or several voices. A voice track instead analyzes audio
5//! independently and assigns labels by each paragraph's audio span.
6
7mod grid;
8mod pyannote;
9mod timeline;
10
11use std::ops::Range;
12
13use super::Label;
14
15pub use pyannote::{Chunk, Grouping, PyannoteVoices, Standing, Voiceprint};
16
17/// A running account of who is speaking in a recording.
18///
19/// Only the question the transcript asks. How a track hears the audio is its
20/// own business, and it hears it on its own thread.
21pub trait Voices {
22    /// Who owns each span. `None` where the track cannot say, such as past
23    /// its reach or over audio nobody could embed.
24    fn attribute(&self, spans: &[Range<u64>]) -> Vec<Option<Label>>;
25}
26
27/// Asks a track who owned each paragraph, skipping the ones nobody timed.
28///
29/// A paragraph without a span has no audio to point at. Passing some other
30/// span in its place would still get a confident answer, about somebody
31/// else's audio, so it is left `None` for the settling pass to fill from a
32/// neighbour.
33pub fn attribute_spans(voices: &impl Voices, spans: &[Option<Range<u64>>]) -> Vec<Option<Label>> {
34    let timed: Vec<Range<u64>> = spans.iter().flatten().cloned().collect();
35    let mut answers = voices.attribute(&timed).into_iter();
36    spans
37        .iter()
38        .map(|span| span.as_ref().and_then(|_| answers.next().flatten()))
39        .collect()
40}
41
42/// One stretch of the recording, and whose voice was in it.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Spoken {
45    pub span: Range<u64>,
46    pub speaker: Label,
47}
48
49/// What an observation changed about names already written.
50///
51/// A voice opening for the first time changes nothing: audio nobody had
52/// attributed is now attributed. Two voices turning out to be one changes
53/// every paragraph the loser owned, and that is the only reason to rewrite a
54/// transcript mid-session — or to tell a reader watching it.
55#[derive(Debug, Clone, Default, PartialEq, Eq)]
56pub struct Revision {
57    pub merges: Vec<Merged>,
58}
59
60impl Revision {
61    pub fn revised(&self) -> bool {
62        !self.merges.is_empty()
63    }
64}
65
66/// A voice that turned out to be one already heard.
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct Merged {
69    pub from: Label,
70    pub into: Label,
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    /// Names each span after where it starts, and records what it was asked.
78    struct Positional(std::cell::RefCell<Vec<Range<u64>>>);
79
80    impl Voices for Positional {
81        fn attribute(&self, spans: &[Range<u64>]) -> Vec<Option<Label>> {
82            self.0.borrow_mut().extend_from_slice(spans);
83            spans
84                .iter()
85                .map(|span| Some(Label::Anonymous(span.start as u32)))
86                .collect()
87        }
88    }
89
90    #[test]
91    fn a_paragraph_with_no_span_is_never_asked_about() {
92        let voices = Positional(Vec::new().into());
93        let named = attribute_spans(&voices, &[Some(10..20), None, Some(30..40)]);
94
95        assert_eq!(
96            voices.0.borrow().as_slice(),
97            [10..20, 30..40],
98            "the track hears only the spans somebody measured"
99        );
100        assert_eq!(
101            named,
102            [Some(Label::Anonymous(10)), None, Some(Label::Anonymous(30))],
103            "and its answers land back on the paragraphs they were about"
104        );
105    }
106
107    #[test]
108    fn a_track_that_answers_short_leaves_the_rest_unnamed() {
109        struct Silent;
110        impl Voices for Silent {
111            fn attribute(&self, _spans: &[Range<u64>]) -> Vec<Option<Label>> {
112                Vec::new()
113            }
114        }
115        assert_eq!(attribute_spans(&Silent, &[Some(0..10), None]), [None, None]);
116    }
117}