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
18/// One transcription run over audio that arrives while the run is live.
19///
20/// Constructed over a recording the caller feeds, or straight from the
21/// microphone where one exists. `finish` waits for input to end, drains the
22/// tail, and returns the completed transcript.
23pub struct Session<'a, T, M: Mode, L = ()> {
24    operation: Operation<'a, T, L>,
25    recording: RecordingReader,
26    /// Held so the capture worker outlives the run.
27    #[cfg(all(feature = "microphone", not(target_family = "wasm")))]
28    _microphone: Option<Microphone>,
29    #[cfg(not(target_family = "wasm"))]
30    stopper: Option<Stopper>,
31    mode: PhantomData<M>,
32}
33
34impl<'a, T: Transcriber, M: Mode, L: Layer> Session<'a, T, M, L> {
35    /// Runs against a recording the caller already has. Whoever feeds it also
36    /// ends it; `finish` only waits for that end and finishes the transcript.
37    pub fn over(
38        transcription: &'a Transcription<T, M, L>,
39        language: &'a Language,
40        recording: RecordingReader,
41    ) -> Result<Self, TranscriptionError> {
42        Ok(Self {
43            operation: transcription.start(language)?,
44            recording,
45            #[cfg(all(feature = "microphone", not(target_family = "wasm")))]
46            _microphone: None,
47            #[cfg(not(target_family = "wasm"))]
48            stopper: None,
49            mode: PhantomData,
50        })
51    }
52
53    /// Waits for input to end, drains whatever remains, and returns the
54    /// completed transcript — or [`TranscriptionError::NoSpeech`] when
55    /// nothing was said.
56    pub fn finish(mut self) -> Result<Transcript, TranscriptionError> {
57        #[cfg(not(target_family = "wasm"))]
58        if let Some(stopper) = &self.stopper {
59            stopper.stop();
60        }
61        self.operation.finish(&mut self.recording)
62    }
63}
64
65#[cfg(all(feature = "microphone", not(target_family = "wasm")))]
66impl<'a, T: Transcriber, M: Mode, L: Layer> Session<'a, T, M, L> {
67    /// Opens the microphone and records one utterance until stopped. `finish`
68    /// ends the recording; until then every [`Streaming`] step blocks on
69    /// captured audio.
70    pub fn microphone(
71        transcription: &'a Transcription<T, M, L>,
72        language: &'a Language,
73    ) -> Result<Self, TranscriptionError> {
74        // Recording starts first: `start` can block, and speech during it
75        // must not be lost.
76        let mut microphone = Microphone::open().map_err(TranscriptionError::Capture)?;
77        let (recording, stopper) = microphone
78            .record(RecordingMode::UntilStopped)
79            .map_err(TranscriptionError::Capture)?;
80        let operation = match transcription.start(language) {
81            Ok(operation) => operation,
82            Err(error) => {
83                stopper.stop();
84                return Err(error);
85            }
86        };
87        Ok(Self {
88            operation,
89            recording,
90            _microphone: Some(microphone),
91            stopper: Some(stopper),
92            mode: PhantomData,
93        })
94    }
95}
96
97impl<'a, T: Transcriber, L: Layer> Session<'a, T, Streaming, L> {
98    /// The next committed stretch of words, or `None` once the input has
99    /// ended and the tail is delivered. Committed words are never revised.
100    pub fn next_finalized(&mut self) -> Result<Option<TranscriptSegment>, TranscriptionError> {
101        self.operation.next_finalized(&mut self.recording)
102    }
103}
104
105#[cfg(not(target_family = "wasm"))]
106impl<T, M: Mode, L> Drop for Session<'_, T, M, L> {
107    fn drop(&mut self) {
108        // A session dropped without finish() must not leave the microphone
109        // recording to its ceiling. Stopping an already-stopped input is a
110        // no-op, so this composes with finish().
111        if let Some(stopper) = &self.stopper {
112            stopper.stop();
113        }
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use std::collections::VecDeque;
120    use std::sync::Mutex;
121
122    use super::*;
123    use crate::audio::CANONICAL_SAMPLE_RATE;
124    use crate::audio::testing::{Pace, paced, recorded};
125    use crate::cancel::CancelToken;
126    use crate::transcription::{Cadence, Hypothesis, TranscriptionAudio};
127
128    const STRIDE_MS: usize = 100;
129    const STRIDE: usize = STRIDE_MS * CANONICAL_SAMPLE_RATE as usize / 1000;
130
131    /// Answers each decode with the next entry of its script, releasing the
132    /// next paced batch as it goes.
133    struct Scripted {
134        answers: Mutex<VecDeque<&'static str>>,
135        pace: Option<Pace>,
136    }
137
138    impl Scripted {
139        fn new(answers: &[&'static str]) -> Self {
140            Self {
141                answers: Mutex::new(answers.iter().copied().collect()),
142                pace: None,
143            }
144        }
145
146        fn paced(answers: &[&'static str], pace: Pace) -> Self {
147            Self {
148                answers: Mutex::new(answers.iter().copied().collect()),
149                pace: Some(pace),
150            }
151        }
152    }
153
154    impl Transcriber for Scripted {
155        fn validate_language(
156            &self,
157            _language: &Language,
158            _cancel: &CancelToken,
159        ) -> Result<(), TranscriptionError> {
160            Ok(())
161        }
162
163        fn transcribe_with(
164            &self,
165            _audio: TranscriptionAudio<'_>,
166            _language: &Language,
167            _cancel: &CancelToken,
168        ) -> Result<Hypothesis, TranscriptionError> {
169            if let Some(pace) = &self.pace {
170                pace.advance();
171            }
172            let answer = self
173                .answers
174                .lock()
175                .unwrap_or_else(|poisoned| poisoned.into_inner())
176                .pop_front()
177                .expect("the script covers every decode");
178            Ok(Hypothesis::new(answer))
179        }
180    }
181
182    fn stride_batch() -> Vec<i16> {
183        vec![8_192; STRIDE]
184    }
185
186    #[test]
187    fn a_streaming_session_commits_words_as_their_agreement_holds() {
188        let (_directory, recording, pace) = paced(vec![stride_batch(); 4]);
189        let cadence = Cadence::from_millis(STRIDE_MS, 20_000);
190        let transcription = Transcription::agreement(
191            cadence,
192            Scripted::paced(&["one", "one two", "one two three", "one two three"], pace),
193        );
194        let language = Language::Automatic;
195        let mut session =
196            Session::over(&transcription, &language, recording).expect("the session starts");
197
198        let mut segments = Vec::new();
199        while let Some(segment) = session.next_finalized().expect("every step succeeds") {
200            segments.push(segment.into_text());
201        }
202        let transcript = session.finish().expect("the transcript completes");
203
204        assert_eq!(segments, ["one", "two", "three"]);
205        assert_eq!(transcript.as_str(), "one two three");
206    }
207
208    #[test]
209    fn a_whole_session_has_only_its_end() {
210        let (_directory, recording) = recorded(&[0; 64]);
211        let transcription = Transcription::complete_samples(Scripted::new(&["hello world"]));
212        let language = Language::Automatic;
213
214        let transcript = Session::over(&transcription, &language, recording)
215            .expect("the session starts")
216            .finish()
217            .expect("the transcript completes");
218
219        assert_eq!(transcript.as_str(), "hello world");
220    }
221}