createSpeechRecognizer / isSpeechRecognitionSupported
A wrapper around the Web Speech API's SpeechRecognition — the counterpart to AudioRecorder, which captures audio bytes; this one asks the platform to turn speech into text.
The native API is worth wrapping once rather than touching directly: it's still prefixed on WebKit (webkitSpeechRecognition), it's absent from lib.dom.d.ts, and it reports routine non-events — a silent pause, a programmatic stop() — through the same error channel as a denied microphone.
Usage
import { createSpeechRecognizer } from 'ranuts/utils';
const mic = createSpeechRecognizer({
lang: () => currentLocale(), // read fresh on every capture, not just once
onResult: (text, isFinal) => {
input.value = text;
},
onError: (e) => {
if (e.kind === 'denied') toast('Microphone access was refused');
},
onStart: () => button.classList.add('recording'),
onEnd: () => button.classList.remove('recording'),
});
if (!mic.supported) button.style.display = 'none'; // hide the mic button up front
button.addEventListener('click', () => mic.toggle());API
isSpeechRecognitionSupported()
Returns boolean. Checked at call time (not cached at module load), so it's safe to import this module during server-side rendering and call the check once the page hydrates.
createSpeechRecognizer(options?)
Builds a reusable SpeechRecognizer. start() constructs a fresh native recognition instance every time, so any option passed as a function (lang, notably) is re-read at the start of each capture rather than frozen at creation time.
Parameters (SpeechRecognizerOptions)
| Option | Description | Type | Default |
|---|---|---|---|
lang | BCP 47 tag ('en-US', 'zh-CN'), or a function read at the start of each capture | string | (() => string) | '' |
continuous | Keep listening across pauses instead of stopping at the first one | boolean | true |
interimResults | Emit partial results as the speaker talks | boolean | true |
onResult | Called with the transcript of the whole capture so far, and whether it's final | (transcript: string, isFinal: boolean) => void | - |
onError | Called with a classified error | (error: SpeechError) => void | - |
onStart | Fires when a capture begins | () => void | - |
onEnd | Fires once per capture, however it ended — stopped, timed out, or errored | () => void | - |
SpeechRecognizer
| Member | Description | Type |
|---|---|---|
supported | false when the platform has no speech recognition; every method is then a no-op | boolean (getter) |
active | Whether a capture is currently running | boolean (getter) |
start() | Begin a capture. Ignored if one is already running | () => void |
stop() | End the current capture; results already recognized are kept, onEnd follows | () => void |
abort() | End the current capture and discard pending results | () => void |
toggle() | Start if idle, stop if running — what a single microphone button wants | () => void |
SpeechError
| Field | Description | Type |
|---|---|---|
kind | 'denied' (mic refused — worth surfacing), 'noSpeech' / 'aborted' (routine, usually not worth showing), 'failed' (everything else) | SpeechErrorKind |
detail | The raw error string from the platform event | string |
Notes
- Not supported everywhere. Firefox has no
SpeechRecognitionimplementation at all; always checkrecognizer.supported(orisSpeechRecognitionSupported()) before showing a microphone affordance rather than assuming the constructor exists. supportedandactiveare getters, re-evaluated on every access — not captured once at creation. That matters ifcreateSpeechRecognizer()runs beforewindowor the vendor-prefixed constructor is available (SSR, an early module-scope call before hydration): the recognizer picks up the real API once it appears, rather than being permanently stuck reportingsupported === false.onResult's transcript is cumulative, not incremental — it's the full text of the capture so far, revised as interim results firm up. Don't concatenate results yourself.- Instantiating the native recognizer or calling its
start()can throw synchronously (e.g. a Permissions-Policy restriction, or Chrome'sInvalidStateErrorwhen a capture is already in flight) —createSpeechRecognizercatches this and reports it throughonError/onEndrather than letting it escape.