LiteRT is Google's on-device runtime, the new name for TensorFlow Lite (Android: com.google.ai.edge.litert:litert), and litert-torch, the renamed ai-edge-torch, is its PyTorch converter: a PyTorch model converted unmodified with litert_torch.convert matched the original to 4e-7 on a Galaxy S26 (measured, LiteRT 2.2.0, Android 16, 2026-09-05).
KittenTTS nano 0.8 β LiteRT, dynamic length + streaming
KittenTTS nano (15M params, StyleTTS2 +
ISTFTNet + mini-ALBERT, 8 voices, 24 kHz) converted to LiteRT CPU/XNNPACK
graphs with a dynamic sequence length β any sentence length runs on the same
graphs, no padding buckets. Built for small-CPU targets (Raspberry Pi class):
RTF 0.187 measured on a Raspberry Pi 5 (~1.6Γ faster than the ONNX Runtime
baseline on this device class).
[Image blocked: KittenTTS nano 0.8 β LiteRT on-device output]
Listen (golden sentence, same inputs):
samples/litert_golden.wav
β this port (fp32, deterministic) Β·
samples/ref_onnx.wav
β the official ONNX (stochastic reference).
Upstream ships ONNX only (Apache-2.0). This port re-authors the model in
TF/Keras from the ONNX weights and converts with the official
TFLiteConverter, which emits fused dynamic-length TFLite LSTM kernels for
the five BiLSTMs β the piece that torch-path conversions cannot keep dynamic
(see "Why TF re-authoring" below).
Graphs
Host glue between graphs is ~10 lines of numpy: en = repeat(d, durations),
asr = repeat(t_en, durations) (equivalent to the in-graph Loop alignment of
the ONNX β verified bit-exact), then slice har/f0/n per vocoder call.
style comes from voices.npz exactly as in the pip package
(voices[voice][min(len(text), 399)]).
Measured on a real Raspberry Pi 5 (2026-08-06)
Pi 5 8 GB, Raspberry Pi OS 64-bit, Python 3.13.5, ai-edge-litert 2.1.6,
4 threads. vcgencmd get_throttled = 0x0 before/after each run (no
undervoltage/throttling during measurement).
Overall RTF 0.187 (fp32; durations bit-OK, log-spec corr β₯ 0.9954). fp16 is
speed-identical on the Pi (XNNPACK unpacks fp16 weights to fp32 compute) with
slightly lower corr β deploy fp32. For reference, the same model on ONNX
Runtime was measured at RTF 0.30 on this device class, so the LiteRT path is
~1.6Γ faster.
GPU (v3dv WebGPU) status β measured on the Pi 5, 2026-08-06
With Mesa built from git (v3dv Vulkan 1.3, driver 26.2.99,
V3D_WEBGPU_OVERRIDE=1): the static-chunk vocoder
(kitten_vocoder_static80.tflite) compiles and runs fully accelerated
(is_fully_accelerated=True) with output corr 0.9997 vs CPU (fp16-class
divergence). The dynamic vocoder does not compile (the GPU delegate requires
static shapes), and the fused-LSTM predictor/prosody graphs are CPU-only. The
CPU is faster on this board; the all-CPU config above is the recommended
deployment.
Verification (vs. the official ONNX, same inputs)
The reference model is stochastic (SineGen draws a random initial harmonic
phase and additive noise every run), so the fair bar is the ONNX's own
run-to-run variability. All numbers on the golden sentence, Mac M-series CPU:
i.e. the port sits inside the model's intrinsic noise floor. Predicted
durations are bit-identical to the ONNX output. The decoder/vocoder chain is
float-exact in isolation (corr 1.000000 against the deterministic reference
when fed the reference harmonics). int8 dynamic-range quantization was tried
and rejected (log-mel corr 0.913, durations change).
Raw-waveform correlation is not a meaningful metric here: sub-0.5 % f0
differences de-correlate the waveform via accumulated sine phase while being
inaudible and spectrally identical.
Speed (Mac M-series, 4 threads, XNNPACK)
In a python:3.12-slim linux/arm64 container (same aarch64
ai-edge-litert 2.1.6 wheel the Pi uses): durations still bit-identical,
log-spec corr β₯ 0.9955.
Snapdragon NPU (Hexagon)
kitten_predictor.tflite β neither accelerator produced a usable row on the S26. Both ended the same way: LiteRtException: Failed to load model from file.
kitten_predictor_fp16.tflite β neither accelerator produced a usable row on the S26. Both ended the same way: LiteRtException: Failed to load model from file.
kitten_prosody.tflite β neither accelerator produced a usable row on the S26. Both ended the same way: LiteRtException: Failed to load model from file.
kitten_prosody_fp16.tflite β neither accelerator produced a usable row on the S26. Both ended the same way: LiteRtException: Failed to load model from file.
kitten_vocoder.tflite β neither accelerator produced a usable row on the S26. NPU: LiteRtException: ERROR: [./third_party/odml/litert/litert/cc/litert_compiled_model.h:2070]. GPU: LiteRtException: Failed to compile model.
kitten_vocoder_fp16.tflite β neither accelerator produced a usable row on the S26. NPU: LiteRtException: ERROR: [./third_party/odml/litert/litert/cc/litert_compiled_model.h:2070]. GPU: LiteRtException: Failed to compile model.
kitten_vocoder_static80.tflite β the GPU is faster: 11.32 ms against 75.92 ms on the NPU, a factor of 6.70. The NPU still loads 11.42x faster (175 ms against 1997 ms).
Measured on a Samsung Galaxy S26 (Snapdragon 8 Elite Gen 5 / SM8850, Hexagon v81, Android 16) with LiteRT CompiledModel 2.2.0, one accelerator per process, 5 warm-up runs then N=50 timed runs, median reported. Every run held thermal status NONE throughout. Headroom 0.77β0.78, where 1.0 is the throttling threshold.
The NPU rows ran the published file unchanged. LiteRT compiled it for the Hexagon on the device at first load. That first compile took 30 s here. The load column above is the cached load every later run pays. Recipe and the runtime libraries it needs: NPU guide.
GPU wiring: GPU guide.
Quickstart
One-command benchmark (no espeak needed on the device β inputs are
pre-tokenized in bench_inputs.npz):
Reports per-graph latency, per-sentence synthesis latency, RTF, and an output
identity check (durations bit-compare + log-spectrogram correlation vs the
bundled reference).
Drop-in synthesis (Piper replacement; frontend tokenization verified
byte-identical to the official pip package):
G2P options: the phonemizer library (exact reference tokenization; espeak-ng
GPL-3.0 in-process β same situation as Piper) or --g2p cli to call the
espeak-ng binary as a subprocess (GPL isolation).
Deployment note β LSTM state: the fused TFLite LSTM kernels keep their
hidden state in variable tensors that persist across invoke(). Call
interpreter.reset_all_variables() (after allocate_tensors()) before every
utterance, or the second synthesis on a reused interpreter is corrupted.
bench.py/say.py already do this.
Streaming
- Sentence-level (exact, recommended) β synthesize per sentence and play
while the next sentence synthesizes; this is the same granularity the
official pip package uses (
chunk_text). First-audio latency = one short
sentence (β0.4β0.7 s on the Pi 5).
- Intra-sentence chunked vocoder (approximate) β the vocoder alone can be
run on overlapping frame chunks (~1 s chunks, 20-frame overlap). It is not
exact because StyleTTS2's AdaIN InstanceNorms take statistics over the whole
utterance: chunked output measures log-mel corr 0.970 against the full
decode. Use sentence-level unless latency demands force this mode.
Files
Why TF re-authoring (and not litert-torch / onnx2tf)
litert-torch (0.9.2) cannot export a dynamic-length LSTM: torch.export's
LSTM decomposition specializes the time axis. Worse, even conv-only graphs
exported with a dynamic axis bake the example length into internal RESHAPEs,
so the resulting model only runs at the trace length. Dynamic shapes
effectively require the TF converter path today.
- The TF path also dodges a constant-dedup hazard that corrupts the ISTFTNet
cos/sin inverse-DFT ConvTranspose pair on the torch path β here the iSTFT
converts cleanly in-graph.
onnx2tf was not used (accuracy hazards on attention models).
Conversion notes that generalize: ONNX exporters value-deduplicate identical
initializers and CSE-merge whole InstanceNorm nodes (two AdaINs normalizing
the same tensor shared one node here) β map norm parameters by graph
connectivity, not by module name. tf.nn.leaky_relu defaults to Ξ±=0.2 while
torch defaults to 0.01. SineGen's rad % 1 matters because unvoiced frames
carry small negative f0. TFLite has no ATAN builtin β atan2 is the
supported route to the harmonic STFT phase.
Text frontend / licensing
The model consumes espeak-ng IPA phoneme IDs (same 178-symbol table as
Kokoro/StyleTTS2). The pip package phonemizes with espeak-ng (GPL-3.0) β
fine as a separate process, or swap in an Apache-licensed neural G2P such as
litert-community/Kokoro-G2P-en-US
(same symbol table) for a GPL-free stack.
This repo also carries a text frontend that needs no espeak-ng, in g2p/: text
to phoneme IDs on the host. A pronunciation dictionary gives the phonemes of
each word it has; a G2P graph converts the rest. The g2p/ files other than
symbols.json are byte-identical copies of the same-named files in
litert-community/Matcha-TTS
at commit 8d650e79, whose card gives their licenses: the dictionary is from
OpenPhonemizer (BSD-3-Clause-Clear)
and the graph is DeepPhonemizer (MIT).
The frontend's phoneme IDs are not espeak-ng's: for the three sentences of
bench_inputs.npz, they differ from the espeak-ng IDs stored there (0/3 equal),
so the audio from this path is not sample-identical to say.py's. The dictionary
keeps each word's own stress, where espeak-ng weakens function words inside a
sentence ("how can" is hΛaΚ kΛΓ¦n here, hΛaΚ kΓ¦n from espeak-ng).
Model weights: Apache-2.0 (KittenML).
Deterministic SineGen: the random initial phase / noise of the reference are
fixed to zero (the reference itself produces a different waveform every run;
zeroing selects one deterministic sample).