Skip to content

Speech

Read a message, a hint or an article aloud, with a choice of voice, language, pitch and rate. @symbiote-native/speech wraps expo-speech so every SymbioteNative adapter can reach it, not just React. Every export is a stateless function or constant, so the React, Vue, Angular, Svelte, and Solid entry points are plain re-exports of the same core.

OS platform Support
iOS live
Android live
Framework adapter Support
React live
Vue live
Angular live
Svelte live
Solid live
Terminal window
npm install @symbiote-native/speech

Scaffolding or extending a SymbioteNative app? npx @symbiote-native/cli new --speech (or add --speech in an existing app) installs and wires this for you - see @symbiote-native/cli.

expo-speech and expo-modules-core come along as regular dependencies, pinned to exact versions. Never install either yourself, and never add the expo meta-package to your project (it bundles its own Metro/Babel pipeline, which conflicts with this project’s own).

No permission, manifest edit or config plugin is involved.

All five adapters re-export the same functions; there is no per-adapter hook, composable or service, since nothing here holds live state. speak returns at once and reports progress through the onStart, onDone, onStopped and onError callbacks in its options.

import { useState } from 'react';
import { speak, stop } from '@symbiote-native/speech/react';
export default function ReadAloud({ text }: { text: string }) {
const [speaking, setSpeaking] = useState(false);
function onPress() {
if (speaking) {
void stop();
return;
}
speak(text, {
language: 'en-US',
onStart: () => setSpeaking(true),
onDone: () => setSpeaking(false),
onStopped: () => setSpeaking(false),
});
}
return <button title={speaking ? 'Stop' : 'Read aloud'} onPress={onPress} />;
}
import { getAvailableVoicesAsync, speak, VoiceQuality } from '@symbiote-native/speech';
const voices = await getAvailableVoicesAsync();
const enhanced = voices.find(
v => v.language === 'en-US' && v.quality === VoiceQuality.Enhanced,
);
speak('Hello world', { voice: enhanced?.identifier });
Signature Description
speak(text, options?): void Speaks the text. If something is already speaking, the new utterance is queued
stop(): Promise<void> Interrupts the current utterance and clears the whole queue
pause(): Promise<void> Pauses speaking. iOS only; throws UnavailabilityError on Android
resume(): Promise<void> Resumes after pause. iOS only; throws UnavailabilityError on Android
isSpeakingAsync(): Promise<boolean> Whether the engine is speaking. true while paused too
getAvailableVoicesAsync(): Promise<IVoice[]> Lists the voices installed on the device. Throws UnavailabilityError if unsupported
maxSpeechInputLength: number Longest text one speak call accepts. Number.MAX_VALUE on iOS
VoiceQuality Default and Enhanced, the values of IVoice.quality
Field Type Description
language string | undefined IETF BCP 47 language code, such as en-US
voice string | undefined Identifier of a voice from getAvailableVoicesAsync
pitch number | undefined Pitch multiplier. 1.0 is normal
rate number | undefined Speed multiplier. 1.0 is normal
volume number | undefined From 0.0 (muted) to 1.0 (maximum). Defaults to 1.0
useApplicationAudioSession boolean | undefined iOS only. false lets the system manage a separate audio session for speech
onStart (() => void) | undefined Called when speaking begins
onDone (() => void) | undefined Called when the utterance finishes
onStopped (() => void) | undefined Called when speaking is cut off by stop()
onError ((error: Error) => void) | undefined Called when speaking fails
onBoundary ((event: INativeBoundaryEvent) => void) | null | undefined Called at word boundaries with charIndex and charLength
Field Type Description
identifier string Pass this as ISpeechOptions.voice
name string Display name of the voice
quality VoiceQuality Default or Enhanced
language string The voice’s language as an IETF BCP 47 code
  • speak queues, it does not interrupt. To replace what is being said, call stop() first.
  • Long text can exceed the limit. Android rejects text longer than maxSpeechInputLength; split a long article into sentences and queue them.
  • pause and resume are iOS only. Guard them with a platform check, or catch the UnavailabilityError.
  • Web-only fields are dropped. Upstream’s WebVoice, SpeechEventCallback, _voiceIndex and onMark/onPause/onResume belong to its separate web implementation; this package wraps only the iOS and Android native modules.
  • Sound can only be verified on a device. The headless tests fake the native module, so they prove the callback registry, event routing and the UnavailabilityError branches.

There is no sound on iOS. First check that the device is not in silent mode: on a physical iPhone speak is silent then. If other audio in your app changes the audio session, that can also quiet speech.

The voice I picked is ignored, or the voice list is short. Take the voice identifier from getAvailableVoicesAsync() and pass exactly that. Newer iOS versions can return fewer or older voices than you expect, and the system voices installed differ by device.

Speech in a non-English language sounds wrong or does not play on Android. Set language to a BCP 47 code (de-DE) and make sure the device has that voice installed in its text-to-speech settings.

onDone never fires after I pause() and resume(). Pausing reports onStopped, and the completion callback is not guaranteed afterwards on iOS. Track state yourself if you pause.

My text is cut off or rejected. Android rejects text longer than maxSpeechInputLength. Split long text into sentences and queue them.

Sources: Expo docs: Speech, expo/expo#10827 playsInSilentModeIOS does not influence Speech.speak, expo/expo#24243 events like onDone do not fire on iOS, expo/expo#12654 pause emits stopped and resume never emits done, expo/expo#7260 Speech.speak not working for non-English on Android.

expo-speech’s JS is hand-ported into this package’s core/, resolving ExpoSpeech through expo-modules-core rather than the expo meta-package. The five adapter entry points are plain re-exports of core (Angular stays a physical subpath for its separate ngc/AOT build). The native code is never vendored: expo-modules-autolinking resolves it from node_modules (see the native setup guide).