Offline voice replies on a $35 Raspberry Pi: LinnetTTS integration in Python A developer documented integrating LinnetTTS, a 41M-parameter on-device text-to-speech runtime released November 4, 2026, into an offline Raspberry Pi voice assistant. The Apache-2.0 Rust-based runtime runs without PyTorch at inference, uses roughly 160 MB of resident memory, and produces 24 kHz audio on a Pi 4, avoiding the network dependency, API quota and 200-400 ms latency of cloud TTS. The guide covers pip installation, downloading about 85 MB of weights and voice files from GitHub, and batch synthesis to WAV via the Python API. A Pi voice assistant usually comes in four pieces: wake-word catches "Hey Assistant", VAD detects when someone is talking, KWS or streaming ASR converts the speech into a command, and something answers with a voice. That last piece was usually the problem. Cloud TTS APIs work, but they need network, burn API quota, add 200 to 400 ms of latency, and send user text somewhere outside your box. For privacy-sensitive apps or offline-only devices, not an option. LinnetTTS shipped on 2026-11-04. 41M params, on-device, Rust runtime, no PyTorch at inference. It runs comfortably on a Raspberry Pi 4 Pi Zero 2 W also handles it, but on the edge . This tutorial shows how to pip install it and plug voice replies into your Pi voice assistant in one sitting. If you built the Pi 4 voice pipeline from my earlier tutorial https://dev.to/voxrtio/building-a-fully-offline-voice-assistant-on-raspberry-pi-4-3d1m , this article closes the loop. Adding offline TTS is not just a drop-in replacement for cloud TTS. It changes three things on a Pi: With that frame, the install. linnet-en-f1.npy female, American and linnet-en-m1.npy male, American On licensing: LinnetTTS is an open-source on-device TTS runtime Apache-2.0 with openly available model weights under a free license that permits commercial use; voice cloning and model training are licensed separately. For this tutorial we use the built-in linnet-en-f1 . Custom voice cloning is a separately licensed service from VoxRT and not part of the free tier. Python packages: pip install linnet-tts soundfile sounddevice System dependency for sounddevice on Pi OS: sudo apt install libportaudio2 Make sure your user is in the audio group prevents silent playback failure : sudo usermod -aG audio $USER Log out and back in for the group change to take effect. soundfile is for writing WAV files, sounddevice is for real-time playback through ALSA. PyPI wheels available: manylinux 2.17 x86 64 and aarch64, macOS Apple silicon. Pi 4 on 64-bit Raspberry Pi OS is aarch64, so you get a wheel. On 32-bit Pi OS there is no wheel, so reflash to 64-bit or build from source. Pi 4 with 2 GB or more RAM is recommended. The model resident is about 160 MB, plus Python, plus your voice-assistant stack. A 1 GB Pi 4 can work but is tight. Important: the model files are not in the wheel. Download the weights release from GitHub releases tag weights-v1 https://github.com/VoxRT/LinnetTTS/releases/tag/weights-v1 : mkdir -p local/voices Weights ~83 MB : wget -O local/linnet-en-35m.fp16.safetensors \ https://github.com/VoxRT/LinnetTTS/releases/download/weights-v1/linnet-en-35m.fp16.safetensors Tokenizer: wget -O local/tokenizer.model \ https://github.com/VoxRT/LinnetTTS/releases/download/weights-v1/tokenizer.model Female voice: wget -O local/voices/linnet-en-f1.npy \ https://github.com/VoxRT/LinnetTTS/releases/download/weights-v1/linnet-en-f1.npy Optional male voice: wget -O local/voices/linnet-en-m1.npy \ https://github.com/VoxRT/LinnetTTS/releases/download/weights-v1/linnet-en-m1.npy Total download is about 85 MB. Minimal batch synthesize to WAV: python import linnet tts import soundfile as sf Load model + tokenizer: model = linnet tts.Model "local/linnet-en-35m.fp16.safetensors", "local/tokenizer.model", Load voice prepared for the model : voice = model.load voice "local/voices/linnet-en-f1.npy" Synthesize a full sentence to float32 array at 24 kHz: audio = model.synthesize "Hello from your Raspberry Pi.", voice, temp=0.3, cfg=2.0, seed=0, Save: sf.write "hello.wav", audio, model.sample rate Run on Pi 4: python synthesize.py produces hello.wav with the female voice reading "Hello from your Raspberry Pi." Short sentences complete quickly in batch mode. Params: temp=0.3 is the sampling temperature lower means more deterministic output cfg=2.0 is the classifier-free guidance scale controls how closely output follows the voice prompt seed=0 makes output reproducible same text, same params, same seed produces bit-exact same audio Batch model.synthesize returns the full audio array when generation is done. Simpler code, one call, you own the array. Good for pre-generated phrases, voice samples saved to disk, or tests where latency does not matter. Streaming session.prime + chunk iteration yields chunks every 80 ms so playback can start before the full sentence is generated. First audio arrives after the prime pass plus one step. On a Pi 4 that is usually tens of ms. Good for real-time assistant replies, live chatbot voices, or any UX where the user is waiting for the first sound. For the Pi assistant integration below we use streaming, because the alternative waiting for the whole reply to generate before playing it adds perceivable delay to confirmation messages. Batch mode is fine for pre-generated phrases. For a real-time assistant you want streaming: the user hears first audio within the first prime pass, not after the whole generation finishes. Streaming API: python import linnet tts model = linnet tts.Model "local/linnet-en-35m.fp16.safetensors", "local/tokenizer.model", voice = model.load voice "local/voices/linnet-en-f1.npy" Create a streaming session with voice + seed: session = model.session voice, seed=0 Prime with text text-only pass thanks to load voice : session.prime "Ready to help. What can I do?" Iterate chunks as they arrive: for chunk in session: chunk = numpy float32 at 24 kHz first chunk: 50 ms 1200 samples middle chunks: 80 ms each 1920 samples last chunk: 30 ms of zeros 720 samples pass feed chunk to your audio output concrete sounddevice example below Playback on Pi 4 via sounddevice is the easy route: python import sounddevice as sd import numpy as np Start audio output stream once: stream = sd.OutputStream samplerate=model.sample rate, channels=1, dtype='float32', stream.start Streaming synthesize + play: session = model.session voice, seed=0 session.prime "Everything is ready on the Pi." for chunk in session: stream.write chunk.astype np.float32 stream.stop The first chunk arrives after the prime pass voice latent load + text processing + context decode . On Pi 4 that is a few tens of ms. After prime, chunks stream every 80 ms of wall-clock time or faster the model runs faster than real time on Pi 4 . If prime latency matters to you, the v0.2 API caches voice preparation inside load voice , so prime itself only runs text rows. That optimization alone cuts cold-path first-audio by 2 to 3 times across our test configurations Core i9 desktop, iPhone 13 Pro Max, iPhone 11, Snapdragon 662 Android . If you built the pipeline from my "Pi 4 voice assistant" tutorial https://dev.to/voxrtio/building-a-fully-offline-voice-assistant-on-raspberry-pi-4-3d1m , this is a simple add-in. Current flow: mic → wake-word → VAD → KWS 14 commands → trigger action Add a TTS reply for confirmation or feedback: python import linnet tts import sounddevice as sd import numpy as np Setup once at startup: model = linnet tts.Model "local/linnet-en-35m.fp16.safetensors", "local/tokenizer.model", voice = model.load voice "local/voices/linnet-en-f1.npy" Audio output stream keep alive for the lifetime of the assistant : stream = sd.OutputStream samplerate=model.sample rate, channels=1, dtype='float32', stream.start Response phrases per command: RESPONSES = { "play music": "Playing music now.", "lights on": "Lights on.", "lights off": "Lights off.", "volume up": "Volume up.", "volume down": "Volume down.", ... for your 14 commands } def speak text : session = model.session voice, seed=0 session.prime text for chunk in session: stream.write chunk In your KWS handler: def on command cmd : if cmd in RESPONSES: speak RESPONSES cmd ... actually do the command Keep model and voice pre-loaded at startup. Re-initializing per command adds prime overhead to every reply. Verified public numbers exist only for Pi Zero 2 W: 59 to 60 ms per step on 4 cores 75% RTF, real-time with margin , confirmed via an independent sustained 30-minute test. Pi 4 is architecturally higher in clock speed and cores, so we expect comfortable margin. A specific Pi 4 RTF measurement is still pending. In practice: if Pi Zero 2 W can sustain LinnetTTS, Pi 4 can do it comfortably. For confirmation in your own deployment, measure prime time and step time on your target configuration, and decode vcgencmd get throttled once a second if the workload is sustained and always on. Thermal nuance: Pi Zero 2 W in a closed case without airflow can get close to the soft limit 80°C under sustained TTS load. Pi 4 with a stock heatsink or active cooling is usually fine. On production deployments log get throttled sticky bits bits 16-19 to catch throttle events that fire between polling samples. A few things that trip people on the first run: Wheel install fails. Check your Python version 3.9+ required and your Pi OS architecture uname -m should be aarch64 on 64-bit Pi OS . 32-bit Pi OS does not get a wheel. Reflash the SD card to 64-bit, or pick the 64-bit image in Raspberry Pi Imager. "File not found" when loading model. The paths in linnet tts.Model ... are relative to where you run the script, not where the script lives. Either cd to the directory with local/ in it before running, or use absolute paths. Silent playback. If the streaming code runs but you hear nothing: aplay -l lists audio devices. Make sure one is connected HDMI, 3.5 mm jack, or USB audio . sudo raspi-config lets you pick the audio output. audio group see install block above . sounddevice default device can be set explicitly via sd.default.device =