The MediaPipe Text-to-Speech task lets you synthesize natural-sounding speech audio from text on Android devices using the Kokoro-82M neural TTS model. These instructions show you how to use Text-to-Speech with Android apps written in Kotlin.
For more information about the capabilities, .litertlm model bundles, and
supported voices of this task, see the Overview.
Code example
The MediaPipe Tasks example code is a basic implementation of a Text-to-Speech app for Android. The example synthesizes the text that you enter with a voice that you choose, either as a complete utterance or as a stream of audio chunks, and plays the audio as it arrives.
You can use the app as a starting point for your own Android app, or refer to it when modifying an existing app. The Text-to-Speech example code is hosted on GitHub.
Download the code
The following instructions show you how to create a local copy of the example code using the git command line tool.
To download the example code:
- Clone the git repository using the following command:
git clone https://github.com/google-ai-edge/mediapipe-samples
- Optionally, configure your git instance to use sparse checkout,
so you have only the files for the Text-to-Speech example app:
cd mediapipe-samples git sparse-checkout init --cone git sparse-checkout set examples/text_to_speech/android
After creating a local version of the example code, get the model as described in the example's README. Then import the project into Android Studio and run the app. For instructions, see the Setup guide for Android.
Key components
The following files contain the main code for the Text-to-Speech example app:
- MainActivity.kt - Creates the Text-to-Speech task, lists the available voices, runs unary and streaming synthesis, and implements the user interface.
- TtsAudioPlayer.kt -
Plays the synthesized audio with
AudioTrackand can save it as a WAV file.
Setup
This section describes key steps for setting up your development environment and Android project to use Text-to-Speech. For general information on setting up your development environment for MediaPipe tasks, see the Setup guide for Android.
Dependencies
Add the MediaPipe Audio Tasks dependency to your module's build.gradle.kts
(requires minSdk = 26, arm64-v8a):
dependencies {
implementation("com.google.mediapipe:tasks-audio:latest.release")
}
Model
The MediaPipe Text-to-Speech task requires the
kokoro_int8_gpu.litertlm
model bundle. See the task overview Models section for
details.
Place the downloaded .litertlm file on device storage (or copy it from app
assets into context.filesDir / context.cacheDir) and pass its file system
path to BaseOptions.builder().setModelAssetPath(...).
Create the task
Use TextToSpeech.create(options) to initialize the task with
TextToSpeechOptions. Pass a writable cacheDirectory (such as
context.cacheDir.absolutePath) so unpacked phonetic dictionary data and
XNNPACK weight caches persist across app launches.
import com.google.mediapipe.tasks.audio.texttospeech.TextToSpeech
import com.google.mediapipe.tasks.audio.texttospeech.TextToSpeechOptions
import com.google.mediapipe.tasks.core.BaseOptions
import com.google.mediapipe.tasks.core.Delegate
val options = TextToSpeechOptions(
baseOptions = BaseOptions.builder()
.setModelAssetPath("/data/user/0/com.example.app/files/kokoro_int8_gpu.litertlm")
.setDelegate(Delegate.CPU)
.build(),
defaultVoiceId = "af_heart",
speakingRate = 1.0f,
targetBucket = 64,
cacheDirectory = context.cacheDir.absolutePath,
)
val tts = TextToSpeech.create(options)
Configuration options
This task has the following configuration options for Android apps:
Task options (TextToSpeechOptions)
| Option Name | Description | Value Range | Default Value |
|---|---|---|---|
baseOptions.modelAssetPath |
Path (or URL on Web) to the single .litertlm model bundle containing the Kokoro-82M acoustic and vocoder graphs, phonetic lexicons, and voice embeddings. |
String / Path | Required |
baseOptions.delegate |
Hardware acceleration delegate used by the LiteRT runtime. When set to GPU, the Kokoro acoustic stage executes on CPU (XNNPACK) and the neural vocoder executes on GPU (OpenCL/Metal/WebGPU) with CPU fallback. | Delegate.CPU, Delegate.GPU |
Delegate.CPU |
speakingRate |
Speaking rate multiplier applied to synthesized Kokoro audio (1.0 represents normal speaking speed). |
Float (0.1, 4.0] |
1.0 |
targetBucket |
Target sequence-length bucket for the Kokoro acoustic/vocoder graph (for example, 64, 128, or 256 tokens). Smaller buckets reduce synthesis latency for short sentences; 0 uses the model's default capacity. |
0, 64, 128, 256 |
0 |
defaultVoiceId |
Default Kokoro voice profile identifier (for example, "af_heart", "ef_dora", or "zf_xiaobei"). Can be overridden per utterance using SynthesisOptions. |
String (Optional) | "af_heart" |
defaultLocale |
Optional default BCP 47 language tag (for example, "en-US", "es", "zh-CN"). When omitted, the task automatically infers the language from the selected Kokoro voice prefix. |
BCP 47 String (Optional) | Inferred from voice |
cacheDirectory |
Writable directory path used to persist unpacked phonetic dictionary assets and LiteRT XNNPACK weight caches across runs for faster cold starts. | Directory Path (Optional) | "" |
Per-utterance options (SynthesisOptions)
| Option Name | Description | Default Value |
|---|---|---|
voiceId |
Overrides the voice profile for a single synthesize call (for example, switching from "af_heart" to "ef_dora" in under 1 ms without reloading model weights). |
null / None (uses task default) |
locale |
Optional BCP 47 locale override for a single utterance. Leave unset (null / None) when switching voices so the engine automatically resolves the voice's target language from its prefix. |
null / None (inferred from voiceId) |
Inspect available voices
Call listVoices() on the initialized TextToSpeech instance to enumerate all
Kokoro voices packaged in the .litertlm bundle:
val voices = tts.listVoices()
for (voice in voices) {
println("${voice.voiceId} (${voice.displayName}) - locale=${voice.locale}, ${voice.sampleRateHz} Hz")
}
Run the task
You can run synthesis synchronously on a background thread with
synthesize(text, options) or non-blockingly from a Kotlin Coroutine with
synthesizeAsync(text, options).
Synchronous and coroutine synthesis
// Non-blocking coroutine call (recommended for UI / ViewModel scopes):
val audioResult = tts.synthesizeAsync(
"Hello from MediaPipe Text to Speech on Android! Multi-sentence inputs are chunked automatically."
)
// Or blocking synchronous call on a background worker thread:
val syncResult = tts.synthesize("Hello from a background thread.")
Fast bilingual voice switching
To switch voices or languages on a single utterance, pass SynthesisOptions
with the target voiceId. Leave locale unset (null) so the Kokoro engine
automatically resolves the language from the voice prefix in < 1 ms:
import com.google.mediapipe.tasks.audio.texttospeech.SynthesisOptions
// Speak English with "af_heart", then immediately switch to Spanish with "ef_dora":
val enAudio = tts.synthesizeAsync(
"Good morning! Let's practice Spanish.",
SynthesisOptions(voiceId = "af_heart"),
)
val esAudio = tts.synthesizeAsync(
"¡Buenos días! Practiquemos español.",
SynthesisOptions(voiceId = "ef_dora"),
)
Streaming speech synthesis
For lower Time-To-First-Audio (TTFA) on multi-sentence text, call
synthesizeStream(text, options) to receive a cold Kotlin Flow<AudioChunk>.
Each AudioChunk (samples, sampleRateHz, chunkIndex, isFinal) is
emitted as soon as its clause finishes vocoding, so you can write it directly to
an AudioTrack configured in AudioTrack.MODE_STREAM while subsequent clauses
are synthesized:
import kotlinx.coroutines.flow.collect
tts.synthesizeStream(
"Streaming synthesis plays the first clause immediately. Subsequent clauses synthesize in the background.",
SynthesisOptions(voiceId = "af_heart"),
).collect { chunk ->
// Write chunk.samples to a streaming AudioTrack as each clause arrives:
streamingAudioTrack.write(chunk.samples, 0, chunk.samples.size, AudioTrack.WRITE_BLOCKING)
}
When streaming tokens incrementally from an on-device LLM, open a TtsStream
with openStream(), push text deltas as tokens arrive with pushText(), call
finishInput() at the end of the LLM response, and pull AudioChunks with
readNext():
tts.openStream(options = SynthesisOptions(voiceId = "af_heart")).use { stream ->
stream.pushText("Hello from an ")
stream.pushText("on-device LLM! ")
stream.pushText("Each completed clause streams out.")
stream.finishInput()
while (true) {
val chunk = stream.readNext() ?: break
streamingAudioTrack.write(chunk.samples, 0, chunk.samples.size, AudioTrack.WRITE_BLOCKING)
}
}
Barge-in cancellation
If the user interrupts speech playback ("barge-in"), you can abort an in-flight unary or streaming synthesis call immediately:
- When collecting
synthesizeStream, canceling the collectingJob(or using short-circuit operators such astake(1)orfirst()) automatically cancels and closes the underlyingTtsStream. - When using
synthesizeAsync, canceling the calling KotlinJoborCoroutineScopeautomatically invokestts.cancel(). - When using blocking
synthesizeorTtsStream.readNext(), calltts.cancel()(orstream.cancel()) directly from any thread.cancel()is lock-free and aborts the active Kokoro inference pass within milliseconds.
val job = coroutineScope.launch {
tts.synthesizeStream("A long paragraph being streamed...").collect { chunk ->
streamingAudioTrack.write(chunk.samples, 0, chunk.samples.size, AudioTrack.WRITE_BLOCKING)
}
}
// On user barge-in:
job.cancel() // Automatically cancels the active TtsStream
Handle and play results
synthesize and synthesizeAsync return an AudioResult containing:
samples:FloatArrayof mono PCM samples normalized to[-1.0f, 1.0f]sampleRateHz:Int(24000Hz for Kokoro-82M)durationSeconds:Floatcomputed duration of the generated clip in seconds
You can play the returned AudioResult using Android's AudioTrack in
ENCODING_PCM_FLOAT mode, and close the TextToSpeech instance when finished:
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioTrack
fun playAudioResult(result: AudioResult) {
val byteSize = result.samples.size * Float.SIZE_BYTES
val audioTrack = AudioTrack.Builder()
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build()
)
.setAudioFormat(
AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_FLOAT)
.setSampleRate(result.sampleRateHz)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.build()
)
.setBufferSizeInBytes(byteSize)
.setTransferMode(AudioTrack.MODE_STATIC)
.build()
audioTrack.write(result.samples, 0, result.samples.size, AudioTrack.WRITE_BLOCKING)
audioTrack.play()
}
// Release underlying C++ resources when done (TextToSpeech implements AutoCloseable):
tts.close()