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