Speech-to-Text

Local, Private Speech-to-Text With Whisper

⏱ 17 min

Lesson 2’s browser-based recognition is the simplest starting point, but it has two real limits: Chrome’s implementation sends audio to a server you don’t control, and Firefox does not support it by default. When either of those matters, privacy on a site handling sensitive input, or reliability across every visitor’s browser, the real alternative is running speech-to-text yourself. OpenAI’s Whisper is a real, MIT-licensed, open-weight model that runs fully offline, and faster-whisper is the actively maintained, CTranslate2-based reimplementation that makes running it locally actually fast enough to use. This lesson sets it up as a small local service and calls it from WordPress over plain HTTP, the same pattern Course 13 used for provider-hosted transcription, just pointed at a process on your own machine instead of a third-party API.

What you'll learn in this lesson
When local Whisper beats the browser API
Privacy-sensitive input, consistent behavior across browsers, or full offline operation.
Running faster-whisper as a small local HTTP service
A minimal Python wrapper, since faster-whisper itself is a library, not a server.
Calling it from WordPress with wp_remote_post()
The same multipart-audio pattern this track already uses for a third-party transcription API, now pointed at localhost.
The real accuracy and latency tradeoffs
Model size choice matters, and this is not a drop-in replacement for the browser API's zero-setup convenience.
Prerequisites

Course 13’s Audio Transcription lesson for the wp_remote_post() multipart pattern this lesson reuses, and a machine that can run Python, this is not something you install as a WordPress plugin alone.

Step 1: Why reach for this instead of the browser API

Real reasons to choose local Whisper over Lesson 2's approach
Privacy
Audio never leaves your own infrastructure. Chrome's SpeechRecognition sends audio to a Google-operated server for processing, faster-whisper does not send it anywhere.
Consistent browser support
Firefox visitors, and any browser without SpeechRecognition, still get transcription, since the browser only needs to record and upload audio, not run recognition itself.
Full offline operation
Once the model is downloaded, transcription works with no internet connection at all, useful for an intranet or fully air-gapped WordPress install.

None of that is free. Running your own model means managing a server process, choosing a model size that fits your hardware, and accepting that a large model on modest hardware is genuinely slower than a cloud round trip. Reach for this when one of the reasons above actually applies to your project, not by default.

Step 2: Install faster-whisper

# Context: on the machine that will run transcription, separate from your WordPress host
python3 -m venv whisper-env
source whisper-env/bin/activate
pip install faster-whisper fastapi uvicorn python-multipart

Step 3: Wrap it in a minimal local HTTP service

faster-whisper is a Python library, not a server, so a thin wrapper is what makes it callable over HTTP the way this track’s other AI integrations expect:

# File: whisper-server/main.py
from fastapi import FastAPI, UploadFile
from faster_whisper import WhisperModel
import tempfile

app = FastAPI()
model = WhisperModel( "small", device="cpu", compute_type="int8" )

@app.post( "/transcribe" )
async def transcribe( file: UploadFile ):
    with tempfile.NamedTemporaryFile( suffix=".wav" ) as tmp:
        tmp.write( await file.read() )
        tmp.flush()
        segments, info = model.transcribe( tmp.name )
        text = " ".join( segment.text.strip() for segment in segments )
    return { "text": text.strip(), "language": info.language }
# Context: starting the local transcription service on port 5001
uvicorn main:app --host 127.0.0.1 --port 5001

"small" is a reasonable middle ground for CPU-only hardware, "base" is faster and less accurate, "medium" or "large-v3" are more accurate and meaningfully slower without a GPU. compute_type="int8" trades a small amount of accuracy for real speed on CPU, this is exactly the kind of tuning faster-whisper exists to make practical.

Step 4: Call it from WordPress over plain HTTP

This reuses the exact multipart pattern Course 13 established, just pointed at 127.0.0.1 instead of a third-party provider:

// File: my-plugin/class-local-transcription.php
function my_plugin_transcribe_locally( string $file_path ): string|WP_Error {
	$file_data = file_get_contents( $file_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents

	if ( false === $file_data ) {
		return new WP_Error( 'unreadable_file', 'Could not read the audio file.' );
	}

	$boundary = wp_generate_password( 24, false );
	$body  = "--{$boundary}\r\n";
	$body .= 'Content-Disposition: form-data; name="file"; filename="' . basename( $file_path ) . "\"\r\n";
	$body .= "Content-Type: audio/wav\r\n\r\n";
	$body .= $file_data . "\r\n";
	$body .= "--{$boundary}--\r\n";

	$response = wp_remote_post(
		'http://127.0.0.1:5001/transcribe',
		array(
			'timeout' => 60,
			'headers' => array( 'Content-Type' => 'multipart/form-data; boundary=' . $boundary ),
			'body'    => $body,
		)
	);

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

	$data = json_decode( wp_remote_retrieve_body( $response ), true );

	if ( empty( $data['text'] ) ) {
		return new WP_Error( 'transcription_failed', 'Local transcription returned no text.' );
	}

	return $data['text'];
}
127.0.0.1 only works if WordPress and the model share a host

This assumes the whisper-server process runs on the same machine as WordPress, or somewhere reachable at that address. On typical managed WordPress hosting, you cannot run a Python process alongside it at all, this pattern fits a VPS, a dedicated server, or a separate machine on the same private network, reachable by its actual address rather than localhost. Confirm your hosting model can actually run a second long-lived process before committing to this approach.

Step 5: Test it against a real short clip

wp eval '
$result = my_plugin_transcribe_locally( "/path/to/a/short-test-clip.wav" );
if ( is_wp_error( $result ) ) {
	echo "Error: " . $result->get_error_message() . "\n";
} else {
	echo "Transcript: " . $result . "\n";
}
'

Compare the output against what was actually said. With the "small" model on typical CPU hardware, expect a short delay, a few seconds for a short clip, and occasional small errors on uncommon names or heavy accents, real, measurable tradeoffs against the cloud-backed accuracy of a large hosted model, not a one-for-one replacement.

Model size is the real lever here, not a WordPress setting

If accuracy on a specific test clip is disappointing, the first thing to try is a larger model ("medium" or "large-v3"), not a WordPress-side change. That tradeoff against speed and hardware requirements is faster-whisper’s, not this plugin code’s, to manage.

Recap

faster-whisper, the actively maintained, MIT-licensed, CTranslate2-based reimplementation of OpenAI’s real Whisper model, runs fully offline and gives you a transcription pipeline that never sends audio anywhere you don’t control. Since it’s a library, not a server, a small FastAPI wrapper makes it callable over plain HTTP, and wp_remote_post() calls it from WordPress using the same multipart pattern this track already established for third-party transcription APIs. The real cost is infrastructure you have to run and manage yourself, and a genuine speed and accuracy tradeoff tied to model size, reach for this when Lesson 2’s browser API’s privacy or support gaps actually matter for your project.

Resources & further reading

← Speech-to-Text in the Browser With the Web Speech API Text-to-Speech With Piper →