Add speech-LM text-to-speech sample (Qwen3-TTS, Compiled Model host loop)#216
Merged
Merged
Conversation
…oop) Runs Qwen3-TTS-12Hz-0.6B-Base fully on device as three LiteRT graphs (talker LM, MTP code predictor, codec decoder) orchestrated by a host-side Python loop, with x-vector voice cloning in 10 languages. - python/: pipeline library + CLI (models auto-download from litert-community/Qwen3-TTS-12Hz-0.6B-Base) - conversion/: checkpoint synthesis, graph exports, host-table extraction, voice enrollment; every graph verified vs the PyTorch reference (correlation 1.0; end-to-end token-for-token under greedy)
Kotlin port of the Python pipeline: byte-level BPE tokenizer with a startup self-test against reference token ids (passes on device incl Japanese + U+3000), mmapped fp16 embedding tables, signature-based talker prefill/decode with ping-pong KV buffers (no per-step cache copies), 17-invoke MTP inner loop, chunked codec decode, AudioTrack playback. install_to_device.sh downloads from Hugging Face and pushes to the app filesDir. Device-verified on Pixel 8a: 4.16s of audio in 28.0s (RTF 6.7; prefill 585ms, talker 3.1s, mtp 19.2s, codec 5.1s).
- Kotlin: import TensorBuffer/Normalizer/JSONArray instead of qualified names, Arrays.fill -> fill(), wrap >100-col lines (Google Kotlin style) - Python: wrap the one >80-col line (Google Python style) - UI: NoActionBar theme (the action-bar title overlaid the content under edge-to-edge) + explicit vertical margins between views; layout re-verified on Pixel 8a
Same migration as ppocrv5-sample acfbb38 (review feedback on google-ai-edge#165): the desktop parity/verify runs (export_codec, export_mtp, verify_talker) and the qwen3_tts_pipeline.py host decode loop now go through ai_edge_litert.compiled_model.CompiledModel. The talker/MTP/codec signatures run via run_by_name with named tensor buffers created once and rewritten per step (KV-cache feedback, int32 token ids and per-step invoke semantics unchanged; XNNPACK thread counts preserved via CpuOptions), and the single-shot parity runs use run_by_index. No behavior change; output prints preserved; greedy end-to-end waveform is bit-identical to the Interpreter-based pipeline.
…cense header wording, copyright 2026
The codec decoder runs on fixed-T chunks with left context. The loop advanced by CODEC_CHUNK new frames but the window it feeds is (new frames + ctx), which exceeds the graph's T (and the input buffer) once ctx > 0 — so any utterance longer than one chunk (~5 s) overran the buffer. Advance by CODEC_CHUNK - ctx new frames instead, so the window always fits. Same fix in the Kotlin and Python host loops.
Wrap every line in the conversion scripts to the 80-column limit of the Google Python Style Guide and add Args/Returns sections to the module-level function docstrings. No behavior changes.
smilingday
self-requested a review
July 11, 2026 02:04
smilingday
approved these changes
Jul 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A text-to-speech sample for the speech-LM model family: Qwen3-TTS-12Hz-0.6B-Base (Apache-2.0, 10 languages, 3-second voice cloning) running fully on device as three LiteRT graphs orchestrated by a host-side loop with the Compiled Model API. It complements the Matcha-TTS sample (#162): Matcha covers the compact acoustic-model family, this covers the modern codec-LM family (talker LM → 12.5 Hz × 16-codebook audio tokens → neural codec decoder → 24 kHz PCM).
compiled_model_api/text_to_speech_lm/python/— pipeline library + CLI. Models auto-download from litert-community/Qwen3-TTS-12Hz-0.6B-Base (~1.4 GB default int4 configuration).kotlin_cpu/android/— Android app (Kotlin, Compiled Model API on CPU): byte-level BPE tokenizer with a startup self-test against reference token ids, memory-mapped fp16 embedding tables, signature-based talker prefill/decode with ping-pong KV-cache buffers (no per-step cache copies), the 17-invoke MTP inner loop, chunked codec decode, AudioTrack playback.install_to_device.shfetches the models from Hugging Face and pushes them to the app's filesDir.conversion/— full conversion pipeline: talker checkpoint synthesis (the stock litert-torch LLM exporter applies unchanged), MTP decode-step graph authoring, codec decoder export, host-table extraction, and voice enrollment.Why a host-side loop
Qwen3-TTS's decode loop cannot be expressed by the LiteRT-LM Engine today: each step aggregates 16 codebook embeddings plus streamed per-frame text conditioning, needs the hidden state alongside logits (it seeds a 15-step inner AR loop over a second transformer with per-step lm_heads), and outputs audio rather than text. The host loop in this sample is a reference for what an Engine-level audio-LM session would absorb.
Verification
Performance
Pixel 8a end to end: 4.16 s of audio in 28.0 s. The MTP inner loop is the known optimization target (single unrolled graph + calibrated low-bit weights).