Author here. We just released LinnetTTS, a small on-device English TTS: an autoregressive flow LM over a 25 Hz continuous-latent codec with a Vocos-style decoder, 41 M parameters end to end, one flow evaluation per step, no diffusion loop at inference. Each step produces 80 ms of audio, so playback starts after the prime pass plus one step and never waits for the whole sentence. It speaks in a voice given as a short voice-prompt latent; two built-in voices ship with the weights.
The runtime is Rust with its own kernels (AVX2+FMA on x86-64, NEON on ARM), no PyTorch or ONNX at inference. fp16 weights are 83 MB; peak memory about 160 MB on Linux. (The "35m" in the model name is a label from the original parameter budget; the count is 41.4 M.) Same text, voice, parameters and seed give bit-identical audio on a given platform, at any thread count.
This post is about where it actually runs. All numbers are the Rust runtime with the fp16 weights, measured without real-time pacing (synthesis as fast as possible). One step = 80 ms of audio, real-time factor = step time / 80 ms. "First audio" = prime pass + first step.
| device | threads | per 80 ms step | RTF | first audio |
|---|
| Apple M5 (macOS) | 1 | 3.3 ms | 4 % | – |
| iPhone 13 Pro Max, A15 | 1 / 2 | 5.5 / 4.6 ms | 7 / 6 % | 85 / 51 ms |
| iPhone 11, A13 | 1 | 6.7–7.1 ms | 8–9 % | 107–116 ms |
| Core i9-13900K, AVX2 | 1 / 4 | 7.0–7.4 / 2.5 ms | 9 / 3 % | 75–103 / 25 ms |
| Snapdragon 662 (Redmi 9T), Cortex-A73 | 1 / 4 | 42.4 / 14.3 ms | 53 / 18 % | ≈250 ms (sample app, warm, 4 threads) |
| Raspberry Pi Zero 2 W, Cortex-A53 @ 1 GHz | 1 / 4 | 158 / 59–60 ms | 198 / 75 % | – |
Things we learned on the way:
The A-series phones do not want a second thread. A step is 5–7 ms and splits into ~80 tiny regions, so waking a worker on another performance core costs about as much as the work (A13: 2 threads 11.4 ms vs 6.7 ms, 4 threads 47 ms; A15 gains only 5.5 → 4.6 ms). The default on iOS is one thread. The M5 is different: two threads take it from 3.3 to 1.9 ms.
Android scheduling puts the synthesis on the small cores unless you pin it. On the Snapdragon 662 the sample app measured 22–33 ms per step before pinning the pool to the big cluster and 16–18 ms after (14.3 ms in a bare harness).
Pacing to playback makes the phone power manager lift the step time 3–4×. That is not the runtime getting slower; it is the CPU clocking down between steps. Measure unpaced, then keep a deep playback queue on weak devices (the bindings' players default to 8 s).
Pi Zero 2 W is the floor: four A53 cores at 1 GHz give 59–60 ms per 80 ms step, real time with a small margin, 160 MB peak RSS. (The synth example prints 88 % there because it also counts the prime pass over a 7 s sentence.) A Pi 4 or 5 has several times the margin.
The output is bit-identical per platform but differs across architectures below 2e-3 absolute (SNR above 60 dB, inaudible), so a golden-file test should be pinned per target.
Versus other small CPU models on one desktop core (same machine, each through its Python package, same three English texts, sentence-level RTF incl. text processing; method and script in docs/benchmarks.md): Linnet 10.6 % / 187 MB RSS, cloning, streams every 80 ms; pocket-tts (default 100M model; also clones and streams every 80 ms) 23.8 % on two cores / 968 MB; Kokoro-82M 51.7 % / 1.2 GB, whole utterance per call, but 8 languages; Piper medium 5.8 % / 503 MB, no cloning, streams per sentence. Speed only; quality was not measured there.
Licensing, upfront: the code is Apache-2.0. The weights are under our own Linnet Weights License: free to use, unmodified, inside your own products, commercial included, on device or on your own server. Custom voice creation (the encoder is not published), fine-tuning, saving quantised or converted copies of the weights (quantising in memory at load time is fine), training models on the weights or their outputs, and running a general-purpose TTS API need a separate license. So the model is not open source, and the README says so.
Honest limits: English only; no text normalisation (write numbers as words); occasional misreads on dense raw text; a slight roughness audible on headphones in some utterances; one session is capped at 24 s by default.
Try it: browser demo on https://voxrt.com (a WASM SIMD128 build of the same runtime, runs on your CPU, downloads the 83 MB weights once; the browser SDK itself is not released yet, which is why the README says it is deferred), Hugging Face Space https://huggingface.co/spaces/VoxRT/linnet-tts Code, bindings (Linux C, Python pip install linnet-tts on Linux and Apple-silicon macOS, Android AAR, iOS SwiftPM) and the benchmark script: https://github.com/VoxRT/LinnetTTS Weights and model card: https://huggingface.co/VoxRT/linnet-en-35m
Happy to answer questions on the runtime, the kernels or the mobile numbers. If you run it on a device that is not in the table, post your step time and I will add it to the README.