English text to IPA phonemes using eSpeak NG — WASM-based, fast, offline
  • Python 49.4%
  • TypeScript 22.8%
  • Shell 15.3%
  • JavaScript 12.5%
Find a file
2026-06-05 07:25:32 -04:00
.forgejo/workflows Handle cases where publish fails succesfully 2026-06-05 07:25:32 -04:00
espeak-ng@fbe4b37642 Initial commit: espeak-phonemes WASM build 2026-06-04 14:56:39 -04:00
patches Initial commit: espeak-phonemes WASM build 2026-06-04 14:56:39 -04:00
scripts Let the server compress as it is and add module type stubs for the wasm glue 2026-06-04 18:57:09 -04:00
src Support phoneme seperators 2026-06-04 20:57:37 -04:00
tests Support phoneme seperators 2026-06-04 20:57:37 -04:00
.gitignore Let the server compress as it is and add module type stubs for the wasm glue 2026-06-04 18:57:09 -04:00
.gitmodules Initial commit: espeak-phonemes WASM build 2026-06-04 14:56:39 -04:00
espeak-phonemes.d.ts Let the server compress as it is and add module type stubs for the wasm glue 2026-06-04 18:57:09 -04:00
LICENSE Initial commit: espeak-phonemes WASM build 2026-06-04 14:56:39 -04:00
package-lock.json Initial commit: espeak-phonemes WASM build 2026-06-04 14:56:39 -04:00
package.json Let the server compress as it is and add module type stubs for the wasm glue 2026-06-04 18:57:09 -04:00
PORT_PLAN.md Initial commit: espeak-phonemes WASM build 2026-06-04 14:56:39 -04:00
README.md Let the server compress as it is and add module type stubs for the wasm glue 2026-06-04 18:57:09 -04:00
tsconfig.json Initial commit: espeak-phonemes WASM build 2026-06-04 14:56:39 -04:00

espeak-phonemes

English text → IPA phonemes, using a size-optimized WebAssembly build of eSpeak NG. Works in Node.js and browsers, offline, with no audio-synthesis code shipped.

import { textToIPA } from "@jocelyn-stericker/espeak-phonemes";

const ipa = await textToIPA("Hello world");
// → "həlˈəʊ wˈɜːld"

This repo is not a fork of eSpeak NG. It pins upstream as a git submodule, applies a small patch (patches/espeak-ng-phonemes.patch) that trims the build to translation-only and adds a phonemes-only data mode, then cross-compiles a ~300 KB WASM module plus English data that compresses to ~120 KB over the wire. See Build and PORT_PLAN.md.

Install

This package is published to a Forgejo (Codeberg) npm registry. Add the scope to your project's .npmrc:

@jocelyn-stericker:registry=https://codeberg.org/api/packages/jocelyn-stericker/npm/

Then install a pinned version (builds are not guaranteed compatible across versions):

npm install --save-exact @jocelyn-stericker/espeak-phonemes

Usage

Node.js (auto-resolves the bundled assets)

import { textToIPA } from "@jocelyn-stericker/espeak-phonemes";

await textToIPA("hello");                          // "həlˈəʊ"
await textToIPA("hello", { voice: "en-us" });      // "həlˈoʊ"
await textToIPA("hello", { keepStress: false });   // "hələʊ"

Browser (supply the assets explicitly)

The Node convenience wrapper resolves files from disk; in the browser use the low-level createESpeak and hand it the emscripten factory + data archive. With a bundler like Vite, the ?url suffix yields the served asset URL:

import { createESpeak } from "@jocelyn-stericker/espeak-phonemes";
import initWasm from "@jocelyn-stericker/espeak-phonemes/espeak-phonemes.js";
import wasmUrl from "@jocelyn-stericker/espeak-phonemes/espeak-phonemes.wasm?url";
import dataUrl from "@jocelyn-stericker/espeak-phonemes/espeak-ng-data.tar?url";

const resp = await fetch(dataUrl);
const engine = await createESpeak({
  moduleFactory: initWasm,
  moduleOverrides: { locateFile: () => wasmUrl }, // so the glue finds the .wasm
  data: { archive: await resp.arrayBuffer() }, // raw tar; omit `compression`
});

engine.textToIPA("Hello from the browser!"); // synchronous after init

Options

Option Type Default Description
voice "en" | "en-us" "en" English voice variant
separator string " " String between phoneme groups (words + clauses)
keepStress boolean true Keep ˈˌ stress marks
tie string Tie character inserted within multi-char phonemes (e.g. "͡"ə͡ʊ)

Differences from the espeak-ng CLI

Output matches espeak-ng -q --ipa -v en except for one case: the CLI's intonation pass (part of audio synthesis, which this build omits) adds an emphasis mark to a lone single-syllable function word — theðˈə, isˈɪz. This build emits the unstressed lexical form (ðə, ɪz). Lexical stress on content words and stress within running text are unaffected.

Build

The whole pipeline lives in scripts/ and runs on push via .forgejo/workflows/ci.yaml (the version bumps every run).

git clone --recurse-submodules <this repo>
cd espeak-phonemes
npm install
source "$EMSDK/emsdk_env.sh"   # an activated Emscripten SDK
npm run build                  # patch → native data → wasm → TypeScript
npm run test:golden            # parity vs the captured CLI corpus

scripts/build.sh orchestrates:

  1. scripts/apply-patch.sh — overlays patches/espeak-ng-phonemes.patch onto the pinned espeak-ng/ submodule (idempotent).
  2. A native build to compile the phonemes-only English data (ESPEAK_PHONEMES_ONLY=1; the data compiler runs on the host).
  3. scripts/build-wasm.sh — emscripten build of the translation-only library, emitting espeak-phonemes.{wasm,js} and espeak-ng-data.tar.
  4. tsc — compiles the TypeScript wrapper to dist/esm/.

Bumping upstream eSpeak NG

git -C espeak-ng fetch && git -C espeak-ng checkout <new-sha>
git -C espeak-ng apply ../patches/espeak-ng-phonemes.patch   # re-roll if it fails
git add espeak-ng
npm run build && npm run test:golden                          # the corpus is the safety net

If the patch no longer applies after an upstream refactor, re-create it against the new tree and commit the updated patches/espeak-ng-phonemes.patch. The golden corpus (tests/golden-corpus.ndjson) is the oracle that confirms a bump didn't change pronunciations.

License

AGPL-3.0-or-later

A derivative of eSpeak NG (GPLv3). Copyright (C) 20062024 Jonathan Duddington and contributors. The bundled espeak-ng-data.tar and the WASM binary are built from eSpeak NG source.