One Logo Pool Ball
wwwwwwwwwwwwwwwwwww

Speech

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.

Usage

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.

How it works

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.

API reference

MethodsTypeDescription
isAvailable() => booleanWhether dictation can run here.
() => booleanWhether 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) => SpeechSessionStart dictating. Replaces any running session.
OptionsTypeDescription
langstringBCP-47 tag. Defaults to the device language.
stringBCP-47 tag. Defaults to the device language.
EventTypeDescription
typeSpeechEventTypeStart, transcript, end, or error.
SpeechEventTypeStart, transcript, end, or error.
transcriptstringThe full transcript so far.
stringThe full transcript so far.
errorSpeechErrorCodeThe failure. Error events only.
SpeechErrorCodeThe failure. Error events only.
messagestringDetail on the failure, when any.
stringDetail on the failure, when any.
SessionTypeDescription
stop() => voidFinish listening; the transcript arrives in end.
() => voidFinish listening; the transcript arrives in end.
abort() => voidEnd silently with no further events.
() => voidEnd silently with no further events.
PermissionTypeDescription
statusSpeechPermissionStatusGranted, denied, or undetermined.
SpeechPermissionStatusGranted, denied, or undetermined.
grantedbooleanWhether dictation may run.
booleanWhether dictation may run.
canAskAgainbooleanWhether a prompt may still appear.
booleanWhether a prompt may still appear.
restrictedbooleaniOS management blocks recognition.
booleaniOS management blocks recognition.

Platform differences

iOSAndroidWeb
EngineSystem recognizerSystem recognizerBrowser API
AudioEngine tapSystem captureBrowser capture
TranscriptWhole so farWhole so farWhole so far
RestrictionFlaggedDeniedDenied

Limits and gotchas

Examples

Dictate into a note field

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.

Search by voice with error feedback

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.