Instructions to use litert-community/Ming-3.0-tts-0.5B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- LiteRT
How to use litert-community/Ming-3.0-tts-0.5B with LiteRT:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
Ming-3.0-tts-0.5B on LiteRT
Type text, and the phone speaks it in a 44.1 kHz voice generated on the device, with no network. LiteRT graphs converted with litert-torch run Ming-3.0-tts-0.5B through the CompiledModel API. On a Galaxy S26 with LiteRT 2.2.0, the language model and the aggregator run on the GPU, the flow head on the Qualcomm NPU and the decoder on the GPU: an 8.9-second English sentence takes 9.6 s (0.93ร real time), and the first audio is generated 1.87 s after the text goes in. Measured 2026-10-27. A switch in the sample app runs every graph on the NPU instead: 1.35ร real time and the first audio generated 1.10 s after the text goes in, at lower precision (faster placement).
Demo with sound: on a Galaxy S26 in airplane mode, the sample app reads a 57-word story aloud in the faster placement. The app times it from the start of generation, just after the tap: the first audio chunk after 1.31 s, 17.0 s of speech generated in 14.7 s. Unlike the 8.9-second sentence above, the text is longer and a screen recorder was running.
| PyTorch reference (Mac CPU) | Galaxy S26 (this package) |
|---|---|
How it works
Ming-3.0-tts makes speech one 64-number latent at a time; each latent is 80 ms of audio. For every latent, the language model (Qwen2.5-0.5B, 24 layers) takes one input row and returns a hidden state. The flow head turns that state, the previous latent and Gaussian noise into the next latent, in 10 solver steps with classifier-free guidance. The aggregator turns the latent into the next input row. A stop head on the hidden state ends the sentence. The decoder, a 24-layer causal transformer, turns 16 latents per call into an ISTFT spectrum, and the host turns that into 44.1 kHz audio.
| Graph | Signatures | Default placement (Galaxy S26) | Faster placement |
|---|---|---|---|
lm2_wfp16.tflite |
prefill (64 prompt rows), step (one row); the KV cache is an input and an output |
GPU, FP32 precision | not used |
lm2_epsdiv_prefill_wfp16.tflite, lm2_epsdiv_step_wfp16.tflite |
prefill, step: the same signatures, one per file, with the normalization change described under Faster placement |
not used | NPU |
flow_r4b_s10_wfp16.tflite |
flow (10 solver steps in one call) |
NPU (compiled on the phone) | NPU |
agg_r4b_wfp16.tflite |
agg |
GPU, FP32 precision | NPU |
dec_c16_wfp16.tflite |
dec (16 latents, a 64-position cache per layer in and out) |
GPU, default precision | NPU |
Weights are stored as float16; inputs and outputs are float32. The host keeps the tokenizer, the prompt, the embedding lookup, the RoPE tables, the attention masks, both KV caches, the stop decision, the noise and the ISTFT. contract.json lists every input and output and the host steps in order.
Speed on phones
| Phone | Where each graph runs | Real-time factor (audio รท generation time) | First audio | One 80 ms frame | Decoder call (16 latents) | Playback underruns |
|---|---|---|---|---|---|---|
| Galaxy S26 (Snapdragon SM8850, 12 GB) | LM and aggregator: GPU FP32 ยท flow: NPU ยท decoder: GPU | 0.93 (0.928 / 0.931 / 0.917) | 1.87 s | 78 ms (LM 31 + flow 37 + aggregator 10) | 111 ms | 6/6/6 |
| Pixel 8a (Tensor G3, Mali-G715, 8 GB) | no Qualcomm NPU: the flow moves to the GPU | 0.31 (0.312 / 0.307 / 0.308) | 5.2 s | 244 ms (94 + 127 + 23) | 264 ms | 7/7/7 |
Each row is the sample app's own loop with playback, on the same English sentence (8.9 s of audio on the S26, 8.8 s on the Pixel 8a, whose run stopped one latent earlier): one warm-up, then 3 runs, median (the 3 values in parentheses). On the S26 the runs were at thermal status 1 (on Android's scale 0 is none and 1 is light throttling) with the GPU clock capped at 902 MHz (the maximum clock the kernel reported). The prompt prefill took 386โ396 ms of the first-audio time.
The phone generates a little slower than it plays, so the stream waits for audio a few times per sentence (the underruns column, per run). The first launch compiles the flow graph for the NPU, from the same file the GPU uses: 49โ63 s over four first launches; compiled alone, it peaked at 1.6 GB of memory (memory figures here are GiB). The compiled form is then cached (213 MB) and later launches load it in 0.35 s. In the speed run the GPU graphs loaded in 5.3 s (language model), 1.5 s (aggregator) and 4.4 s (decoder), and the app's peak memory (VmHWM) was 4.6 GB.
Output checks
Two checks: each graph alone on the S26, given recorded inputs, against the float32 export of the same graph run on the Mac CPU (the files here are that export with the weights cast to float16); then whole sentences against the upstream PyTorch code.
Every node of the GPU graphs ran on the GPU (LM prefill 2060/2060, LM step 1841/1841, aggregator 319/319, decoder 2054/2054); the flow ran on the NPU as one partition (2834 of 2834 operations).
| Graph | Galaxy S26 | Against the float32 export (Mac CPU) | Pass |
|---|---|---|---|
| LM prefill (64 rows) | GPU FP32 | hidden state max|ฮ| 1.46e-03 (limit 0.357), corr 1.000000 | yes |
| LM step | GPU FP32 | hidden state max|ฮ| 1.9e-05 (limit 0.029), stop decision the same | yes |
| flow (10 steps) | NPU | latent corr 0.9999976 (limit 0.99) | yes |
| aggregator | GPU FP32 | max|ฮ| 1.1e-05 (limit 0.035), corr 1.000000 | yes |
| decoder (16 latents) | GPU | waveform corr after the host ISTFT 0.99796 (limit 0.99) | yes |
The language model and aggregator rows pass when max|ฮ| โค max(0.01, 0.002 ร the largest absolute value) and corr โฅ 0.999; the flow and decoder rows use the correlation limits shown.
Whole sentences on the S26, through the app's loop with the same noise as PyTorch: 10 of 10 sentences stay within both limits. When the phone's sentence has the same length as PyTorch's, voiced frames are compared one by one; otherwise each output's own voiced frames are averaged. HNR is the harmonics-to-noise ratio of voiced frames, and the band column is the energy at 4โ12 kHz relative to 0โ4 kHz. Speaker cosine uses CAM++ embeddings. Transcripts come from openai/whisper-large-v3; the error in parentheses is PyTorch's own on the same sentence.
| Sentence | Language ยท voice | Compared | HNR change (limit ยฑ1 dB) | 4โ12 kHz energy change (limit ยฑ1.5 dB) | Speaker cosine to PyTorch | Whisper error (PyTorch's) |
|---|---|---|---|---|---|---|
| E1 | English ยท US_Emma | voiced frames | -0.23 dB | +0.28 dB | 0.985 | WER 0.00 (0.00) |
| E2 | English ยท US_James | frame by frame | +0.62 dB | +1.28 dB | 0.983 | WER 0.00 (0.00) |
| E3 | English ยท US_Emma | voiced frames | +0.51 dB | -0.42 dB | 0.996 | WER 0.10 (0.10) |
| E4 | English ยท UK_Charlotte | voiced frames | +0.43 dB | -0.23 dB | 0.985 | WER 0.00 (0.00) |
| E5 | English ยท US_Emma | voiced frames | -0.04 dB | -1.21 dB | 0.986 | WER 0.00 (0.00) |
| E5L | English ยท upstream preset voice (not included) | frame by frame | -0.47 dB | -0.61 dB | 0.989 | WER 0.00 (0.00) |
| J1 | Japanese ยท upstream preset voice (not included) | frame by frame | -0.34 dB | +0.13 dB | 0.991 | CER 0.00 (0.00) |
| J2 | Japanese ยท US_Emma | frame by frame | -0.42 dB | +0.32 dB | 0.992 | CER 0.03 (0.03) |
| Z1 | Chinese ยท upstream preset voice (not included) | frame by frame | -0.77 dB | -0.47 dB | 0.978 | CER 0.05 (0.05) |
| Z2 | Chinese ยท ็ตๅฐ่ | frame by frame | -0.01 dB | -0.53 dB | 0.996 | CER 0.00 (0.00) |
The three preset-voice rows (E5L, J1, Z1) use the upstream repository's lingguang preset: its prompt latents and speaker embedding were computed once on the Mac with the upstream audio encoder and speaker model and given to the phone as prompt rows. The app does not offer that voice, and its file is not included.
On a Pixel 8a (flow on the GPU), 2 of 2 sentences (E2, Z2) stay within the limits: worst HNR change -0.74 dB, band -0.96 dB, speaker cosine at least 0.958.
Faster placement: every graph on the NPU
On phones where the app can use the Qualcomm NPU, the switch "Faster: run everything on the NPU" runs the language model, the flow head, the aggregator and the decoder all on the NPU.
| Placement on the Galaxy S26 | Real-time factor | First audio | One 80 ms frame | Decoder call (16 latents) | Playback underruns |
|---|---|---|---|---|---|
| default: LM and aggregator on the GPU at FP32, flow on the NPU, decoder on the GPU | 0.93 (0.928 / 0.931 / 0.917) | 1.87 s | 78 ms (LM 31 + flow 37 + aggregator 10) | 111 ms | 6/6/6 |
| faster: every graph on the NPU | 1.35 (1.358 / 1.332 / 1.347) | 1.10 s | 58 ms (LM 16 + flow 37 + aggregator 5) | 22 ms | 0/0/0 |
Both rows are the app's own loop with playback on the same Galaxy S26 and the same English sentence, one warm-up and 3 runs (median; the 3 values in parentheses). The faster row's runs were at thermal status 1.
The NPU computes in fp16. Whole sentences stay within both limits of the sentence check above: 10 of 10 sentences, worst HNR change -0.83 dB, worst 4โ12 kHz band change +1.22 dB, speaker cosine at least 0.966, Whisper error no worse than PyTorch's on 10 of 10. On the NPU the flow head stays (latent corr 0.9999976) and one 16-latent decoder call stays (waveform corr 0.99994) within the per-graph limits; two graphs miss theirs:
- the language model: over sentence E5, with the reference inputs fed to each step call, its hidden state reaches 8.0ร the limit (correlation at least 0.99986; the stop decisions agree on 110 of 110 steps); one step call is at 2.5ร and one prefill call at 2.8ร;
- the aggregator: 100 of its 109 calls on sentence E5 (one per latent) are within the limit, the worst at 1.39ร.
At fp16 on the Mac's GPU (Metal, default precision), over the 10 sentences, the language model (up to 24ร its limit) and the aggregator (up to 2.48ร) miss their limits as well.
On the NPU the language model is two files, lm2_epsdiv_prefill_wfp16.tflite and lm2_epsdiv_step_wfp16.tflite: the two signatures of lm2_wfp16.tflite, one per file (an NPU compile on the phone keeps one copy of the weights per signature), with one change in the normalization that is the same in real arithmetic (the NPU's fp16 divide returns 0 or NaN for divisors above 2^15, so eps / (m * m) is computed as (eps / m) / m). The first switch compiles the four graphs the default placement does not run on the NPU, one at a time (63 s on the S26: lm2_epsdiv_prefill_wfp16.tflite 22.0 s, lm2_epsdiv_step_wfp16.tflite 15.0 s, agg_r4b_wfp16.tflite 3.4 s, dec_c16_wfp16.tflite 22.8 s); after that, a launch in this placement loads the five graphs from the compiled cache in 5.5โ7.0 s on the S26 (the app first opens each graph alone, 3.2โ3.9 s, then loads all five, 2.2โ3.1 s). Phones without a Qualcomm NPU, and APKs built without Qualcomm's libraries, do not show the switch.
Minimal usage: Python
This block reads the files from a local copy of this repository and writes speech.wav, on the computer's CPU. In a new Python 3.12.11 environment set up with its first line, it made 8.7 s of audio (109 latents) in 9.7 s on an Apple M4 Max, loading included. With PyTorch's noise stream in place of rng.standard_normal(64), it stops after the same 110 latents as PyTorch, and its waveform correlates 0.9994 with PyTorch's. Whisper transcribed its own output with word error 0.00.
# pip install ai-edge-litert==2.2.0 numpy tokenizers huggingface_hub
# hf download litert-community/Ming-3.0-tts-0.5B --local-dir Ming-3.0-tts-0.5B
import json
import wave
import numpy as np
from ai_edge_litert.compiled_model import CompiledModel, CpuOptions, HardwareAccelerator, Options
from tokenizers import Tokenizer
D = "Ming-3.0-tts-0.5B/"
TEXT = ("When the small bakery on the corner opened its doors, the smell of fresh bread drifted "
"down the whole street, and a line formed before the sun was fully up.")
VOICE, LANGUAGE = "US_Emma", "English" # built-in voices need no voice file
C = json.load(open(D + "contract.json"))
class Graph:
"""One .tflite on the CPU. call(signature, *inputs): args_0, args_1, ... -> [output_0, ...]."""
def __init__(self, name):
self.sigs = C["graphs"][name]["signatures"]
self.m = CompiledModel.from_file(D + name, options=Options(
hardware_accelerators=HardwareAccelerator.CPU, cpu_options=CpuOptions(num_threads=4)))
self.io = {}
def call(self, sig, *xs):
shapes = {o["name"]: o["shape"] for o in self.sigs[sig]["outputs"]}
if sig not in self.io:
self.io[sig] = ({f"args_{i}": self.m.create_input_buffer_by_name(sig, f"args_{i}") for i in range(len(xs))},
{n: self.m.create_output_buffer_by_name(sig, n) for n in shapes})
ins, outs = self.io[sig]
for i, x in enumerate(xs):
ins[f"args_{i}"].write(np.ascontiguousarray(x, np.float32).ravel())
self.m.run_by_name(sig, ins, outs)
return [np.array(outs[f"output_{j}"].read(int(np.prod(shapes[f"output_{j}"])), np.float32))
.reshape(shapes[f"output_{j}"]) for j in range(len(shapes))]
lm, flow, agg, dec = (Graph(f) for f in ("lm2_wfp16.tflite", "flow_r4b_s10_wfp16.tflite",
"agg_r4b_wfp16.tflite", "dec_c16_wfp16.tflite"))
# 1. prompt: the upstream TTS prompt with the built-in voice's instruction (scripts/host_mirror.py)
tok = Tokenizer.from_file(D + "tokenizer.json")
enc = lambda s: tok.encode(s, add_special_tokens=False).ids # noqa: E731
instruction = json.dumps({"audio_sequence": [{
"ๅบๅท": 1, "่ฏด่ฏไบบ": "speaker_1", "ๆน่จ": None, "้ฃๆ ผ": None, "่ฏญ้": None, "ๅบ้ข": None, "้ณ้": None,
"ๆ
ๆ": None, "BGM": {"Genre": None, "Mood": None, "Instrument": None, "Theme": None, "ENV": None, "SNR": None},
"IP": VOICE}]}, ensure_ascii=False)
ids = (enc("<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n") + enc("<|im_start|>user\n")
+ enc(f"Please generate speech based on the following description.\nTarget language: {LANGUAGE}.\n")
+ enc(" speaker_1:") + enc("<|vision_start|>") + enc("<|vision_pad|>") + enc("<|vision_end|>\n")
+ enc(" Text input:\n") + enc(TEXT) + enc("<|im_end|>\n") + enc("<|im_start|>assistant\n")
+ enc(instruction) + enc("<|im_end|>") + enc("<audio>"))
rows = np.memmap(D + "embed_tokens_f16.bin", np.float16, mode="r").reshape(-1, 896)[ids].astype(np.float32)
rows[ids.index(C["constants"]["token_ids"]["<|vision_start|>"]) + 1] = 0 # speaker row of a built-in voice
# 2. RoPE tables and additive masks (the KV cache is a graph input: 511 past slots for the LM)
inv = np.array(C["constants"]["qwen_inv_freq"], np.float32)
def rope(start, n):
a = (np.arange(start, start + n, dtype=np.float32)[:, None] * inv).astype(np.float64)
a = np.concatenate([a, a], 1)
return np.cos(a).astype(np.float32), np.sin(a).astype(np.float32)
def lm_mask(start, n):
m = np.full((n, 511 + n), -30000, np.float32)
m[:, :start] = 0
for i in range(n):
m[i, 511:512 + i] = 0
return m
def dec_mask(p0): # 64 new positions see the last 64 positions (sliding window), cached or new
q = p0 + np.arange(64)[:, None]
j = np.concatenate([p0 - 64 + np.arange(64), p0 + np.arange(64)])[None]
return np.where((j >= 0) & (j > q - 64) & (j <= q), 0, -30000).astype(np.float32)
# 3. prefill the prompt in chunks of 64 rows
L = len(ids)
pk, pv = np.zeros((1, 48, 511, 64), np.float32), np.zeros((1, 48, 511, 64), np.float32)
for s in range(0, L - 1, 64):
n = min(64, L - 1 - s)
x = np.zeros((64, 896), np.float32)
x[:n] = rows[s:s + n]
_, k, v = lm.call("prefill", x, *rope(s, 64), lm_mask(s, 64), pk, pv)
pk[:, :, s:s + n], pv[:, :, s:s + n] = k[:, :, :n], v[:, :, :n]
# 4. one 80 ms latent per frame: LM step -> stop head (host) -> 10-step flow -> aggregator
w = np.fromfile(D + "stop_head.bin", np.float32)
stop_w, stop_b = w[:2 * 896].reshape(2, 896), w[2 * 896:]
temb = np.fromfile(D + "temb_epss10.bin", np.float32)
rng = np.random.default_rng(0)
x, hist, lat = rows[L - 1], np.zeros(64, np.float32), []
for step in range(512 - L):
pos = L - 1 + step
z, k, v = lm.call("step", x, *rope(pos, 1), lm_mask(pos, 1), pk, pv)
pk[:, :, pos], pv[:, :, pos] = k[:, :, 0], v[:, :, 0]
lat.append(flow.call("flow", z, hist, rng.standard_normal(64), temb)[0].reshape(64))
logit = stop_w @ z.reshape(896) + stop_b
if 1 / (1 + np.exp(logit[0] - logit[1])) > 0.5 and step > 3: # softmax(logit)[1] > 0.5
break
x, hist = agg.call("agg", lat[-1])[0], lat[-1]
# 5. decoder, 16 latents per call with a 64-position KV cache per layer, then the ISTFT on the host
S, lat = len(lat), np.stack(lat)
dk, dv = np.zeros((1, 48, 64, 64), np.float32), np.zeros((1, 48, 64, 64), np.float32)
head = []
for a in range(0, S, 16):
window = lat[[max(a - 1, 0)] + [min(i, S - 1) for i in range(a, a + 17)]] # 1 latent each side
h, dk, dv = dec.call("dec", window, *rope(4 * a, 64), dec_mask(4 * a), dk, dv)
head.append(h[0, :4 * min(16, S - a)])
h = np.concatenate(head)
win = np.fromfile(D + "istft_window.bin", np.float32)
mag, ph = np.minimum(np.exp(h[:, :1765]), np.float32(100)), h[:, 1765:]
spec = (mag * np.cos(ph)).astype(np.float64) + 1j * (mag * np.sin(ph)).astype(np.float64)
frames = np.fft.irfft(spec, n=3528, axis=1).astype(np.float32) * win
y, env = np.zeros((len(frames) - 1) * 882 + 3528, np.float32), np.zeros((len(frames) - 1) * 882 + 3528, np.float32)
for f, fr in enumerate(frames):
y[f * 882:f * 882 + 3528] += fr
env[f * 882:f * 882 + 3528] += win * win
wav = y[1323:-1323] / env[1323:-1323]
with wave.open("speech.wav", "wb") as out:
out.setnchannels(1)
out.setsampwidth(2)
out.setframerate(44100)
out.writeframes((np.clip(wav, -1, 1) * 32767).astype("<i2").tobytes())
print(f"{S} frames, {len(wav) / 44100:.2f} s of 44.1 kHz audio -> speech.wav")
Minimal usage: Kotlin (Android)
MingGraphs places the default placement's four graphs as measured on the S26. Without a Qualcomm NPU, or in an APK built without the Qualcomm libraries, the flow loads on the GPU instead. It is android/app/src/main/java/com/ming3tts/MingGraphs.kt. The sample's MingEngine.kt is the host loop the Python block also implements (prompt, prefill, one frame per latent, the decoder, the ISTFT); the app calls its graphs through Graph.kt, and its self-test mode snippet runs the same loop through this class's run(). In that mode on the Galaxy S26, this class ran sentences E2 and Z2. With the NPU, the latents and the audio were identical to the app's own graph code on the same sentences, and both sentences stayed within the limits above. From an APK built without the Qualcomm libraries the flow loaded on the GPU, with the same latents as the app on the GPU, and both sentences stayed within the limits. With the model files missing it stopped with "โฆ/flow_r4b_s10_wfp16.tflite is missing: copy the model files with scripts/install_to_device.sh".
import android.content.Context
import android.os.Build
import com.google.ai.edge.litert.Accelerator
import com.google.ai.edge.litert.CompiledModel
import com.google.ai.edge.litert.Environment
import com.google.ai.edge.litert.TensorBuffer
import java.io.File
import java.util.concurrent.ConcurrentHashMap
/**
* The four Ming-3.0-tts graphs placed as measured on a Galaxy S26: the LM and the aggregator on the
* GPU at FP32, the flow on the Qualcomm NPU (on the GPU when the phone or the APK has no NPU), the
* decoder on the GPU. [run] calls one signature: inputs args_0, args_1, ... in export order, outputs
* output_0, output_1, ... (names and shapes in contract.json).
*/
class MingGraphs(context: Context, private val dir: File) : AutoCloseable {
private val libs = context.applicationInfo.nativeLibraryDir
private val soc = if (Build.VERSION.SDK_INT >= 31) Build.SOC_MANUFACTURER.lowercase() else ""
private val env = if (("qti" in soc || "qualcomm" in soc) && File(libs, "libLiteRtDispatch_Qualcomm.so").exists()) {
Environment.create(
context,
mapOf(
Environment.Option.DispatchLibraryDir to libs,
Environment.Option.CompilerPluginLibraryDir to libs,
),
)
} else {
Environment.create()
}
val npu = Accelerator.NPU in env.getAvailableAccelerators()
private fun load(file: String, accelerator: Accelerator, fp32: Boolean = false): CompiledModel {
val path = File(dir, file)
require(path.isFile) { "$path is missing: copy the model files with scripts/install_to_device.sh" }
val options = CompiledModel.Options(accelerator)
if (accelerator == Accelerator.GPU) {
options.gpuOptions = CompiledModel.GpuOptions(
precision = if (fp32) CompiledModel.GpuOptions.Precision.FP32 else null,
constantTensorSharing = true, // the LM's prefill and step signatures share one copy of the weights
)
}
if (npu) {
options.qualcommOptions = CompiledModel.QualcommOptions(
htpPerformanceMode = CompiledModel.QualcommOptions.HtpPerformanceMode.BURST,
)
}
return CompiledModel.create(path.absolutePath, options, env)
}
// The NPU graph loads first: its first load compiles it for this phone (about a minute, cached after).
val flow = load("flow_r4b_s10_wfp16.tflite", if (npu) Accelerator.NPU else Accelerator.GPU)
val lm = load("lm2_wfp16.tflite", Accelerator.GPU, fp32 = true)
val agg = load("agg_r4b_wfp16.tflite", Accelerator.GPU, fp32 = true)
val dec = load("dec_c16_wfp16.tflite", Accelerator.GPU)
private class Io(val ins: Map<String, TensorBuffer>, val outs: Map<String, TensorBuffer>)
private val io = ConcurrentHashMap<Pair<CompiledModel, String>, Io>() // the decoder may run on its own thread
fun run(model: CompiledModel, signature: String, vararg inputs: FloatArray): List<FloatArray> {
val b = io.getOrPut(model to signature) {
Io(
inputs.indices.associate { "args_$it" to model.createInputBuffer("args_$it", signature) },
(0 until OUTPUTS.getValue(signature)).associate { "output_$it" to model.createOutputBuffer("output_$it", signature) },
)
}
inputs.forEachIndexed { i, x -> b.ins.getValue("args_$i").writeFloat(x) }
model.run(b.ins, b.outs, signature)
return List(b.outs.size) { b.outs.getValue("output_$it").readFloat() }
}
override fun close() {
io.values.forEach { b -> (b.ins.values + b.outs.values).forEach { it.close() } }
listOf(flow, lm, agg, dec).forEach { it.close() }
env.close()
}
companion object {
val OUTPUTS = mapOf("prefill" to 3, "step" to 3, "flow" to 1, "agg" to 1, "dec" to 3)
}
}
Android sample
android/ holds the sample app: type text, pick one of 7 languages and a built-in voice, tap Speak. Audio plays while the decoder is still working. Where the app can use the Qualcomm NPU, the switch "Faster: run everything on the NPU" selects the faster placement, and the app remembers it. android/README.md has the build steps.
The screenshot shows the app's own example sentence (7.5 s of audio in 10.7 s, with the GPU clock capped at 461 MHz at the time), so its status line has that run's numbers, not the speed table's.
The NPU needs Qualcomm's libraries inside the APK (not included; android/README.md lists them); without them the app runs everything on the GPU and says so. Copy the model files with scripts/install_to_device.sh after installing the debug build.
Limits
- Text goes in as typed. No text normalization runs, so the model reads digits, dates and abbreviations by itself. Sentence E3 is "Dr. Mira Calloway will present 3 new results at Harbor Hall on March 14, 2027, at 9:30 a.m.". Whisper hears the same words from the phone and from PyTorch: "Dr. Mira Callaway will present three new results at Harbor Hall on March 14, 2027, at 9.30 a.m." (WER 0.10 on both).
- Length per prompt. The language model holds 512 positions: the prompt, then one per 80 ms latent. The example sentence above (E5, 30 words) makes a 178-token prompt, which leaves room for 26.7 s of speech. The app splits longer text into fragments with the upstream splitting rule and speaks them in turn.
- Built-in voices only. Voice cloning needs the audio encoder and the CAM++ speaker model, which are not converted. The preset voice files of the upstream repository are not included.
- One NPU measured. The NPU numbers are from one phone (Galaxy S26, Hexagon v81, QAIRT 2.47.0). Phones without a Qualcomm NPU run the flow on the GPU (Pixel 8a: 0.31ร real time).
- Languages checked. The app and the upstream model also offer Korean, German, French and Spanish; the sentence checks above cover English, Japanese and Chinese only.
Notes on placement
- On the GPU at FP32, load
lm2_wfp16.tflitewithconstantTensorSharing = truesoprefillandstepshare one copy of the weights. Without it each signature keeps its own copy. On the S26, an earlier two-signature file that fused this language model with the flow head and the aggregator peaked at 7.0 GB while compiling at FP32, and the low-memory killer closed the app; with sharing the same file compiled at 3.6 GB. - In the default placement the language model stays on the GPU, because at the NPU's fp16 precision its hidden state misses the per-graph limit (faster placement). An NPU compile on the phone keeps one copy of the weights per signature: of 3 compiles of two-signature files with this language model, 2 were closed by the low-memory killer, so the faster placement loads it as one file per signature.
Files
| File | Bytes | What it is |
|---|---|---|
| lm2_wfp16.tflite | 716,899,024 | language model (Qwen2.5-0.5B): prefill and step |
| flow_r4b_s10_wfp16.tflite | 204,500,864 | flow head: 10 solver steps per call |
| agg_r4b_wfp16.tflite | 203,752,128 | aggregator: latent to the next input row |
| dec_c16_wfp16.tflite | 722,909,248 | audio decoder: 16 latents per call |
| lm2_epsdiv_prefill_wfp16.tflite | 716,458,304 | language model prefill, for the faster placement |
| lm2_epsdiv_step_wfp16.tflite | 716,389,696 | language model step, for the faster placement |
| embed_tokens_f16.bin | 272,269,312 | input embedding table [151936, 896], float16 |
| temb_epss10.bin | 40,960 | flow time embeddings [10, 1024], float32 |
| istft_window.bin | 14,112 | ISTFT window [3528], float32 |
| stop_head.bin | 7,176 | stop head weight [2, 896] and bias [2], float32 |
| tokenizer.json | 11,431,389 | tokenizer (the upstream file) |
| tokenizer_vocab.txt | 1,521,126 | tokenizer vocabulary for the Android app |
| tokenizer_merges.txt | 1,671,839 | tokenizer merges for the Android app |
| tokenizer_added.txt | 1,344 | tokenizer added tokens for the Android app |
| char_classes.bin | 13,540 | Unicode classes for the Android tokenizer |
The repository also holds contract.json, LICENSE, NOTICE, REPRODUCE.md, android/, scripts/ and assets/. manifest.json lists every other file with its size and SHA256 (59 files, 3.57 GB). REPRODUCE.md rebuilds the six graphs and the host files from the upstream checkpoint; the rebuilt files have the same SHA256 as the files here.
License and credits
The model is inclusionAI/Ming-3.0-tts-0.5B (MIT), converted from revision 8d66b70a00f05ad183fa8a863b4ce609fc83cbc0 with the inference code of inclusionAI/Ming-3.0-tts at commit 028c1eace193e7acdb256453da89b6e9207e052b. LICENSE is the upstream license, unchanged; NOTICE lists what the conversion changed. Converted with litert-torch 0.9.4 and ai-edge-quantizer 0.9.0; run with LiteRT 2.2.0.
The upstream README asks, in two lines:
- "Voice cloning and built-in character voices are provided for research and legitimate creative use. Do not use them to impersonate a real person without consent, to deceive, or for any illegal purpose."
- "Label synthetic speech as synthetic where it could be mistaken for a real recording."
- Downloads last month
- 259
Model tree for litert-community/Ming-3.0-tts-0.5B
Base model
inclusionAI/Ming-3.0-tts-0.5B
