Piper Free code examples in C#, Python and JavaScript
Fast neural text to speech that runs entirely on CPU, offline and free.
WHAT THIS PAGE COVERS
Self-hosting it — a speech engine on your own CPU, with no API to call.
en_US-lessac-medium
piper-tts
Model ids change. If a sample returns a 404 on the model, that is the first thing to check against the official docs.
There is no .NET binding, and the GPL-3.0 licence is a reason not to want one in-process. Run Piper as a service and call it over HTTP — which is exactly what FreeYantra's own TTS page does.
# On the box that will do the speaking:
pip install 'piper-tts[http]'
python3 -m piper.download_voices en_US-lessac-medium
python3 -m piper.http_server -m en_US-lessac-medium --port 5000
# Nothing to install on the .NET side.
The body is the raw text, not JSON. The response is a WAV — 16-bit mono at whatever rate the voice was trained on, which for en_US-lessac-medium is 22.05 kHz.
using System.Text;
using var http = new HttpClient
{
BaseAddress = new Uri("http://localhost:5000/"),
// Synthesis is synchronous and CPU-bound. The .NET default of
// 100 seconds is the single most common way this integration
// fails on a long passage.
Timeout = TimeSpan.FromMinutes(5),
};
var response = await http.PostAsync(
"",
new StringContent("This is a test.", Encoding.UTF8, "text/plain"));
response.EnsureSuccessStatusCode();
await File.WriteAllBytesAsync("test.wav", await response.Content.ReadAsByteArrayAsync());
A typed client with the timeout and base address set once. Every tool page on this site is wired this way, for the reason above: the default timeout is wrong for synthesis.
// Program.cs
builder.Services.AddHttpClient("piper", client =>
{
client.BaseAddress = new Uri(
builder.Configuration["Piper:BaseUrl"] ?? "http://localhost:5000/");
client.Timeout = TimeSpan.FromMinutes(
builder.Configuration.GetValue("Piper:TimeoutMinutes", 5));
});
// Somewhere that needs speech
app.MapGet("/speak", async (string text, IHttpClientFactory factory) =>
{
var client = factory.CreateClient("piper");
var response = await client.PostAsync(
"", new StringContent(text, Encoding.UTF8, "text/plain"));
response.EnsureSuccessStatusCode();
return Results.File(
await response.Content.ReadAsStreamAsync(), "audio/wav");
});
One server holds one voice. Multiple languages means multiple processes on different ports — which is fine, because a medium Piper voice is about 60 MB of RAM and no GPU.
// Start one process per voice:
// python3 -m piper.http_server -m en_US-lessac-medium --port 5000
// python3 -m piper.http_server -m hi_IN-pratham-medium --port 5001
static readonly Dictionary<string, string> Voices = new()
{
["en"] = "http://localhost:5000/",
["hi"] = "http://localhost:5001/",
};
app.MapGet("/speak/{language}", async (
string language, string text, IHttpClientFactory factory) =>
{
if (!Voices.TryGetValue(language, out var baseUrl))
{
return Results.BadRequest($"No voice configured for '{language}'.");
}
var client = factory.CreateClient();
client.Timeout = TimeSpan.FromMinutes(5);
var response = await client.PostAsync(
baseUrl, new StringContent(text, Encoding.UTF8, "text/plain"));
response.EnsureSuccessStatusCode();
return Results.File(await response.Content.ReadAsStreamAsync(), "audio/wav");
});
THE LICENCE IS GPL-3.0, not MIT. Piper moved from rhasspy/piper to OHF-Voice/piper1-gpl in October 2025 and relicensed on the way. Check that against what you are shipping before it is load-bearing.
pip install piper-tts
# Voices are separate downloads, one ONNX file plus a JSON config.
python3 -m piper.download_voices en_US-lessac-medium
# Hindi, and everything else, works the same way.
python3 -m piper.download_voices hi_IN-pratham-medium
Useful for auditioning a voice and wrong for anything repeated: the CLI reloads the model on every invocation, so a loop over a thousand lines pays the load cost a thousand times.
python3 -m piper -m en_US-lessac-medium -f test.wav -- 'This is a test.'
# With ffplay installed, drop -f to hear it immediately.
python3 -m piper -m en_US-lessac-medium -- 'This is a test.'
PiperVoice.load() reads the model once. Hold that object for the life of the process — loading is the expensive part and synthesis is not.
import wave
from piper import PiperVoice
# Load once, at startup. This is the slow step.
voice = PiperVoice.load("en_US-lessac-medium.onnx")
with wave.open("test.wav", "wb") as wav_file:
voice.synthesize_wav(
"Welcome to the world of speech synthesis!",
wav_file,
)
SynthesisConfig is the whole tuning surface. length_scale is the one that matters: it is a MULTIPLIER on duration, so 0.8 is faster and 1.2 is slower — the opposite of what a field called speed would do.
from piper import SynthesisConfig
config = SynthesisConfig(
length_scale=0.9, # below 1.0 is FASTER
noise_scale=0.667, # audio variation
noise_w_scale=0.8, # speaking variation
volume=1.0,
normalize_audio=True,
)
with wave.open("faster.wav", "wb") as wav_file:
voice.synthesize_wav(
"The same sentence, read a little more quickly.",
wav_file,
syn_config=config,
)
This is the shape worth deploying. One process holds the model, everything else makes an HTTP request, and no application needs onnxruntime as a dependency.
pip install 'piper-tts[http]'
python3 -m piper.http_server -m en_US-lessac-medium --port 5000
# Then, from anywhere:
curl -X POST --data 'This is a test.' http://localhost:5000 --output test.wav
No Node binding exists, and the GPL-3.0 licence is a reason to keep Piper behind a process boundary anyway. Run the HTTP server and fetch from it.
# On the box that will do the speaking:
pip install 'piper-tts[http]'
python3 -m piper.download_voices en_US-lessac-medium
python3 -m piper.http_server -m en_US-lessac-medium --port 5000
# Nothing to install on the Node side.
Plain text in, WAV bytes out. No JSON, no key, no account — the whole appeal of running it yourself.
import { writeFile } from "node:fs/promises";
const response = await fetch("http://localhost:5000", {
method: "POST",
headers: { "content-type": "text/plain" },
body: "This is a test.",
});
if (!response.ok) {
throw new Error(`Piper returned ${response.status}`);
}
await writeFile("test.wav", Buffer.from(await response.arrayBuffer()));
Piper has no CORS headers, so a browser on another origin cannot call it directly. Same-origin proxying is the fix, and it is the reason FreeYantra's TTS tool has a server route at all.
import express from "express";
const app = express();
app.use(express.text());
app.post("/api/speak", async (request, response) => {
const upstream = await fetch("http://localhost:5000", {
method: "POST",
headers: { "content-type": "text/plain" },
body: request.body,
});
if (!upstream.ok) {
return response.status(502).json({ error: "synthesis failed" });
}
response.setHeader("Content-Type", "audio/wav");
response.send(Buffer.from(await upstream.arrayBuffer()));
});
app.listen(3000);
Synthesis is CPU-bound and synchronous in the server. Concurrent requests compete for the same cores, so a queue in front of it is the difference between slow and unavailable.
let inFlight = 0;
const MAX_CONCURRENT = 2; // roughly one per spare core
async function speak(text) {
if (inFlight >= MAX_CONCURRENT) {
throw Object.assign(new Error("busy"), { status: 429 });
}
inFlight++;
try {
const response = await fetch("http://localhost:5000", {
method: "POST",
headers: { "content-type": "text/plain" },
body: text,
signal: AbortSignal.timeout(5 * 60 * 1000),
});
if (!response.ok) throw new Error(`Piper returned ${response.status}`);
return Buffer.from(await response.arrayBuffer());
} finally {
inFlight--;
}
}
QUESTIONS
Which languages are the Piper examples in?
C#, Python and JavaScript. Every sample runs as pasted and reads its credentials from an environment variable — no key is ever written into the code.
Which package do I install for Piper?
The samples use piper-tts. The install step on each language tab gives the exact command.
Which Piper model do these examples use?
en_US-lessac-medium, checked against the official documentation on 26 September 2026. Model ids change often; if a call returns a model-not-found error, check the current list in the official docs.
Where is the official Piper documentation?
https://github.com/OHF-Voice/piper1-gpl — it is linked at the bottom of this page, and it is the authority whenever it disagrees with a sample here.
ALSO IN SPEECH & AUDIO
The authority
Official Piper documentation
Model names, pricing and rate limits change faster than any page here can. When this page and the official docs disagree, the official docs are right.
https://github.com/OHF-Voice/piper1-gpl