#![cfg(all(target_arch = "wasm32", feature = "web-speech"))]
use std::cell::RefCell;
use std::rc::Rc;
use js_sys::{Function, Reflect};
use wasm_bindgen::closure::Closure;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{SpeechRecognition, SpeechRecognitionEvent};
use super::SttSource;
#[derive(Debug, Clone, Default)]
pub struct WebSpeechSttOptions {
pub lang: Option<String>,
pub continuous: bool,
pub interim_results: bool,
pub max_alternatives: Option<u32>,
}
#[derive(Debug, Clone)]
pub struct WebSpeechSttResult {
pub text: String,
pub is_final: bool,
pub confidence: f32,
}
#[derive(Debug, Clone)]
pub struct WebSpeechSttError {
pub error: String,
pub message: Option<String>,
}
struct StoredCallbacks {
on_result: Option<Closure<dyn FnMut(JsValue)>>,
on_error: Option<Closure<dyn FnMut(JsValue)>>,
on_end: Option<Closure<dyn FnMut(JsValue)>>,
}
pub struct WebSpeechStt {
recognition: SpeechRecognition,
callbacks: Rc<RefCell<StoredCallbacks>>,
}
impl WebSpeechStt {
pub fn new() -> Result<Self, JsValue> {
let recognition = construct_speech_recognition()?;
Ok(Self {
recognition,
callbacks: Rc::new(RefCell::new(StoredCallbacks {
on_result: None,
on_error: None,
on_end: None,
})),
})
}
pub fn on_result<F>(&self, mut cb: F)
where
F: FnMut(WebSpeechSttResult) + 'static,
{
let closure = Closure::wrap(Box::new(move |evt: JsValue| {
let event: SpeechRecognitionEvent = match evt.dyn_into() {
Ok(e) => e,
Err(_) => return,
};
let result_index = event.result_index();
let Some(results) = event.results() else {
return;
};
let total = results.length();
for i in result_index..total {
let Some(result) = results.get(i) else {
continue;
};
let Some(alt) = result.get(0) else { continue };
cb(WebSpeechSttResult {
text: alt.transcript(),
is_final: result.is_final(),
confidence: alt.confidence(),
});
}
}) as Box<dyn FnMut(JsValue)>);
self.recognition
.set_onresult(Some(closure.as_ref().unchecked_ref::<Function>()));
self.callbacks.borrow_mut().on_result = Some(closure);
}
pub fn on_error<F>(&self, mut cb: F)
where
F: FnMut(WebSpeechSttError) + 'static,
{
let closure = Closure::wrap(Box::new(move |evt: JsValue| {
let error = Reflect::get(&evt, &JsValue::from_str("error"))
.ok()
.and_then(|v| v.as_string())
.unwrap_or_else(|| "unknown".to_string());
let message = Reflect::get(&evt, &JsValue::from_str("message"))
.ok()
.and_then(|v| v.as_string());
cb(WebSpeechSttError { error, message });
}) as Box<dyn FnMut(JsValue)>);
self.recognition
.set_onerror(Some(closure.as_ref().unchecked_ref::<Function>()));
self.callbacks.borrow_mut().on_error = Some(closure);
}
pub fn on_end<F>(&self, mut cb: F)
where
F: FnMut() + 'static,
{
let closure = Closure::wrap(Box::new(move |_evt: JsValue| {
cb();
}) as Box<dyn FnMut(JsValue)>);
self.recognition
.set_onend(Some(closure.as_ref().unchecked_ref::<Function>()));
self.callbacks.borrow_mut().on_end = Some(closure);
}
pub fn start(&self, opts: WebSpeechSttOptions) -> Result<(), JsValue> {
if let Some(lang) = opts.lang.as_deref() {
self.recognition.set_lang(lang);
}
let _ = self.recognition.set_continuous(opts.continuous);
self.recognition.set_interim_results(opts.interim_results);
if let Some(n) = opts.max_alternatives {
self.recognition.set_max_alternatives(n);
}
self.recognition.start()
}
pub fn stop(&self) {
self.recognition.stop();
}
pub fn abort(&self) {
self.recognition.abort();
}
}
impl SttSource for WebSpeechStt {
type Options = WebSpeechSttOptions;
type Result = WebSpeechSttResult;
type Error = JsValue;
fn start(&self, opts: Self::Options) -> Result<(), Self::Error> {
WebSpeechStt::start(self, opts)
}
fn stop(&self) {
WebSpeechStt::stop(self);
}
fn abort(&self) {
WebSpeechStt::abort(self);
}
}
fn construct_speech_recognition() -> Result<SpeechRecognition, JsValue> {
if let Ok(r) = SpeechRecognition::new() {
return Ok(r);
}
let global = js_sys::global();
let ctor = Reflect::get(&global, &JsValue::from_str("webkitSpeechRecognition"))?;
if ctor.is_undefined() || ctor.is_null() {
return Err(JsValue::from_str(
"SpeechRecognition is not available in this environment",
));
}
let ctor: Function = ctor
.dyn_into()
.map_err(|_| JsValue::from_str("webkitSpeechRecognition is not a constructor function"))?;
let instance = Reflect::construct(&ctor, &js_sys::Array::new())?;
instance.dyn_into::<SpeechRecognition>().map_err(|_| {
JsValue::from_str("webkitSpeechRecognition() did not return a SpeechRecognition")
})
}