Text-to-Speech

Text-to-Speech With Piper

⏱ 16 min

Text-to-speech has a genuinely free, local, standard option already, SpeechSynthesis, the output half of the Web Speech API, and it’s worth trying first for anything simple: it needs no server and generally works offline once a browser has its voices loaded. Piper exists for when you need more than that, a specific voice, better prosody, or speech generated server-side rather than only in a visitor’s browser. This lesson gets Piper running locally and calls it from WordPress. It also flags something that actually matters for anyone shipping this in a real product: the original project was archived, and its actively maintained successor changed license.

What you'll learn in this lesson
The real, current status of the Piper project
The original repository archived in October 2025, and where active development actually continues.
Installing and running the current, maintained package
A real pip install, not the archived repository's old instructions.
Generating speech from text as a local process
A command-line call producing a real audio file, no API key.
Calling it from a small local HTTP wrapper
The same wp_remote_post() pattern this course has used for every local model so far.
The license implication that actually matters for shipping
GPL-3.0's reciprocal terms, and what that means if you distribute a product built on it.
Prerequisites

Lesson 3’s pattern of running a local Python service and calling it from WordPress over wp_remote_post(), this lesson repeats that shape for the opposite direction, text in, audio out.

Step 1: Piper’s real, current status, verified directly

rhasspy/piper is archived, read this before installing anything

The original rhasspy/piper repository was archived by its owner on October 6, 2025, and is now read-only. It was MIT-licensed. Active development did not stop, it moved to a new repository, OHF-Voice/piper1-gpl, maintained by the Open Home Foundation’s Voice team, and that successor is licensed under GPL-3.0, not MIT. The installable package from the maintained project is piper-tts on PyPI. Before following any older tutorial that points at the archived repository’s build instructions, confirm you’re installing the current package, since the setup steps changed along with the license.

That license change is worth sitting with for a moment before writing any code. MIT let you bundle Piper into a closed-source commercial product with no obligation to share your own source. GPL-3.0 carries real reciprocal terms, if you distribute a product that includes GPL-3.0-licensed code, you generally need to make that product’s source available under a compatible license too. Whether that’s a problem depends entirely on how you ship: a WordPress plugin already released under the GPL, which most are, has no new conflict here. A closed-source SaaS product bundling Piper for distribution is a different situation, and worth a real conversation with whoever handles licensing for that project, not a decision to make silently in a code review.

Step 2: Install the current, maintained package

# Context: on the machine that will run text-to-speech, can be the same host as Lesson 3's whisper-server
python3 -m venv piper-env
source piper-env/bin/activate
pip install piper-tts fastapi uvicorn

Then download a voice model, Piper’s voices are separate .onnx files distributed alongside the project:

python3 -m piper.download_voices en_US-lessac-medium

Step 3: Confirm it works from the command line first

echo "This is a test of local text to speech." | piper \
  --model en_US-lessac-medium \
  --output_file test-output.wav

Play test-output.wav. Hearing an actual synthesized voice, not an error, confirms the model downloaded correctly and the package is runnable before wiring any WordPress code around it.

Step 4: Wrap it as a small local HTTP service

# File: piper-server/main.py
from fastapi import FastAPI
from fastapi.responses import Response
from piper import PiperVoice
import io
import wave

app = FastAPI()
voice = PiperVoice.load( "en_US-lessac-medium.onnx" )

@app.post( "/speak" )
async def speak( payload: dict ):
    text = payload.get( "text", "" )
    buffer = io.BytesIO()
    with wave.open( buffer, "wb" ) as wav_file:
        voice.synthesize( text, wav_file )
    return Response( content=buffer.getvalue(), media_type="audio/wav" )
# Context: starting the local text-to-speech service on port 5002
uvicorn main:app --host 127.0.0.1 --port 5002

Step 5: Call it from WordPress

// File: my-plugin/class-local-tts.php
function my_plugin_speak_text( string $text ): string|WP_Error {
	$response = wp_remote_post(
		'http://127.0.0.1:5002/speak',
		array(
			'timeout' => 30,
			'headers' => array( 'Content-Type' => 'application/json' ),
			'body'    => wp_json_encode( array( 'text' => $text ) ),
		)
	);

	if ( is_wp_error( $response ) ) {
		return $response;
	}

	if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
		return new WP_Error( 'tts_failed', 'Local text-to-speech request failed.' );
	}

	$upload_dir = wp_upload_dir();
	$filename   = 'tts-' . wp_generate_password( 8, false ) . '.wav';
	$file_path  = trailingslashit( $upload_dir['path'] ) . $filename;

	file_put_contents( $file_path, wp_remote_retrieve_body( $response ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents

	return trailingslashit( $upload_dir['url'] ) . $filename;
}

This writes the returned WAV audio into the uploads directory and hands back a real, playable URL, the shape Lesson 5’s widget expects when it plays a spoken reply back to a visitor.

Clean up generated audio files

Every call to my_plugin_speak_text() writes a new file into uploads. Left unchecked, that grows without bound. A scheduled cleanup, deleting files under this pattern older than a day, is a real production requirement, not an optional polish step, add it before this ships anywhere with real traffic.

Test it: generate and play a real reply

wp eval '
$url = my_plugin_speak_text( "This is a test of the local text to speech pipeline." );
echo is_wp_error( $url ) ? $url->get_error_message() : $url;
'

Open the returned URL directly in a browser tab and confirm you actually hear the sentence spoken back, not silence or a corrupted file.

Recap

SpeechSynthesis, the browser’s own built-in text-to-speech, is worth trying first since it needs nothing installed. Piper is the real, local, neural alternative for when a specific voice or server-side generation matters, run as a small local HTTP service and called with the same wp_remote_post() pattern this course has used throughout. The one detail that genuinely changes how you ship this: the original MIT-licensed rhasspy/piper repository is archived, the maintained project now lives at OHF-Voice/piper1-gpl under GPL-3.0, and that license’s reciprocal terms are worth a real decision, not an assumption, before bundling it into a distributed product.

Resources & further reading

← Local, Private Speech-to-Text With Whisper Building a Voice-Driven Chatbot Front End →