The MediaPipe Text-to-Speech task lets you synthesize natural-sounding speech audio from text in the browser using neural TTS models such as Kokoro-82M. These instructions show you how to use Text-to-Speech for web apps.
You can see this task in action by viewing the
demo.
For more information about the capabilities, .litertlm model bundles, and
supported voices of this task, see the Overview.
Code example
The example code for Text-to-Speech provides a complete implementation of this task in TypeScript for your reference. The example runs the task in a web worker, lists the available voices, synthesizes and plays speech, streams speech clause by clause, highlights words as they are spoken, and saves the audio as a WAV file. You can view, run, and edit the Text-to-Speech example using just your web browser. You can view the source code for this example on GitHub.
Setup
This section describes key steps for setting up your development environment and code projects specifically to use Text-to-Speech. For general information on setting up your development environment for using MediaPipe tasks, including platform version requirements, see the Setup guide for web.
JavaScript packages
Text-to-Speech code is available through the MediaPipe @mediapipe/tasks-audio
NPM package. You can
install the package with the following command:
npm install @mediapipe/tasks-audio
If you want to import the task code through a content delivery network (CDN) service, load the task's Wasm files from the CDN as shown in Create the task.
Model
The MediaPipe Text-to-Speech task requires a .litertlm model bundle. This guide uses
kokoro_int8_gpu_fp16.litertlm.
See the task overview Models section for the other supported
models.
The task downloads the model from the URL that you pass in the options. The
model bundles are large, so consider caching them in the browser, for example
with the Cache Storage API,
and passing the cached model contents to the task as
baseOptions.modelAssetBuffer. The example code shows one way to do this.
Create the task
Use the createFromOptions() function to create the task with
TextToSpeechOptions. For more information on configuration options, see
Configuration options.
import {FilesetResolver, TextToSpeech} from '@mediapipe/tasks-audio';
const audio = await FilesetResolver.forAudioTasks(
'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-audio@latest/wasm');
const tts = await TextToSpeech.createFromOptions(audio, {
baseOptions: {
modelAssetPath:
'https://huggingface.co/litert-community/Kokoro-82M/resolve/main/kokoro_int8_gpu_fp16.litertlm',
delegate: 'GPU',
},
defaultVoiceId: 'af_heart',
speakingRate: 1.0,
cacheDirectory: ':nocache',
});
The GPU delegate uses WebGPU.
If the browser doesn't support WebGPU, or the device has no suitable GPU,
createFromOptions() rejects, and you can create the task with
delegate: 'CPU' instead.
To use PocketTTS or Phonon, pass that model's bundle URL and set
defaultVoiceId to one of its voices (or leave it unset). Both models run on
CPU, so they ignore the delegate setting.
Loading a model and synthesizing speech are compute-intensive and can block the page's main thread. To keep your user interface responsive, run the task in a web worker and send the audio to the main thread for playback, as shown in the example code.
Configuration options
This task has the following configuration options for web applications:
Task options (TextToSpeechOptions)
| Option Name | Description | Value Range | Default Value |
|---|---|---|---|
baseOptions.modelAssetPath |
Path (or URL on Web) to a supported .litertlm model bundle (see Models). Keep the original filename, which the task uses to detect the model type. |
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. PocketTTS and Phonon always run on CPU. | "CPU", "GPU" |
"CPU" |
speakingRate |
Speaking rate multiplier applied to synthesized 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. PocketTTS and Phonon ignore this option. |
0, 64, 128, 256 |
0 |
defaultVoiceId |
Default voice ID (for example, "af_heart" for Kokoro-82M, "alba" for PocketTTS, or "freya" for Phonon). Can be overridden per utterance using SynthesisOptions. |
String (Optional) | The model's default voice |
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 voice. |
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. Used by Kokoro-82M. On the web, set it to ":nocache", because the Wasm virtual file system can't hold the weight cache. |
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 the voice ID. |
null / None (inferred from voiceId) |
On the web, you pass the per-utterance options as a SynthesisOptions object
to synthesize() and synthesizeStream().
Inspect available voices
Call listVoices() on the initialized task to enumerate all voices packaged
in the loaded .litertlm bundle:
for (const voice of await tts.listVoices()) {
console.log(`${voice.voiceId} (${voice.displayName}) - ` +
`locale=${voice.locale}, ${voice.sampleRateHz} Hz`);
}
Run the task
Synthesize speech
Call synthesize() to convert a piece of text into speech in a single call.
Multi-sentence text is split into clauses automatically.
const result = await tts.synthesize(
'Hello from MediaPipe Text to Speech! Multi-sentence inputs are chunked ' +
'automatically.');
Switch voices
To switch voices or languages on a single utterance, pass the target voiceId.
Leave locale unset so the task resolves the language from the voice:
// Speak English with "af_heart", then switch to Spanish with "ef_dora":
const enResult = await tts.synthesize(
"Good morning! Let's practice Spanish.", {voiceId: 'af_heart'});
const esResult = await tts.synthesize(
'¡Buenos días! Practiquemos español.', {voiceId: 'ef_dora'});
Streaming speech synthesis
For a lower time to first audio on multi-sentence text, call
synthesizeStream(). It returns a TextToSpeechStream that yields an audio
chunk for each clause as soon as it's ready, so you can start playing the first
clause while the following clauses are synthesized:
const stream = await tts.synthesizeStream(
'Streaming synthesis plays the first clause immediately. ' +
'Subsequent clauses synthesize in the background.');
try {
for await (const chunk of stream) {
// Play chunk.samples as each clause arrives.
play(chunk.samples, chunk.sampleRateHz);
}
} finally {
stream.close();
}
When streaming tokens incrementally from an LLM, open the stream with an empty
initial text and finishInput set to false. Push text as tokens arrive with
pushText(), and call finishInput() at the end of the response. Read the
audio chunks from the stream while you push text:
const stream = await tts.synthesizeStream('', {voiceId: 'af_heart'},
/* finishInput= */ false);
async function pushLlmResponse() {
for await (const token of llmTokens) {
await stream.pushText(token);
}
await stream.finishInput();
}
const pushing = pushLlmResponse();
for await (const chunk of stream) {
play(chunk.samples, chunk.sampleRateHz);
}
await pushing;
stream.close();
Barge-in cancellation
If the user interrupts speech playback ("barge-in"), call tts.cancel() (or
stream.cancel()) to abort the active streaming synthesis. The next read from
the stream rejects, and you can keep using the task afterwards.
The task runs on the thread that calls it, and it synthesizes each clause when
you read it from the stream. A cancel request therefore takes effect between
clauses. A synthesize() call runs to completion, so use
synthesizeStream() for speech that the user can interrupt. When the task runs
in a web worker, handle the cancel message right away, not after the synthesis
request that it's meant to stop, as shown in the
example code.
let bargeIn = false; // Set to true when the user interrupts.
try {
for await (const chunk of await tts.synthesizeStream(longText)) {
if (bargeIn) {
tts.cancel();
break;
}
play(chunk.samples, chunk.sampleRateHz);
}
} catch (e) {
// Synthesis was cancelled.
}
Handle and play results
synthesize() returns a TextToSpeechResult, and the stream yields one
TextToSpeechAudioChunk per clause. Each result contains:
samples:Float32Arrayof mono PCM samples in the range [-1.0, 1.0]sampleRateHz: sample rate in Hz (24000for all supported models)chunkIndexandisFinal(stream chunks only): the position of the chunk in the stream, and whether it's the last chunkwordTimestamps: array ofWordTimestampobjects with theword, its position in the input text (startChar,endChar), and when it's spoken (startSec,endSec)
You can play the samples with the Web Audio API:
const audioCtx = new AudioContext();
function play(samples, sampleRateHz) {
const buffer = audioCtx.createBuffer(1, samples.length, sampleRateHz);
buffer.copyToChannel(samples, 0);
const source = audioCtx.createBufferSource();
source.buffer = buffer;
source.connect(audioCtx.destination);
source.start();
}
To play stream chunks back to back without gaps, schedule each chunk to start when the previous one ends, as shown in the example code.
Use the word timestamps to highlight words as they are read aloud. In streaming synthesis, the timestamps and character positions are relative to the start of the stream and the full input text.
for (const word of result.wordTimestamps ?? []) {
console.log(`${word.startSec.toFixed(2)}s - ${word.endSec.toFixed(2)}s: ` +
text.slice(word.startChar, word.endChar));
}
Release resources
When you no longer need the task, call close() to release the model and its
memory:
tts.close();