use std::time::Duration;
use crate::audio::{RecordingReader, canonical_duration, canonical_samples};
use crate::cancel::CancelToken;
use super::{
ChunkStep, Hypothesis, Transcript, TranscriptSegment, TranscriptUpdate, TranscriptionError,
check_cancelled,
};
#[derive(Clone, Copy)]
pub struct Cadence {
stride_samples: usize,
max_buffer_samples: usize,
}
impl Cadence {
pub const fn from_millis(stride_millis: usize, max_buffer_millis: usize) -> Self {
assert!(stride_millis > 0, "a stride must advance the buffer");
assert!(
stride_millis < max_buffer_millis,
"a buffer must hold more than one stride"
);
Self {
stride_samples: canonical_samples(Duration::from_millis(stride_millis as u64)) as usize,
max_buffer_samples: canonical_samples(Duration::from_millis(max_buffer_millis as u64))
as usize,
}
}
pub const fn stride_samples(&self) -> usize {
self.stride_samples
}
pub const fn max_buffer_samples(&self) -> usize {
self.max_buffer_samples
}
}
const TRIM_FRAME_SAMPLES: usize = 512;
const TRIM_SILENCE_FRAMES: usize = 13;
const TRIM_QUIET_FRACTION: f32 = 0.05;
const TOLERATED_LOSS: usize = 2;
const HEARD_MARGIN_FRAMES: usize = 3;
const UNHEARD_SPEECH_FRAMES: usize = 16;
const CUT_OVERLAP_SAMPLES: u64 = canonical_samples(Duration::from_millis(240));
const CARRIED_WORDS: usize = 4;
const REPEAT_REACH: usize = 2;
const CRAMMED_WORDS: usize = 8;
const RESTORE_ANCHOR_WORDS: usize = 4;
const RESTORE_MIN_MISSING_WORDS: usize = 3;
const RESTORE_TIMING_TOLERANCE_SAMPLES: u64 = canonical_samples(Duration::from_millis(240));
const RESTORE_MARGIN_SAMPLES: u64 = canonical_samples(Duration::from_millis(96));
pub struct AgreementState {
cadence: Cadence,
buffer_start: u64,
decoded_end: u64,
pause_level: Option<f32>,
agreement: Agreement,
transcript: Vec<String>,
pending_segment: Option<TranscriptSegment>,
pending_preview: Option<String>,
last_preview: String,
finished: bool,
}
impl AgreementState {
pub(super) fn new(cadence: Cadence) -> Self {
Self {
cadence,
buffer_start: 0,
decoded_end: 0,
pause_level: None,
agreement: Agreement::default(),
transcript: Vec::new(),
pending_segment: None,
pending_preview: None,
last_preview: String::new(),
finished: false,
}
}
pub(super) fn retained_from(&self) -> u64 {
let pending = self
.pending_segment
.as_ref()
.and_then(|segment| segment.span())
.map(|span| span.start);
match pending {
Some(start) => start.min(self.buffer_start),
None => self.buffer_start,
}
}
#[cfg(test)]
pub(super) fn next_finalized_with(
&mut self,
recording: &mut RecordingReader,
cancel: &CancelToken,
decode: impl FnMut(&[f32]) -> Result<Hypothesis, TranscriptionError>,
) -> Result<Option<ChunkStep>, TranscriptionError> {
self.next_with(recording, cancel, false, decode)
}
pub(super) fn next_with(
&mut self,
recording: &mut RecordingReader,
cancel: &CancelToken,
previews: bool,
mut decode: impl FnMut(&[f32]) -> Result<Hypothesis, TranscriptionError>,
) -> Result<Option<ChunkStep>, TranscriptionError> {
if previews {
check_cancelled(cancel)?;
}
if let Some(segment) = self.pending_segment.take() {
return Ok(Some(ChunkStep {
update: Some(TranscriptUpdate::Finalized(segment)),
transcript: None,
}));
}
if let Some(preview) = self.pending_preview.take()
&& previews
{
return Ok(Some(ChunkStep {
update: Some(TranscriptUpdate::Provisional(preview)),
transcript: None,
}));
}
if self.finished {
return Ok(None);
}
loop {
let step = match self.await_stride(recording, cancel)? {
Some(available) => self.step(recording, available, cancel, &mut decode)?,
None => self.finish_after(None)?,
};
if previews {
let preview = self.agreement.preview();
let finalized = step.as_ref().is_some_and(|step| {
matches!(step.update, Some(TranscriptUpdate::Finalized(_)))
});
if preview != self.last_preview || (finalized && !preview.is_empty()) {
self.last_preview.clone_from(&preview);
if finalized {
self.pending_preview = Some(preview);
} else {
let mut step = step.unwrap_or(ChunkStep {
update: None,
transcript: None,
});
step.update = Some(TranscriptUpdate::Provisional(preview));
return Ok(Some(step));
}
}
}
if step.is_some() {
return Ok(step);
}
}
}
fn await_stride(
&self,
recording: &mut RecordingReader,
cancel: &CancelToken,
) -> Result<Option<u64>, TranscriptionError> {
check_cancelled(cancel)?;
recording
.fill_through(self.decoded_end + self.cadence.stride_samples as u64)
.map_err(TranscriptionError::Capture)?;
check_cancelled(cancel)?;
let available = recording.held_to();
if available == self.decoded_end && recording.reached_end() {
return Ok(None);
}
Ok(Some(available))
}
fn step(
&mut self,
recording: &RecordingReader,
available: u64,
cancel: &CancelToken,
decode: &mut impl FnMut(&[f32]) -> Result<Hypothesis, TranscriptionError>,
) -> Result<Option<ChunkStep>, TranscriptionError> {
let behind =
available.saturating_sub(self.decoded_end + self.cadence.stride_samples as u64);
self.decoded_end = available;
let decoded = available - self.buffer_start;
let buffer = recording.span(self.buffer_start..available);
let hypothesis = decode(buffer)?;
check_cancelled(cancel)?;
let loudness = Loudness::of(buffer);
let heard = self.hear(&hypothesis, &loudness);
let heard_count = heard.len();
let skipped = self.skipped_speech(&loudness, &heard);
let (confirmed, settled) = self.agreement.agree(heard, skipped);
let confirmed = self.commit(confirmed);
if recording.reached_end() {
return self.finish_after(confirmed);
}
let skipped = skipped.map(|at| at - self.buffer_start);
let tail = self.trim_when_due(&loudness, available, settled);
let segment = self.one_at_a_time(confirmed, tail);
log_step(
decoded,
behind,
heard_count,
skipped,
segment.as_ref(),
settled,
);
Ok(segment.map(|segment| ChunkStep {
update: Some(TranscriptUpdate::Finalized(segment)),
transcript: None,
}))
}
fn hear(&self, hypothesis: &Hypothesis, loudness: &Loudness) -> Vec<Word> {
if self
.pause_level
.is_some_and(|level| loudness.loudest < level)
{
tracing::debug!("buffer holds only the pause; decode ignored");
return Vec::new();
}
let words = self.words_of(hypothesis);
if crammed(&words) {
tracing::debug!(words = words.len(), "decode invented words");
return Vec::new();
}
words
}
fn words_of(&self, hypothesis: &Hypothesis) -> Vec<Word> {
let texts: Vec<&str> = hypothesis.as_str().split_whitespace().collect();
let timed = hypothesis.words().len() == texts.len();
texts
.iter()
.enumerate()
.map(|(index, text)| Word {
text: (*text).to_owned(),
span: timed.then(|| hypothesis.words()[index].span_from(self.buffer_start)),
})
.collect()
}
fn commit(&mut self, segment: Option<TranscriptSegment>) -> Option<TranscriptSegment> {
if let Some(segment) = &segment {
self.transcript.push(segment.as_str().to_owned());
}
segment
}
fn trim_when_due(
&mut self,
loudness: &Loudness,
available: u64,
settled: bool,
) -> Option<TranscriptSegment> {
if settled {
if let Some(pause) = loudness.pause_cut() {
self.pause_level = Some(loudness.quiet);
return self.trim(self.buffer_start + pause as u64, None);
}
}
if available - self.buffer_start >= self.cadence.max_buffer_samples as u64 {
return self.cap_cut(loudness, available);
}
None
}
fn cap_cut(&mut self, loudness: &Loudness, available: u64) -> Option<TranscriptSegment> {
let skipped = self.skipped_speech(loudness, &self.agreement.candidate);
let tail = self.agreement.take_tail(skipped);
let tail = self.commit(tail);
let cut = skipped
.or(self.agreement.delivered_through)
.map(|through| through.saturating_sub(CUT_OVERLAP_SAMPLES))
.filter(|&through| through > self.buffer_start)
.map_or(available, |through| through.min(available));
self.trim(cut, tail)
}
fn skipped_speech(&self, loudness: &Loudness, heard: &[Word]) -> Option<u64> {
let from = self
.agreement
.delivered_through
.unwrap_or(self.buffer_start)
.max(self.buffer_start + self.cadence.stride_samples as u64);
loudness.unheard_speech(self.buffer_start, heard, from)
}
fn trim(&mut self, cut: u64, tail: Option<TranscriptSegment>) -> Option<TranscriptSegment> {
self.buffer_start = cut;
self.agreement = self.agreement.next_buffer();
tail
}
fn one_at_a_time(
&mut self,
first: Option<TranscriptSegment>,
second: Option<TranscriptSegment>,
) -> Option<TranscriptSegment> {
match (first, second) {
(Some(first), Some(second)) => {
self.pending_segment = Some(second);
Some(first)
}
(first, second) => first.or(second),
}
}
fn finish_after(
&mut self,
committed: Option<TranscriptSegment>,
) -> Result<Option<ChunkStep>, TranscriptionError> {
let tail = self.agreement.take_tail(None);
let tail = self.commit(tail);
let words = std::mem::take(&mut self.transcript).join(" ");
let transcript = (!words.is_empty())
.then(|| Transcript::new(words))
.transpose()?;
self.finished = true;
Ok(Some(ChunkStep {
update: self
.one_at_a_time(committed, tail)
.map(TranscriptUpdate::Finalized),
transcript,
}))
}
}
#[derive(Default)]
struct Agreement {
candidate: Vec<Word>,
emitted: usize,
delivered: Vec<Word>,
delivered_through: Option<u64>,
}
impl Agreement {
fn next_buffer(&mut self) -> Self {
let carried = self.delivered.len().saturating_sub(CARRIED_WORDS);
Self {
delivered: self.delivered.split_off(carried),
delivered_through: self.delivered_through,
..Self::default()
}
}
fn agree(
&mut self,
mut current: Vec<Word>,
skipped: Option<u64>,
) -> (Option<TranscriptSegment>, bool) {
if current.is_empty() {
return (None, false);
}
let retained_prefix_len = self.missing_prefix_len(¤t);
if let Some(count) = retained_prefix_len {
let mut retained = self.candidate[..count].to_vec();
retained.append(&mut current);
current = retained;
}
if self.dropped_window(¤t) {
return (None, false);
}
let Some((start, emitted)) = self.placed(¤t) else {
return (None, false);
};
let agreed = start
+ if retained_prefix_len.is_some() {
0
} else {
lcp(
&self.candidate[self.emitted.min(self.candidate.len())..],
¤t[start..],
)
};
let deliverable = match skipped {
Some(at) => heard_before(¤t[..agreed], at),
None => agreed,
};
let confirmed = if deliverable > start {
self.deliver(¤t[start..deliverable])
} else {
None
};
self.emitted = deliverable.max(emitted);
let settled =
agreed == current.len() && current.len() == self.candidate.len() && skipped.is_none();
self.candidate = current;
(confirmed, settled)
}
fn missing_prefix_len(&self, current: &[Word]) -> Option<usize> {
let anchor = current.get(..RESTORE_ANCHOR_WORDS)?;
let opening = anchor[0].span.as_ref()?;
for offset in self.emitted + RESTORE_MIN_MISSING_WORDS..self.candidate.len() {
let Some(previous) = self.candidate.get(offset..offset + RESTORE_ANCHOR_WORDS) else {
break;
};
if lcp(previous, anchor) != RESTORE_ANCHOR_WORDS {
continue;
}
let same_audio = previous.iter().zip(anchor).all(|(old, new)| {
old.span
.as_ref()
.zip(new.span.as_ref())
.is_some_and(|(old, new)| {
old.start.abs_diff(new.start) <= RESTORE_TIMING_TOLERANCE_SAMPLES
})
});
let uncovered = self.candidate[self.emitted..offset].iter().all(|word| {
word.span
.as_ref()
.is_some_and(|span| span.end <= opening.start + RESTORE_MARGIN_SAMPLES)
});
if same_audio && uncovered {
return Some(offset);
}
}
None
}
fn placed(&self, current: &[Word]) -> Option<(usize, usize)> {
let least = if self.emitted > 0 { 2 } else { 1 };
let (start, emitted) = match self.aligned(current) {
Some(start) if self.emitted > 0 => (start, self.emitted),
_ => {
let repeated = self.repeated(current, least)?;
(repeated, repeated)
}
};
if start == 0 && self.emitted > 0 {
tracing::debug!(
heard = self.candidate.len(),
current = current.len(),
"decode lost its place"
);
return None;
}
Some((start, emitted))
}
fn repeated(&self, words: &[Word], least: usize) -> Option<usize> {
let carried = &self.delivered[self.delivered.len().saturating_sub(CARRIED_WORDS)..];
for count in (least..=carried.len().min(words.len())).rev() {
for at in (0..=carried.len() - count).rev() {
if let Some(end) = self.opening_run(&carried[at..at + count], words) {
let rest = carried.len() - at - count;
return (end + rest <= words.len()).then_some(end + rest);
}
}
}
Some(0)
}
fn opening_run(&self, run: &[Word], words: &[Word]) -> Option<usize> {
let count = run.len();
let reach = if count < CARRIED_WORDS {
count + REPEAT_REACH
} else {
words.len()
};
let before_delivered_end = |word: &Word, edge: fn(&std::ops::Range<u64>) -> u64| match (
&word.span,
self.delivered_through,
) {
(Some(span), Some(through)) => edge(span) < through,
(None, _) => true,
_ => false,
};
let last_delivered = |word: &Word| {
self.delivered
.last()
.is_some_and(|last| spoken(&last.text) == spoken(&word.text))
};
(count..=reach.min(words.len())).find(|&end| {
let first = &words[end - count];
let last = &words[end - 1];
lcp(run, &words[end - count..]) == count
&& (count >= CARRIED_WORDS || before_delivered_end(first, |span| span.start))
&& (count > 1
|| last_delivered(last)
|| before_delivered_end(last, |span| span.end))
})
}
fn aligned(&self, words: &[Word]) -> Option<usize> {
let compared = self.emitted.min(words.len()).min(self.candidate.len());
let revised = self.candidate[..compared]
.iter()
.zip(&words[..compared])
.filter(|(heard, word)| spoken(&heard.text) != spoken(&word.text))
.count();
(revised <= TOLERATED_LOSS).then_some(self.emitted.min(words.len()))
}
fn dropped_window(&self, current: &[Word]) -> bool {
let lost = self.candidate.len().saturating_sub(current.len());
if lost > TOLERATED_LOSS {
tracing::debug!(
heard = self.candidate.len(),
current = current.len(),
"decode dropped part of its window"
);
}
lost > TOLERATED_LOSS
}
fn deliver(&mut self, words: &[Word]) -> Option<TranscriptSegment> {
self.delivered_through = ends_at(words, self.delivered_through);
self.delivered.extend_from_slice(words);
segment_of(words)
}
fn tail_range(&self, skipped: Option<u64>) -> std::ops::Range<usize> {
let undelivered = self
.repeated(&self.candidate, 1)
.unwrap_or(self.candidate.len())
.max(self.emitted.min(self.candidate.len()));
let through = match skipped {
Some(at) => heard_before(&self.candidate, at).max(undelivered),
None => self.candidate.len(),
};
undelivered..through
}
fn preview(&self) -> String {
self.candidate[self.tail_range(None)]
.iter()
.map(|word| word.text.as_str())
.collect::<Vec<_>>()
.join(" ")
}
fn take_tail(&mut self, skipped: Option<u64>) -> Option<TranscriptSegment> {
let range = self.tail_range(skipped);
self.emitted = range.end;
let tail: Vec<Word> = self.candidate.drain(range).collect();
self.deliver(&tail)
}
}
#[derive(Clone)]
struct Word {
text: String,
span: Option<std::ops::Range<u64>>,
}
fn segment_of(words: &[Word]) -> Option<TranscriptSegment> {
let texts: Vec<String> = words.iter().map(|word| word.text.clone()).collect();
let span = match (words.first(), words.last()) {
(
Some(Word {
span: Some(first), ..
}),
Some(Word {
span: Some(last), ..
}),
) => Some(first.start..last.end.max(first.start)),
_ => None,
};
TranscriptSegment::spanned(&texts, span)
}
fn lcp(previous: &[Word], current: &[Word]) -> usize {
previous
.iter()
.zip(current)
.take_while(|(left, right)| spoken(&left.text) == spoken(&right.text))
.count()
}
fn spoken(word: &str) -> String {
word.chars()
.filter(|character| character.is_alphanumeric())
.flat_map(char::to_lowercase)
.collect()
}
fn log_step(
decoded: u64,
behind: u64,
heard: usize,
skipped: Option<u64>,
segment: Option<&TranscriptSegment>,
settled: bool,
) {
let millis = |samples: u64| canonical_duration(samples).as_millis();
tracing::debug!(
buffer_ms = millis(decoded),
behind_ms = millis(behind),
heard,
skipped_ms = skipped.map(millis),
words = segment.map_or(0, |segment| segment.as_str().split_whitespace().count()),
settled,
"decode step"
);
}
fn crammed(words: &[Word]) -> bool {
words.windows(CRAMMED_WORDS).any(|run| {
let (Some(first), Some(last)) = (&run[0].span, &run[run.len() - 1].span) else {
return false;
};
last.end.saturating_sub(first.start) < canonical_samples(Duration::from_secs(1))
})
}
fn heard_before(words: &[Word], at: u64) -> usize {
words
.iter()
.take_while(|word| word.span.as_ref().is_none_or(|span| span.end <= at))
.count()
}
fn ends_at(words: &[Word], before: Option<u64>) -> Option<u64> {
let Some(last) = words.last() else {
return before;
};
last.span
.as_ref()
.map(|span| span.end.max(before.unwrap_or(0)))
}
struct Loudness {
squares: Vec<f32>,
loudest: f32,
quiet: f32,
}
impl Loudness {
fn of(buffer: &[f32]) -> Self {
let squares: Vec<f32> = buffer
.chunks(TRIM_FRAME_SAMPLES)
.filter(|frame| frame.len() == TRIM_FRAME_SAMPLES)
.map(|frame| {
frame.iter().map(|sample| sample * sample).sum::<f32>() / TRIM_FRAME_SAMPLES as f32
})
.collect();
let loudest = squares.iter().copied().fold(0.0_f32, f32::max);
Self {
squares,
loudest,
quiet: loudest * TRIM_QUIET_FRACTION,
}
}
fn is_loud(&self, frame: usize) -> bool {
self.squares[frame] > self.quiet
}
fn pause_cut(&self) -> Option<usize> {
let quiet = (0..self.squares.len())
.rev()
.take_while(|&frame| !self.is_loud(frame))
.count();
if quiet < TRIM_SILENCE_FRAMES {
return None;
}
let start = self.squares.len() - quiet;
Some((start + quiet / 2) * TRIM_FRAME_SAMPLES)
}
fn unheard_speech(&self, buffer_start: u64, words: &[Word], from: u64) -> Option<u64> {
let frame =
|position: u64| position.saturating_sub(buffer_start) as usize / TRIM_FRAME_SAMPLES;
let mut heard = vec![false; self.squares.len()];
for word in words {
let span = word.span.as_ref()?;
let first = frame(span.start).saturating_sub(HEARD_MARGIN_FRAMES);
let last = (frame(span.end) + HEARD_MARGIN_FRAMES).min(heard.len());
heard[first.min(last)..last].fill(true);
}
let mut run = 0;
for (index, &heard) in heard.iter().enumerate().skip(frame(from)) {
run = if self.is_loud(index) && !heard {
run + 1
} else {
0
};
if run > UNHEARD_SPEECH_FRAMES {
let first = index + 1 - run;
return Some(buffer_start + (first * TRIM_FRAME_SAMPLES) as u64);
}
}
None
}
}
#[cfg(test)]
mod tests {
use std::collections::VecDeque;
use super::*;
use crate::audio::CANONICAL_SAMPLE_RATE;
use crate::audio::testing::{Pace, paced, recorded};
use crate::transcription::TimedWord;
const STRIDE_MS: usize = 100;
const STRIDE: usize = STRIDE_MS * CANONICAL_SAMPLE_RATE as usize / 1000;
fn cadence(max_buffer_ms: usize) -> Cadence {
Cadence::from_millis(STRIDE_MS, max_buffer_ms)
}
fn loud_batch() -> Vec<i16> {
vec![8_192; STRIDE]
}
fn quiet_batch() -> Vec<i16> {
vec![0; STRIDE]
}
struct Run {
segments: Vec<String>,
final_transcript: Option<String>,
windows: Vec<usize>,
steps: usize,
}
impl Run {
fn transcript(&self) -> &str {
self.final_transcript
.as_deref()
.expect("the run finishes with a transcript")
}
}
fn run(
mut state: AgreementState,
recording: &mut RecordingReader,
pace: Option<&Pace>,
mut decode: impl FnMut(&[f32]) -> Hypothesis,
mut inspect: impl FnMut(&AgreementState),
) -> Run {
let cancel = CancelToken::new();
let mut run = Run {
segments: Vec::new(),
final_transcript: None,
windows: Vec::new(),
steps: 0,
};
loop {
let mut heard = |window: &[f32]| {
run.windows.push(window.len());
if let Some(pace) = pace {
pace.advance();
}
Ok(decode(window))
};
let step = state
.next_finalized_with(recording, &cancel, &mut heard)
.expect("every step succeeds");
inspect(&state);
let Some(step) = step else { break };
run.steps += 1;
if let Some(TranscriptUpdate::Finalized(segment)) = step.update {
run.segments.push(segment.into_text());
}
if let Some(transcript) = step.transcript {
run.final_transcript = Some(transcript.into_text());
}
}
run
}
#[test]
fn previews_replace_the_accepted_tail_and_drain_after_final_segments() {
let answers = ["turn lift", "turn left now", "", "turn left now"];
let (_directory, mut recording, pace) = paced(vec![loud_batch(); answers.len()]);
let mut state = AgreementState::new(cadence(20_000));
let mut decode = scripted(&answers);
let mut events = Vec::new();
while let Some(step) = state
.next_with(&mut recording, &CancelToken::never(), true, |samples| {
pace.advance();
Ok(decode(samples))
})
.expect("script succeeds")
{
match step.update {
Some(TranscriptUpdate::Finalized(segment)) => {
events.push(format!("final:{}", segment.as_str()));
}
Some(TranscriptUpdate::Provisional(preview)) => {
assert!(state.pending_segment.is_none());
events.push(format!("preview:{preview}"));
}
None => {}
}
}
assert_eq!(
events,
[
"preview:turn lift",
"final:turn",
"preview:left now",
"final:left now",
"preview:",
]
);
}
#[test]
fn consuming_previews_preserves_decodes_segments_and_completion_through_trims() {
for (cap, batches, answers) in [
(
20_000,
vec![loud_batch(); 3],
vec!["one two", "one two three", "one two three four"],
),
(
200,
vec![loud_batch(); 4],
vec!["one two", "one two three", "three four", "three four five"],
),
(
20_000,
vec![quiet_batch(); 7],
vec![
"one two", "one two", "one two", "one two", "one two", "one two", "one two",
],
),
(
20_000,
vec![loud_batch(); 4],
vec!["one two three", "one two", "", "one two four"],
),
] {
let baseline = drive(cap, batches.clone(), &answers);
let (_directory, mut recording, pace) = paced(batches);
let mut state = AgreementState::new(cadence(cap));
let mut decode = scripted(&answers);
let mut segments = Vec::new();
let mut windows = Vec::new();
let mut completed = None;
while let Some(step) = state
.next_with(&mut recording, &CancelToken::never(), true, |samples| {
windows.push(samples.len());
pace.advance();
Ok(decode(samples))
})
.expect("script succeeds")
{
if matches!(step.update, Some(TranscriptUpdate::Provisional(_))) {
assert!(state.pending_segment.is_none());
}
if let Some(TranscriptUpdate::Finalized(segment)) = step.update {
segments.push(segment.into_text());
}
if let Some(transcript) = step.transcript {
completed = Some(transcript.into_text());
}
}
assert_eq!(segments, baseline.segments);
assert_eq!(windows, baseline.windows);
assert_eq!(completed, baseline.final_transcript);
}
}
#[test]
fn a_repeated_word_tail_is_republished_after_commitment() {
let (_directory, mut recording, pace) = paced(vec![loud_batch(); 3]);
let mut state = AgreementState::new(cadence(20_000));
let mut decode = scripted(&["go", "go go", "go go home"]);
let mut updates = Vec::new();
for _ in 0..3 {
let step = state
.next_with(&mut recording, &CancelToken::never(), true, |samples| {
pace.advance();
Ok(decode(samples))
})
.expect("reads")
.expect("update");
updates.push(match step.update {
Some(TranscriptUpdate::Finalized(segment)) => (Some(segment.into_text()), None),
Some(TranscriptUpdate::Provisional(preview)) => (None, Some(preview)),
None => (None, None),
});
}
assert_eq!(
updates,
[
(None, Some("go".to_owned())),
(Some("go".to_owned()), None),
(None, Some("go".to_owned())),
]
);
}
#[test]
fn cancellation_does_not_deliver_a_queued_preview() {
let (_directory, mut recording, pace) = paced(vec![loud_batch(); 3]);
let mut state = AgreementState::new(cadence(20_000));
let cancel = CancelToken::new();
let mut decode = scripted(&["one two", "one two three", "one two three"]);
for _ in 0..2 {
state
.next_with(&mut recording, &cancel, true, |samples| {
pace.advance();
Ok(decode(samples))
})
.expect("script succeeds");
}
assert!(state.pending_preview.is_some());
cancel.cancel();
assert!(matches!(
state.next_with(&mut recording, &cancel, true, |_| {
panic!("cancelled operation cannot decode")
}),
Err(TranscriptionError::Cancelled)
));
}
fn scripted(answers: &[&str]) -> impl FnMut(&[f32]) -> Hypothesis {
let mut script: VecDeque<String> =
answers.iter().map(|answer| answer.to_string()).collect();
move |_| Hypothesis::new(script.pop_front().expect("the script covers every decode"))
}
fn drive(max_buffer_ms: usize, batches: Vec<Vec<i16>>, answers: &[&str]) -> Run {
let (_directory, mut recording, pace) = paced(batches);
run(
AgreementState::new(cadence(max_buffer_ms)),
&mut recording,
Some(&pace),
scripted(answers),
|_| {},
)
}
fn drive_whole(samples: usize, max_buffer_ms: usize, answers: &[&str]) -> Run {
let (_directory, mut recording) = recorded(&vec![0; samples]);
run(
AgreementState::new(cadence(max_buffer_ms)),
&mut recording,
None,
scripted(answers),
|_| {},
)
}
mod agreement {
use super::*;
fn heard(list: &[&str]) -> Vec<Word> {
list.iter()
.map(|word| Word {
text: word.to_string(),
span: None,
})
.collect()
}
fn text_of(segment: Option<TranscriptSegment>) -> Option<String> {
segment.map(TranscriptSegment::into_text)
}
fn opening_loss() -> [Vec<Word>; 2] {
let decodes: [Vec<(String, std::ops::Range<u64>)>; 2] =
serde_json::from_str(include_str!("opening-loss.json")).unwrap();
decodes.map(|words| {
words
.into_iter()
.map(|(text, span)| Word {
text,
span: Some(span),
})
.collect()
})
}
#[test]
fn an_omitted_opening_and_new_final_words_both_survive() {
let [previous, current] = opening_loss();
let suffix = text_of(segment_of(¤t)).unwrap();
let expected = format!("And to spread abroad its widening tale. {suffix}");
for repetitions in [1, 2] {
let mut agreement = Agreement::default();
agreement.agree(previous.clone(), None);
for _ in 0..repetitions {
let (segment, settled) = agreement.agree(current.clone(), None);
assert!(
segment.is_none(),
"retained words are not a second observation"
);
assert!(!settled);
}
assert_eq!(
text_of(agreement.take_tail(None)).as_deref(),
Some(expected.as_str())
);
}
}
#[test]
fn a_later_complete_decode_can_correct_the_retained_opening() {
let [previous, current] = opening_loss();
let mut recovered = previous[..7].to_vec();
recovered[6].text = "tail.".to_owned();
recovered.extend(current.clone());
let expected = text_of(segment_of(&recovered)).unwrap();
let mut agreement = Agreement::default();
agreement.agree(previous, None);
agreement.agree(current, None);
let (confirmed, _) = agreement.agree(recovered, None);
let actual = format!(
"{} {}",
text_of(confirmed).unwrap(),
text_of(agreement.take_tail(None)).unwrap()
);
assert_eq!(actual, expected);
}
#[test]
fn a_revision_covering_the_earlier_audio_replaces_the_opening() {
let [previous, mut current] = opening_loss();
let shift =
current[0].span.as_ref().unwrap().start - previous[0].span.as_ref().unwrap().start;
for word in &mut current {
let span = word.span.as_mut().unwrap();
span.start -= shift;
span.end -= shift;
}
let expected = text_of(segment_of(¤t));
let mut agreement = Agreement::default();
agreement.agree(previous, None);
agreement.agree(current.clone(), None);
let (confirmed, settled) = agreement.agree(current, None);
assert_eq!(text_of(confirmed), expected);
assert!(settled);
}
#[test]
fn an_untimed_revision_does_not_restore_an_opening() {
for previous_untimed in [false, true] {
let [mut previous, mut current] = opening_loss();
for word in if previous_untimed {
&mut previous
} else {
&mut current
} {
word.span = None;
}
let expected = text_of(segment_of(¤t));
let mut agreement = Agreement::default();
agreement.agree(previous, None);
agreement.agree(current.clone(), None);
let (confirmed, _) = agreement.agree(current, None);
assert_eq!(text_of(confirmed), expected);
}
}
#[test]
fn a_delivered_opening_is_not_repeated() {
let [previous, current] = opening_loss();
let suffix = text_of(segment_of(¤t)).unwrap();
let expected = format!("And to spread abroad its widening tale. {suffix}");
let mut agreement = Agreement::default();
agreement.agree(previous.clone(), None);
let (first, _) = agreement.agree(previous, None);
agreement.agree(current.clone(), None);
let (second, _) = agreement.agree(current, None);
assert_eq!(
format!("{} {}", text_of(first).unwrap(), text_of(second).unwrap()),
expected
);
}
#[test]
fn agreement_delivers_a_confirmed_prefix_once() {
let mut agreement = Agreement::default();
let (first, settled) = agreement.agree(heard(&["one", "two"]), None);
assert_eq!(text_of(first), None, "one decode confirms nothing");
assert!(!settled);
let (second, settled) = agreement.agree(heard(&["one", "two", "three"]), None);
assert_eq!(text_of(second).as_deref(), Some("one two"));
assert!(!settled, "a decode that grew has not settled");
let (third, settled) = agreement.agree(heard(&["one", "two", "three"]), None);
assert_eq!(text_of(third).as_deref(), Some("three"));
assert!(settled, "two identical decodes settle");
assert_eq!(text_of(agreement.take_tail(None)), None);
}
#[test]
fn an_empty_decode_keeps_the_last_one_in_force() {
let mut agreement = Agreement::default();
agreement.agree(heard(&["one", "two"]), None);
let (confirmed, settled) = agreement.agree(heard(&[]), None);
assert_eq!(text_of(confirmed), None);
assert!(!settled);
assert_eq!(
text_of(agreement.take_tail(None)).as_deref(),
Some("one two")
);
}
#[test]
fn lcp_stops_at_the_first_disagreement_ignoring_case_and_punctuation() {
assert_eq!(lcp(&[], &heard(&["any"])), 0);
assert_eq!(lcp(&heard(&["Hello,"]), &heard(&["hello"])), 1);
assert_eq!(
lcp(
&heard(&["one", "two", "three"]),
&heard(&["one", "too", "three"])
),
1
);
assert_eq!(
lcp(&heard(&["one", "two"]), &heard(&["one", "two", "three"])),
2
);
}
}
mod state {
use super::*;
#[test]
fn agreement_commits_only_the_shared_prefix() {
let run = drive(
5_000,
vec![loud_batch(); 3],
&[
"alpha beta gamma",
"alpha beta delta epsilon",
"alpha beta delta epsilon zeta",
],
);
assert_eq!(run.segments, ["alpha beta", "delta epsilon", "zeta"]);
assert_eq!(run.transcript(), "alpha beta delta epsilon zeta");
assert_eq!(run.windows, [STRIDE, 2 * STRIDE, 3 * STRIDE]);
}
#[test]
fn decodes_sharing_nothing_publish_nothing_until_the_end() {
let run = drive(
5_000,
vec![loud_batch(); 3],
&["one two", "three four", "five six"],
);
assert_eq!(run.segments, ["five six"]);
assert_eq!(run.transcript(), "five six");
}
#[test]
fn a_revision_inside_the_emitted_prefix_emits_nothing_and_does_not_panic() {
let run = drive(
5_000,
vec![loud_batch(); 3],
&["a b c", "a b c d", "a x y z"],
);
assert_eq!(run.segments, ["a b c", "z"]);
assert_eq!(run.transcript(), "a b c z");
}
#[test]
fn a_revision_shorter_than_what_was_emitted_does_not_panic() {
let run = drive(
400,
vec![loud_batch(), loud_batch(), loud_batch(), loud_batch()],
&[
"alpha beta",
"alpha beta gamma",
"alpha beta gamma delta",
"zeta",
],
);
assert!(
run.transcript().starts_with("alpha beta"),
"the words confirmed before the revision survive it: {}",
run.transcript()
);
}
#[test]
fn an_empty_decode_does_not_poison_the_next_step() {
let run = drive(
5_000,
vec![loud_batch(); 3],
&["alpha beta", "", "alpha beta gamma"],
);
assert_eq!(run.segments, ["alpha beta", "gamma"]);
assert_eq!(run.transcript(), "alpha beta gamma");
}
#[test]
fn the_end_commits_a_tail_that_never_got_a_second_decode() {
let run = drive(5_000, vec![loud_batch()], &["never confirmed"]);
assert_eq!(run.segments, ["never confirmed"]);
assert_eq!(run.transcript(), "never confirmed");
assert_eq!(run.windows, [STRIDE]);
}
#[test]
fn a_silent_recording_finishes_without_a_transcript_instead_of_failing() {
let (_directory, mut recording) = recorded(&vec![0; STRIDE * 4]);
let run = run(
AgreementState::new(cadence(20_000)),
&mut recording,
None,
|_| Hypothesis::new(""),
|_| {},
);
assert_eq!(run.steps, 1, "the finishing step is delivered once");
assert!(run.segments.is_empty(), "silence yields no segment");
assert_eq!(run.final_transcript, None, "silence yields no transcript");
}
#[test]
fn a_settled_trim_opens_the_next_buffer_at_the_pause() {
let mut batches = vec![loud_batch()];
batches.extend(vec![quiet_batch(); 5]);
batches.push(loud_batch());
batches.push(quiet_batch());
let run = drive(
5_000,
batches,
&[
"one",
"one two",
"one two three",
"one two three four",
"one two three four five",
"one two three four five",
"six seven",
"six seven",
],
);
let cut = (4 + 7) * 512;
assert_eq!(
run.windows,
[
STRIDE,
2 * STRIDE,
3 * STRIDE,
4 * STRIDE,
5 * STRIDE,
6 * STRIDE,
7 * STRIDE - cut,
8 * STRIDE - cut,
]
);
assert_eq!(
run.segments,
["one", "two", "three", "four", "five", "six seven"]
);
assert_eq!(run.transcript(), "one two three four five six seven");
}
#[test]
fn a_decode_that_comes_back_short_does_not_reopen_delivered_words() {
let run = drive(
20_000,
vec![quiet_batch(); 6],
&["a b c", "a b c x", "a b", "a b c d", "a b c d", "e f"],
);
assert_eq!(run.segments, ["a b c", "d", "e f"]);
assert!(
!run.transcript().contains("c c"),
"transcript repeated a word: {:?}",
run.transcript()
);
}
#[test]
fn a_cap_forced_cut_commits_every_word_exactly_once() {
let run = drive_whole(4 * STRIDE, 300, &["p q r s t"]);
assert_eq!(run.segments, ["p q r s t"]);
assert_eq!(run.transcript(), "p q r s t");
assert_eq!(run.windows, [4 * STRIDE]);
}
#[test]
fn a_pending_segment_keeps_its_audio_from_being_released() {
let mut state = AgreementState::new(cadence(5_000));
state.buffer_start = 10 * STRIDE as u64;
state.pending_segment = TranscriptSegment::spanned(
&["alpha".to_owned()],
Some(STRIDE as u64..2 * STRIDE as u64),
);
assert_eq!(state.retained_from(), STRIDE as u64);
state.pending_segment = None;
assert_eq!(state.retained_from(), 10 * STRIDE as u64);
}
#[test]
fn a_decode_behind_its_producer_queues_no_backlog() {
let whole = 2 * CANONICAL_SAMPLE_RATE as usize;
let run = drive_whole(whole, 20_000, &["whole buffer"]);
assert_eq!(run.windows, [whole]);
assert_eq!(run.segments, ["whole buffer"]);
assert_eq!(run.transcript(), "whole buffer");
}
#[test]
fn segments_carry_absolute_sample_spans_from_word_timings() {
let (_directory, mut recording) = recorded(&vec![0_i16; 2 * STRIDE]);
let mut state = AgreementState::new(cadence(5_000));
let cancel = CancelToken::new();
let decode = |_window: &[f32]| {
Ok(Hypothesis::with_words(
"alpha beta",
vec![
TimedWord {
start: 0.0,
end: 0.4,
},
TimedWord {
start: 0.5,
end: 0.9,
},
],
))
};
let step = state
.next_finalized_with(&mut recording, &cancel, decode)
.expect("the step succeeds")
.expect("a closed recording finishes in one step");
let Some(TranscriptUpdate::Finalized(segment)) = step.update else {
panic!("the tail is delivered");
};
assert_eq!(segment.as_str(), "alpha beta");
let span = segment.span.expect("timed words produce a span");
assert_eq!(span.start, 0);
assert_eq!(span.end, 14_400);
}
#[test]
fn a_hypothesis_without_timings_yields_spanless_segments() {
let (_directory, mut recording) = recorded(&vec![0_i16; 2 * STRIDE]);
let mut state = AgreementState::new(cadence(5_000));
let cancel = CancelToken::new();
let decode = |_window: &[f32]| Ok(Hypothesis::new("no timings here"));
let step = state
.next_finalized_with(&mut recording, &cancel, decode)
.expect("the step succeeds")
.expect("a closed recording finishes in one step");
let Some(TranscriptUpdate::Finalized(segment)) = step.update else {
panic!("the tail is delivered");
};
assert_eq!(segment.as_str(), "no timings here");
assert!(segment.span().is_none(), "untimed words carry no span");
}
#[test]
fn delivered_segments_concatenate_into_the_final_transcript_across_both_trims() {
let mut answers: Vec<String> = vec![
"one".into(),
"one two".into(),
"one two three".into(),
"one two three four".into(),
"one two three four five".into(),
"one two three four five".into(),
];
let mut running = String::new();
for word in [
"six",
"seven",
"eight",
"nine",
"ten",
"eleven",
"twelve",
"thirteen",
"fourteen",
"fifteen",
"sixteen",
"seventeen",
] {
running.push_str(if running.is_empty() { "" } else { " " });
running.push_str(word);
answers.push(running.clone());
}
answers.push(format!("{running} eighteen nineteen"));
let answers: Vec<&str> = answers.iter().map(String::as_str).collect();
let mut batches = vec![loud_batch()];
batches.extend(vec![quiet_batch(); 5]);
batches.push(loud_batch());
batches.extend(vec![loud_batch(); 12]);
let run = drive(1_500, batches, &answers);
let delivered = run.segments.join(" ");
assert_eq!(delivered, run.transcript());
assert_eq!(
delivered,
"one two three four five six seven eight nine ten eleven twelve thirteen \
fourteen fifteen sixteen seventeen eighteen nineteen"
);
}
}
mod trimming {
use super::*;
fn loud_frames(count: usize) -> Vec<f32> {
vec![0.5; count * TRIM_FRAME_SAMPLES]
}
fn timed_word(first_frame: usize, frames: usize) -> Word {
let start = (first_frame * TRIM_FRAME_SAMPLES) as u64;
Word {
text: "word".to_owned(),
span: Some(start..start + (frames * TRIM_FRAME_SAMPLES) as u64),
}
}
#[test]
fn skipped_speech_is_a_run_of_loud_frames_no_word_covers() {
let scattered: Vec<Word> = (0..10).map(|word| timed_word(word * 18, 10)).collect();
assert_eq!(
Loudness::of(&loud_frames(180)).unheard_speech(0, &scattered, 0),
None
);
let skipped: Vec<Word> = (0..10)
.map(|word| timed_word(word * 18 + if word >= 3 { 30 } else { 0 }, 10))
.collect();
let gap_opens = (3 * 18 - 8 + HEARD_MARGIN_FRAMES) * TRIM_FRAME_SAMPLES;
assert_eq!(
Loudness::of(&loud_frames(210)).unheard_speech(0, &skipped, 0),
Some(gap_opens as u64)
);
}
#[test]
fn a_decode_of_a_buffer_under_the_pause_level_is_ignored() {
let (_directory, mut recording) = recorded(&vec![0_i16; 2 * STRIDE]);
let mut state = AgreementState::new(cadence(5_000));
state.pause_level = Some(f32::EPSILON);
let cancel = CancelToken::new();
let decode = |_window: &[f32]| Ok(Hypothesis::new("thank you"));
let step = state
.next_finalized_with(&mut recording, &cancel, decode)
.expect("the step succeeds")
.expect("a closed recording finishes in one step");
assert!(step.update.is_none());
assert!(step.transcript.is_none());
}
#[test]
fn a_pause_cut_needs_a_real_pause_at_the_tail() {
let speech = [0.5_f32; TRIM_FRAME_SAMPLES];
let silence = [0.0_f32; TRIM_FRAME_SAMPLES];
let mut buffer = Vec::new();
buffer.extend_from_slice(&speech);
buffer.extend_from_slice(&speech);
buffer.extend_from_slice(&speech);
buffer.extend_from_slice(&silence.repeat(16)[..]);
let cut = Loudness::of(&buffer)
.pause_cut()
.expect("five hundred ms of silence is a pause");
assert_eq!(cut, (3 + 16 / 2) * TRIM_FRAME_SAMPLES);
let mut short = Vec::new();
short.extend_from_slice(&speech);
short.extend_from_slice(&silence.repeat(12)[..]);
assert!(
Loudness::of(&short).pause_cut().is_none(),
"300 ms is not a pause"
);
let mut mid = Vec::new();
mid.extend_from_slice(&speech);
mid.extend_from_slice(&silence.repeat(16)[..]);
mid.extend_from_slice(&speech);
assert!(
Loudness::of(&mid).pause_cut().is_none(),
"a pause that does not reach the tail is not a stop"
);
assert!(
Loudness::of(&speech).pause_cut().is_none(),
"constant speech has no pause"
);
}
}
mod simulated {
use super::*;
use crate::audio::PCM_SCALE;
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Self(seed + 1)
}
fn next(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn below(&mut self, bound: u64) -> u64 {
self.next() % bound
}
fn chance(&mut self, percent: u64) -> bool {
self.below(100) < percent
}
}
const BASE_AMPLITUDE: i16 = 8_000;
const AMPLITUDE_STEP: i16 = 8;
fn word_name(index: usize) -> String {
format!("w{index}")
}
struct Speech {
batches: Vec<Vec<i16>>,
names: Vec<usize>,
}
impl Speech {
fn random(rng: &mut Rng) -> Self {
let distinct = 40 + rng.below(40) as usize;
let mut batches = Vec::new();
let mut names = Vec::new();
for name in 0..distinct {
batches.push(word_batch(name));
names.push(name);
if rng.chance(10) {
batches.push(quiet_batch());
batches.push(word_batch(name));
names.push(name);
}
if rng.chance(10) {
for _ in 0..5 + rng.below(4) {
batches.push(quiet_batch());
}
}
}
batches.push(quiet_batch());
Self { batches, names }
}
}
fn word_batch(name: usize) -> Vec<i16> {
vec![BASE_AMPLITUDE + AMPLITUDE_STEP * name as i16; 2 * STRIDE]
}
struct Heard {
words: Vec<String>,
timings: Vec<TimedWord>,
}
impl Heard {
fn from_window(window: &[f32]) -> Self {
let mut words = Vec::new();
let mut timings = Vec::new();
let mut start = 0;
while start < window.len() {
let value = window[start];
let mut end = start + 1;
while end < window.len() && window[end] == value {
end += 1;
}
if value != 0.0 && end - start >= STRIDE / 2 && end < window.len() {
let amplitude = (value * PCM_SCALE) as i16;
let index = (amplitude - BASE_AMPLITUDE) / AMPLITUDE_STEP;
words.push(word_name(index as usize));
let rate = CANONICAL_SAMPLE_RATE as f32;
timings.push(TimedWord {
start: start as f32 / rate,
end: end as f32 / rate,
});
}
start = end;
}
Self { words, timings }
}
fn into_hypothesis(self, timed: bool) -> Hypothesis {
if timed {
Hypothesis::with_words(self.words.join(" "), self.timings)
} else {
Hypothesis::new(self.words.join(" "))
}
}
}
#[derive(Clone, Copy, PartialEq)]
enum Corruption {
Empty,
Short,
Unstable,
Revision,
Partial,
}
impl Corruption {
fn adds_junk(self) -> bool {
matches!(self, Self::Unstable | Self::Revision)
}
fn apply(self, heard: Heard, rng: &mut Rng) -> Heard {
let Heard {
mut words,
mut timings,
} = heard;
match self {
Self::Empty => {
words.clear();
timings.clear();
}
Self::Partial => {
let dropped = 8 + rng.below(5) as usize;
let from = rng.below((words.len() - dropped) as u64 + 1) as usize;
words.drain(from..from + dropped);
timings.drain(from..from + dropped);
}
Self::Short => {
let keep = words.len() - (1 + rng.below(3) as usize);
words.truncate(keep);
timings.truncate(keep);
}
Self::Unstable => {
let junked = (1 + rng.below(2) as usize).min(words.len());
let from = words.len() - junked;
for word in &mut words[from..] {
*word = junk(rng);
}
}
Self::Revision => {
let target = rng.below((words.len() / 2) as u64) as usize;
words[target] = junk(rng);
}
}
Heard { words, timings }
}
}
fn junk(rng: &mut Rng) -> String {
format!("~{}", rng.next())
}
struct Noise {
timed: bool,
empty: u64,
short: u64,
unstable: u64,
revision: u64,
partial: u64,
}
const NOISELESS: Noise = Noise {
timed: true,
empty: 0,
short: 0,
unstable: 0,
revision: 0,
partial: 0,
};
const NOISY: Noise = Noise {
timed: true,
empty: 5,
short: 10,
unstable: 15,
revision: 5,
partial: 10,
};
const NOISY_UNTIMED: Noise = Noise {
timed: false,
partial: 0,
..NOISY
};
impl Noise {
fn roll(&self, heard: &Heard, rng: &mut Rng) -> Option<Corruption> {
let words = heard.words.len();
if rng.chance(self.empty) {
Some(Corruption::Empty)
} else if words > 2 && rng.chance(self.short) {
Some(Corruption::Short)
} else if words > 0 && rng.chance(self.unstable) {
Some(Corruption::Unstable)
} else if words >= 4 && rng.chance(self.revision) {
Some(Corruption::Revision)
} else if words >= 14 && rng.chance(self.partial) {
Some(Corruption::Partial)
} else {
None
}
}
}
struct Delivered {
names: Vec<usize>,
junk: usize,
}
impl Delivered {
fn parse(transcript: &str) -> Self {
let mut names = Vec::new();
let mut junk = 0;
for word in transcript.split_whitespace() {
match word.strip_prefix('w').and_then(|name| name.parse().ok()) {
Some(name) => names.push(name),
None => junk += 1,
}
}
Self { names, junk }
}
fn in_order_of(&self, truth: &[usize]) -> bool {
let mut truth = truth.iter();
self.names
.iter()
.all(|name| truth.by_ref().any(|said| said == name))
}
}
struct Sim {
run: Run,
injected: Vec<Corruption>,
truth: Vec<usize>,
}
fn simulate(seed: u64, noise: &Noise) -> Sim {
let mut rng = Rng::new(seed);
let speech = Speech::random(&mut rng);
let truth = speech.names.clone();
let max_buffer_ms = [1_500, 3_000, 20_000][rng.below(3) as usize];
let max_window = max_buffer_ms * CANONICAL_SAMPLE_RATE as usize / 1000 + 2 * STRIDE;
let (_directory, mut recording, pace) = paced(speech.batches);
let mut injected = Vec::new();
let mut last_retained = 0;
let run = run(
AgreementState::new(cadence(max_buffer_ms)),
&mut recording,
Some(&pace),
|window| {
assert!(
window.len() <= max_window,
"seed {seed}: a {} sample window outgrew the {max_window} cap",
window.len()
);
let heard = Heard::from_window(window);
let heard = match noise.roll(&heard, &mut rng) {
Some(corruption) => {
injected.push(corruption);
corruption.apply(heard, &mut rng)
}
None => heard,
};
heard.into_hypothesis(noise.timed)
},
|state| retention_holds(seed, state, &mut last_retained),
);
Sim {
run,
injected,
truth,
}
}
fn retention_holds(seed: u64, state: &AgreementState, last_retained: &mut u64) {
let retained = state.retained_from();
assert!(
retained >= *last_retained,
"seed {seed}: retained_from moved backwards ({last_retained} -> {retained})"
);
*last_retained = retained;
if let Some(span) = state.pending_segment.as_ref().and_then(|tail| tail.span()) {
assert!(
retained <= span.start,
"seed {seed}: a pending segment's audio at {} was released at {retained}",
span.start
);
}
}
#[test]
fn a_noiseless_simulated_decoder_delivers_the_truth_exactly() {
for seed in 0..25 {
let sim = simulate(seed, &NOISELESS);
let truth: Vec<String> = sim.truth.iter().copied().map(word_name).collect();
assert_eq!(
sim.run.transcript(),
truth.join(" "),
"seed {seed}: the transcript strayed from the truth"
);
assert_eq!(
sim.run.segments.join(" "),
sim.run.transcript(),
"seed {seed}: delivery and the final transcript disagree"
);
}
}
#[test]
fn noisy_decodes_keep_every_agreement_invariant_across_seeds() {
for seed in 0..200 {
invariants_hold(seed, &simulate(seed, &NOISY));
}
}
#[test]
fn an_untimed_decoder_keeps_the_invariants_too() {
for seed in 0..100 {
invariants_hold(seed, &simulate(seed, &NOISY_UNTIMED));
}
}
fn invariants_hold(seed: u64, sim: &Sim) {
let delivered = Delivered::parse(sim.run.transcript());
let junk_sources = sim
.injected
.iter()
.filter(|corruption| corruption.adds_junk())
.count();
let truth_words = sim.truth.len();
assert_eq!(
sim.run.segments.join(" "),
sim.run.transcript(),
"seed {seed}: delivery and the final transcript disagree"
);
assert!(
delivered.in_order_of(&sim.truth),
"seed {seed}: a word was duplicated or reordered: {}",
sim.run.transcript()
);
assert!(
delivered.junk <= 2 * junk_sources,
"seed {seed}: {} junk words from {junk_sources} junk corruptions",
delivered.junk
);
let missing = truth_words - delivered.names.len();
assert!(
missing <= 3 * sim.injected.len(),
"seed {seed}: lost {missing} of {truth_words} words to {} corruptions",
sim.injected.len()
);
}
}
}