Piper
Text-to-speech with Piper voices, run locally by PiperTTSService or through a Piper HTTP server by PiperHttpTTSService.
Overview
PiperTTSService: runs a Piper voice on your machine, on the CPU, with no API key. Its engine (@pipedog/piper/piper_voice) is a TypeScript reimplementation of what upstream uses from the Pythonpiper-ttspackage (1.8.0), on@pipedog/onnxand@pipedog/espeak-ng.PiperHttpTTSService: posts each sentence to a Piper HTTP server overfetch. It needs no model, no ONNX runtime and no eSpeak NG, and runs on Cloudflare Workers.
Upstream page
Pipecat's documentation for the same services.
Source
packages/piper/src/tts.ts and the engine, piper_voice.ts.
Piper voices
Every voice, by language and quality, with samples.
Installation
bun add @pipedog/piper
# for PiperTTSService (local voices) only:
bun add onnxruntime-node @echogarden/espeak-ng-emscriptenonnxruntime-noderuns the voice model.onnxruntime-web@1.30.0(WebAssembly) works too, slower.@echogarden/espeak-ng-emscriptenis eSpeak NG compiled to WebAssembly, for phonemization. It is GPL-3.0 (see Licenses).
PiperHttpTTSService needs neither: the local engine is imported lazily, only by
PiperTTSService.
import { PiperHttpTTSService, PiperTTSService } from "@pipedog/piper/tts";What you need
PiperTTSService: a Piper voice, <voice>.onnx and <voice>.onnx.json from Hugging Face
rhasspy/piper-voices. It is downloaded once, when
the pipeline starts, into downloadDir if it is missing, as piper.download_voices does. Sizes
depend on the quality:
| Quality | Sample rate | Size |
|---|---|---|
x_low | 16 kHz | 20-28 MB |
low | 16 kHz | 28-69 MB |
medium | 22.05 kHz | 62-81 MB |
high | 22.05 kHz | 63-136 MB |
Audio is resampled to the pipeline's output rate. downloadDir defaults to
$PIPEDOG_CACHE_DIR/piper, else $XDG_CACHE_HOME/pipedog/piper, else ~/.cache/pipedog/piper.
Put the two files there yourself to skip the download:
// Voices already on disk: put <voice>.onnx and <voice>.onnx.json in downloadDir.
const fromDisk = new PiperTTSService({
downloadDir: "/models/piper",
settings: new PiperTTSService.Settings({ voice: "en_US-lessac-medium" }),
});eSpeak NG adds about 25 MB (the WebAssembly build and its voice data), loaded into memory once per
process. @pipedog/espeak-ng wraps it and reproduces piper-tts' clause-by-clause phonemization, so
the phonemes match piper-tts 1.8.0's for the tested languages, except where piper-tts bundles newer
espeak-ng data (en-gb-x-rp, pt-br and it differ slightly).
PiperHttpTTSService: a running Piper HTTP server, for example:
uv pip install "piper-tts[http]"
uv run python -m piper.http_server -m en_US-ryan-highRuntimes
| Runtime | Works | What it needs |
|---|---|---|
| Bun | ✅ yes | PiperTTSService: onnxruntime-node (or onnxruntime-web) and @echogarden/espeak-ng-emscripten, plus a Piper voice (20-136 MB) downloaded on first use. PiperHttpTTSService: only fetch and a Piper HTTP server. |
| Node.js | ✅ yes | PiperTTSService: onnxruntime-node (or onnxruntime-web) and @echogarden/espeak-ng-emscripten, plus a Piper voice (20-136 MB) downloaded on first use. PiperHttpTTSService: only fetch and a Piper HTTP server. |
| Deno | ✅ yes | PiperTTSService: onnxruntime-node (or onnxruntime-web) and @echogarden/espeak-ng-emscripten, plus a Piper voice (20-136 MB) downloaded on first use. PiperHttpTTSService: only fetch and a Piper HTTP server. |
| Cloudflare Workers | ✅ yes | PiperTTSService: onnxruntime-node (or onnxruntime-web) and @echogarden/espeak-ng-emscripten, plus a Piper voice (20-136 MB) downloaded on first use. PiperHttpTTSService: only fetch and a Piper HTTP server. PiperHttpTTSService only (PiperTTSService needs a filesystem and exceeds Workers' limits). |
Generated from pipecat.runtimes and pipecat.compat in packages/piper/package.json: ✅ means the compat smoke test passes on that runtime.
PiperHttpTTSService runs on Cloudflare Workers. PiperTTSService does not: it needs a filesystem
for the voice cache, and the voice model (20-136 MB) and eSpeak NG's data don't fit a Worker's
128 MB of memory or its 10 MB bundle limit.
PiperTTSService
Configuration
Prop
Type
Settings
Settings, passed as settings: new PiperTTSService.Settings({...}) (PiperTTSSettings).
| Setting | Type | Default | Description |
|---|---|---|---|
model | null | null | Not used. |
voice | string | none, required | Piper voice name, for example "en_US-ryan-high". |
language | null | null | Not used: the voice sets the language. |
The voice is loaded once. Settings changes are stored but not applied, with a warning, as upstream.
Usage
import { PiperHttpTTSService, PiperTTSService } from "@pipedog/piper/tts";
// Downloads en_US-ryan-high.onnx and .onnx.json from rhasspy/piper-voices on first start.
const tts = new PiperTTSService({
settings: new PiperTTSService.Settings({ voice: "en_US-ryan-high" }),
});The engine works on its own too:
import { downloadVoice, PiperVoice } from "@pipedog/piper/piper_voice";
await downloadVoice("en_US-lessac-medium", "/models");
const voice = await PiperVoice.load("/models/en_US-lessac-medium.onnx");
for await (const chunk of voice.synthesize("Hello world. How are you?")) {
chunk.audioInt16Bytes; // one sentence, 16-bit PCM at voice.config.sampleRate
}Notes
- The voice is downloaded and loaded in
start(). A failure is pushed as an error and retried on the first sentence. - Two services with the same voice in one process share one model session.
- Each sentence is phonemized clause by clause, synthesized, and pushed as 16-bit PCM.
forceRedownload: truedownloads the voice again even if it is on disk.useCudais accepted but has no effect: inference runs on the ONNX backend's default (CPU) execution provider.- Not supported: voices whose phoneme type isn't
espeakortext(Chinese, Hebrew, Japanese and Thai voices need other G2P models), and Arabic diacritization.
PiperHttpTTSService
Posts { text, voice } to baseUrl for each sentence and streams the WAV reply, without its
header.
Configuration
Prop
Type
Settings
Settings, passed as settings: new PiperHttpTTSService.Settings({...}) (PiperHttpTTSSettings).
| Setting | Type | Default | Description |
|---|---|---|---|
model | null | null | Not used. |
voice | string | null | null | Voice the server should use. null leaves the server's default. |
language | null | null | Not used. |
Usage
// A Piper HTTP server: python -m piper.http_server -m en_US-ryan-high
const remote = new PiperHttpTTSService({
baseUrl: "http://localhost:5000",
settings: new PiperHttpTTSService.Settings({ voice: "en_US-ryan-high" }),
});
Notes
- A trailing slash on
baseUrlis removed, with a warning. - A non-200 reply is pushed as an
ErrorFrame. fetchreplaces the globalfetch.
Differences from upstream
PiperTTSServicedownloads and loads the voice instart(), not in the constructor, since loading is asynchronous. A missing or blank voice is still rejected by the constructor.- Default download directory:
~/.cache/pipedog/piper, where upstream uses~/.cache/pipecat/piper. useCudahas no effect (above).fetchandbackendare pipedog options.PiperHttpTTSService: upstream's requiredaiohttp_sessionis an optionalfetch.- The deprecated
voice_idargument is not ported. Usesettings.
Licenses
@pipedog/piper: BSD 2-Clause.- Piper voices: each has its own license, in its
MODEL_CARDon Hugging Face. - eSpeak NG and
@echogarden/espeak-ng-emscripten, used byPiperTTSService: GPL-3.0. Upstream'sPiperTTSServiceusespiper-tts, which is GPL-3.0 itself. To keep GPL code out of your application, usePiperHttpTTSServicewith a separately installed Piper server.
pipedog is an unofficial community TypeScript port of Pipecat. It is not affiliated with or endorsed by Daily or the Pipecat project.
Portions adapted from the Pipecat documentation, Copyright (c) 2024–2025 Daily, BSD-2-Clause (see apps/docs/LICENSE-pipecat-docs).