Build a Real-Time Speech-to-Text Pipeline with Whisper and WebSockets A developer tutorial by Priya Nair shows how to build a self-hosted real-time speech-to-text pipeline using faster-whisper 1.2.1 and websockets 17.1, streaming 16 kHz mono PCM audio from a browser microphone to a local Whisper server over WebSockets. The server re-transcribes a growing audio buffer once per second to emit gray interim captions and commits black final text every 15 seconds, with all audio processed locally and no data leaving the machine. The guide requires Python 3.11 or newer, Chrome, Edge, or Safari, and about 150 MB of disk for the base model, which runs on CPU with int8 compute. Build a Real-Time Speech-to-Text Pipeline with Whisper and WebSockets Stream microphone audio to a self-hosted Whisper server over WebSockets and render live captions in the browser. Priya Nair https://sourcefeed.dev/u/priya nair What you'll build A browser page that streams your microphone to a self-hosted Whisper https://github.com/openai/whisper model over a WebSocket and shows live captions: gray interim text that updates about once a second, committed black text every 15 seconds. Everything runs on your machine. No audio leaves it. Prerequisites Verified against faster-whisper 1.2.1, websockets 17.1, and ctranslate2 4.8.2 on Python 3.13 macOS 15 . Linux and Windows work the same way. - Python 3.11 or newer websockets 17.x requires 3.11+ - Chrome, Edge, or Safari. Firefox can't resample mic input to a 16 kHz AudioContext; see Troubleshooting - ~150 MB of disk for the base model, downloaded from Hugging Face on first run - A CPU is enough for the base model. GPU needs CUDA 12 and cuDNN 9 Step 1: Install the server dependencies faster-whisper https://github.com/SYSTRAN/faster-whisper runs Whisper on CTranslate2, about 4x faster than the reference implementation, which is what makes once-a-second re-transcription workable on a laptop CPU. websockets https://websockets.readthedocs.io/ handles the socket side. mkdir whisper-live && cd whisper-live python3 -m venv venv && source venv/bin/activate pip install "faster-whisper==1.2.1" "websockets==17.1" Step 2: Write the transcription server The protocol is deliberately dumb: the client sends raw 16-bit little-endian PCM at 16 kHz as binary frames, the server replies with JSON. The server appends every frame to a buffer and, once a second, re-transcribes the whole buffer and pushes the result as a partial . When the buffer hits 15 seconds it commits the text as a final and starts fresh. Re-transcribing the growing buffer costs more than a true incremental decoder, but it self-corrects earlier words as context arrives and needs no alignment logic. Save this as server.py : python import asyncio import json import numpy as np from faster whisper import WhisperModel from websockets.asyncio.server import serve SAMPLE RATE = 16000 BYTES PER SECOND = SAMPLE RATE 2 16-bit mono PCM MAX WINDOW SECONDS = 15 model = WhisperModel "base", device="cpu", compute type="int8" def transcribe pcm: bytes - str: audio = np.frombuffer pcm, dtype=np.int16 .astype np.float32 / 32768.0 segments, = model.transcribe audio, language="en", beam size=1, vad filter=True, condition on previous text=False, return " ".join segment.text.strip for segment in segments async def handler websocket : buffer = bytearray async def worker : last len = 0 while True: await asyncio.sleep 1.0 if len buffer == last len or len buffer < BYTES PER SECOND: continue snapshot = bytes buffer last len = len snapshot text = await asyncio.to thread transcribe, snapshot if len snapshot = MAX WINDOW SECONDS BYTES PER SECOND: Drop only what was transcribed, keeping audio that arrived while the model was busy. del buffer : len snapshot last len = 0 await websocket.send json.dumps {"type": "final", "text": text} else: await websocket.send json.dumps {"type": "partial", "text": text} task = asyncio.create task worker try: async for message in websocket: if isinstance message, bytes : buffer.extend message finally: task.cancel async def main : async with serve handler, "localhost", 8765 as server: print "Listening on ws://localhost:8765" await server.serve forever if name == " main ": asyncio.run main Four choices matter here. model.transcribe accepts a float32 numpy array at 16 kHz directly, so there's no temp-file dance. asyncio.to thread keeps the CPU-bound decode off the event loop, so incoming frames keep landing in the buffer while the model runs. beam size=1 with condition on previous text=False trades a little accuracy for latency and stops the model hallucinating repeats on partial audio. vad filter=True runs Silero VAD first, so silence and keyboard noise don't get decoded into phantom words. Step 3: Build the browser client Creating the AudioContext at 16 kHz makes the browser resample the mic for you, so the worklet only has to convert float32 samples to int16. Save this as index.html : < doctype html