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::speech_gate;
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>(self, voices: Diarizer<E>) -> HeardSession<'a, T, E, L> {
112        self.with_reader(SegmentReader::new(SegmentLabeler::new(
113            speech_gate(),
114            Some(voices),
115        )))
116    }
117
118    /// The same session, with every committed segment judged by a reader
119    /// the caller built.
120    #[cfg(all(feature = "silero", not(target_family = "wasm")))]
121    pub fn with_reader<E: Embedder>(self, reader: SegmentReader<E>) -> HeardSession<'a, T, E, L> {
122        HeardSession {
123            session: self,
124            reader,
125            kept: Vec::new(),
126        }
127    }
128}
129
130/// A streaming session whose segments come with a verdict on whose voice
131/// they are in.
132#[cfg(all(feature = "silero", not(target_family = "wasm")))]
133pub struct HeardSession<'a, T, E, L = ()> {
134    session: Session<'a, T, Streaming, L>,
135    reader: SegmentReader<E>,
136    /// What passed the reader, for the transcript of a session that keeps
137    /// strangers out.
138    kept: Vec<String>,
139}
140
141#[cfg(all(feature = "silero", not(target_family = "wasm")))]
142impl<T: Transcriber, E: Embedder, L: Layer> HeardSession<'_, T, E, L> {
143    /// Keeps out every segment in a voice that is not enrolled.
144    pub fn only_own_voice(mut self) -> Self {
145        self.reader.labeler_mut().set_only_own_voice(true);
146        self
147    }
148
149    /// The next committed segment and whose voice it was, or `None` once
150    /// the input has ended and the tail is delivered.
151    pub fn next_heard(&mut self) -> Result<Option<Heard>, TranscriptionError> {
152        let heard = self
153            .reader
154            .next(&mut self.session.operation, &mut self.session.recording)?;
155        if let Some(heard) = &heard
156            && matches!(heard.verdict, Verdict::Keep { .. })
157        {
158            self.kept.push(heard.segment.as_str().to_owned());
159        }
160        Ok(heard)
161    }
162
163    pub fn reader(&self) -> &SegmentReader<E> {
164        &self.reader
165    }
166
167    /// The completed transcript. A session that keeps strangers out answers
168    /// with what it kept, and [`TranscriptionError::NoSpeech`] when that is
169    /// nothing.
170    pub fn stop(self) -> Result<Transcript, TranscriptionError> {
171        let transcript = self.session.stop();
172        if !self.reader.labeler().only_own_voice() {
173            return transcript;
174        }
175        // Wait for the input to end first, and only then hand back what was
176        // kept.
177        transcript?;
178        Transcript::new(self.kept.join(" "))
179    }
180}
181
182#[cfg(not(target_family = "wasm"))]
183impl<T, M: Mode, L> Drop for Session<'_, T, M, L> {
184    fn drop(&mut self) {
185        // A session dropped without stop() must not leave the microphone
186        // recording to its ceiling. Stopping an already-stopped input is a
187        // no-op, so this composes with stop().
188        if let Some(stopper) = &self.stopper {
189            stopper.stop();
190        }
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use std::collections::VecDeque;
197    use std::sync::Mutex;
198
199    use super::*;
200    use crate::audio::CANONICAL_SAMPLE_RATE;
201    use crate::audio::testing::{Pace, paced, recorded};
202    use crate::cancel::CancelToken;
203    use crate::transcription::{Cadence, TranscriptionAudio};
204
205    const STRIDE_MS: usize = 100;
206    const STRIDE: usize = STRIDE_MS * CANONICAL_SAMPLE_RATE as usize / 1000;
207
208    /// Answers each decode with the next entry of its script, releasing the
209    /// next paced batch as it goes.
210    struct Scripted {
211        answers: Mutex<VecDeque<&'static str>>,
212        pace: Option<Pace>,
213    }
214
215    impl Scripted {
216        fn new(answers: &[&'static str]) -> Self {
217            Self {
218                answers: Mutex::new(answers.iter().copied().collect()),
219                pace: None,
220            }
221        }
222
223        fn paced(answers: &[&'static str], pace: Pace) -> Self {
224            Self {
225                answers: Mutex::new(answers.iter().copied().collect()),
226                pace: Some(pace),
227            }
228        }
229    }
230
231    impl Transcriber for Scripted {
232        fn validate_language(
233            &self,
234            _language: &Language,
235            _cancel: &CancelToken,
236        ) -> Result<(), TranscriptionError> {
237            Ok(())
238        }
239
240        fn transcribe_with(
241            &self,
242            _audio: TranscriptionAudio<'_>,
243            _language: &Language,
244            _cancel: &CancelToken,
245        ) -> Result<super::super::Hypothesis, TranscriptionError> {
246            if let Some(pace) = &self.pace {
247                pace.advance();
248            }
249            let answer = self
250                .answers
251                .lock()
252                .unwrap_or_else(|poisoned| poisoned.into_inner())
253                .pop_front()
254                .expect("the script covers every decode");
255            Ok(super::super::Hypothesis::new(answer))
256        }
257    }
258
259    fn stride_batch() -> Vec<i16> {
260        vec![8_192; STRIDE]
261    }
262
263    #[test]
264    fn a_streaming_session_commits_words_as_their_agreement_holds() {
265        let (_directory, recording, pace) = paced(vec![stride_batch(); 4]);
266        let cadence = Cadence::from_millis(STRIDE_MS, 20_000);
267        let transcription = Transcription::agreement(
268            cadence,
269            Scripted::paced(&["one", "one two", "one two three", "one two three"], pace),
270        );
271        let language = Language::Automatic;
272        let mut session =
273            Session::over(&transcription, &language, recording).expect("the session starts");
274
275        let mut segments = Vec::new();
276        while let Some(segment) = session.next_finalized().expect("every step succeeds") {
277            segments.push(segment.into_text());
278        }
279        let transcript = session.stop().expect("the transcript completes");
280
281        assert_eq!(segments, ["one", "two", "three"]);
282        assert_eq!(transcript.as_str(), "one two three");
283    }
284
285    #[cfg(feature = "silero")]
286    #[test]
287    fn a_session_with_a_reader_says_whose_voice_each_segment_is_in() {
288        use crate::speaker::{Diarizer, Embedder, SegmentLabeler, SpeakerError, Verdict};
289
290        const OWN: [f32; 2] = [1.0, 0.0];
291        const OTHER: [f32; 2] = [0.0, 1.0];
292
293        struct Voice([f32; 2]);
294
295        impl Embedder for Voice {
296            fn embed(&mut self, _window: &[f32]) -> Result<Option<Vec<f32>>, SpeakerError> {
297                Ok(Some(self.0.to_vec()))
298            }
299
300            fn dimension(&self) -> usize {
301                2
302            }
303        }
304
305        /// Hears "one" spanning the whole window it is given.
306        struct Timed;
307
308        impl Transcriber for Timed {
309            fn validate_language(
310                &self,
311                _language: &Language,
312                _cancel: &CancelToken,
313            ) -> Result<(), TranscriptionError> {
314                Ok(())
315            }
316
317            fn transcribe_with(
318                &self,
319                audio: TranscriptionAudio<'_>,
320                _language: &Language,
321                _cancel: &CancelToken,
322            ) -> Result<super::super::Hypothesis, TranscriptionError> {
323                let TranscriptionAudio::Samples(samples) = audio else {
324                    unreachable!("the session hands over samples");
325                };
326                let end = samples.len() as f32 / CANONICAL_SAMPLE_RATE as f32;
327                Ok(super::super::Hypothesis::with_words(
328                    "one",
329                    vec![super::super::TimedWord { start: 0.0, end }],
330                ))
331            }
332        }
333
334        // A finished recording is heard in one decode, so the one segment
335        // spans its whole 3.4 s: enough to judge.
336        let judged = |voice: [f32; 2]| {
337            let (_directory, recording) = recorded(&vec![8_192; 34 * STRIDE]);
338            let cadence = Cadence::from_millis(STRIDE_MS, 20_000);
339            let transcription = Transcription::agreement(cadence, Timed);
340            let language = Language::Automatic;
341            let enrolled = vec![crate::speaker::enrolled_voice(
342                "alex",
343                crate::speaker::Role::Owner,
344                OWN.to_vec(),
345            )];
346            let labeler = SegmentLabeler::new(None, Some(Diarizer::new(Voice(voice), enrolled)));
347            let mut session = Session::over(&transcription, &language, recording)
348                .expect("the session starts")
349                .with_reader(SegmentReader::new(labeler))
350                .only_own_voice();
351
352            let mut heard = Vec::new();
353            while let Some(segment) = session.next_heard().expect("every step succeeds") {
354                let stranger = matches!(segment.verdict, Verdict::Stranger);
355                heard.push((segment.segment.into_text(), stranger));
356            }
357            (heard, session.stop().map(Transcript::into_text))
358        };
359
360        let (heard, transcript) = judged(OTHER);
361        assert_eq!(heard, [("one".to_owned(), true)]);
362        assert!(matches!(transcript, Err(TranscriptionError::NoSpeech)));
363
364        let (heard, transcript) = judged(OWN);
365        assert_eq!(heard, [("one".to_owned(), false)]);
366        assert_eq!(transcript.expect("the kept words complete"), "one");
367    }
368
369    #[test]
370    fn a_whole_session_has_only_its_end() {
371        let (_directory, recording) = recorded(&[0; 64]);
372        let transcription = Transcription::complete_samples(Scripted::new(&["hello world"]));
373        let language = Language::Automatic;
374
375        let transcript = Session::over(&transcription, &language, recording)
376            .expect("the session starts")
377            .stop()
378            .expect("the transcript completes");
379
380        assert_eq!(transcript.as_str(), "hello world");
381    }
382}