Skip to main content

aria_kit/transcription/
session.rs

1//! One transcription run: a recording the session owns, the transcription
2//! over it, and the lifecycle between them. A [`Streaming`] session yields
3//! words as they are committed; a `Whole` session has only its end.
4
5use std::marker::PhantomData;
6
7use super::{
8    Language, Layer, Mode, Operation, Streaming, Transcriber, Transcript, TranscriptSegment,
9    Transcription, TranscriptionError,
10};
11use crate::audio::RecordingReader;
12
13#[cfg(not(target_family = "wasm"))]
14use crate::audio::Stopper;
15#[cfg(all(feature = "microphone", not(target_family = "wasm")))]
16use crate::audio::{Microphone, RecordingMode};
17#[cfg(all(feature = "silero", not(target_family = "wasm")))]
18use crate::speaker::{Diarizer, Embedder, Heard, SegmentLabeler, SegmentReader, Verdict};
19#[cfg(all(feature = "silero", not(target_family = "wasm")))]
20use crate::voice_activity::{SileroVad, VoiceActivityError};
21
22/// One transcription run over audio that arrives while the run is live.
23///
24/// Constructed over a recording the caller feeds, or straight from the
25/// microphone where one exists. `stop` waits for input to end, drains the
26/// tail, and returns the completed transcript.
27pub struct Session<'a, T, M: Mode, L = ()> {
28    operation: Operation<'a, T, L>,
29    recording: RecordingReader,
30    /// Held so the capture worker outlives the run.
31    #[cfg(all(feature = "microphone", not(target_family = "wasm")))]
32    _microphone: Option<Microphone>,
33    #[cfg(not(target_family = "wasm"))]
34    stopper: Option<Stopper>,
35    mode: PhantomData<M>,
36}
37
38impl<'a, T: Transcriber, M: Mode, L: Layer> Session<'a, T, M, L> {
39    /// Runs against a recording the caller already has. Whoever feeds it also
40    /// ends it; `stop` only waits for that end and finishes the transcript.
41    pub fn over(
42        transcription: &'a Transcription<T, M, L>,
43        language: &'a Language,
44        recording: RecordingReader,
45    ) -> Result<Self, TranscriptionError> {
46        Ok(Self {
47            operation: transcription.start(language)?,
48            recording,
49            #[cfg(all(feature = "microphone", not(target_family = "wasm")))]
50            _microphone: None,
51            #[cfg(not(target_family = "wasm"))]
52            stopper: None,
53            mode: PhantomData,
54        })
55    }
56
57    /// Waits for input to end, drains whatever remains, and returns the
58    /// completed transcript — or [`TranscriptionError::NoSpeech`] when
59    /// nothing was said.
60    pub fn stop(mut self) -> Result<Transcript, TranscriptionError> {
61        #[cfg(not(target_family = "wasm"))]
62        if let Some(stopper) = &self.stopper {
63            stopper.stop();
64        }
65        self.operation.finish(&mut self.recording)
66    }
67}
68
69#[cfg(all(feature = "microphone", not(target_family = "wasm")))]
70impl<'a, T: Transcriber, M: Mode, L: Layer> Session<'a, T, M, L> {
71    /// Opens the microphone and records one utterance until stopped. `stop`
72    /// ends the recording; until then every [`Streaming`] step blocks on
73    /// captured audio.
74    pub fn microphone(
75        transcription: &'a Transcription<T, M, L>,
76        language: &'a Language,
77    ) -> Result<Self, TranscriptionError> {
78        // Recording starts first: `start` can block, and speech during it
79        // must not be lost.
80        let mut microphone = Microphone::open().map_err(TranscriptionError::Capture)?;
81        let (recording, stopper) = microphone
82            .record(RecordingMode::UntilStopped)
83            .map_err(TranscriptionError::Capture)?;
84        let operation = match transcription.start(language) {
85            Ok(operation) => operation,
86            Err(error) => {
87                stopper.stop();
88                return Err(error);
89            }
90        };
91        Ok(Self {
92            operation,
93            recording,
94            _microphone: Some(microphone),
95            stopper: Some(stopper),
96            mode: PhantomData,
97        })
98    }
99}
100
101impl<'a, T: Transcriber, L: Layer> Session<'a, T, Streaming, L> {
102    /// The next committed stretch of words, or `None` once the input has
103    /// ended and the tail is delivered. Committed words are never revised.
104    pub fn next_finalized(&mut self) -> Result<Option<TranscriptSegment>, TranscriptionError> {
105        self.operation.next_finalized(&mut self.recording)
106    }
107
108    /// The same session, with every committed segment's voice named by
109    /// `voices`, behind the bundled speech gate.
110    #[cfg(all(feature = "silero", not(target_family = "wasm")))]
111    pub fn hearing<E: Embedder>(
112        self,
113        voices: Diarizer<E>,
114    ) -> Result<HeardSession<'a, T, E, L>, VoiceActivityError> {
115        let gate = SileroVad::load()?;
116        Ok(self.with_reader(SegmentReader::new(SegmentLabeler::new(
117            Some(gate),
118            Some(voices),
119        ))))
120    }
121
122    /// The same session, with every committed segment judged by a reader
123    /// the caller built.
124    #[cfg(all(feature = "silero", not(target_family = "wasm")))]
125    pub fn with_reader<E: Embedder>(self, reader: SegmentReader<E>) -> HeardSession<'a, T, E, L> {
126        HeardSession {
127            session: self,
128            reader,
129            kept: Vec::new(),
130        }
131    }
132}
133
134/// A streaming session whose segments come with a verdict on whose voice
135/// they are in.
136#[cfg(all(feature = "silero", not(target_family = "wasm")))]
137pub struct HeardSession<'a, T, E, L = ()> {
138    session: Session<'a, T, Streaming, L>,
139    reader: SegmentReader<E>,
140    /// What passed the reader, for the transcript of a session that keeps
141    /// strangers out.
142    kept: Vec<String>,
143}
144
145#[cfg(all(feature = "silero", not(target_family = "wasm")))]
146impl<T: Transcriber, E: Embedder, L: Layer> HeardSession<'_, T, E, L> {
147    /// Keeps out every segment in a voice that is not enrolled.
148    pub fn only_own_voice(mut self) -> Self {
149        self.reader.labeler_mut().set_only_own_voice(true);
150        self
151    }
152
153    /// The next committed segment and whose voice it was, or `None` once
154    /// the input has ended and the tail is delivered.
155    pub fn next_heard(&mut self) -> Result<Option<Heard>, TranscriptionError> {
156        let heard = self
157            .reader
158            .next(&mut self.session.operation, &mut self.session.recording)?;
159        if let Some(heard) = &heard
160            && matches!(heard.verdict, Verdict::Keep { .. })
161        {
162            self.kept.push(heard.segment.as_str().to_owned());
163        }
164        Ok(heard)
165    }
166
167    pub fn reader(&self) -> &SegmentReader<E> {
168        &self.reader
169    }
170
171    /// The completed transcript. A session that keeps strangers out answers
172    /// with what it kept, and [`TranscriptionError::NoSpeech`] when that is
173    /// nothing.
174    pub fn stop(self) -> Result<Transcript, TranscriptionError> {
175        let transcript = self.session.stop();
176        if !self.reader.labeler().only_own_voice() {
177            return transcript;
178        }
179        // Wait for the input to end first, and only then hand back what was
180        // kept.
181        transcript?;
182        Transcript::new(self.kept.join(" "))
183    }
184}
185
186#[cfg(not(target_family = "wasm"))]
187impl<T, M: Mode, L> Drop for Session<'_, T, M, L> {
188    fn drop(&mut self) {
189        // A session dropped without stop() must not leave the microphone
190        // recording to its ceiling. Stopping an already-stopped input is a
191        // no-op, so this composes with stop().
192        if let Some(stopper) = &self.stopper {
193            stopper.stop();
194        }
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use std::collections::VecDeque;
201    use std::sync::Mutex;
202
203    use super::*;
204    use crate::audio::CANONICAL_SAMPLE_RATE;
205    use crate::audio::testing::{Pace, paced, recorded};
206    use crate::cancel::CancelToken;
207    use crate::transcription::{Cadence, TranscriptionAudio};
208
209    const STRIDE_MS: usize = 100;
210    const STRIDE: usize = STRIDE_MS * CANONICAL_SAMPLE_RATE as usize / 1000;
211
212    /// Answers each decode with the next entry of its script, releasing the
213    /// next paced batch as it goes.
214    struct Scripted {
215        answers: Mutex<VecDeque<&'static str>>,
216        pace: Option<Pace>,
217    }
218
219    impl Scripted {
220        fn new(answers: &[&'static str]) -> Self {
221            Self {
222                answers: Mutex::new(answers.iter().copied().collect()),
223                pace: None,
224            }
225        }
226
227        fn paced(answers: &[&'static str], pace: Pace) -> Self {
228            Self {
229                answers: Mutex::new(answers.iter().copied().collect()),
230                pace: Some(pace),
231            }
232        }
233    }
234
235    impl Transcriber for Scripted {
236        fn validate_language(
237            &self,
238            _language: &Language,
239            _cancel: &CancelToken,
240        ) -> Result<(), TranscriptionError> {
241            Ok(())
242        }
243
244        fn transcribe_with(
245            &self,
246            _audio: TranscriptionAudio<'_>,
247            _language: &Language,
248            _cancel: &CancelToken,
249        ) -> Result<super::super::Hypothesis, TranscriptionError> {
250            if let Some(pace) = &self.pace {
251                pace.advance();
252            }
253            let answer = self
254                .answers
255                .lock()
256                .unwrap_or_else(|poisoned| poisoned.into_inner())
257                .pop_front()
258                .expect("the script covers every decode");
259            Ok(super::super::Hypothesis::new(answer))
260        }
261    }
262
263    fn stride_batch() -> Vec<i16> {
264        vec![8_192; STRIDE]
265    }
266
267    #[test]
268    fn a_streaming_session_commits_words_as_their_agreement_holds() {
269        let (_directory, recording, pace) = paced(vec![stride_batch(); 4]);
270        let cadence = Cadence::from_millis(STRIDE_MS, 20_000);
271        let transcription = Transcription::agreement(
272            cadence,
273            Scripted::paced(&["one", "one two", "one two three", "one two three"], pace),
274        );
275        let language = Language::Automatic;
276        let mut session =
277            Session::over(&transcription, &language, recording).expect("the session starts");
278
279        let mut segments = Vec::new();
280        while let Some(segment) = session.next_finalized().expect("every step succeeds") {
281            segments.push(segment.into_text());
282        }
283        let transcript = session.stop().expect("the transcript completes");
284
285        assert_eq!(segments, ["one", "two", "three"]);
286        assert_eq!(transcript.as_str(), "one two three");
287    }
288
289    #[cfg(feature = "silero")]
290    #[test]
291    fn a_session_with_a_reader_says_whose_voice_each_segment_is_in() {
292        use crate::speaker::{Diarizer, Embedder, SegmentLabeler, SpeakerError, Verdict};
293
294        const OWN: [f32; 2] = [1.0, 0.0];
295        const OTHER: [f32; 2] = [0.0, 1.0];
296
297        struct Voice([f32; 2]);
298
299        impl Embedder for Voice {
300            fn embed(&mut self, _window: &[f32]) -> Result<Option<Vec<f32>>, SpeakerError> {
301                Ok(Some(self.0.to_vec()))
302            }
303
304            fn dimension(&self) -> usize {
305                2
306            }
307        }
308
309        /// Hears "one" spanning the whole window it is given.
310        struct Timed;
311
312        impl Transcriber for Timed {
313            fn validate_language(
314                &self,
315                _language: &Language,
316                _cancel: &CancelToken,
317            ) -> Result<(), TranscriptionError> {
318                Ok(())
319            }
320
321            fn transcribe_with(
322                &self,
323                audio: TranscriptionAudio<'_>,
324                _language: &Language,
325                _cancel: &CancelToken,
326            ) -> Result<super::super::Hypothesis, TranscriptionError> {
327                let TranscriptionAudio::Samples(samples) = audio else {
328                    unreachable!("the session hands over samples");
329                };
330                let end = samples.len() as f32 / CANONICAL_SAMPLE_RATE as f32;
331                Ok(super::super::Hypothesis::with_words(
332                    "one",
333                    vec![super::super::TimedWord { start: 0.0, end }],
334                ))
335            }
336        }
337
338        // A finished recording is heard in one decode, so the one segment
339        // spans its whole 3.4 s: enough to judge.
340        let judged = |voice: [f32; 2]| {
341            let (_directory, recording) = recorded(&vec![8_192; 34 * STRIDE]);
342            let cadence = Cadence::from_millis(STRIDE_MS, 20_000);
343            let transcription = Transcription::agreement(cadence, Timed);
344            let language = Language::Automatic;
345            let enrolled = vec![crate::speaker::enrolled_voice(
346                "alex",
347                crate::speaker::Role::Owner,
348                OWN.to_vec(),
349            )];
350            let labeler = SegmentLabeler::new(None, Some(Diarizer::new(Voice(voice), enrolled)));
351            let mut session = Session::over(&transcription, &language, recording)
352                .expect("the session starts")
353                .with_reader(SegmentReader::new(labeler))
354                .only_own_voice();
355
356            let mut heard = Vec::new();
357            while let Some(segment) = session.next_heard().expect("every step succeeds") {
358                let stranger = matches!(segment.verdict, Verdict::Stranger);
359                heard.push((segment.segment.into_text(), stranger));
360            }
361            (heard, session.stop().map(Transcript::into_text))
362        };
363
364        let (heard, transcript) = judged(OTHER);
365        assert_eq!(heard, [("one".to_owned(), true)]);
366        assert!(matches!(transcript, Err(TranscriptionError::NoSpeech)));
367
368        let (heard, transcript) = judged(OWN);
369        assert_eq!(heard, [("one".to_owned(), false)]);
370        assert_eq!(transcript.expect("the kept words complete"), "one");
371    }
372
373    #[test]
374    fn a_whole_session_has_only_its_end() {
375        let (_directory, recording) = recorded(&[0; 64]);
376        let transcription = Transcription::complete_samples(Scripted::new(&["hello world"]));
377        let language = Language::Automatic;
378
379        let transcript = Session::over(&transcription, &language, recording)
380            .expect("the session starts")
381            .stop()
382            .expect("the transcript completes");
383
384        assert_eq!(transcript.as_str(), "hello world");
385    }
386}