1mod agreement;
4mod heard;
5pub mod layer;
6mod native;
7mod session;
8mod spacing;
9
10pub use heard::{Heard, HeardUpdate, next_heard, next_heard_update};
11pub use layer::{Backtrack, Layer, Layered, RemoveHesitations, RemoveRepeats, Vocabulary};
12pub use spacing::Caret;
13
14use std::path::PathBuf;
15
16#[cfg(not(target_family = "wasm"))]
17use crate::audio::RecordedAudio;
18use crate::audio::{CANONICAL_SAMPLE_RATE, CaptureError, RecordingReader};
19use crate::backend::{BackendError, ModelDownload, ModelError};
20use crate::cancel::CancelToken;
21pub use agreement::Cadence;
22pub use session::Session;
23
24pub mod prelude {
29 pub use super::{
30 Backtrack, Caret, Language, Layer, RemoveHesitations, RemoveRepeats, Session, Streaming,
31 Transcript, TranscriptSegment, Transcription, TranscriptionError, Vocabulary, Whole,
32 };
33 pub use crate::audio::RecordingReader;
34 #[cfg(all(feature = "microphone", not(target_family = "wasm")))]
35 pub use crate::audio::{Microphone, RecordingMode};
36 pub use crate::cancel::CancelToken;
37 pub use crate::speaker::{Diarizer, Embedder};
38}
39
40const MAX_LANGUAGE_CODE_BYTES: usize = 64;
41
42pub const SILENCE_FLOOR: f32 = 5e-5;
44
45pub trait Transcriber: Send + Sync + 'static {
59 fn validate_language(
60 &self,
61 language: &Language,
62 cancel: &CancelToken,
63 ) -> Result<(), TranscriptionError>;
64
65 fn transcribe_with(
68 &self,
69 audio: TranscriptionAudio<'_>,
70 language: &Language,
71 cancel: &CancelToken,
72 ) -> Result<Hypothesis, TranscriptionError>;
73
74 fn open_stream(
80 &self,
81 language: &Language,
82 cancel: &CancelToken,
83 ) -> Result<Option<Box<dyn ChunkStream>>, TranscriptionError> {
84 let _ = (language, cancel);
85 Ok(None)
86 }
87
88 fn open_operation(
93 &self,
94 language: &Language,
95 incremental: bool,
96 cancel: &CancelToken,
97 ) -> Result<Option<OperationSource>, TranscriptionError> {
98 let _ = (language, incremental, cancel);
99 Ok(None)
100 }
101}
102
103pub trait ChunkStream: Send {
106 fn chunk_samples(&self) -> usize;
108
109 fn feed(&mut self, chunk: &[f32]) -> Result<Emitted, TranscriptionError>;
112}
113
114pub struct Emitted {
119 pub words: Hypothesis,
120 pub joins_previous: bool,
121}
122
123#[derive(Clone, Copy)]
126pub enum Stream {
127 Agreement(Cadence),
128 Native,
129}
130
131pub trait Engine: Transcriber + Sized {
133 const NAME: &'static str;
134
135 const MODEL: &'static [ModelDownload];
139
140 const INPUT: AudioInputKind;
143
144 type Config;
145
146 fn check_from(config: &Self::Config) -> Result<PathBuf, TranscriptionError>;
149
150 fn load_from(config: Self::Config, cancel: &CancelToken) -> Result<Self, TranscriptionError>;
151}
152
153pub trait StreamingEngine: Engine {
155 const STREAM: Stream;
156}
157
158impl<T: Transcriber + ?Sized> Transcriber for std::sync::Arc<T> {
159 fn validate_language(
160 &self,
161 language: &Language,
162 cancel: &CancelToken,
163 ) -> Result<(), TranscriptionError> {
164 (**self).validate_language(language, cancel)
165 }
166
167 fn transcribe_with(
168 &self,
169 audio: TranscriptionAudio<'_>,
170 language: &Language,
171 cancel: &CancelToken,
172 ) -> Result<Hypothesis, TranscriptionError> {
173 (**self).transcribe_with(audio, language, cancel)
174 }
175
176 fn open_stream(
177 &self,
178 language: &Language,
179 cancel: &CancelToken,
180 ) -> Result<Option<Box<dyn ChunkStream>>, TranscriptionError> {
181 (**self).open_stream(language, cancel)
182 }
183
184 fn open_operation(
185 &self,
186 language: &Language,
187 incremental: bool,
188 cancel: &CancelToken,
189 ) -> Result<Option<OperationSource>, TranscriptionError> {
190 (**self).open_operation(language, incremental, cancel)
191 }
192}
193
194impl<T: Transcriber + ?Sized> Transcriber for Box<T> {
195 fn validate_language(
196 &self,
197 language: &Language,
198 cancel: &CancelToken,
199 ) -> Result<(), TranscriptionError> {
200 (**self).validate_language(language, cancel)
201 }
202
203 fn transcribe_with(
204 &self,
205 audio: TranscriptionAudio<'_>,
206 language: &Language,
207 cancel: &CancelToken,
208 ) -> Result<Hypothesis, TranscriptionError> {
209 (**self).transcribe_with(audio, language, cancel)
210 }
211
212 fn open_stream(
213 &self,
214 language: &Language,
215 cancel: &CancelToken,
216 ) -> Result<Option<Box<dyn ChunkStream>>, TranscriptionError> {
217 (**self).open_stream(language, cancel)
218 }
219
220 fn open_operation(
221 &self,
222 language: &Language,
223 incremental: bool,
224 cancel: &CancelToken,
225 ) -> Result<Option<OperationSource>, TranscriptionError> {
226 (**self).open_operation(language, incremental, cancel)
227 }
228}
229
230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
231pub enum AudioInputKind {
232 Samples,
233 Wav,
234}
235
236impl std::fmt::Display for AudioInputKind {
237 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238 match self {
239 Self::Samples => formatter.write_str("canonical samples"),
240 Self::Wav => formatter.write_str("a completed WAV"),
241 }
242 }
243}
244
245pub enum TranscriptionAudio<'a> {
247 Samples(&'a [f32]),
248 #[cfg(not(target_family = "wasm"))]
249 Wav(&'a RecordedAudio),
250}
251
252impl<'a> TranscriptionAudio<'a> {
253 pub fn kind(&self) -> AudioInputKind {
254 match self {
255 Self::Samples(_) => AudioInputKind::Samples,
256 #[cfg(not(target_family = "wasm"))]
257 Self::Wav(_) => AudioInputKind::Wav,
258 }
259 }
260
261 pub fn require_samples(self, backend: &'static str) -> Result<&'a [f32], TranscriptionError> {
262 #[cfg(target_family = "wasm")]
263 let _ = backend;
264 match self {
265 Self::Samples(samples) => Ok(samples),
266 #[cfg(not(target_family = "wasm"))]
267 Self::Wav(_) => Err(TranscriptionError::UnsupportedAudioInput {
268 backend,
269 received: AudioInputKind::Wav,
270 required: AudioInputKind::Samples,
271 }),
272 }
273 }
274
275 #[cfg(not(target_family = "wasm"))]
276 pub fn require_wav(
277 self,
278 backend: &'static str,
279 ) -> Result<&'a RecordedAudio, TranscriptionError> {
280 match self {
281 Self::Wav(audio) => Ok(audio),
282 Self::Samples(_) => Err(TranscriptionError::UnsupportedAudioInput {
283 backend,
284 received: AudioInputKind::Samples,
285 required: AudioInputKind::Wav,
286 }),
287 }
288 }
289}
290
291impl std::fmt::Debug for TranscriptionAudio<'_> {
292 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293 formatter
294 .debug_struct("TranscriptionAudio")
295 .field("kind", &self.kind())
296 .finish_non_exhaustive()
297 }
298}
299
300#[derive(Debug, Clone, Copy, PartialEq)]
302pub struct TimedWord {
303 pub start: f32,
304 pub end: f32,
305}
306
307impl TimedWord {
308 pub fn span_from(&self, origin: u64) -> Result<std::ops::Range<u64>, TranscriptionError> {
310 if !self.start.is_finite()
311 || !self.end.is_finite()
312 || self.start < 0.0
313 || self.end < self.start
314 {
315 return Err(TranscriptionError::InvalidSegment);
316 }
317 let position = |seconds: f32| {
318 let samples = (f64::from(seconds) * f64::from(CANONICAL_SAMPLE_RATE)).round();
319 if samples >= u64::MAX as f64 {
320 return Err(TranscriptionError::InvalidSegment);
321 }
322 origin
323 .checked_add(samples as u64)
324 .ok_or(TranscriptionError::InvalidSegment)
325 };
326 Ok(position(self.start)?..position(self.end)?)
327 }
328}
329
330pub struct Hypothesis {
332 text: String,
333 words: Vec<TimedWord>,
334}
335
336impl Hypothesis {
337 pub fn new(text: impl Into<String>) -> Self {
338 Self {
339 text: text.into().trim().to_owned(),
340 words: Vec::new(),
341 }
342 }
343
344 pub fn with_words(text: impl Into<String>, words: Vec<TimedWord>) -> Self {
347 let mut hypothesis = Self::new(text);
348 if hypothesis.text.split_whitespace().count() == words.len() {
349 hypothesis.words = words;
350 }
351 hypothesis
352 }
353
354 pub fn as_str(&self) -> &str {
355 &self.text
356 }
357
358 pub fn words(&self) -> &[TimedWord] {
360 &self.words
361 }
362
363 pub fn into_text(self) -> String {
364 self.text
365 }
366}
367
368pub fn word_timings<'a>(tokens: impl IntoIterator<Item = (&'a str, TimedWord)>) -> Vec<TimedWord> {
370 let mut words: Vec<TimedWord> = Vec::new();
371 let mut open = false;
372 let mut has_content = false;
373 for (text, timing) in tokens {
374 for character in text.chars() {
375 if character.is_whitespace() || character == '\u{2581}' {
376 open = false;
377 has_content = false;
378 continue;
379 }
380 let punctuation = sentence_punctuation(character);
381 match words.last_mut() {
382 Some(word) if open => {
383 if !punctuation || !has_content {
385 word.end = word.end.max(timing.end);
386 }
387 }
388 _ => {
389 words.push(timing);
390 open = true;
391 }
392 }
393 has_content |= !punctuation;
394 }
395 }
396 words
397}
398
399fn sentence_punctuation(character: char) -> bool {
400 matches!(character, '.' | ',' | '!' | '?' | ':' | ';')
401}
402
403impl std::fmt::Debug for Hypothesis {
404 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405 formatter
406 .debug_struct("Hypothesis")
407 .field("bytes", &self.text.len())
408 .field("timed_words", &self.words.len())
409 .finish()
410 }
411}
412
413pub struct Transcription<T, M: Mode, L = ()> {
414 plan: Plan<M::Config>,
415 transcriber: T,
416 layers: L,
417}
418
419#[derive(Clone, Copy)]
420enum Plan<C> {
421 Local(C),
422 Delegated,
423}
424
425pub struct Operation<'a, T, L = ()> {
426 transcriber: &'a T,
427 language: &'a Language,
428 cancel: CancelToken,
429 strategy: Strategy,
430 read_to: u64,
431 transcript: Option<Transcript>,
432 layers: &'a L,
433 output: Option<layer::Output>,
434}
435
436use sealed::{Strategy, WholeRecordingInput};
437
438struct ChunkStep {
439 update: Option<TranscriptUpdate>,
440 transcript: Option<Transcript>,
441}
442
443pub enum OperationStep {
444 Update {
445 update: TranscriptUpdate,
446 retained_from: u64,
447 read_to: u64,
448 completed: Option<Transcript>,
449 },
450 Complete {
451 transcript: Transcript,
452 retained_from: u64,
453 read_to: u64,
454 },
455}
456
457type OperationCallback =
458 dyn FnMut(&mut RecordingReader, bool) -> Result<OperationStep, TranscriptionError> + Send;
459
460pub struct OperationSource {
461 served_by: &'static str,
462 next: Box<OperationCallback>,
463 retained_from: u64,
464 read_to: u64,
465 completed: Option<Transcript>,
466 finished: bool,
467}
468
469impl OperationSource {
470 pub fn new(
474 served_by: &'static str,
475 next: impl FnMut(&mut RecordingReader, bool) -> Result<OperationStep, TranscriptionError>
476 + Send
477 + 'static,
478 ) -> Self {
479 Self {
480 served_by,
481 next: Box::new(next),
482 retained_from: 0,
483 read_to: 0,
484 completed: None,
485 finished: false,
486 }
487 }
488
489 fn next(
490 &mut self,
491 recording: &mut RecordingReader,
492 previews: bool,
493 cancel: &CancelToken,
494 ) -> Result<Option<ChunkStep>, TranscriptionError> {
495 while !self.finished {
496 check_cancelled(cancel)?;
497 let step = (self.next)(recording, previews)?;
498 check_cancelled(cancel)?;
499 match step {
500 OperationStep::Update {
501 update,
502 retained_from,
503 read_to,
504 completed,
505 } => {
506 self.retained_from = retained_from;
507 self.read_to = read_to;
508 if let Some(transcript) = completed {
509 self.completed = Some(transcript);
510 }
511 if previews || matches!(update, TranscriptUpdate::Finalized(_)) {
512 return Ok(Some(ChunkStep {
513 update: Some(update),
514 transcript: self.completed.take(),
515 }));
516 }
517 }
518 OperationStep::Complete {
519 transcript,
520 retained_from,
521 read_to,
522 } => {
523 self.retained_from = retained_from;
524 self.read_to = read_to;
525 self.finished = true;
526 return Ok(Some(ChunkStep {
527 update: None,
528 transcript: Some(transcript),
529 }));
530 }
531 }
532 }
533 Ok(None)
534 }
535}
536
537mod sealed {
539 use super::{
540 CancelToken, Language, OperationSource, Transcriber, TranscriptionError, agreement, native,
541 };
542
543 pub enum Strategy {
544 WholeRecording(WholeRecordingInput),
545 Agreement(Box<agreement::AgreementState>),
546 Native(Box<native::NativeState>),
547 Delegated(OperationSource),
548 }
549
550 #[derive(Clone, Copy)]
551 pub enum WholeRecordingInput {
552 Samples,
553 #[cfg(not(target_family = "wasm"))]
554 Wav,
555 }
556
557 pub trait Sealed {
559 type Config: Copy + Send + Sync + 'static;
560
561 fn strategy<T: Transcriber>(
564 config: Self::Config,
565 transcriber: &T,
566 language: &Language,
567 cancel: &CancelToken,
568 ) -> Result<Strategy, TranscriptionError>;
569 }
570}
571
572#[doc(hidden)]
575pub trait Mode: sealed::Sealed + 'static {
576 const INCREMENTAL: bool;
577}
578
579#[derive(Clone, Copy)]
581pub struct Whole;
582
583impl Mode for Whole {
584 const INCREMENTAL: bool = false;
585}
586
587impl sealed::Sealed for Whole {
588 type Config = WholeRecordingInput;
589
590 fn strategy<T: Transcriber>(
591 config: Self::Config,
592 _transcriber: &T,
593 _language: &Language,
594 _cancel: &CancelToken,
595 ) -> Result<Strategy, TranscriptionError> {
596 Ok(Strategy::WholeRecording(config))
597 }
598}
599
600#[derive(Clone, Copy)]
603pub struct Streaming;
604
605impl Mode for Streaming {
606 const INCREMENTAL: bool = true;
607}
608
609impl sealed::Sealed for Streaming {
610 type Config = Stream;
611
612 fn strategy<T: Transcriber>(
613 config: Self::Config,
614 transcriber: &T,
615 language: &Language,
616 cancel: &CancelToken,
617 ) -> Result<Strategy, TranscriptionError> {
618 match config {
619 Stream::Agreement(cadence) => Ok(Strategy::Agreement(Box::new(
620 agreement::AgreementState::new(cadence),
621 ))),
622 Stream::Native => match transcriber.open_stream(language, cancel)? {
623 Some(stream) => Ok(Strategy::Native(Box::new(native::NativeState::new(stream)))),
624 None => Err(TranscriptionError::NoNativeStream),
625 },
626 }
627 }
628}
629
630impl<T> Transcription<T, Whole> {
631 pub fn complete_samples(transcriber: T) -> Self {
632 Self {
633 plan: Plan::Local(WholeRecordingInput::Samples),
634 transcriber,
635 layers: (),
636 }
637 }
638
639 #[cfg(not(target_family = "wasm"))]
640 pub fn complete_wav(transcriber: T) -> Self {
641 Self {
642 plan: Plan::Local(WholeRecordingInput::Wav),
643 transcriber,
644 layers: (),
645 }
646 }
647
648 #[cfg(not(target_family = "wasm"))]
651 pub fn complete(input: AudioInputKind, transcriber: T) -> Self {
652 match input {
653 AudioInputKind::Samples => Self::complete_samples(transcriber),
654 AudioInputKind::Wav => Self::complete_wav(transcriber),
655 }
656 }
657}
658
659impl<T> Transcription<T, Streaming> {
660 pub fn agreement(cadence: Cadence, transcriber: T) -> Self {
664 Self::stream(Stream::Agreement(cadence), transcriber)
665 }
666
667 pub fn stream(stream: Stream, transcriber: T) -> Self {
670 Self {
671 plan: Plan::Local(stream),
672 transcriber,
673 layers: (),
674 }
675 }
676}
677
678impl<T, M: Mode> Transcription<T, M> {
679 pub fn delegated(transcriber: T) -> Self {
682 Self {
683 plan: Plan::Delegated,
684 transcriber,
685 layers: (),
686 }
687 }
688}
689
690impl<T: StreamingEngine> Transcription<T, Streaming> {
691 pub fn streaming(engine: T) -> Self {
692 Self::stream(T::STREAM, engine)
693 }
694}
695
696impl<T, M: Mode, L: Layer> Transcription<T, M, L> {
697 pub fn with<N: Layer>(self, layer: N) -> Transcription<T, M, Layered<N, L>> {
698 Transcription {
699 plan: self.plan,
700 transcriber: self.transcriber,
701 layers: Layered::new(self.layers, layer),
702 }
703 }
704}
705
706impl<T: Transcriber, M: Mode, L: Layer> Transcription<T, M, L> {
707 pub fn start<'a>(
709 &'a self,
710 language: &'a Language,
711 ) -> Result<Operation<'a, T, L>, TranscriptionError> {
712 self.start_with(language, &CancelToken::never())
713 }
714
715 pub fn start_with<'a>(
716 &'a self,
717 language: &'a Language,
718 cancel: &CancelToken,
719 ) -> Result<Operation<'a, T, L>, TranscriptionError> {
720 check_cancelled(cancel)?;
721 self.transcriber.validate_language(language, cancel)?;
722 check_cancelled(cancel)?;
723 let strategy = match self.plan {
724 Plan::Local(config) => M::strategy(config, &self.transcriber, language, cancel)?,
725 Plan::Delegated => self
726 .transcriber
727 .open_operation(language, M::INCREMENTAL, cancel)?
728 .map(Strategy::Delegated)
729 .ok_or(TranscriptionError::NoDelegatedOperation)?,
730 };
731 check_cancelled(cancel)?;
732 Ok(Operation {
733 transcriber: &self.transcriber,
734 language,
735 cancel: cancel.clone(),
736 strategy,
737 read_to: 0,
738 transcript: None,
739 layers: &self.layers,
740 output: self.layers.enabled().then(layer::Output::default),
741 })
742 }
743
744 pub fn transcribe_with(
745 &self,
746 recording: &mut RecordingReader,
747 language: &Language,
748 cancel: &CancelToken,
749 ) -> Result<Transcript, TranscriptionError> {
750 self.start_with(language, cancel)?.finish(recording)
751 }
752
753 pub fn transcribe(
754 &self,
755 recording: &mut RecordingReader,
756 language: &Language,
757 ) -> Result<Transcript, TranscriptionError> {
758 self.transcribe_with(recording, language, &CancelToken::never())
759 }
760
761 #[cfg(not(target_family = "wasm"))]
762 pub fn transcribe_file(
763 &self,
764 path: impl AsRef<std::path::Path>,
765 ) -> Result<Transcript, TranscriptionError> {
766 let mut recording =
767 RecordingReader::from_wav_file(path).map_err(TranscriptionError::Capture)?;
768 self.transcribe(&mut recording, &Language::Automatic)
769 }
770}
771
772impl<T: Transcriber, L: Layer> Operation<'_, T, L> {
773 pub fn next_finalized(
774 &mut self,
775 recording: &mut RecordingReader,
776 ) -> Result<Option<TranscriptSegment>, TranscriptionError> {
777 loop {
778 match self.next(recording, false)? {
779 Some(TranscriptUpdate::Finalized(segment)) => return Ok(Some(segment)),
780 Some(TranscriptUpdate::Provisional { .. }) => {}
781 None => return Ok(None),
782 }
783 }
784 }
785
786 pub fn completed(&self) -> Option<&Transcript> {
791 self.transcript.as_ref()
792 }
793
794 pub fn read_to(&self) -> u64 {
798 self.read_to
799 }
800
801 pub fn served_by(&self) -> Option<&'static str> {
803 match &self.strategy {
804 Strategy::Delegated(source) => Some(source.served_by),
805 Strategy::WholeRecording(_) | Strategy::Agreement(_) | Strategy::Native(_) => None,
806 }
807 }
808
809 pub fn retained_from(&self) -> u64 {
815 let strategy = self.strategy_retained_from();
816 self.output
817 .as_ref()
818 .and_then(|output| output.retained_from)
819 .map_or(strategy, |retained| retained.min(strategy))
820 }
821
822 fn strategy_retained_from(&self) -> u64 {
823 match &self.strategy {
824 Strategy::WholeRecording(_) => 0,
825 Strategy::Agreement(state) => state.retained_from(),
826 Strategy::Native(state) => state.retained_from(),
827 Strategy::Delegated(source) => source.retained_from,
828 }
829 }
830
831 pub fn next_update(
832 &mut self,
833 recording: &mut RecordingReader,
834 ) -> Result<Option<TranscriptUpdate>, TranscriptionError> {
835 self.next(recording, true)
836 }
837
838 pub fn next(
841 &mut self,
842 recording: &mut RecordingReader,
843 previews: bool,
844 ) -> Result<Option<TranscriptUpdate>, TranscriptionError> {
845 let Some(mut output) = self.output.take() else {
846 return self.next_raw_step(recording, previews);
847 };
848 let result = check_cancelled(&self.cancel)
849 .and_then(|()| self.next_processed(recording, previews, &mut output));
850 if result.is_err() {
851 output.finished = true;
852 output.pending.clear();
853 output.text.clear();
854 self.transcript = None;
855 }
856 self.output = Some(output);
857 result
858 }
859
860 fn next_processed(
861 &mut self,
862 recording: &mut RecordingReader,
863 previews: bool,
864 output: &mut layer::Output,
865 ) -> Result<Option<TranscriptUpdate>, TranscriptionError> {
866 loop {
867 if let Some(update) = output.pending.pop_front() {
868 return Ok(Some(update));
869 }
870 if output.finished {
871 return Ok(None);
872 }
873 let update = self.next_raw_step(recording, previews)?;
874 let finished = update.is_none() || self.transcript.is_some();
875 if let Some(update) = update {
876 output.push(update, self.layers, &self.cancel)?;
877 }
878 if finished {
879 output.retained_from = Some(self.strategy_retained_from());
881 while let Some(update) = self.next_raw_step(recording, previews)? {
882 output.push(update, self.layers, &self.cancel)?;
883 }
884 self.transcript = if output.text.is_empty() {
885 None
886 } else {
887 Some(Transcript(std::mem::take(&mut output.text)))
888 };
889 output.finished = true;
890 }
891 }
892 }
893
894 fn next_raw_step(
895 &mut self,
896 recording: &mut RecordingReader,
897 previews: bool,
898 ) -> Result<Option<TranscriptUpdate>, TranscriptionError> {
899 let update = self.decode_step(recording, previews);
900 self.read_to = match &self.strategy {
901 Strategy::Delegated(source) => source.read_to,
902 Strategy::WholeRecording(_) | Strategy::Agreement(_) | Strategy::Native(_) => {
903 recording.held_to()
904 }
905 };
906 update
907 }
908
909 fn decode_step(
910 &mut self,
911 recording: &mut RecordingReader,
912 previews: bool,
913 ) -> Result<Option<TranscriptUpdate>, TranscriptionError> {
914 let step = match &mut self.strategy {
915 Strategy::WholeRecording(input) => {
916 if self.transcript.is_some() {
917 return Ok(None);
918 }
919 let segment = transcribe_complete(
920 *input,
921 recording,
922 self.transcriber,
923 self.language,
924 &self.cancel,
925 )?;
926 self.transcript = Some(Transcript::new(segment.as_str())?);
927 ChunkStep {
928 update: Some(TranscriptUpdate::Finalized(segment)),
929 transcript: None,
930 }
931 }
932 Strategy::Agreement(state) => {
933 let cancel = self.cancel.clone();
934 match state.next_with(recording, &cancel, previews, |samples| {
935 self.transcriber.transcribe_with(
936 TranscriptionAudio::Samples(samples),
937 self.language,
938 &cancel,
939 )
940 })? {
941 Some(step) => step,
942 None => return Ok(None),
943 }
944 }
945 Strategy::Native(state) => {
946 match state.next_finalized_with(recording, &self.cancel, previews)? {
947 Some(step) => step,
948 None => return Ok(None),
949 }
950 }
951 Strategy::Delegated(source) => match source.next(recording, previews, &self.cancel)? {
952 Some(step) => step,
953 None => return Ok(None),
954 },
955 };
956 if let Some(transcript) = step.transcript {
957 self.transcript = Some(transcript);
958 }
959 Ok(step.update)
960 }
961
962 pub fn finish(
963 &mut self,
964 recording: &mut RecordingReader,
965 ) -> Result<Transcript, TranscriptionError> {
966 while self.next_finalized(recording)?.is_some() {}
967 self.transcript.take().ok_or(TranscriptionError::NoSpeech)
968 }
969}
970
971fn transcribe_complete<T: Transcriber>(
972 input: WholeRecordingInput,
973 recording: &mut RecordingReader,
974 transcriber: &T,
975 language: &Language,
976 cancel: &CancelToken,
977) -> Result<TranscriptSegment, TranscriptionError> {
978 check_cancelled(cancel)?;
979 let (hypothesis, origin) = match input {
980 WholeRecordingInput::Samples => {
981 recording.fill().map_err(TranscriptionError::Capture)?;
982 check_cancelled(cancel)?;
983 let hypothesis = transcriber.transcribe_with(
984 TranscriptionAudio::Samples(recording.samples()),
985 language,
986 cancel,
987 )?;
988 (hypothesis, recording.held_from())
989 }
990 #[cfg(not(target_family = "wasm"))]
991 WholeRecordingInput::Wav => {
992 let audio = recording
993 .finished_audio()
994 .map_err(TranscriptionError::Capture)?;
995 check_cancelled(cancel)?;
996 let hypothesis =
997 transcriber.transcribe_with(TranscriptionAudio::Wav(audio), language, cancel)?;
998 (hypothesis, 0)
999 }
1000 };
1001 check_cancelled(cancel)?;
1002 if hypothesis.as_str().is_empty() {
1003 return Err(TranscriptionError::NoSpeech);
1004 }
1005 let source_window = origin..recording.held_to();
1006 let words = hypothesis
1007 .words()
1008 .iter()
1009 .map(|timing| bounded_span(timing.span_from(origin)?, source_window.end))
1010 .collect::<Result<Vec<_>, _>>()?;
1011 let span = envelope(words.iter().cloned());
1012 let mut segment = TranscriptSegment::new(hypothesis.into_text(), span)?;
1013 if !words.is_empty() {
1014 segment = segment.with_word_spans(words)?;
1015 }
1016 segment.with_source_window(source_window)
1017}
1018
1019pub fn check_cancelled(cancel: &CancelToken) -> Result<(), TranscriptionError> {
1021 if cancel.is_cancelled() {
1022 return Err(TranscriptionError::Cancelled);
1023 }
1024 Ok(())
1025}
1026
1027pub enum TranscriptUpdate {
1028 Finalized(TranscriptSegment),
1029 Provisional {
1030 text: String,
1031 source_window: Option<std::ops::Range<u64>>,
1032 },
1033}
1034
1035#[derive(Clone)]
1036pub struct TranscriptSegment {
1037 text: String,
1038 span: Option<std::ops::Range<u64>>,
1039 source_window: Option<std::ops::Range<u64>>,
1041 word_spans: Option<Vec<std::ops::Range<u64>>>,
1044}
1045
1046impl TranscriptSegment {
1047 pub fn new(
1048 text: impl Into<String>,
1049 span: Option<std::ops::Range<u64>>,
1050 ) -> Result<Self, TranscriptionError> {
1051 let text = text.into().trim().to_owned();
1052 if text.is_empty() || span.as_ref().is_some_and(|span| span.start > span.end) {
1053 return Err(TranscriptionError::InvalidSegment);
1054 }
1055 Ok(Self {
1056 text,
1057 span,
1058 word_spans: None,
1059 source_window: None,
1060 })
1061 }
1062
1063 fn spanned(words: &[String], span: Option<std::ops::Range<u64>>) -> Option<Self> {
1064 let text = words.join(" ");
1065 (!text.is_empty()).then_some(Self {
1066 text,
1067 span,
1068 word_spans: None,
1069 source_window: None,
1070 })
1071 }
1072
1073 fn timed(words: &[(String, std::ops::Range<u64>)]) -> Option<Self> {
1075 let span = envelope(words.iter().map(|(_, span)| span.clone()))?;
1076 let texts: Vec<&str> = words.iter().map(|(text, _)| text.as_str()).collect();
1077 Some(Self {
1078 text: texts.join(" "),
1079 span: Some(span),
1080 word_spans: Some(words.iter().map(|(_, span)| span.clone()).collect()),
1081 source_window: None,
1082 })
1083 }
1084
1085 pub fn as_str(&self) -> &str {
1086 &self.text
1087 }
1088
1089 pub fn span(&self) -> Option<std::ops::Range<u64>> {
1092 self.span.clone()
1093 }
1094
1095 pub fn word_spans(&self) -> Option<&[std::ops::Range<u64>]> {
1097 self.word_spans.as_deref()
1098 }
1099
1100 pub fn source_window(&self) -> Option<std::ops::Range<u64>> {
1101 self.source_window.clone()
1102 }
1103
1104 pub fn with_source_window(
1105 mut self,
1106 window: std::ops::Range<u64>,
1107 ) -> Result<Self, TranscriptionError> {
1108 if window.start > window.end
1109 || self
1110 .span
1111 .as_ref()
1112 .is_some_and(|span| span.start < window.start || span.end > window.end)
1113 {
1114 return Err(TranscriptionError::InvalidSegment);
1115 }
1116 self.source_window = Some(window);
1117 Ok(self)
1118 }
1119
1120 pub fn with_word_spans(
1121 mut self,
1122 words: Vec<std::ops::Range<u64>>,
1123 ) -> Result<Self, TranscriptionError> {
1124 let Some(span) = &self.span else {
1125 return Err(TranscriptionError::InvalidSegment);
1126 };
1127 if words.len() != self.text.split_whitespace().count()
1128 || words
1129 .iter()
1130 .any(|word| word.start > word.end || word.start < span.start || word.end > span.end)
1131 {
1132 return Err(TranscriptionError::InvalidSegment);
1133 }
1134 self.word_spans = Some(words);
1135 Ok(self)
1136 }
1137
1138 pub fn bounded(mut self, audio_end: u64) -> Result<Self, TranscriptionError> {
1139 if let Some(window) = self.source_window.take() {
1140 self.source_window = Some(bounded_span(window, audio_end)?);
1141 }
1142 if let Some(span) = self.span.take() {
1143 self.span = Some(bounded_span(span, audio_end)?);
1144 }
1145 if let Some(words) = &mut self.word_spans {
1146 for span in words {
1147 *span = bounded_span(span.clone(), audio_end)?;
1148 }
1149 }
1150 Ok(self)
1151 }
1152
1153 #[cfg(all(feature = "silero", not(target_family = "wasm")))]
1157 pub(crate) fn keeping(&self, keep: &[bool]) -> Option<Self> {
1158 let marks = self.word_spans.as_ref().map_or(1, Vec::len);
1159 assert_eq!(keep.len(), marks, "one mark per word");
1160 let Some(spans) = &self.word_spans else {
1161 return keep[0].then(|| self.clone());
1162 };
1163 let words: Vec<(String, std::ops::Range<u64>)> = self
1164 .text
1165 .split_whitespace()
1166 .zip(spans)
1167 .zip(keep)
1168 .filter(|(_, kept)| **kept)
1169 .map(|((text, span), _)| (text.to_owned(), span.clone()))
1170 .collect();
1171 let mut segment = Self::timed(&words)?;
1172 segment.source_window.clone_from(&self.source_window);
1173 Some(segment)
1174 }
1175
1176 pub fn into_text(self) -> String {
1177 self.text
1178 }
1179
1180 pub(crate) fn rewrite(&mut self, text: String) {
1182 if text != self.text {
1183 self.word_spans = None;
1184 }
1185 self.text = text;
1186 }
1187}
1188
1189fn envelope(spans: impl IntoIterator<Item = std::ops::Range<u64>>) -> Option<std::ops::Range<u64>> {
1190 spans
1191 .into_iter()
1192 .reduce(|covered, span| covered.start.min(span.start)..covered.end.max(span.end))
1193}
1194
1195fn bounded_span(
1196 span: std::ops::Range<u64>,
1197 audio_end: u64,
1198) -> Result<std::ops::Range<u64>, TranscriptionError> {
1199 let end = span.end.min(audio_end);
1200 if span.start > end || (span.start == end && span.end > span.start) {
1201 return Err(TranscriptionError::InvalidSegment);
1202 }
1203 let start = if span.start == audio_end && audio_end > 0 {
1205 audio_end - 1
1206 } else {
1207 span.start
1208 };
1209 Ok(start..end)
1210}
1211
1212impl std::fmt::Debug for TranscriptSegment {
1213 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1214 formatter
1215 .debug_struct("TranscriptSegment")
1216 .field("bytes", &self.text.len())
1217 .field("span", &self.span)
1218 .finish()
1219 }
1220}
1221
1222#[derive(Debug, Clone, PartialEq, Eq)]
1223pub enum Language {
1224 Automatic,
1225 Specific(LanguageCode),
1226}
1227
1228impl Language {
1229 const AUTOMATIC: &str = "auto";
1230
1231 pub fn as_str(&self) -> &str {
1232 match self {
1233 Self::Automatic => Self::AUTOMATIC,
1234 Self::Specific(code) => code.as_str(),
1235 }
1236 }
1237}
1238
1239impl std::str::FromStr for Language {
1241 type Err = LanguageCodeError;
1242
1243 fn from_str(value: &str) -> Result<Self, Self::Err> {
1244 if value.trim().eq_ignore_ascii_case(Self::AUTOMATIC) {
1245 return Ok(Self::Automatic);
1246 }
1247 LanguageCode::new(value).map(Self::Specific)
1248 }
1249}
1250
1251#[derive(Debug, Clone, PartialEq, Eq)]
1252pub struct LanguageCode(String);
1253
1254impl LanguageCode {
1255 pub fn english() -> Self {
1256 Self("en".to_owned())
1257 }
1258
1259 pub fn new(value: impl Into<String>) -> Result<Self, LanguageCodeError> {
1260 let value = value.into();
1261 let value = value.trim();
1262 if value.is_empty() {
1263 return Err(LanguageCodeError::Empty);
1264 }
1265 if value.len() > MAX_LANGUAGE_CODE_BYTES {
1266 return Err(LanguageCodeError::TooLong {
1267 actual_bytes: value.len(),
1268 maximum_bytes: MAX_LANGUAGE_CODE_BYTES,
1269 });
1270 }
1271 let mut segments = value.split('-');
1272 let valid_primary = segments.next().is_some_and(|segment| {
1273 !segment.is_empty() && segment.bytes().all(|byte| byte.is_ascii_alphabetic())
1274 });
1275 let valid_subtags = segments.all(|segment| {
1276 !segment.is_empty() && segment.bytes().all(|byte| byte.is_ascii_alphanumeric())
1277 });
1278 if !valid_primary || !valid_subtags {
1279 return Err(LanguageCodeError::Malformed(value.to_owned()));
1280 }
1281 Ok(Self(value.to_ascii_lowercase()))
1282 }
1283
1284 pub fn as_str(&self) -> &str {
1285 &self.0
1286 }
1287}
1288
1289#[derive(Debug, thiserror::Error)]
1290pub enum LanguageCodeError {
1291 #[error("recognition language code must not be empty")]
1292 Empty,
1293
1294 #[error(
1295 "recognition language code contains {actual_bytes} bytes; maximum is {maximum_bytes} bytes"
1296 )]
1297 TooLong {
1298 actual_bytes: usize,
1299 maximum_bytes: usize,
1300 },
1301
1302 #[error("recognition language code is malformed: {0:?}")]
1303 Malformed(String),
1304}
1305
1306#[derive(Clone, PartialEq, Eq)]
1307pub struct Transcript(String);
1308
1309impl Transcript {
1310 pub fn new(text: impl Into<String>) -> Result<Self, TranscriptionError> {
1311 let text = text.into();
1312 let text = text.trim();
1313 if text.is_empty() {
1314 return Err(TranscriptionError::NoSpeech);
1315 }
1316 Ok(Self(text.to_owned()))
1317 }
1318
1319 pub fn as_str(&self) -> &str {
1320 &self.0
1321 }
1322
1323 pub fn into_text(self) -> String {
1324 self.0
1325 }
1326}
1327
1328impl std::fmt::Debug for Transcript {
1329 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1330 formatter
1331 .debug_struct("Transcript")
1332 .field("bytes", &self.0.len())
1333 .finish()
1334 }
1335}
1336
1337#[derive(Debug, thiserror::Error)]
1338pub enum TranscriptionError {
1339 #[error("the {backend} model cannot be used")]
1340 Model {
1341 backend: &'static str,
1342 #[source]
1343 source: ModelError,
1344 },
1345
1346 #[error("failed to load the {backend} model {model}")]
1347 Load {
1348 backend: &'static str,
1349 model: String,
1350 #[source]
1351 source: BackendError,
1352 },
1353
1354 #[error("the recording being transcribed failed")]
1355 Capture(#[source] CaptureError),
1356
1357 #[error("{backend} inference failed")]
1358 Inference {
1359 backend: &'static str,
1360 #[source]
1361 source: BackendError,
1362 },
1363
1364 #[error("{backend} cannot transcribe {language:?}")]
1365 UnsupportedLanguage {
1366 backend: &'static str,
1367 language: String,
1368 },
1369
1370 #[error("{backend} needs a UTF-8 recording path; got {}", path.display())]
1371 UnsupportedAudioPath {
1372 backend: &'static str,
1373 path: PathBuf,
1374 },
1375
1376 #[error("{backend} requires {required}, but received {received}")]
1377 UnsupportedAudioInput {
1378 backend: &'static str,
1379 received: AudioInputKind,
1380 required: AudioInputKind,
1381 },
1382
1383 #[error("the {backend} backend is not available on {}", std::env::consts::OS)]
1384 UnsupportedBackend { backend: &'static str },
1385
1386 #[error("the {backend} backend has no gpu support on {}", std::env::consts::OS)]
1387 UnsupportedDevice { backend: &'static str },
1388
1389 #[error("the engine was asked to stream natively but offers no stream")]
1390 NoNativeStream,
1391
1392 #[error("the model loader stopped unexpectedly")]
1393 LoadPanicked,
1394
1395 #[error("transcription was cancelled")]
1396 Cancelled,
1397
1398 #[error("no speech was transcribed")]
1399 NoSpeech,
1400
1401 #[error("the transcriber was asked to run an operation but offers none")]
1402 NoDelegatedOperation,
1403
1404 #[error("transcript segment has invalid text or audio bounds")]
1405 InvalidSegment,
1406
1407 #[error("transcript processing failed")]
1408 Processing(#[source] layer::Error),
1409}
1410
1411#[cfg(not(target_family = "wasm"))]
1413pub fn validate_model_file(
1414 path: PathBuf,
1415 backend: &'static str,
1416) -> Result<PathBuf, TranscriptionError> {
1417 crate::backend::validate_model_file(path)
1418 .map_err(|source| TranscriptionError::Model { backend, source })
1419}
1420
1421#[cfg(not(target_family = "wasm"))]
1422pub fn validate_model_directory(
1423 path: PathBuf,
1424 backend: &'static str,
1425 required: &[&'static str],
1426) -> Result<PathBuf, TranscriptionError> {
1427 crate::backend::validate_model_directory(path, required)
1428 .map_err(|source| TranscriptionError::Model { backend, source })
1429}
1430
1431pub fn english_only(backend: &'static str, language: &Language) -> Result<(), TranscriptionError> {
1434 let Language::Specific(code) = language else {
1435 return Ok(());
1436 };
1437 if code.as_str().split('-').next() == Some(LanguageCode::english().as_str()) {
1438 return Ok(());
1439 }
1440 Err(TranscriptionError::UnsupportedLanguage {
1441 backend,
1442 language: code.as_str().to_owned(),
1443 })
1444}
1445
1446#[cfg(test)]
1447mod operation_source_tests {
1448 use std::collections::VecDeque;
1449 use std::sync::{Arc, Mutex};
1450
1451 use super::*;
1452
1453 #[derive(Clone)]
1454 struct Delegated {
1455 openings: Arc<Mutex<Vec<bool>>>,
1456 }
1457
1458 impl Transcriber for Delegated {
1459 fn validate_language(
1460 &self,
1461 _: &Language,
1462 _: &CancelToken,
1463 ) -> Result<(), TranscriptionError> {
1464 Ok(())
1465 }
1466
1467 fn transcribe_with(
1468 &self,
1469 _: TranscriptionAudio<'_>,
1470 _: &Language,
1471 _: &CancelToken,
1472 ) -> Result<Hypothesis, TranscriptionError> {
1473 panic!("the delegated operation owns decoding")
1474 }
1475
1476 fn open_operation(
1477 &self,
1478 _: &Language,
1479 incremental: bool,
1480 _: &CancelToken,
1481 ) -> Result<Option<OperationSource>, TranscriptionError> {
1482 self.openings.lock().unwrap().push(incremental);
1483 let mut steps = VecDeque::from([
1484 OperationStep::Update {
1485 update: TranscriptUpdate::Provisional {
1486 text: "hello".into(),
1487 source_window: None,
1488 },
1489 retained_from: 0,
1490 read_to: 20,
1491 completed: None,
1492 },
1493 OperationStep::Update {
1494 update: TranscriptUpdate::Finalized(TranscriptSegment::new(
1495 "hello world",
1496 Some(10..30),
1497 )?),
1498 retained_from: 10,
1499 read_to: 30,
1500 completed: Some(Transcript::new("hello world")?),
1501 },
1502 OperationStep::Complete {
1503 transcript: Transcript::new("hello world")?,
1504 retained_from: 30,
1505 read_to: 40,
1506 },
1507 ]);
1508 Ok(Some(OperationSource::new("remote", move |recording, _| {
1509 recording.fill().map_err(TranscriptionError::Capture)?;
1510 Ok(steps.pop_front().expect("completion stops polling"))
1511 })))
1512 }
1513 }
1514
1515 #[test]
1516 fn delegated_updates_preserve_completion_and_audio_spans() {
1517 let openings = Arc::new(Mutex::new(Vec::new()));
1518 let transcriber: Arc<dyn Transcriber> = Arc::new(Delegated {
1519 openings: Arc::clone(&openings),
1520 });
1521 let transcription = Transcription::<_, Streaming>::delegated(transcriber);
1522 let mut recording = RecordingReader::from_samples(&[0.5; 40]);
1523 let mut operation = transcription.start(&Language::Automatic).unwrap();
1524
1525 assert!(matches!(
1526 operation.next_update(&mut recording).unwrap(),
1527 Some(TranscriptUpdate::Provisional { text, .. }) if text == "hello"
1528 ));
1529 assert!(operation.completed().is_none());
1530 assert_eq!(recording.held_to(), 40);
1531 assert_eq!(operation.read_to(), 20);
1532 let Some(TranscriptUpdate::Finalized(segment)) =
1533 operation.next_update(&mut recording).unwrap()
1534 else {
1535 panic!("the next update is finalized");
1536 };
1537 assert_eq!(segment.span(), Some(10..30));
1538 assert_eq!(operation.retained_from(), 10);
1539 assert_eq!(operation.read_to(), 30);
1540 assert_eq!(operation.completed().unwrap().as_str(), "hello world");
1541 assert!(operation.next_update(&mut recording).unwrap().is_none());
1542 assert_eq!(operation.retained_from(), 30);
1543 assert_eq!(operation.read_to(), 40);
1544 assert_eq!(
1545 operation.finish(&mut recording).unwrap().as_str(),
1546 "hello world"
1547 );
1548 assert_eq!(*openings.lock().unwrap(), [true]);
1549 }
1550
1551 #[test]
1552 fn boxed_whole_operations_skip_previews_and_open_once() {
1553 let openings = Arc::new(Mutex::new(Vec::new()));
1554 let transcriber: Box<dyn Transcriber> = Box::new(Delegated {
1555 openings: Arc::clone(&openings),
1556 });
1557 let transcription = Transcription::<_, Whole>::delegated(transcriber);
1558 let mut recording = RecordingReader::from_samples(&[0.5; 40]);
1559 let mut operation = transcription.start(&Language::Automatic).unwrap();
1560
1561 assert!(matches!(
1562 operation.next(&mut recording, false).unwrap(),
1563 Some(TranscriptUpdate::Finalized(_))
1564 ));
1565 assert_eq!(
1566 operation.finish(&mut recording).unwrap().as_str(),
1567 "hello world"
1568 );
1569 assert_eq!(*openings.lock().unwrap(), [false]);
1570 }
1571
1572 struct Local;
1573
1574 impl Transcriber for Local {
1575 fn validate_language(
1576 &self,
1577 _: &Language,
1578 _: &CancelToken,
1579 ) -> Result<(), TranscriptionError> {
1580 Ok(())
1581 }
1582
1583 fn transcribe_with(
1584 &self,
1585 _: TranscriptionAudio<'_>,
1586 _: &Language,
1587 _: &CancelToken,
1588 ) -> Result<Hypothesis, TranscriptionError> {
1589 Ok(Hypothesis::new("local words"))
1590 }
1591 }
1592
1593 #[test]
1594 fn only_a_delegated_plan_runs_the_transcribers_operation() {
1595 let openings = Arc::new(Mutex::new(Vec::new()));
1596 let delegating = Delegated {
1597 openings: Arc::clone(&openings),
1598 };
1599 let local = Transcription::agreement(Cadence::from_millis(10, 20), delegating.clone());
1600 assert_eq!(local.start(&Language::Automatic).unwrap().served_by(), None);
1601 assert!(openings.lock().unwrap().is_empty());
1602 let delegated = Transcription::<_, Whole>::delegated(delegating);
1603 assert_eq!(
1604 delegated.start(&Language::Automatic).unwrap().served_by(),
1605 Some("remote")
1606 );
1607 assert!(matches!(
1608 Transcription::<_, Whole>::delegated(Local).start(&Language::Automatic),
1609 Err(TranscriptionError::NoDelegatedOperation)
1610 ));
1611 }
1612
1613 #[test]
1614 fn cancellation_during_a_delegated_step_discards_its_result() {
1615 let cancel = CancelToken::new();
1616 let trigger = cancel.clone();
1617 let mut source = OperationSource::new("remote", move |_, _| {
1618 trigger.cancel();
1619 Ok(OperationStep::Complete {
1620 transcript: Transcript::new("must not escape")?,
1621 retained_from: 0,
1622 read_to: 0,
1623 })
1624 });
1625 let mut recording = RecordingReader::from_samples(&[]);
1626
1627 assert!(matches!(
1628 source.next(&mut recording, true, &cancel),
1629 Err(TranscriptionError::Cancelled)
1630 ));
1631 }
1632
1633 #[cfg(all(feature = "silero", not(target_family = "wasm")))]
1634 #[test]
1635 fn a_segment_keeps_the_words_asked_for_with_their_timings() {
1636 let words = [
1637 ("one".to_owned(), 0..10),
1638 ("two".to_owned(), 20..30),
1639 ("three".to_owned(), 40..50),
1640 ];
1641 let timed = TranscriptSegment::timed(&words)
1642 .expect("words to join")
1643 .with_source_window(0..60)
1644 .unwrap();
1645 let untimed = TranscriptSegment::new("one two", None).unwrap();
1646 let overlapping = TranscriptSegment::new("one two three", Some(10..80))
1647 .unwrap()
1648 .with_word_spans(vec![30..80, 10..50, 40..60])
1649 .unwrap()
1650 .with_source_window(0..100)
1651 .unwrap();
1652
1653 for (segment, keep, text, span, word_spans, source_window) in [
1654 (
1655 &timed,
1656 &[false, true, true][..],
1657 "two three",
1658 Some(20..50),
1659 Some(&[20..30, 40..50][..]),
1660 Some(0..60),
1661 ),
1662 (&untimed, &[true][..], "one two", None, None, None),
1663 (
1664 &overlapping,
1665 &[true, true, false][..],
1666 "one two",
1667 Some(10..80),
1668 Some(&[30..80, 10..50][..]),
1669 Some(0..100),
1670 ),
1671 ] {
1672 let kept = segment.keeping(keep).expect("words kept");
1673
1674 assert_eq!(kept.as_str(), text);
1675 assert_eq!(kept.span(), span);
1676 assert_eq!(kept.word_spans(), word_spans);
1677 assert_eq!(kept.source_window(), source_window);
1678 assert!(segment.keeping(&vec![false; keep.len()]).is_none());
1679 if let Some(words) = word_spans {
1680 assert!(kept.with_word_spans(words.to_vec()).is_ok());
1681 }
1682 }
1683 }
1684
1685 #[test]
1686 fn word_spans_must_match_the_text_and_its_audio() {
1687 let segment = || TranscriptSegment::new("one two", Some(0..30)).unwrap();
1688 assert!(segment().with_word_spans(vec![0..10, 20..30]).is_ok());
1689 let reversed = std::ops::Range { start: 25, end: 20 };
1690 let missing = std::iter::once(0..10).collect();
1691 for spans in [missing, vec![0..10, 20..31], vec![0..10, reversed]] {
1692 assert!(segment().with_word_spans(spans).is_err());
1693 }
1694 }
1695
1696 #[test]
1697 fn bounding_a_segment_preserves_its_word_alignment() {
1698 let segment = TranscriptSegment::new("one two", Some(0..40))
1699 .unwrap()
1700 .with_word_spans(vec![0..10, 20..40])
1701 .unwrap()
1702 .with_source_window(0..45)
1703 .unwrap()
1704 .bounded(30)
1705 .unwrap();
1706 assert_eq!(segment.span(), Some(0..30));
1707 assert_eq!(segment.word_spans(), Some(&[0..10, 20..30][..]));
1708 assert_eq!(segment.source_window(), Some(0..30));
1709 let eof = TranscriptSegment::new("end", Some(30..30))
1710 .unwrap()
1711 .with_word_spans(std::iter::once(30..30).collect())
1712 .unwrap()
1713 .bounded(30)
1714 .unwrap();
1715 assert_eq!(eof.word_spans(), Some(std::slice::from_ref(&(29..30))));
1716 }
1717
1718 #[test]
1719 fn external_segments_reject_empty_text_and_reversed_spans() {
1720 assert!(TranscriptSegment::new(" ", None).is_err());
1721 let reversed = std::ops::Range { start: 30, end: 10 };
1722 assert!(TranscriptSegment::new("hello", Some(reversed)).is_err());
1723 let segment = TranscriptSegment::new(" hello ", Some(10..10)).unwrap();
1724 assert_eq!(segment.as_str(), "hello");
1725 assert_eq!(segment.span(), Some(10..10));
1726 }
1727}
1728
1729#[cfg(test)]
1730mod word_timing_tests {
1731 use super::*;
1732
1733 fn token(text: &str, start: f32, end: f32) -> (&str, TimedWord) {
1734 (text, TimedWord { start, end })
1735 }
1736
1737 #[test]
1738 fn punctuation_only_words_and_spoken_symbols_keep_their_timings() {
1739 let words = word_timings([
1740 token(" .", 0.0, 0.1),
1741 token("..", 0.1, 0.2),
1742 token(" $", 0.2, 0.3),
1743 token("5", 0.3, 0.4),
1744 token(" -", 0.4, 0.5),
1745 token("20", 0.5, 0.6),
1746 token("%", 0.6, 0.7),
1747 token(" .", 0.7, 0.8),
1748 token("5", 0.8, 0.9),
1749 token(".", 0.9, 1.0),
1750 ]);
1751 assert_eq!(
1752 words,
1753 [
1754 TimedWord {
1755 start: 0.0,
1756 end: 0.2
1757 },
1758 TimedWord {
1759 start: 0.2,
1760 end: 0.4
1761 },
1762 TimedWord {
1763 start: 0.4,
1764 end: 0.7
1765 },
1766 TimedWord {
1767 start: 0.7,
1768 end: 0.9
1769 },
1770 ]
1771 );
1772 }
1773
1774 #[test]
1775 fn tokens_group_into_words_at_the_sentencepiece_marker() {
1776 let tokens = [
1777 token("\u{2581}hel", 0.0, 0.2),
1778 token("lo", 0.2, 0.4),
1779 token(",", 0.4, 0.5),
1780 token("\u{2581}world", 0.6, 0.9),
1781 ];
1782 let words = word_timings(tokens);
1783 assert_eq!(words.len(), 2);
1784 assert_eq!((words[0].start, words[0].end), (0.0, 0.4));
1785 assert_eq!((words[1].start, words[1].end), (0.6, 0.9));
1786 }
1787
1788 #[test]
1789 fn tokens_group_into_words_at_a_leading_space() {
1790 let tokens = [
1791 token(" hel", 0.0, 0.2),
1792 token("lo", 0.2, 0.4),
1793 token(",", 0.4, 0.5),
1794 token(" world", 0.6, 0.9),
1795 ];
1796 let words = word_timings(tokens);
1797 assert_eq!(words.len(), 2);
1798 assert_eq!((words[0].start, words[0].end), (0.0, 0.4));
1799 assert_eq!((words[1].start, words[1].end), (0.6, 0.9));
1800 }
1801
1802 #[test]
1803 fn word_timings_count_what_the_text_splits_into() {
1804 let tokens = [
1806 token("\u{2581}", 0.0, 0.1),
1807 token("\u{2581}1", 0.1, 0.2),
1808 token("8", 0.2, 0.3),
1809 token("9", 0.3, 0.4),
1810 token("\u{2581}years old", 0.4, 0.8),
1811 token(".", 0.8, 0.9),
1812 ];
1813 let text: String = tokens
1814 .iter()
1815 .map(|(text, _)| text.replace('\u{2581}', " "))
1816 .collect();
1817 let words = word_timings(tokens);
1818 assert_eq!(words.len(), text.split_whitespace().count());
1819 assert_eq!((words[0].start, words[0].end), (0.1, 0.4));
1820 assert_eq!((words[2].start, words[2].end), (0.4, 0.8));
1821 }
1822
1823 #[test]
1824 fn word_timing_conversion_rejects_invalid_values_and_overflow() {
1825 for (start, end, origin) in [
1826 (f32::NAN, 0.5, 0),
1827 (0.0, f32::NAN, 0),
1828 (f32::NEG_INFINITY, 0.5, 0),
1829 (0.0, f32::INFINITY, 0),
1830 (-0.1, 0.5, 0),
1831 (0.5, 0.1, 0),
1832 (0.0, f32::MAX, 0),
1833 (0.0, 0.5, u64::MAX - 7_999),
1834 ] {
1835 assert!(matches!(
1836 TimedWord { start, end }.span_from(origin),
1837 Err(TranscriptionError::InvalidSegment)
1838 ));
1839 }
1840 }
1841
1842 #[test]
1843 fn word_timing_conversion_accepts_zero_duration() {
1844 let (start, end, origin, expected) = (0.0, 0.0, 0, 0..0);
1845 assert_eq!(
1846 TimedWord { start, end }.span_from(origin).unwrap(),
1847 expected
1848 );
1849 }
1850
1851 #[test]
1852 fn a_word_timing_rounds_to_the_nearest_sample() {
1853 let word = TimedWord {
1855 start: 0.100_03,
1856 end: 0.200_04,
1857 };
1858
1859 assert_eq!(word.span_from(1_000).unwrap(), 2_600..4_201);
1860 }
1861}
1862
1863#[cfg(test)]
1864mod language_tests {
1865 use super::*;
1866
1867 const BACKEND: &str = "test-backend";
1868
1869 #[test]
1870 fn detection_and_english_are_accepted() {
1871 assert!(english_only(BACKEND, &Language::Automatic).is_ok());
1872 assert!(english_only(BACKEND, &Language::Specific(LanguageCode::english())).is_ok());
1873
1874 let regional = LanguageCode::new("en-GB").expect("en-GB is a valid code");
1875 assert!(english_only(BACKEND, &Language::Specific(regional)).is_ok());
1876 }
1877
1878 #[test]
1879 fn language_parses_what_as_str_writes() {
1880 for language in [
1881 Language::Automatic,
1882 Language::Specific(LanguageCode::new("en-GB").expect("en-GB is a valid code")),
1883 ] {
1884 assert_eq!(language.as_str().parse::<Language>().ok(), Some(language));
1885 }
1886 assert_eq!(" AUTO".parse::<Language>().ok(), Some(Language::Automatic));
1887 assert!(matches!(
1888 "en_GB".parse::<Language>(),
1889 Err(LanguageCodeError::Malformed(_))
1890 ));
1891 }
1892
1893 #[test]
1894 fn another_language_is_refused_by_name() {
1895 let code = LanguageCode::new("de").expect("de is a valid code");
1896
1897 let error = english_only(BACKEND, &Language::Specific(code))
1898 .expect_err("another language must be refused");
1899
1900 assert!(matches!(
1901 error,
1902 TranscriptionError::UnsupportedLanguage { language, .. } if language == "de"
1903 ));
1904 }
1905}
1906
1907#[cfg(test)]
1908mod coordinator_tests {
1909 use std::sync::{Arc, Mutex};
1910
1911 use crate::audio::testing::recorded;
1912
1913 use super::*;
1914
1915 struct FakeTranscriber {
1916 response: &'static str,
1917 words: Vec<TimedWord>,
1918 reject_language: bool,
1919 inputs: Mutex<Vec<AudioInputKind>>,
1920 }
1921
1922 impl FakeTranscriber {
1923 fn answering(response: &'static str) -> Self {
1924 Self {
1925 response,
1926 words: Vec::new(),
1927 reject_language: false,
1928 inputs: Mutex::new(Vec::new()),
1929 }
1930 }
1931
1932 fn with_words(mut self, words: Vec<TimedWord>) -> Self {
1933 self.words = words;
1934 self
1935 }
1936
1937 fn inputs(&self) -> Vec<AudioInputKind> {
1938 self.inputs
1939 .lock()
1940 .unwrap_or_else(|poisoned| poisoned.into_inner())
1941 .clone()
1942 }
1943 }
1944
1945 impl Transcriber for FakeTranscriber {
1946 fn validate_language(
1947 &self,
1948 language: &Language,
1949 _cancel: &CancelToken,
1950 ) -> Result<(), TranscriptionError> {
1951 if self.reject_language {
1952 return Err(TranscriptionError::UnsupportedLanguage {
1953 backend: "fake",
1954 language: language.as_str().to_owned(),
1955 });
1956 }
1957 Ok(())
1958 }
1959
1960 fn transcribe_with(
1961 &self,
1962 audio: TranscriptionAudio<'_>,
1963 _language: &Language,
1964 _cancel: &CancelToken,
1965 ) -> Result<Hypothesis, TranscriptionError> {
1966 self.inputs
1967 .lock()
1968 .unwrap_or_else(|poisoned| poisoned.into_inner())
1969 .push(audio.kind());
1970 Ok(Hypothesis::with_words(self.response, self.words.clone()))
1971 }
1972 }
1973
1974 struct Chunked;
1976
1977 struct HelloStream;
1978
1979 impl ChunkStream for HelloStream {
1980 fn chunk_samples(&self) -> usize {
1981 2
1982 }
1983
1984 fn feed(&mut self, chunk: &[f32]) -> Result<Emitted, TranscriptionError> {
1985 Ok(Emitted {
1986 words: Hypothesis::new(match chunk.iter().any(|sample| *sample != 0.0) {
1987 true => "hello",
1988 false => "",
1989 }),
1990 joins_previous: false,
1991 })
1992 }
1993 }
1994
1995 impl Transcriber for Chunked {
1996 fn validate_language(
1997 &self,
1998 _language: &Language,
1999 _cancel: &CancelToken,
2000 ) -> Result<(), TranscriptionError> {
2001 Ok(())
2002 }
2003
2004 fn transcribe_with(
2005 &self,
2006 _audio: TranscriptionAudio<'_>,
2007 _language: &Language,
2008 _cancel: &CancelToken,
2009 ) -> Result<Hypothesis, TranscriptionError> {
2010 unreachable!("a native stream never decodes a bounded input")
2011 }
2012
2013 fn open_stream(
2014 &self,
2015 _language: &Language,
2016 _cancel: &CancelToken,
2017 ) -> Result<Option<Box<dyn ChunkStream>>, TranscriptionError> {
2018 Ok(Some(Box::new(HelloStream)))
2019 }
2020 }
2021
2022 #[test]
2023 fn a_boxed_or_shared_transcriber_still_opens_its_stream() {
2024 let (_directory, mut boxed_recording) = recorded(&[1, 2, 3, 4]);
2025 let (_directory, mut shared_recording) = recorded(&[1, 2, 3, 4]);
2026 let boxed: Box<dyn Transcriber> = Box::new(Chunked);
2027 let shared = Arc::new(Chunked);
2028
2029 let from_boxed = Transcription::stream(Stream::Native, boxed)
2030 .transcribe(&mut boxed_recording, &Language::Automatic)
2031 .expect("the boxed stream opens");
2032 let from_shared = Transcription::stream(Stream::Native, shared)
2033 .transcribe(&mut shared_recording, &Language::Automatic)
2034 .expect("the shared stream opens");
2035
2036 assert_eq!(from_boxed.as_str(), "hello hello");
2037 assert_eq!(from_shared.as_str(), "hello hello");
2038 }
2039
2040 #[test]
2041 fn completion_is_available_before_pending_final_segments_and_preview_clear() {
2042 struct Ending(std::sync::atomic::AtomicBool);
2043 impl Transcriber for Ending {
2044 fn validate_language(
2045 &self,
2046 _: &Language,
2047 _: &CancelToken,
2048 ) -> Result<(), TranscriptionError> {
2049 Ok(())
2050 }
2051 fn transcribe_with(
2052 &self,
2053 _: TranscriptionAudio<'_>,
2054 _: &Language,
2055 _: &CancelToken,
2056 ) -> Result<Hypothesis, TranscriptionError> {
2057 Ok(Hypothesis::new(
2058 if self.0.swap(true, std::sync::atomic::Ordering::Relaxed) {
2059 "one two three"
2060 } else {
2061 "one two"
2062 },
2063 ))
2064 }
2065 }
2066 let (mut recording, mut capture) = crate::audio::Recording::open();
2067 let transcription = Transcription::agreement(
2068 Cadence::from_millis(100, 20_000),
2069 Ending(std::sync::atomic::AtomicBool::new(false)),
2070 );
2071 let mut operation = transcription.start(&Language::Automatic).expect("starts");
2072 capture.push(&[0.5; 1600]);
2073 assert!(
2074 matches!(operation.next_update(&mut recording).expect("preview"), Some(TranscriptUpdate::Provisional { text, .. }) if text == "one two")
2075 );
2076 capture.push(&[0.5; 1600]);
2077 capture.finish();
2078 recording.fill().expect("capture ended");
2079 assert!(
2080 matches!(operation.next_update(&mut recording).expect("final"), Some(TranscriptUpdate::Finalized(segment)) if segment.as_str() == "one two")
2081 );
2082 assert_eq!(
2083 operation.completed().expect("already complete").as_str(),
2084 "one two three"
2085 );
2086 assert!(
2087 matches!(operation.next_update(&mut recording).expect("tail"), Some(TranscriptUpdate::Finalized(segment)) if segment.as_str() == "three")
2088 );
2089 assert!(
2090 matches!(operation.next_update(&mut recording).expect("clear"), Some(TranscriptUpdate::Provisional { text, .. }) if text.is_empty())
2091 );
2092 assert!(
2093 operation
2094 .next_update(&mut recording)
2095 .expect("ended")
2096 .is_none()
2097 );
2098 }
2099
2100 #[test]
2101 fn native_and_whole_updates_finalize_every_word() {
2102 fn check<T: Transcriber, M: Mode>(transcription: Transcription<T, M>, expected: &str) {
2103 let (_directory, mut recording) = recorded(&[1, 2, 3, 4]);
2104 let mut operation = transcription.start(&Language::Automatic).expect("starts");
2105 let mut segments = Vec::new();
2106 while let Some(update) = operation.next_update(&mut recording).expect("reads") {
2107 if let TranscriptUpdate::Finalized(segment) = update {
2109 segments.push(segment.into_text());
2110 }
2111 }
2112 assert_eq!(segments.join(" "), expected);
2113 assert_eq!(operation.completed().expect("completed").as_str(), expected);
2114 assert_eq!(
2115 operation.finish(&mut recording).expect("finished").as_str(),
2116 expected
2117 );
2118 }
2119 check(
2120 Transcription::stream(Stream::Native, Chunked),
2121 "hello hello",
2122 );
2123 check(
2124 Transcription::complete_samples(FakeTranscriber::answering("hello there")),
2125 "hello there",
2126 );
2127 }
2128
2129 #[test]
2130 fn streaming_natively_without_a_stream_is_an_error() {
2131 let transcription =
2132 Transcription::stream(Stream::Native, FakeTranscriber::answering("hello"));
2133
2134 let outcome = transcription.start(&Language::Automatic).map(|_| ());
2135
2136 assert!(matches!(outcome, Err(TranscriptionError::NoNativeStream)));
2137 }
2138
2139 #[test]
2140 fn complete_samples_invokes_the_engine_once() {
2141 let (_directory, mut recording) = recorded(&[1, 2, 3]);
2142 let transcriber = Arc::new(FakeTranscriber::answering("hello there"));
2143 let transcription = Transcription::complete_samples(Arc::clone(&transcriber));
2144
2145 let transcript = transcription
2146 .transcribe(&mut recording, &Language::Automatic)
2147 .expect("the fake returns speech");
2148
2149 assert_eq!(transcript.as_str(), "hello there");
2150 assert_eq!(transcriber.inputs(), [AudioInputKind::Samples]);
2151 }
2152
2153 #[cfg(not(target_family = "wasm"))]
2154 #[test]
2155 fn complete_wav_preserves_the_file_input_boundary() {
2156 let (_directory, mut recording) = recorded(&[1, 2, 3]);
2157 let transcriber = Arc::new(FakeTranscriber::answering("hello there"));
2158 let transcription = Transcription::complete_wav(Arc::clone(&transcriber));
2159
2160 transcription
2161 .transcribe(&mut recording, &Language::Automatic)
2162 .expect("the fake returns speech");
2163
2164 assert_eq!(transcriber.inputs(), [AudioInputKind::Wav]);
2165 assert!(recording.samples().is_empty());
2166 }
2167
2168 #[test]
2169 fn complete_samples_preserves_timings_relative_to_the_decoded_window() {
2170 let mut recording = RecordingReader::from_samples(&[0.5; 32_000]);
2171 recording.fill().unwrap();
2172 recording.release_before(16_000);
2173 let transcription = Transcription::complete_samples(
2174 FakeTranscriber::answering("hello there").with_words(vec![
2175 TimedWord {
2176 start: 0.1,
2177 end: 0.2,
2178 },
2179 TimedWord {
2180 start: 0.3,
2181 end: 0.4,
2182 },
2183 ]),
2184 );
2185 let mut operation = transcription.start(&Language::Automatic).unwrap();
2186
2187 let segment = operation.next_finalized(&mut recording).unwrap().unwrap();
2188
2189 assert_eq!(segment.as_str(), "hello there");
2190 assert_eq!(segment.span(), Some(17_600..22_400));
2191 assert_eq!(
2192 segment.word_spans(),
2193 Some(&[17_600..19_200, 20_800..22_400][..])
2194 );
2195 assert_eq!(segment.source_window(), Some(16_000..32_000));
2196 assert_eq!(operation.completed().unwrap().as_str(), "hello there");
2197 assert!(operation.next_finalized(&mut recording).unwrap().is_none());
2198 }
2199
2200 #[cfg(not(target_family = "wasm"))]
2201 #[test]
2202 fn complete_wav_preserves_recording_coordinates_without_retaining_samples() {
2203 let (_directory, mut recording) = recorded(&[1; 32_000]);
2204 recording.fill().unwrap();
2205 recording.release_before(16_000);
2206 let transcription =
2207 Transcription::complete_wav(FakeTranscriber::answering("hello").with_words(vec![
2208 TimedWord {
2209 start: 0.1,
2210 end: 0.2,
2211 },
2212 ]));
2213 let mut operation = transcription.start(&Language::Automatic).unwrap();
2214
2215 let segment = operation.next_finalized(&mut recording).unwrap().unwrap();
2216
2217 assert_eq!(
2218 segment.word_spans(),
2219 Some(std::slice::from_ref(&(1_600..3_200)))
2220 );
2221 assert_eq!(segment.source_window(), Some(0..32_000));
2222 assert!(recording.samples().is_empty());
2223 }
2224
2225 #[test]
2226 fn complete_without_aligned_timings_stays_untimed() {
2227 for words in [
2228 vec![],
2229 vec![TimedWord {
2230 start: 0.1,
2231 end: 0.2,
2232 }],
2233 ] {
2234 let mut recording = RecordingReader::from_samples(&[0.5; 16_000]);
2235 let transcription = Transcription::complete_samples(
2236 FakeTranscriber::answering("hello there").with_words(words),
2237 );
2238 let mut operation = transcription.start(&Language::Automatic).unwrap();
2239
2240 let segment = operation.next_finalized(&mut recording).unwrap().unwrap();
2241
2242 assert_eq!(segment.word_spans(), None);
2243 assert_eq!(segment.span(), None);
2244 assert_eq!(segment.source_window(), Some(0..16_000));
2245 }
2246 }
2247
2248 #[test]
2249 fn complete_bounds_word_timings_to_the_actual_audio() {
2250 let timing = TimedWord {
2251 start: 0.5,
2252 end: 1.5,
2253 };
2254 let expected = 8_000..16_000;
2255 let mut recording = RecordingReader::from_samples(&[0.5; 16_000]);
2256 let transcription = Transcription::complete_samples(
2257 FakeTranscriber::answering("hello").with_words(vec![timing]),
2258 );
2259 let mut operation = transcription.start(&Language::Automatic).unwrap();
2260
2261 let segment = operation.next_finalized(&mut recording).unwrap().unwrap();
2262
2263 assert_eq!(segment.word_spans(), Some(std::slice::from_ref(&expected)));
2264 assert_eq!(segment.span(), Some(expected));
2265 assert_eq!(segment.source_window(), Some(0..16_000));
2266 }
2267
2268 #[test]
2269 fn complete_encloses_all_word_bounds_without_reordering_words() {
2270 let mut recording = RecordingReader::from_samples(&[0.5; 16_000]);
2271 let transcription = Transcription::complete_samples(
2272 FakeTranscriber::answering("hello there").with_words(vec![
2273 TimedWord {
2274 start: 0.3,
2275 end: 0.8,
2276 },
2277 TimedWord {
2278 start: 0.1,
2279 end: 0.5,
2280 },
2281 ]),
2282 );
2283 let mut operation = transcription.start(&Language::Automatic).unwrap();
2284
2285 let segment = operation.next_finalized(&mut recording).unwrap().unwrap();
2286
2287 assert_eq!(segment.as_str(), "hello there");
2288 assert_eq!(segment.span(), Some(1_600..12_800));
2289 assert_eq!(
2290 segment.word_spans(),
2291 Some(&[4_800..12_800, 1_600..8_000][..])
2292 );
2293 }
2294
2295 #[test]
2296 fn complete_rejects_malformed_or_unavailable_word_timings() {
2297 for timing in [
2298 TimedWord {
2299 start: f32::NAN,
2300 end: 0.2,
2301 },
2302 TimedWord {
2303 start: 1.1,
2304 end: 1.2,
2305 },
2306 TimedWord {
2307 start: 1.0,
2308 end: 1.2,
2309 },
2310 ] {
2311 let mut recording = RecordingReader::from_samples(&[0.5; 16_000]);
2312 let transcription = Transcription::complete_samples(
2313 FakeTranscriber::answering("hello").with_words(vec![timing]),
2314 );
2315 let mut operation = transcription.start(&Language::Automatic).unwrap();
2316
2317 assert!(matches!(
2318 operation.next_finalized(&mut recording),
2319 Err(TranscriptionError::InvalidSegment)
2320 ));
2321 assert!(operation.completed().is_none());
2322 }
2323 }
2324
2325 #[test]
2326 fn complete_text_layers_drop_timings_only_when_text_changes() {
2327 for text in ["hello there", "um hello"] {
2328 let mut recording = RecordingReader::from_samples(&[0.5; 16_000]);
2329 let transcription =
2330 Transcription::complete_samples(FakeTranscriber::answering(text).with_words(vec![
2331 TimedWord {
2332 start: 0.1,
2333 end: 0.2,
2334 },
2335 TimedWord {
2336 start: 0.3,
2337 end: 0.4,
2338 },
2339 ]))
2340 .with(layer::RemoveHesitations);
2341 let mut operation = transcription.start(&Language::Automatic).unwrap();
2342
2343 let segment = operation.next_finalized(&mut recording).unwrap().unwrap();
2344
2345 assert_eq!(segment.word_spans().is_some(), text == "hello there");
2346 assert_eq!(segment.as_str(), operation.completed().unwrap().as_str());
2347 assert_eq!(segment.source_window(), Some(0..16_000));
2348 }
2349 }
2350
2351 #[test]
2352 fn an_empty_hypothesis_becomes_terminal_no_speech() {
2353 let (_directory, mut recording) = recorded(&[1, 2, 3]);
2354 let transcription = Transcription::complete_samples(FakeTranscriber::answering(" "));
2355
2356 let outcome = transcription.transcribe(&mut recording, &Language::Automatic);
2357
2358 assert!(matches!(outcome, Err(TranscriptionError::NoSpeech)));
2359 }
2360
2361 #[test]
2362 fn cancellation_prevents_complete_inference() {
2363 let (_directory, mut recording) = recorded(&[1, 2, 3]);
2364 let transcriber = Arc::new(FakeTranscriber::answering("unreachable"));
2365 let transcription = Transcription::complete_samples(Arc::clone(&transcriber));
2366 let cancel = CancelToken::new();
2367 cancel.cancel();
2368
2369 let outcome = transcription.transcribe_with(&mut recording, &Language::Automatic, &cancel);
2370
2371 assert!(matches!(outcome, Err(TranscriptionError::Cancelled)));
2372 assert!(transcriber.inputs().is_empty());
2373 }
2374
2375 #[test]
2376 fn language_rejection_prevents_complete_inference() {
2377 let (_directory, mut recording) = recorded(&[1, 2, 3]);
2378 let mut fake = FakeTranscriber::answering("unreachable");
2379 fake.reject_language = true;
2380 let transcriber = Arc::new(fake);
2381 let transcription = Transcription::complete_samples(Arc::clone(&transcriber));
2382
2383 let outcome = transcription.transcribe(&mut recording, &Language::Automatic);
2384
2385 assert!(matches!(
2386 outcome,
2387 Err(TranscriptionError::UnsupportedLanguage { .. })
2388 ));
2389 assert!(transcriber.inputs().is_empty());
2390 }
2391
2392 #[test]
2393 fn sensitive_transcription_types_redact_their_text() {
2394 let secret = "words that must not reach diagnostics";
2395 let hypothesis = Hypothesis::new(secret);
2396 let transcript = Transcript::new(secret).expect("the text is not empty");
2397 let segment =
2398 TranscriptSegment::spanned(&[secret.to_owned()], None).expect("the text is not empty");
2399
2400 assert!(!format!("{hypothesis:?}").contains(secret));
2401 assert!(!format!("{transcript:?}").contains(secret));
2402 assert!(!format!("{segment:?}").contains(secret));
2403 }
2404}