Browsers and workers
import { createEspeak } from "@spelling-creator/spellophone";
// or, explicitly:
import { createEspeak } from "@spelling-creator/spellophone/browser";The browser build is the same API as Node.js. The difference is where the data comes from: it has to be fetched over HTTP, and you choose from where.
Where the data comes from
espeak-ng needs about 725 KB of core data (voices, phoneme tables) plus one dictionary per language, from a few KB up to 4.8 MB for Russian. Everything is gzipped and decompressed in the browser with DecompressionStream, so it does not matter whether your server compresses transfers. A server that sends the .gz files with Content-Encoding: gzip (Vite's dev server does, for example) works too: the browser unzips them on the way in, and the library notices and skips its own decompression.
Self-host it
Copy the package's espeak-ng-data directory to your static assets and pass its URL:
const espeak = await createEspeak({ dataUrl: "/espeak-ng-data/" });The directory is plain files, so any static host works. With Vite, a small plugin can serve it in development and copy it into the build; the examples/browser app in the repository does exactly that.
Fetch it from jsDelivr
If you would rather not host the data, the CDN option fetches it from jsDelivr, pinned to the package version you installed:
const espeak = await createEspeak({ cdn: "jsdelivr" });
// fetches https://cdn.jsdelivr.net/npm/@spelling-creator/spellophone@<version>/espeak-ng-data/...This uses jsDelivr's npm endpoint, which serves the espeak-ng-data directory of the package as published to npmjs.com. It works for published versions, not for a local build. jsdelivrDataUrl(version) returns the URL if you want to construct it yourself, and dataUrl accepts any other host.
Pick the languages
Only English is fetched by default. Ask for more up front, or load them later:
const espeak = await createEspeak({
dataUrl: "/espeak-ng-data/",
languages: ["en", "fr", "de"],
});
// later
await espeak.loadLanguages(["es"]);
espeak.setVoice("es");
// or let loadVoice fetch whatever the voice needs
await espeak.loadVoice("pt-br");setVoice is synchronous and throws DictionaryNotLoadedError if the voice needs a dictionary that has not been fetched. loadVoice is the asynchronous version that fetches it first. languages: "all" fetches every language, about 9 MB compressed.
The wasm file
The Emscripten glue locates espeak-ng.wasm relative to itself with import.meta.url. Bundlers such as Vite, Rollup and webpack 5 recognize that pattern and emit the wasm as an asset, so there is nothing to configure. If your setup needs the file somewhere else, pass wasmUrl.
When WebAssembly is blocked
Safari in Lockdown Mode and pages with a strict Content-Security-Policy cannot run WebAssembly. Pass a fallback and espeak-ng runs there as plain JavaScript instead, about seven times slower:
const espeak = await createEspeak({
cdn: "jsdelivr",
fallback: () => import("@spelling-creator/spellophone/wasm2js"),
});The module is only fetched where it is needed. For browsers too old for this build altogether, there is an ES5 legacy build. Both are covered in Without WebAssembly.
Workers
Synthesis is synchronous and takes a few milliseconds per sentence. For long texts, or to keep the main thread free while dictionaries decompress, run it in a Web Worker. The API is the same there; the module does not touch the DOM.
// worker.ts
import { createEspeak } from "@spelling-creator/spellophone/browser";
const ready = createEspeak({ cdn: "jsdelivr" });
self.onmessage = async (event: MessageEvent<string>) => {
const espeak = await ready;
const { samples, sampleRate, events } = espeak.synthesize(event.data);
self.postMessage({ samples, sampleRate, events }, [samples.buffer]);
};Playing the audio
The samples are 16-bit PCM. toFloat32 scales them to the -1 to 1 range the Web Audio API expects:
import { toFloat32 } from "@spelling-creator/spellophone";
const context = new AudioContext();
const buffer = context.createBuffer(
1,
result.samples.length,
result.sampleRate,
);
buffer.copyToChannel(toFloat32(result.samples), 0);
const source = context.createBufferSource();
source.buffer = buffer;
source.connect(context.destination);
source.start();For an <audio> element or a download, encodeWav produces a WAV file to wrap in a Blob:
const wav = encodeWav(result.samples, result.sampleRate);
audioElement.src = URL.createObjectURL(new Blob([wav], { type: "audio/wav" }));Options
interface BrowserEspeakOptions {
dataUrl?: string | URL; // directory holding manifest.json
cdn?: "jsdelivr"; // or fetch the data from jsDelivr
wasmUrl?: string | URL; // override where espeak-ng.wasm is
fallback?: () => Promise<Wasm2jsFallback>; // where WebAssembly is blocked
languages?: string[] | "all"; // dictionaries to fetch up front, default ["en"]
phonemeEvents?: boolean | "ipa";
fetch?: typeof fetch; // custom fetch, for credentials or caching
}One of dataUrl or cdn is required.