npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-native-local-tts

v0.1.8

Published

On-device text-to-speech for React Native (iOS AVSpeechSynthesizer, Android TextToSpeech). Speak, write WAV files, list voices, word-level progress.

Readme

react-native-local-tts

On-device text-to-speech for React Native. Speaks through the system engines (AVSpeechSynthesizer on iOS, TextToSpeech on Android), can write Int16 mono WAV files for offline playback, and exposes word-level progress while speaking.

Built as an Expo module. Works in Expo apps (dev client / production builds) and in bare React Native once Expo modules are installed. Does not run in Expo Go.

| | | | --- | --- | | Platforms | iOS 13+, Android API 24+ | | Peer deps | expo ≥ 56, react-native ≥ 0.78, react ≥ 19.2 | | Output format | .wav (Int16 mono PCM) |


Install

Expo project

npx expo install react-native-local-tts

Or with your usual package manager, then reinstall pods:

npm install react-native-local-tts
cd ios && pod install && cd ..

Rebuild the native app (npx expo run:ios / run:android or your Xcode / Android Studio flow). Autolinking picks up the module; there is nothing to add to app.json.

Bare React Native (no Expo app workflow)

This package depends on expo-modules-core. If your app does not already include Expo modules, add them first:

npx install-expo-modules@latest

That command wires the expo package into your iOS and Android projects. If it fails on a heavily customized app, follow Expo’s manual install guide.

Then install this library and refresh native deps:

npm install react-native-local-tts
# or: yarn add react-native-local-tts / pnpm add react-native-local-tts

cd ios && pod install && cd ..

Rebuild from Xcode / Android Studio (or npx react-native run-ios / run-android). A Metro reload alone is not enough after the first install — the native module has to be compiled in.

Quick check after a successful rebuild:

import { isAvailable, getVoices } from "react-native-local-tts";

console.log(isAvailable); // true
console.log(await getVoices());

If isAvailable is false, the JS bundle is present but the native binary was not rebuilt (or Expo modules are not linked).


Usage

Speak

import { speak, stop, isSpeaking } from "react-native-local-tts";

await speak({
  text: "Hello from the device TTS engine.",
  language: "en-US",
  rate: 1,
  pitch: 1,
});

if (isSpeaking()) {
  stop();
}

speak resolves when playback finishes (or is stopped), not when it starts.

Voices

import { getVoices } from "react-native-local-tts";

const voices = await getVoices();
const premium = voices.find((v) => v.language.startsWith("en") && v.quality === "premium");

await speak({
  text: "Using a specific voice.",
  voice: premium?.identifier,
});

Identifiers are platform-specific. Prefer picking from getVoices() rather than hard-coding strings across iOS and Android.

Write a WAV file

Paths should be absolute filesystem paths ending in .wav. Strip or keep the file:// prefix — both are accepted.

import { synthesizeToFile } from "react-native-local-tts";
import * as FileSystem from "expo-file-system";

const filePath = `${FileSystem.documentDirectory}hello.wav`;

await synthesizeToFile({
  text: "Saved for later playback.",
  filePath,
  language: "en-US",
  // iOS: false favors the faster spoken-audio session path
  qualityMode: false,
});

Long text (multiple utterances → one file)

For chapter-length content, prefer streaming many utterances into one file instead of concatenating in JS:

import { synthesizeUtterancesToFile } from "react-native-local-tts";

const result = await synthesizeUtterancesToFile({
  filePath: "/path/to/chapter.wav",
  voice: selectedVoiceId,
  qualityMode: true,
  utterances: [
    { text: "First paragraph.", trailingSilenceMs: 400 },
    { text: "Second paragraph.", rate: 1.05, trailingSilenceMs: 600 },
  ],
});

// result.durationSeconds, result.sampleRate, result.frameCount

If a single pass is still too large for your timeouts or UX, synthesize batches and stitch them:

import { concatWavFiles } from "react-native-local-tts";

const { durationSeconds } = await concatWavFiles({
  inputPaths: ["/path/part0.wav", "/path/part1.wav"],
  outputPath: "/path/chapter.wav",
});

concatWavFiles streams PCM and rewrites the WAV sizes correctly for headers that are not the classic 44-byte layout (common with AVAudioFile output).

Events

import {
  onSpeechStart,
  onSpeechDone,
  onSpeechProgress,
  onSpeechError,
} from "react-native-local-tts";

