cd /news/ai-tools/offline-voice-replies-on-a-35-raspbe… · home › topics › ai-tools › article
[ARTICLE · art-168396] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=↑ positive

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.

by read8 min views2 publishedNov 12, 2026

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, 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.

pip install linnet-tts soundfile sounddevice

sudo apt install libportaudio2

sudo usermod -aG audio $USER

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:

mkdir -p local/voices

wget -O local/linnet-en-35m.fp16.safetensors \
  https://github.com/VoxRT/LinnetTTS/releases/download/weights-v1/linnet-en-35m.fp16.safetensors

wget -O local/tokenizer.model \
  https://github.com/VoxRT/LinnetTTS/releases/download/weights-v1/tokenizer.model

wget -O local/voices/linnet-en-f1.npy \
  https://github.com/VoxRT/LinnetTTS/releases/download/weights-v1/linnet-en-f1.npy

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:

import linnet_tts
import soundfile as sf

model = linnet_tts.Model(
    "local/linnet-en-35m.fp16.safetensors",
    "local/tokenizer.model",
)

voice = model.load_voice("local/voices/linnet-en-f1.npy")

audio = model.synthesize(
    "Hello from your Raspberry Pi.",
    voice,
    temp=0.3,
    cfg=2.0,
    seed=0,
)

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:

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")

session = model.session(voice, seed=0)

session.prime("Ready to help. What can I do?")

for chunk in session:
    pass  # feed chunk to your audio output (concrete sounddevice example below)

Playback on Pi 4 via sounddevice is the easy route:

import sounddevice as sd
import numpy as np

stream = sd.OutputStream(
    samplerate=model.sample_rate,
    channels=1,
    dtype='float32',
)
stream.start()

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, 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:

import linnet_tts
import sounddevice as sd
import numpy as np

model = linnet_tts.Model(
    "local/linnet-en-35m.fp16.safetensors",
    "local/tokenizer.model",
)
voice = model.load_voice("local/voices/linnet-en-f1.npy")

stream = sd.OutputStream(
    samplerate=model.sample_rate,
    channels=1,
    dtype='float32',
)
stream.start()

RESPONSES = {
    "play_music": "Playing music now.",
    "lights_on": "Lights on.",
    "lights_off": "Lights off.",
    "volume_up": "Volume up.",
    "volume_down": "Volume down.",
}

def speak(text):
    session = model.session(voice, seed=0)
    session.prime(text)
    for chunk in session:
        stream.write(chunk)

def on_command(cmd):
    if cmd in RESPONSES:
        speak(RESPONSES[cmd])

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 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 = <index> from sd.query_devices(). Audio stutters or drops. Usually CPU contention. Check if wake-word, VAD, and KWS are running at the same time. On Pi 4 this is typically fine, but if you see stutters, log vcgencmd get_throttled once a second to catch throttle events (bits 16-19 are sticky and catch events your per-second poll would miss).

Browser demo (the same LinnetTTS v0.2 with priming optimization): Hugging Face Space

Model card and weights download: huggingface.co/VoxRT/linnet-en-35m

GitHub repo (source, docs, streaming contract): github.com/VoxRT/LinnetTTS

If you have a Pi 4 deployment running LinnetTTS sustained, what is the cooling config? Open case with fan, sealed case, passive heatsink only? Real-world thermal data on these setups is still thin.

What was the breaking point in your Pi voice assistant before adding offline TTS: cost per cloud API call, latency, or privacy? Share in the comments.

── more in #ai-tools 4 stories · sorted by recency
── more on @linnettts 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/offline-voice-replie…] indexed:0 read:8min 2026-11-12 · —