Skip to content

First synthesis

This page covers the full lifecycle: creating a client, loading the model, synthesizing, inspecting the result, and cleaning up.

Create a client

ts
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().

OptionRequiredMeaning
modelyesThe object generated by @optimalai/model-tools.
voiceIdyesDefault voice for this client.
numThreadsnoONNX intra-op thread count. Tune for your device.

Load the model

ts
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

ts
const result = await tts.synthesizeToFile({
  text: "你好,Hello world.",
  voiceId: "af_maple",
  speed: 1.0,
});
FieldRequiredMeaning
textyesChinese, English, or mixed. Up to 2,000 code points.
voiceIdnoOverrides the client default for this WAV only. No model reload.
outputPathnoDestination path. Defaults to a generated name in the cache directory.
speednoPlayback rate, 0.52.0. Defaults to 1.0.

The result

ts
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 load

Note 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:

tsx
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

ts
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

ts
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.