Skip to main content

aria_kit/transcription/
heard.rs

1//! Reads one recording's finalized segments and places each in the
2//! recording, freeing the audio no reader needs any more.
3
4use crate::audio::RecordingReader;
5use crate::transcription::{
6    Layer, Operation, Transcriber, TranscriptSegment, TranscriptUpdate, TranscriptionError,
7};
8
9/// The next finalized segment, or `None` once the recording ends. Decoding,
10/// placing, and freeing the audio behind the decoder have to happen in that
11/// order, so the order lives here rather than in every caller.
12///
13/// Freeing is safe without asking anyone: a second reader of the same
14/// recording holds its own fork and frees its own audio.
15pub fn next_heard<T: Transcriber, L: Layer>(
16    operation: &mut Operation<'_, T, L>,
17    recording: &mut RecordingReader,
18) -> Result<Option<Heard>, TranscriptionError> {
19    loop {
20        match next_heard_update(operation, recording, false)? {
21            Some(HeardUpdate::Finalized(heard)) => return Ok(Some(heard)),
22            Some(HeardUpdate::Provisional { .. }) => {}
23            None => return Ok(None),
24        }
25    }
26}
27
28/// [`next_heard`], with the decoder's provisional text between finalized
29/// segments when `previews` asks for it.
30pub fn next_heard_update<T: Transcriber, L: Layer>(
31    operation: &mut Operation<'_, T, L>,
32    recording: &mut RecordingReader,
33    previews: bool,
34) -> Result<Option<HeardUpdate>, TranscriptionError> {
35    let segment = match operation.next(recording, previews)? {
36        Some(TranscriptUpdate::Finalized(segment)) => segment,
37        Some(TranscriptUpdate::Provisional {
38            text,
39            source_window,
40        }) => {
41            return Ok(Some(HeardUpdate::Provisional {
42                text,
43                source_window,
44            }));
45        }
46        None => return Ok(None),
47    };
48    let span = (segment.word_spans().is_some() || segment.source_window().is_some())
49        .then(|| segment.span())
50        .flatten();
51    let window = span
52        .clone()
53        .or_else(|| segment.source_window())
54        .unwrap_or(0..operation.read_to());
55    recording.release_before(operation.retained_from());
56    Ok(Some(HeardUpdate::Finalized(Heard {
57        segment,
58        span,
59        window,
60    })))
61}
62
63pub enum HeardUpdate {
64    Finalized(Heard),
65    Provisional {
66        text: String,
67        source_window: Option<std::ops::Range<u64>>,
68    },
69}
70
71/// One finalized segment, placed in its recording.
72pub struct Heard {
73    pub segment: TranscriptSegment,
74    /// Where the segment's words sit in the recording's stream, when the
75    /// engine timed them. `None` otherwise — the decoder's own position runs
76    /// ahead of the words it has emitted, so it cannot stand in for theirs.
77    pub span: Option<std::ops::Range<u64>>,
78    /// The audio the segment came from: its words' span when timed, else
79    /// the decoder's source window.
80    pub window: std::ops::Range<u64>,
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use crate::audio::CANONICAL_SAMPLE_RATE;
87
88    #[test]
89    fn preview_reads_preserve_finalized_segments_and_completion() {
90        use crate::audio::testing::{Pace, paced};
91        use crate::cancel::CancelToken;
92        use crate::transcription::{
93            Cadence, Hypothesis, Language, TimedWord, Transcription, TranscriptionAudio,
94        };
95        use std::sync::Mutex;
96
97        struct Script {
98            pace: Pace,
99            calls: Mutex<usize>,
100            timed: bool,
101        }
102        impl Transcriber for Script {
103            fn validate_language(
104                &self,
105                _: &Language,
106                _: &CancelToken,
107            ) -> Result<(), TranscriptionError> {
108                Ok(())
109            }
110            fn transcribe_with(
111                &self,
112                audio: TranscriptionAudio<'_>,
113                _: &Language,
114                _: &CancelToken,
115            ) -> Result<Hypothesis, TranscriptionError> {
116                let mut calls = self.calls.lock().expect("no poison");
117                let text = ["one two", "one two three", "three four", "three four five"][*calls];
118                *calls += 1;
119                self.pace.advance();
120                if self.timed {
121                    let duration = audio.require_samples("script")?.len() as f32
122                        / CANONICAL_SAMPLE_RATE as f32;
123                    let words = text.split_whitespace().count();
124                    Ok(Hypothesis::with_words(
125                        text,
126                        (0..words)
127                            .map(|index| TimedWord {
128                                start: duration * index as f32 / words as f32,
129                                end: duration * (index + 1) as f32 / words as f32,
130                            })
131                            .collect(),
132                    ))
133                } else {
134                    Ok(Hypothesis::new(text))
135                }
136            }
137        }
138        let run = |previews, timed| {
139            let (_directory, mut recording, pace) = paced(vec![vec![8192_i16; 1600]; 4]);
140            let transcription = Transcription::agreement(
141                Cadence::from_millis(100, 200),
142                Script {
143                    pace,
144                    calls: Mutex::new(0),
145                    timed,
146                },
147            );
148            let mut operation = transcription.start(&Language::Automatic).expect("starts");
149            let mut rows = Vec::new();
150            let mut preview_count = 0;
151            loop {
152                let update = if previews {
153                    next_heard_update(&mut operation, &mut recording, true)
154                } else {
155                    next_heard(&mut operation, &mut recording)
156                        .map(|heard| heard.map(HeardUpdate::Finalized))
157                }
158                .expect("reads");
159                match update {
160                    Some(HeardUpdate::Finalized(heard)) => {
161                        rows.push((heard.segment.into_text(), heard.window));
162                    }
163                    Some(HeardUpdate::Provisional { .. }) => preview_count += 1,
164                    None => break,
165                }
166            }
167            let final_text = operation
168                .finish(&mut recording)
169                .expect("transcript")
170                .into_text();
171            (rows, final_text, preview_count)
172        };
173        for timed in [false, true] {
174            let (baseline, baseline_text, _) = run(false, timed);
175            let (previewed, preview_text, count) = run(true, timed);
176            assert!(count > 0);
177            assert_eq!(previewed, baseline);
178            assert_eq!(preview_text, baseline_text);
179        }
180    }
181
182    #[test]
183    fn delayed_untimed_words_keep_the_entire_decode_window() {
184        use crate::audio::testing::{Pace, paced};
185        use crate::cancel::CancelToken;
186        use crate::transcription::{
187            Cadence, Hypothesis, Language, Transcription, TranscriptionAudio,
188        };
189        use std::sync::Mutex;
190
191        struct Script {
192            pace: Pace,
193            calls: Mutex<usize>,
194        }
195        impl Transcriber for Script {
196            fn validate_language(
197                &self,
198                _: &Language,
199                _: &CancelToken,
200            ) -> Result<(), TranscriptionError> {
201                Ok(())
202            }
203
204            fn transcribe_with(
205                &self,
206                _: TranscriptionAudio<'_>,
207                _: &Language,
208                _: &CancelToken,
209            ) -> Result<Hypothesis, TranscriptionError> {
210                let mut calls = self.calls.lock().unwrap();
211                let answers = [
212                    "one two",
213                    "one two",
214                    "one two three four",
215                    "one two three four",
216                    "one two three four five six",
217                    "one two three four five six",
218                    "one two three four five six guest phrase",
219                    "one two three four five six guest phrase",
220                ];
221                let text = answers[*calls];
222                *calls += 1;
223                self.pace.advance();
224                Ok(Hypothesis::new(text))
225            }
226        }
227        let second = CANONICAL_SAMPLE_RATE as usize;
228        let (_directory, mut recording, pace) = paced(vec![vec![8192_i16; second]; 8]);
229        let transcription = Transcription::agreement(
230            Cadence::from_millis(1_000, 20_000),
231            Script {
232                pace,
233                calls: Mutex::new(0),
234            },
235        );
236        let mut operation = transcription.start(&Language::Automatic).unwrap();
237        let mut heard = Vec::new();
238        while let Some(segment) = next_heard(&mut operation, &mut recording).unwrap() {
239            heard.push(segment);
240        }
241        assert_eq!(
242            heard
243                .iter()
244                .map(|heard| heard.segment.as_str())
245                .collect::<Vec<_>>(),
246            ["one two", "three four", "five six", "guest phrase"]
247        );
248        let guest = heard.last().unwrap();
249        assert_eq!(guest.segment.span(), None);
250        assert_eq!(guest.segment.word_spans(), None);
251        assert_eq!(guest.segment.source_window(), Some(0..8 * second as u64));
252        assert_eq!(guest.window, 0..8 * second as u64);
253    }
254}