Skip to main content

gpui_component/speech/
recognizer.rs

1use std::{fmt, rc::Rc, sync::Arc};
2
3use gpui::{App, Context, SharedString, Subscription, WeakEntity};
4
5use super::{SpeechState, state::defer_session_update};
6
7/// The PCM format a [`SpeechRecognizer`] consumes.
8///
9/// Audio always arrives as interleaved signed 16-bit samples; an [`AudioInput`]
10/// converts whatever its device produces to this rate and channel count.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub struct AudioFormat {
13    sample_rate: u32,
14    channels: u16,
15}
16
17impl AudioFormat {
18    /// A format of `sample_rate` samples per second on each of `channels`.
19    pub fn new(sample_rate: u32, channels: u16) -> Self {
20        Self {
21            sample_rate,
22            channels,
23        }
24    }
25
26    /// Samples per second of each channel.
27    pub fn sample_rate(&self) -> u32 {
28        self.sample_rate
29    }
30
31    /// Number of interleaved channels.
32    pub fn channels(&self) -> u16 {
33        self.channels
34    }
35}
36
37impl Default for AudioFormat {
38    /// 16 kHz mono, the format most speech services expect.
39    fn default() -> Self {
40        Self::new(16_000, 1)
41    }
42}
43
44/// Why a speech session failed.
45#[derive(Debug, Clone)]
46pub enum SpeechError {
47    /// The user or the system denied access to the microphone.
48    PermissionDenied,
49    /// No audio input device is available.
50    NoInputDevice,
51    /// Speech input is not supported on this platform or build.
52    Unsupported,
53    /// The audio input failed or its device went away.
54    Input(Arc<anyhow::Error>),
55    /// The recognizer failed, e.g. it could not reach its service.
56    Recognizer(Arc<anyhow::Error>),
57}
58
59impl SpeechError {
60    /// An [`SpeechError::Input`] from any error.
61    pub fn input(error: impl Into<anyhow::Error>) -> Self {
62        Self::Input(Arc::new(error.into()))
63    }
64
65    /// A [`SpeechError::Recognizer`] from any error.
66    pub fn recognizer(error: impl Into<anyhow::Error>) -> Self {
67        Self::Recognizer(Arc::new(error.into()))
68    }
69}
70
71impl fmt::Display for SpeechError {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        match self {
74            Self::PermissionDenied => f.write_str("microphone access was denied"),
75            Self::NoInputDevice => f.write_str("no audio input device is available"),
76            Self::Unsupported => f.write_str("speech input is not supported"),
77            Self::Input(error) => write!(f, "audio input failed: {error:#}"),
78            Self::Recognizer(error) => write!(f, "speech recognition failed: {error:#}"),
79        }
80    }
81}
82
83impl std::error::Error for SpeechError {}
84
85/// Turns speech into text, e.g. by streaming audio to a cloud service.
86///
87/// Implement it for the service the application uses and pass it to
88/// [`SpeechState::recognizer`]. Without one, the state falls back to the
89/// platform's own recognizer where there is one (see
90/// `SystemRecognizer`).
91///
92/// A session runs as follows:
93///
94/// 1. [`start`](Self::start) opens a session. Connecting may take a while, so
95///    return immediately and buffer the audio pushed in the meantime; call
96///    [`SpeechSink::ready`] once the service accepts audio.
97/// 2. [`RecognitionSession::push_audio`] delivers PCM in [`Self::audio_format`].
98///    Report results through [`SpeechSink::hypothesis`] and
99///    [`SpeechSink::phrase`] as they arrive.
100/// 3. [`RecognitionSession::finish`] means the user stopped talking: send the
101///    remaining audio, wait for the last result, then call
102///    [`SpeechSink::finish`].
103///
104/// Dropping the [`RecognitionSession`] cancels it: close the connection and
105/// report nothing more. Every [`SpeechSink`] method may be called from any
106/// point on the main thread, including from inside `start` or `push_audio`.
107pub trait SpeechRecognizer: 'static {
108    /// The audio format this recognizer consumes, default 16 kHz mono.
109    fn audio_format(&self) -> AudioFormat {
110        AudioFormat::default()
111    }
112
113    /// Whether the recognizer can start a session now, default `true`.
114    ///
115    /// Return `false` while it cannot work, e.g. before the user signs in; the
116    /// [`SpeechButton`](super::SpeechButton) then renders disabled. Called on
117    /// every render, so keep it cheap.
118    fn is_available(&self, _cx: &App) -> bool {
119        true
120    }
121
122    /// Open a session that reports its results to `sink`.
123    fn start(
124        &self,
125        sink: SpeechSink,
126        cx: &mut App,
127    ) -> Result<Box<dyn RecognitionSession>, SpeechError>;
128}
129
130impl<T: SpeechRecognizer + ?Sized> SpeechRecognizer for Rc<T> {
131    fn audio_format(&self) -> AudioFormat {
132        (**self).audio_format()
133    }
134
135    fn is_available(&self, cx: &App) -> bool {
136        (**self).is_available(cx)
137    }
138
139    fn start(
140        &self,
141        sink: SpeechSink,
142        cx: &mut App,
143    ) -> Result<Box<dyn RecognitionSession>, SpeechError> {
144        (**self).start(sink, cx)
145    }
146}
147
148/// One running recognition, opened by [`SpeechRecognizer::start`].
149///
150/// Dropping it cancels the session.
151pub trait RecognitionSession: 'static {
152    /// Deliver interleaved PCM samples in the recognizer's [`AudioFormat`].
153    fn push_audio(&mut self, samples: &[i16], cx: &mut App);
154
155    /// No more audio will arrive. Flush what is buffered and call
156    /// [`SpeechSink::finish`] once the final result is in.
157    fn finish(&mut self, cx: &mut App);
158}
159
160/// Where a [`SpeechRecognizer`] reports its session's progress.
161///
162/// The sink is cheap to clone and may outlive its session: once the session is
163/// stopped, cancelled or replaced, its calls are ignored. Calls are applied
164/// after the current update, so they never re-enter the [`SpeechState`].
165#[derive(Clone)]
166pub struct SpeechSink {
167    pub(super) state: WeakEntity<SpeechState>,
168    pub(super) session: usize,
169}
170
171impl SpeechSink {
172    /// The service is connected and consuming audio.
173    pub fn ready(&self, cx: &mut App) {
174        self.apply(cx, |state, cx| state.on_ready(cx));
175    }
176
177    /// Replace the hypothesis for the phrase being spoken.
178    pub fn hypothesis(&self, text: impl Into<SharedString>, cx: &mut App) {
179        let text = text.into();
180        self.apply(cx, move |state, cx| state.on_hypothesis(text, cx));
181    }
182
183    /// Commit a recognized phrase and clear the hypothesis.
184    ///
185    /// Phrases are joined verbatim, so include any separator the language
186    /// needs, such as a leading space between English sentences.
187    pub fn phrase(&self, text: impl Into<SharedString>, cx: &mut App) {
188        let text = text.into();
189        self.apply(cx, move |state, cx| state.on_phrase(text, cx));
190    }
191
192    /// The session is complete; no more results will follow.
193    pub fn finish(&self, cx: &mut App) {
194        self.apply(cx, |state, cx| state.on_finish(cx));
195    }
196
197    /// The session failed.
198    pub fn error(&self, error: SpeechError, cx: &mut App) {
199        self.apply(cx, move |state, cx| state.on_error(error, cx));
200    }
201
202    fn apply(
203        &self,
204        cx: &mut App,
205        f: impl FnOnce(&mut SpeechState, &mut Context<SpeechState>) + 'static,
206    ) {
207        defer_session_update(self.state.clone(), self.session, cx, f);
208    }
209}
210
211/// A source of audio for a [`SpeechState`], such as the microphone.
212///
213/// Enable the `speech` feature for the built-in `Microphone`,
214/// or implement this trait to feed audio from elsewhere, e.g. a file in tests.
215pub trait AudioInput: 'static {
216    /// Start capturing `format` audio into `sink`.
217    ///
218    /// Capture runs until the returned [`Subscription`] is dropped.
219    fn start(
220        &self,
221        format: AudioFormat,
222        sink: AudioSink,
223        cx: &mut App,
224    ) -> Result<Subscription, SpeechError>;
225}
226
227impl<T: AudioInput + ?Sized> AudioInput for Rc<T> {
228    fn start(
229        &self,
230        format: AudioFormat,
231        sink: AudioSink,
232        cx: &mut App,
233    ) -> Result<Subscription, SpeechError> {
234        (**self).start(format, sink, cx)
235    }
236}
237
238/// Where an [`AudioInput`] delivers captured audio.
239///
240/// Like [`SpeechSink`], it is cheap to clone, ignored once its session ends and
241/// applied after the current update.
242#[derive(Clone)]
243pub struct AudioSink {
244    pub(super) state: WeakEntity<SpeechState>,
245    pub(super) session: usize,
246}
247
248impl AudioSink {
249    /// Deliver interleaved PCM samples in the requested [`AudioFormat`].
250    pub fn push(&self, samples: Vec<i16>, cx: &mut App) {
251        self.apply(cx, move |state, cx| state.on_audio(&samples, cx));
252    }
253
254    /// Capture failed; the session ends with this error.
255    pub fn error(&self, error: SpeechError, cx: &mut App) {
256        self.apply(cx, move |state, cx| state.on_error(error, cx));
257    }
258
259    fn apply(
260        &self,
261        cx: &mut App,
262        f: impl FnOnce(&mut SpeechState, &mut Context<SpeechState>) + 'static,
263    ) {
264        defer_session_update(self.state.clone(), self.session, cx, f);
265    }
266}