First synthesis
This page covers the full lifecycle: creating a client, loading the model, synthesizing, inspecting the result, and cleaning up.
Create a client
import { createTts } from "@optimalai/react-native";
import { kokoroZhInt8 } from "./optimalai-models";
const tts = createTts({
model: kokoroZhInt8,
voiceId: "zf_001",
numThreads: 4,
});createTts is cheap — it only stores options. Nothing native happens until load().
| Option | Required | Meaning |
|---|---|---|
model | yes | The object generated by @optimalai/model-tools. |
voiceId | yes | Default voice for this client. |
numThreads | no | ONNX intra-op thread count. Tune for your device. |
Load the model
await tts.load();This is the expensive step. It resolves the asset IDs into real file URIs, validates file sizes, parses the token table and both lexicons, opens the ONNX session, and verifies the voice layout. Expect it to take a noticeable moment on a cold start — measure it on a real device, not a simulator.
Reaching ready twice is harmless; load() returns immediately if the client is already ready.
Synthesize
const result = await tts.synthesizeToFile({
text: "你好,Hello world.",
voiceId: "af_maple",
speed: 1.0,
});| Field | Required | Meaning |
|---|---|---|
text | yes | Chinese, English, or mixed. Up to 2,000 code points. |
voiceId | no | Overrides the client default for this WAV only. No model reload. |
outputPath | no | Destination path. Defaults to a generated name in the cache directory. |
speed | no | Playback rate, 0.5–2.0. Defaults to 1.0. |
The result
result.fileUri; // "file:///.../optimalai-1726000000000.wav"
result.sampleRate; // 24000
result.channels; // 1
result.voiceId; // "af_maple"
result.durationMs; // length of the generated audio
result.modelId; // "kokoro-82m-v1.1-zh-int8"
result.modelVersion; // "1.1-zh-int8"
result.elapsedMs; // synthesis time, excluding loadNote elapsedMs excludes model loading — it measures the synthesis call only. That makes it the number to watch when you tune numThreads.
Play the WAV
The SDK writes a file and stops. Playback is yours:
import { useAudioPlayer, setAudioModeAsync } from "expo-audio";
import { useEffect } from "react";
const player = useAudioPlayer(null);
// iOS silences audio unless you opt in.
useEffect(() => {
void setAudioModeAsync({ playsInSilentMode: true });
}, []);
async function synthesizeAndPlay() {
const result = await tts.synthesizeToFile({ text: "你好,Hello world." });
player.replace(result.fileUri);
player.play();
}Clean up
await tts.reset();reset() closes the ONNX session and returns the client to unloaded. Call it when the screen unmounts, or before you discard the client. You need a reset() before you can load() again after a fatal error — see Error handling.
File lifecycle
The SDK does not download, play, cache, upload, or delete the WAV it produces. Every generated file stays on disk until you remove it. If your app synthesizes repeatedly, manage that directory yourself.
Reusing a path
If you pass an explicit outputPath that already exists, synthesis fails with OUTPUT_EXISTS rather than silently overwriting. Delete the file first, or let the SDK generate a fresh name.
Doing it all at once
const tts = createTts({
model: kokoroZhInt8,
voiceId: "zf_001",
numThreads: 4,
});
await tts.load();
const first = await tts.synthesizeToFile({ text: "第一句。" });
const second = await tts.synthesizeToFile({
text: "Second line.",
voiceId: "af_maple",
speed: 1.15,
});
await tts.reset();Two syntheses, one model load, no reload in between.