← Code Recipes
Speech & Audio

Piper Free code examples in C#, Python and JavaScript

Fast neural text to speech that runs entirely on CPU, offline and free.

C# Python Java JavaScript

WHAT THIS PAGE COVERS

Self-hosting it — a speech engine on your own CPU, with no API to call.

Model en_US-lessac-medium
Package piper-tts
Checked

Model ids change. If a sample returns a 404 on the model, that is the first thing to check against the official docs.

Install

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.
Calling it

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());
Registering it properly

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");
});
Serving more than one voice

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");
});
Install

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
The command line, for trying voices

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.'
The Python API

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,
    )
Speed, pitch and volume

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,
    )
Running it as a server

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
Install

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.
Calling it

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()));
Proxying it to a browser

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);
Not letting one slow request block the rest

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.

Read the docs ↗

https://github.com/OHF-Voice/piper1-gpl

New drops, every week

Free tools & Claude templates, landing regularly.

We keep shipping new free tools, code recipes and Claude Code templates. Follow @freeyantra on Instagram or join the newsletter so you never miss one.

Follow @freeyantra

or get them by email

Double opt-in. Unsubscribe any time. Privacy