Stream dictation through the platform speech recognizer
One.Speech streams dictation from the microphone: live transcript
updates while the user talks, then one closing event. Reach for it for
voice notes, search-by-voice, and hands-free form entry. For reading text
aloud, see speech synthesis.
On iOS an SFSpeechRecognizer fed from AVAudioEngine transcribes. On
Android the system SpeechRecognizer transcribes. On web the browser’s
speech recognition supplies dictation where present.
import { One } from 'one'
export async function dictate(onText: (text: string) => void) { if (!One.Speech.isAvailable()) return null
const permissions = await One.Speech.requestPermissions() if (!permissions.granted) return null
return One.Speech.start({ lang: 'en-US' }, (event) => { if (event.type === 'transcript' || event.type === 'end') onText(event.transcript) if (event.type === 'error') console.error(event.error, event.message) })}start returns a session with stop and abort. Every event carries
the whole transcript so far, so the screen always renders the latest
event’s text.
One session runs at a time. Starting another replaces the running one,
whose callback never fires again and whose session handle goes inert.
The session emits start, full transcript updates, then exactly one
end or error. stop finishes listening and the final transcript
arrives in end, including after trailing silence or no speech at all.
abort tears the session down with no further events. Errors carry one
of eight codes, from not-allowed and audio-capture to network and busy,
plus an optional message.
Permissions resolve granted, denied, or undetermined, with granted and
can-ask-again flags. On iOS restricted adds that Screen Time or device
management blocks recognition. Set native.app.speech so prebuild
writes both iOS usage descriptions and the Android recognition-service
query:
import { one } from 'one/vite'
export default { plugins: [ one({ native: { app: { name: 'MyApp', speech: { recognition: 'Dictate notes by voice', microphone: 'Dictate notes by voice', }, }, }, }), ],}On web, availability reflects constructor presence, but the browser’s
recognition service can still reject a session for network or service
reasons. Permission methods check or request microphone access, and a
denied recognition service also reads denied. Browsers may prompt again
when recognition starts and may need a user gesture. Without a
recognizer, permissions read denied and start throws; server rendering
keeps the same unavailable contract.
| Methods | Type | Description |
|---|---|---|
isAvailable() => booleanWhether dictation can run here. | () => boolean | Whether dictation can run here. |
getPermissions() => Promise<SpeechPermissionResponse>Speech and microphone rights, without prompting. | () => Promise<SpeechPermissionResponse> | Speech and microphone rights, without prompting. |
requestPermissions() => Promise<SpeechPermissionResponse>Ask for speech and microphone rights. | () => Promise<SpeechPermissionResponse> | Ask for speech and microphone rights. |
start(options: SpeechStartOptions, onEvent: (event: SpeechEvent) => void) => SpeechSessionStart dictating. Replaces any running session. | (options: SpeechStartOptions, onEvent: (event: SpeechEvent) => void) => SpeechSession | Start dictating. Replaces any running session. |
| Options | Type | Description |
|---|---|---|
langstringBCP-47 tag. Defaults to the device language. | string | BCP-47 tag. Defaults to the device language. |
| Event | Type | Description |
|---|---|---|
typeSpeechEventTypeStart, transcript, end, or error. | SpeechEventType | Start, transcript, end, or error. |
transcriptstringThe full transcript so far. | string | The full transcript so far. |
errorSpeechErrorCodeThe failure. Error events only. | SpeechErrorCode | The failure. Error events only. |
messagestringDetail on the failure, when any. | string | Detail on the failure, when any. |
| Session | Type | Description |
|---|---|---|
stop() => voidFinish listening; the transcript arrives in end. | () => void | Finish listening; the transcript arrives in end. |
abort() => voidEnd silently with no further events. | () => void | End silently with no further events. |
| Permission | Type | Description |
|---|---|---|
statusSpeechPermissionStatusGranted, denied, or undetermined. | SpeechPermissionStatus | Granted, denied, or undetermined. |
grantedbooleanWhether dictation may run. | boolean | Whether dictation may run. |
canAskAgainbooleanWhether a prompt may still appear. | boolean | Whether a prompt may still appear. |
restrictedbooleaniOS management blocks recognition. | boolean | iOS management blocks recognition. |
| iOS | Android | Web | |
|---|---|---|---|
| Engine | System recognizer | System recognizer | Browser API |
| Audio | Engine tap | System capture | Browser capture |
| Transcript | Whole so far | Whole so far | Whole so far |
| Restriction | Flagged | Denied | Denied |
start kills the old session
silently. Hold one session per screen and stop it on unmount.end or
error, even on silence. abort is the only silent exit.import { useRef, useState } from 'react'import { Button, Text } from 'react-native'import { One } from 'one'
function VoiceNote() { const [text, setText] = useState('') const [live, setLive] = useState(false) const session = useRef<ReturnType<typeof One.Speech.start> | null>(null)
async function toggle() { if (session.current) { session.current.stop() return } const permissions = await One.Speech.requestPermissions() if (!permissions.granted || !One.Speech.isAvailable()) return setLive(true) session.current = One.Speech.start({ lang: 'en-US' }, (event) => { if (event.type === 'end' || event.type === 'error') { session.current = null setLive(false) } setText(event.transcript) }) }
return ( <> <Button title={live ? 'Stop' : 'Dictate'} onPress={toggle} /> <Text>{text}</Text> </> )}The ref holds the session across renders while state tracks the live flag and transcript. The closing event clears both, so a second tap starts fresh.
import { useState } from 'react'import { Button, Text } from 'react-native'import { One } from 'one'
function VoiceSearch({ search }: { search: (query: string) => void }) { const [message, setMessage] = useState('')
async function listen() { if (!One.Speech.isAvailable()) { setMessage('Voice search is unavailable here.') return } const permissions = await One.Speech.requestPermissions() if (!permissions.granted) { setMessage('Microphone access is needed for voice search.') return } One.Speech.start({}, (event) => { if (event.type === 'end' && event.transcript.trim()) { search(event.transcript.trim()) } else if (event.type === 'error') { setMessage('Sorry, I did not catch that. Try again.') } }) }
return ( <> <Button title="Search by voice" onPress={listen} /> {message ? <Text>{message}</Text> : null} </> )}The default device language fits search, so options stay empty. Only a nonblank closing transcript searches; errors show a retry hint instead of searching on a fragment.
Edit this page on GitHub.