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}