Skip to main content

aria_web/
dictation.rs

1use wasm_bindgen::prelude::*;
2
3use aria_kit::audio::{Recording, RecordingReader, RecordingWriter};
4use aria_kit::primitives::error_chain;
5use aria_kit::transcription::{Language, Transcription, TranscriptionError, Whole};
6use aria_stt::whisper_candle::WhisperCandle;
7
8use crate::silence::SilenceGate;
9
10/// One dictation: audio pushed in, a transcript out.
11#[wasm_bindgen]
12pub struct Dictation {
13    capture: RecordingWriter,
14    recording: RecordingReader,
15    transcription: Transcription<WhisperCandle, Whole>,
16    gate: SilenceGate,
17}
18
19#[wasm_bindgen]
20impl Dictation {
21    /// Loads a Whisper model the page has already fetched, and opens a
22    /// recording to push into.
23    ///
24    /// `trailing_silence_ms` is how long the speaker must stay quiet after
25    /// speaking before [`Dictation::speaker_stopped`] returns true.
26    #[wasm_bindgen(constructor)]
27    pub fn new(
28        weights: &[u8],
29        tokenizer: &[u8],
30        config: &[u8],
31        trailing_silence_ms: u32,
32    ) -> Result<Self, JsError> {
33        let gate = SilenceGate::new(trailing_silence_ms)
34            .map_err(|error| JsError::new(&error_chain(&error)))?;
35        let engine = WhisperCandle::from_bytes(weights, tokenizer, config).map_err(failed)?;
36        let transcription = Transcription::complete_samples(engine);
37        let (recording, capture) = Recording::open();
38        Ok(Self {
39            capture,
40            recording,
41            transcription,
42            gate,
43        })
44    }
45
46    /// Takes one frame of mono audio, already resampled to 16 kHz.
47    pub fn push_audio(&mut self, samples: &[f32]) {
48        self.capture.push(samples);
49        self.gate.hear(samples);
50    }
51
52    /// Whether the speaker has spoken and then gone quiet for the configured
53    /// pause. A page polls this to decide when to call [`Dictation::finish`].
54    pub fn speaker_stopped(&self) -> bool {
55        self.gate.stopped()
56    }
57
58    /// Ends the recording and transcribes everything pushed. A recording with
59    /// no speech in it answers with empty text rather than an error.
60    pub fn finish(mut self) -> Result<String, JsError> {
61        self.capture.finish();
62        match self
63            .transcription
64            .transcribe(&mut self.recording, &Language::Automatic)
65        {
66            Ok(transcript) => Ok(transcript.into_text()),
67            Err(TranscriptionError::NoSpeech) => Ok(String::new()),
68            Err(error) => Err(failed(error)),
69        }
70    }
71}
72
73fn failed(error: TranscriptionError) -> JsError {
74    JsError::new(&error_chain(&error))
75}