This is the long version of the quick start for Windows. It builds a small Android app with a text box, a Speak button, and on-device speech generation.
iOS builds require macOS, so this guide focuses on Android.
You will create:
- A React Native Android app
- A text box
- A
Speakbutton - A download progress message
- A debug APK you can install on a device
KittenTTS runs on the device. It does not need a server or cloud TTS key.
| App type | Use this when | Audio package |
|---|---|---|
| Expo development build | You want Expo tooling with native modules | expo-audio |
| Bare React Native | You want the React Native CLI directly | react-native-sound |
Expo Go will not work. KittenTTS needs native modules, and Expo Go does not ship with this SDK inside it.
Install:
- Node.js 20 or newer
- Android Studio
- Git, optional but useful
Open Android Studio, then go to:
Settings > Languages & Frameworks > Android SDK > SDK Tools
Install these tools:
| Tool | Required? | Why |
|---|---|---|
| Android SDK Build-Tools | Yes | Compiles Android projects |
| Android SDK Platform-Tools | Yes | Provides adb |
| Android SDK Command-line Tools | Yes | Required by Gradle and React Native |
| Android Emulator | Yes for emulator testing | Runs an Android virtual device |
| NDK Side by side | Yes | Builds native ONNX Runtime code |
| CMake | Yes | Builds native C++ dependencies |
Open PowerShell and run:
[Environment]::SetEnvironmentVariable("ANDROID_HOME", "$env:LOCALAPPDATA\Android\Sdk", "User")
[Environment]::SetEnvironmentVariable("ANDROID_SDK_ROOT", "$env:LOCALAPPDATA\Android\Sdk", "User")
[Environment]::SetEnvironmentVariable(
"Path",
$env:Path + ";$env:LOCALAPPDATA\Android\Sdk\platform-tools;$env:LOCALAPPDATA\Android\Sdk\cmdline-tools\latest\bin",
"User"
)Close PowerShell, open a new PowerShell window, then check:
node -v
npm -v
adb versionIf adb version works, your Android Platform Tools path is ready.
On Windows, native Android builds can fail when the folder path is too long. Use a short folder:
mkdir C:\ktts
cd C:\kttsAvoid paths like:
C:\Users\Administrator\Downloads\very-long-folder-name\another-long-folder-name
Use this if you want Expo with a native development build.
npx create-expo-app SimpleKittenTTS
cd SimpleKittenTTS
npm install @kittentts/react-native
npx expo install expo-audio expo-dev-client
npx expo prebuild --platform android
npx expo run:androidWhat the commands do:
| Command | Meaning |
|---|---|
create-expo-app |
Creates a new Expo project |
npm install @kittentts/react-native |
Adds the SDK |
expo install expo-audio expo-dev-client |
Adds audio playback and dev-build support |
expo prebuild --platform android |
Creates the native android/ folder |
expo run:android |
Builds and installs the development app |
Use this if you want a normal React Native CLI project.
npx @react-native-community/cli init SimpleKittenTTS
cd SimpleKittenTTS
npm install @kittentts/react-native react-native-sound
npm run androidOpen App.tsx and replace it with the version for your app type.
import { useCallback, useMemo, useState } from 'react';
import {
ActivityIndicator,
Button,
SafeAreaView,
StyleSheet,
Text,
TextInput,
View,
} from 'react-native';
import * as ExpoAudio from 'expo-audio';
import {
KittenTTS,
createExpoAudioPlayer,
isKittenTTSError,
} from '@kittentts/react-native';
export default function App() {
const [text, setText] = useState('Hello from KittenTTS on Android.');
const [status, setStatus] = useState('Ready');
const [busy, setBusy] = useState(false);
const player = useMemo(() => createExpoAudioPlayer(ExpoAudio), []);
const speak = useCallback(async () => {
if (!text.trim()) return;
setBusy(true);
setStatus('Preparing voice model...');
try {
const tts = await KittenTTS.create({ player }, (progress, info) => {
if (info?.stage === 'cached') {
setStatus('Model is already downloaded.');
return;
}
if (info?.stage === 'downloading') {
setStatus(`Downloading model ${Math.round(progress * 100)}%`);
}
});
setStatus('Speaking...');
await tts.speak(text);
setStatus('Done');
await tts.dispose();
} catch (error) {
setStatus(readableError(error));
} finally {
setBusy(false);
}
}, [player, text]);
return (
<SafeAreaView style={styles.screen}>
<View style={styles.content}>
<Text style={styles.title}>Simple KittenTTS</Text>
<TextInput
value={text}
onChangeText={setText}
multiline
editable={!busy}
style={styles.input}
/>
<Button title={busy ? 'Working...' : 'Speak'} onPress={speak} disabled={busy} />
<View style={styles.status}>
{busy ? <ActivityIndicator /> : null}
<Text style={styles.statusText}>{status}</Text>
</View>
</View>
</SafeAreaView>
);
}
function readableError(error: unknown) {
if (isKittenTTSError(error)) {
return error.message;
}
if (error instanceof Error) {
return error.message;
}
return 'Something went wrong.';
}
const styles = StyleSheet.create({
screen: {
flex: 1,
backgroundColor: '#F7F8FA',
},
content: {
flex: 1,
gap: 16,
padding: 24,
justifyContent: 'center',
},
title: {
fontSize: 28,
fontWeight: '700',
},
input: {
minHeight: 120,
borderWidth: 1,
borderColor: '#D5DAE1',
borderRadius: 8,
padding: 14,
backgroundColor: '#FFFFFF',
fontSize: 17,
},
status: {
minHeight: 28,
flexDirection: 'row',
alignItems: 'center',
gap: 10,
},
statusText: {
fontSize: 15,
color: '#4B5563',
},
});For a bare app, use react-native-sound instead of expo-audio:
import Sound from 'react-native-sound';
import {
KittenTTS,
createRNSoundPlayer,
isKittenTTSError,
} from '@kittentts/react-native';
const player = useMemo(() => createRNSoundPlayer(Sound), []);The rest of the Expo App.tsx can stay the same.
Start the emulator from Android Studio or connect a physical Android phone with USB debugging enabled.
Then run:
npx expo run:androidor, for bare React Native:
npm run androidExpected first run:
- The app installs on the emulator or phone.
- The app shows model download progress.
- The app speaks the text.
- The next run uses the local cache and should not download again.
If Wi-Fi drops during the first download, use redownloadModel():
async function retryModelDownload() {
setBusy(true);
try {
await KittenTTS.redownloadModel(undefined, (progress) => {
setStatus(`Redownloading model ${Math.round(progress * 100)}%`);
});
setStatus('Model downloaded. Tap Speak again.');
} catch (error) {
setStatus(readableError(error));
} finally {
setBusy(false);
}
}Add a second button:
<Button title="Retry model download" onPress={retryModelDownload} disabled={busy} />Use this before showing a download screen:
const cache = await KittenTTS.getModelCacheInfo();
if (cache.isCached) {
setStatus('Voice model is ready.');
} else {
setStatus('Voice model needs to download.');
}From your app folder:
cd android
.\gradlew.bat assembleDebugThe debug APK is usually here:
android\app\build\outputs\apk\debug\app-debug.apk
Install it manually with:
adb install -r app\build\outputs\apk\debug\app-debug.apk| Problem | What to do |
|---|---|
adb is not recognized |
Add Platform Tools to Path, then open a new PowerShell |
| CMake object path warning | Move the project to C:\ktts\YourApp |
| Expo Go opens | Use npx expo run:android, not Expo Go |
| Gradle cannot find SDK | Check ANDROID_HOME and ANDROID_SDK_ROOT |
| First generation is slow | Expected only on the first model download |
| Download fails | Keep internet on, then call KittenTTS.redownloadModel() |
- Read EPUB reader on Windows for streaming long text.
- Open the SDK examples in
examples/for complete UI implementations.