const subs = [
  onSpeechStart(() => {}),
  onSpeechDone(() => {}),
  onSpeechProgress(({ charIndex, charLength }) => {
    // highlight text.slice(charIndex, charIndex + charLength)
  }),
  onSpeechError(({ message }) => console.warn(message)),
];

// later
subs.forEach((s) => s.remove());

On Android, word progress needs API 26+. Older devices still get start / done / error.

Hook

import { useLocalTts } from "react-native-local-tts";

function SpeakButton() {
  const { speak, stop, isSpeaking, progress, error } = useLocalTts();

  return (
    <>
      <Button
        title={isSpeaking ? "Stop" : "Speak"}
        onPress={() => (isSpeaking ? stop() : speak({ text: "Hi there" }))}
      />
      {progress ? <Text>@{progress.charIndex}</Text> : null}
      {error ? <Text>{error}</Text> : null}
    </>
  );
}

API

| Export | Returns | Notes | | --- | --- | --- | | speak(options) | Promise<void> | Live playback | | synthesizeToFile(options) | Promise<void> | Single string → one WAV | | synthesizeUtterancesToFile(options) | Promise<SynthesizeFileResult> | Many utterances → one WAV | | concatWavFiles(options) | Promise<SynthesizeFileResult> | Merge WAV parts on disk | | getVoices() | Promise<VoiceInfo[]> | Installed system voices | | stop() | void | Stops live speech | | isSpeaking() | boolean | Live speech only | | isAvailable | boolean | Native module linked | | useLocalTts() | hook | Speech UI state helper | | onSpeechStart / Done / Progress / Error | { remove() } | Event subscriptions | | LocalTtsUnavailableError | class | Thrown when native code is missing |

Options

Speak / synthesize shared fields

| Field | Type | Default | | | --- | --- | --- | --- | | text | string | required | | | rate | number | 1 | Relative speed | | pitch | number | 1 | Relative pitch | | language | string | device default | BCP-47, e.g. en-GB | | voice | string | — | Overrides language when set | | qualityMode | boolean | see below | iOS session / mode only |

Defaults for qualityMode:

  • speaktrue (closer to system Settings playback)
  • file APIs → false (faster path for bulk conversion)

On iOS, true uses AVAudioSession mode .default; false uses .spokenAudio. Android ignores the flag.

synthesizeUtterancesToFile

| Field | Type | | | --- | --- | --- | | utterances | { text, rate?, pitch?, trailingSilenceMs? }[] | Spoken in order into one file | | filePath | string | Absolute .wav path | | language / voice / qualityMode | same as above | Applied to the job |

concatWavFiles

| Field | Type | | | --- | --- | --- | | inputPaths | string[] | Existing WAV parts, same format | | outputPath | string | Destination .wav |

SynthesizeFileResult

durationSeconds, sampleRate, frameCount — metadata only; PCM stays on disk.

VoiceInfo

identifier, name, language, quality ("default" | "enhanced" | "premium").


Platform notes

iOS

  • Live speech and file write both go through AVSpeechSynthesizer.
  • File output is Int16 mono WAV via AVAudioFile.
  • Premium / neural voices can be slow; long jobs use an idle watchdog rather than a short absolute timeout so multi-minute chapters can finish.
  • Changing native code in this package always requires a native rebuild.

Android

  • Uses android.speech.tts.TextToSpeech.
  • Engine init is async; calls wait briefly for readiness on first use.
  • Very long single strings can fail inside the engine — keep utterances under a few thousand characters and batch if needed.

Both

  • Offline / “enhanced” voices must already be installed by the OS user; this library does not download voice packs.
  • stop() affects live speak playback. In-flight file synthesis is cancelled separately inside the native queue when the module tears a job down.

Troubleshooting

| Symptom | Likely cause | | --- | --- | | LocalTtsUnavailableError / isAvailable === false | App not rebuilt after install, or Expo modules missing in a bare app | | synthesizeUtterancesToFile is not available in this native build | Stale binary — rebuild after upgrading the package | | Empty or truncated WAV after batching | Upgrade to a build that includes the chunk-aware concatWavFiles implementation | | No word progress on Android | Device below API 26 | | Sounds different from Settings (iOS) | Try qualityMode: true and a premium voice identifier from getVoices() |


Development

npm install
npm run build     # emits dist/
npm run typecheck

main points at dist/. After changing TypeScript, run build before the consuming app can see new JS exports. Swift / Kotlin changes still need a native rebuild of the app.


License

MIT. See LICENSE.