diff --git a/examples/BareRNExample/App.tsx b/examples/BareRNExample/App.tsx
index f68d6eb..63286d9 100644
--- a/examples/BareRNExample/App.tsx
+++ b/examples/BareRNExample/App.tsx
@@ -1,5 +1,6 @@
import React, {useState, useCallback, useEffect, useRef} from 'react';
import {
+ Image,
SafeAreaView,
ScrollView,
View,
@@ -22,6 +23,8 @@ import {
createRNSoundPlayer,
} from '@kittentts/react-native';
+const LOGO = require('./assets/kittenml_logo.png');
+
type AppState =
| {kind: 'idle'}
| {kind: 'preparing'}
@@ -58,43 +61,43 @@ export default function App() {
state.kind === 'generating' ||
state.kind === 'playing';
- const initTTS = useCallback(
- async (model: KittenModel) => {
- try {
- await ttsRef.current?.dispose();
- setState({kind: 'preparing'});
- setResult(null);
-
- const instance = await KittenTTS.create(
- {model, player: createRNSoundPlayer(Sound)},
- (progress, info) => {
- if (mountedRef.current && info?.stage === 'downloading') {
- setState({
- kind: 'downloading',
- progress,
- });
- }
- },
- );
-
- if (!mountedRef.current) {
- if (!__DEV__) await instance.dispose();
- return;
- }
-
- ttsRef.current = instance;
- setTts(instance);
- setState({kind: 'idle'});
- } catch (error: unknown) {
- ttsRef.current = null;
- if (mountedRef.current) {
- setTts(null);
- setState({kind: 'error', message: getErrorMessage(error, 'Init failed')});
- }
+ const initTTS = useCallback(async (model: KittenModel) => {
+ try {
+ await ttsRef.current?.dispose();
+ setState({kind: 'preparing'});
+ setResult(null);
+
+ const instance = await KittenTTS.create(
+ {model, player: createRNSoundPlayer(Sound)},
+ (progress, info) => {
+ if (mountedRef.current && info?.stage === 'downloading') {
+ setState({
+ kind: 'downloading',
+ progress,
+ });
+ }
+ },
+ );
+
+ if (!mountedRef.current) {
+ if (!__DEV__) await instance.dispose();
+ return;
}
- },
- [],
- );
+
+ ttsRef.current = instance;
+ setTts(instance);
+ setState({kind: 'idle'});
+ } catch (error: unknown) {
+ ttsRef.current = null;
+ if (mountedRef.current) {
+ setTts(null);
+ setState({
+ kind: 'error',
+ message: getErrorMessage(error, 'Init failed'),
+ });
+ }
+ }
+ }, []);
useEffect(() => {
mountedRef.current = true;
@@ -121,7 +124,10 @@ export default function App() {
setResult(res);
setState({kind: 'idle'});
} catch (error: unknown) {
- setState({kind: 'error', message: getErrorMessage(error, 'Generation failed')});
+ setState({
+ kind: 'error',
+ message: getErrorMessage(error, 'Generation failed'),
+ });
}
}, [tts, inputText, selectedVoice, selectedSpeed]);
@@ -135,7 +141,10 @@ export default function App() {
setResult(res);
setState({kind: 'idle'});
} catch (error: unknown) {
- setState({kind: 'error', message: getErrorMessage(error, 'Playback failed')});
+ setState({
+ kind: 'error',
+ message: getErrorMessage(error, 'Playback failed'),
+ });
}
}, [tts, inputText, selectedVoice, selectedSpeed]);
@@ -150,128 +159,111 @@ export default function App() {
return (
- KittenTTS
- On-Device Text-to-Speech
-
-
-
- {/* Text Input */}
-
- Text
-
+
+
+
+
+
+ KittenTTS Example
+
+ Bare React Native example of the React Native SDK for KittenTTS
+
+
- {/* Model Picker */}
-
- Model
-
- {MODELS.map(model => (
- handleModelChange(model)}
- disabled={isWorking}>
-
- {modelDisplayName(model)}
-
-
- ))}
+
+
+
+ Model
+
+ {statusSummary(state)}
+
+
+
+
+ {modelDisplayName(selectedModel)}
+
+
+
+
+
+ Text
+
-
- {/* Voice Picker */}
-
- Voice
-
- {ALL_VOICES.map(voice => (
+
+
+
+
+
+
+
+ Playback
+
setSelectedVoice(voice)}
- disabled={isWorking}>
-
- {voiceDisplayName(voice)}
-
+ onPress={handleGenerate}
+ disabled={isWorking || !inputText.trim() || !tts}>
+ Generate
- ))}
-
-
- {/* Speed Picker */}
-
- Speed: {selectedSpeed.toFixed(1)}x
-
- {SPEED_OPTIONS.map(speed => (
setSelectedSpeed(speed)}
- disabled={isWorking}>
-
- {speed.toFixed(1)}x
+ onPress={handleSpeak}
+ disabled={isWorking || !inputText.trim() || !tts}>
+
+ Speak
- ))}
+
-
- {/* Action Buttons */}
-
-
- Generate
-
-
-
- Speak
-
-
+
+
+
+ This system is for demonstration purposes only and is not intended
+ to process sensitive or personal data.
+
- {/* Result Card */}
- {result && }
+ {result && }
+
);
@@ -281,6 +273,70 @@ function getErrorMessage(error: unknown, fallback: string): string {
return error instanceof Error ? error.message : fallback;
}
+function OptionGroup({
+ label,
+ values,
+ selected,
+ disabled,
+ getLabel,
+ onSelect,
+}: {
+ label: string;
+ values: readonly T[];
+ selected: T;
+ disabled: boolean;
+ getLabel: (value: T) => string;
+ onSelect: (value: T) => void;
+}) {
+ return (
+
+ {label}
+
+ {values.map(value => {
+ const active = value === selected;
+ return (
+ onSelect(value)}>
+
+ {getLabel(value)}
+
+
+ );
+ })}
+
+
+ );
+}
+
+function statusSummary(state: AppState): string {
+ switch (state.kind) {
+ case 'idle':
+ return 'Ready';
+ case 'preparing':
+ return 'Preparing';
+ case 'downloading':
+ return `${Math.round(state.progress * 100)}%`;
+ case 'generating':
+ return 'Generating';
+ case 'playing':
+ return 'Playing';
+ case 'error':
+ return 'Error';
+ }
+}
+
+function speedLabel(speed: number) {
+ return `${speed.toFixed(2).replace(/0$/, '')}x`;
+}
+
function StatusBanner({state}: {state: AppState}) {
switch (state.kind) {
case 'idle':
@@ -288,31 +344,33 @@ function StatusBanner({state}: {state: AppState}) {
case 'preparing':
return (
-
- Preparing model...
+
+
+ Preparing model and phonemizer...
+
);
case 'downloading':
return (
-
+
- Downloading model... {Math.round(state.progress * 100)}%
+ Downloading ({Math.round(state.progress * 100)}%)
);
case 'generating':
return (
-
- Generating speech...
+
+ Generating audio...
);
case 'playing':
return (
-
- Playing...
+
+ Playing audio...
);
case 'error':
@@ -330,9 +388,7 @@ function ResultCard({result}: {result: KittenTTSResult}) {
Generated Audio
Voice
-
- {voiceDisplayName(result.voice)}
-
+ {voiceDisplayName(result.voice)}
Duration
@@ -357,167 +413,248 @@ function ResultCard({result}: {result: KittenTTSResult}) {
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: '#F2F2F7',
+ backgroundColor: '#FAFAFA',
},
content: {
- padding: 20,
+ alignSelf: 'center',
+ maxWidth: 430,
+ width: '100%',
+ paddingHorizontal: 16,
+ paddingTop: 24,
paddingBottom: 40,
},
- title: {
- fontSize: 28,
- fontWeight: '700',
- color: '#000',
- textAlign: 'center',
- },
- subtitle: {
- fontSize: 14,
- color: '#666',
- textAlign: 'center',
+ header: {
+ flexDirection: 'row',
+ alignItems: 'flex-start',
marginBottom: 20,
},
- section: {
- marginBottom: 16,
+ logoMark: {
+ alignItems: 'center',
+ borderRadius: 8,
+ height: 48,
+ justifyContent: 'center',
+ marginRight: 12,
+ overflow: 'hidden',
+ width: 48,
},
- label: {
- fontSize: 13,
- fontWeight: '600',
- color: '#666',
- marginBottom: 6,
- textTransform: 'uppercase',
- letterSpacing: 0.5,
+ logoImage: {
+ height: 48,
+ width: 48,
},
- textInput: {
- backgroundColor: '#FFF',
- borderRadius: 12,
- padding: 14,
+ headerCopy: {
+ flex: 1,
+ },
+ title: {
+ fontSize: 30,
+ fontWeight: '700',
+ color: '#09090B',
+ lineHeight: 32,
+ },
+ subtitle: {
fontSize: 16,
- color: '#000',
- minHeight: 100,
- textAlignVertical: 'top',
+ color: '#71717A',
+ lineHeight: 22,
+ marginTop: 6,
+ },
+ demoCard: {
+ backgroundColor: '#FFFFFF',
+ borderColor: '#E4E4E7',
+ borderRadius: 8,
+ borderWidth: 1,
+ padding: 16,
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: {width: 0, height: 1},
- shadowOpacity: 0.05,
- shadowRadius: 3,
+ shadowOpacity: 0.04,
+ shadowRadius: 2,
},
android: {
elevation: 1,
},
}),
},
+ modelRow: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginBottom: 18,
+ },
+ modelRowLeft: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ flexShrink: 1,
+ gap: 8,
+ },
+ modelRowLabel: {
+ color: '#09090B',
+ fontSize: 14,
+ fontWeight: '600',
+ },
+ pill: {
+ backgroundColor: '#F4F4F5',
+ borderRadius: 8,
+ paddingHorizontal: 9,
+ paddingVertical: 4,
+ },
+ pillText: {
+ color: '#52525B',
+ fontSize: 12,
+ fontWeight: '700',
+ },
+ softBadge: {
+ backgroundColor: '#F4F4F5',
+ borderRadius: 8,
+ flexShrink: 1,
+ paddingHorizontal: 10,
+ paddingVertical: 6,
+ },
+ softBadgeText: {
+ color: '#09090B',
+ fontSize: 13,
+ fontWeight: '700',
+ },
+ section: {
+ marginBottom: 18,
+ },
+ label: {
+ color: '#52525B',
+ fontSize: 12,
+ fontWeight: '700',
+ letterSpacing: 0,
+ marginBottom: 8,
+ },
+ textInput: {
+ backgroundColor: '#FFFFFF',
+ borderColor: '#E4E4E7',
+ borderRadius: 8,
+ borderWidth: 1,
+ color: '#09090B',
+ fontSize: 15,
+ lineHeight: 22,
+ minHeight: 122,
+ padding: 12,
+ textAlignVertical: 'top',
+ },
chipRow: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
chip: {
- paddingHorizontal: 14,
- paddingVertical: 8,
- borderRadius: 20,
- backgroundColor: '#FFF',
- borderWidth: 1,
- borderColor: '#E0E0E0',
+ backgroundColor: '#F4F4F5',
+ borderRadius: 8,
+ justifyContent: 'center',
+ minHeight: 38,
+ paddingHorizontal: 12,
},
chipSelected: {
- backgroundColor: '#007AFF',
- borderColor: '#007AFF',
+ backgroundColor: '#D4D4D8',
},
chipText: {
- fontSize: 13,
- fontWeight: '500',
- color: '#333',
+ color: '#52525B',
+ fontSize: 14,
+ fontWeight: '600',
},
chipTextSelected: {
- color: '#FFF',
+ color: '#09090B',
+ },
+ actionGroup: {
+ backgroundColor: '#F4F4F5',
+ borderRadius: 8,
+ marginTop: 2,
+ padding: 10,
+ },
+ actionGroupLabel: {
+ color: '#52525B',
+ fontSize: 12,
+ fontWeight: '700',
+ marginBottom: 8,
},
buttonRow: {
flexDirection: 'row',
gap: 12,
- marginTop: 8,
- marginBottom: 16,
},
button: {
- flex: 1,
- paddingVertical: 14,
- borderRadius: 12,
alignItems: 'center',
+ backgroundColor: '#FFFFFF',
+ borderRadius: 8,
+ flex: 1,
+ justifyContent: 'center',
+ minHeight: 46,
},
buttonPrimary: {
- backgroundColor: '#007AFF',
+ backgroundColor: '#18181B',
},
- buttonSecondary: {
- backgroundColor: '#FFF',
- borderWidth: 1,
- borderColor: '#007AFF',
- },
- buttonDisabled: {
- opacity: 0.5,
+ buttonText: {
+ color: '#09090B',
+ fontSize: 15,
+ fontWeight: '700',
},
buttonPrimaryText: {
- color: '#FFF',
- fontSize: 16,
- fontWeight: '600',
+ color: '#FFFFFF',
},
- buttonSecondaryText: {
- color: '#007AFF',
- fontSize: 16,
- fontWeight: '600',
+ disabled: {
+ opacity: 0.48,
+ },
+ disclaimer: {
+ color: '#71717A',
+ fontSize: 12,
+ lineHeight: 17,
+ marginTop: 16,
},
banner: {
- flexDirection: 'row',
alignItems: 'center',
- backgroundColor: '#F0F4FF',
- padding: 12,
- borderRadius: 12,
- marginBottom: 16,
+ flexDirection: 'row',
gap: 10,
+ marginTop: 16,
},
bannerText: {
- fontSize: 14,
- color: '#007AFF',
+ color: '#854D0E',
+ flex: 1,
+ fontSize: 13,
+ fontWeight: '600',
+ lineHeight: 18,
},
bannerError: {
- backgroundColor: '#FFF0F0',
+ alignItems: 'flex-start',
},
bannerErrorText: {
- fontSize: 14,
- color: '#FF3B30',
+ color: '#B42318',
+ flex: 1,
+ fontSize: 13,
+ fontWeight: '600',
+ lineHeight: 18,
},
resultCard: {
- backgroundColor: '#FFF',
- borderRadius: 12,
- padding: 16,
- ...Platform.select({
- ios: {
- shadowColor: '#000',
- shadowOffset: {width: 0, height: 2},
- shadowOpacity: 0.08,
- shadowRadius: 4,
- },
- android: {
- elevation: 2,
- },
- }),
+ backgroundColor: '#FAFAFA',
+ borderColor: '#E4E4E7',
+ borderRadius: 8,
+ borderWidth: 1,
+ marginTop: 18,
+ padding: 12,
},
resultTitle: {
+ color: '#09090B',
fontSize: 16,
- fontWeight: '600',
- color: '#34C759',
+ fontWeight: '700',
marginBottom: 12,
},
resultRow: {
flexDirection: 'row',
+ gap: 12,
justifyContent: 'space-between',
paddingVertical: 4,
},
resultLabel: {
+ color: '#71717A',
fontSize: 14,
- color: '#666',
},
resultValue: {
+ color: '#09090B',
+ flexShrink: 1,
fontSize: 14,
- fontWeight: '500',
- color: '#000',
+ fontWeight: '700',
+ textAlign: 'right',
},
});
diff --git a/examples/BareRNExample/assets/kittenml_logo.png b/examples/BareRNExample/assets/kittenml_logo.png
new file mode 100644
index 0000000..d1093df
Binary files /dev/null and b/examples/BareRNExample/assets/kittenml_logo.png differ
diff --git a/examples/ExpoExample/App.tsx b/examples/ExpoExample/App.tsx
index 0f6e7f9..7cda35c 100644
--- a/examples/ExpoExample/App.tsx
+++ b/examples/ExpoExample/App.tsx
@@ -1,6 +1,7 @@
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {
ActivityIndicator,
+ Image,
Platform,
SafeAreaView,
ScrollView,
@@ -10,7 +11,7 @@ import {
Pressable,
View,
} from 'react-native';
-import { StatusBar } from 'expo-status-bar';
+import {StatusBar} from 'expo-status-bar';
import * as ExpoAudio from 'expo-audio';
import {
ALL_VOICES,
@@ -23,14 +24,16 @@ import {
voiceDisplayName,
} from '@kittentts/react-native';
+const LOGO = require('./assets/kittenml_logo.png');
+
type WorkState =
- | { kind: 'booting' }
- | { kind: 'ready' }
- | { kind: 'preparing' }
- | { kind: 'loading'; progress: number }
- | { kind: 'generating' }
- | { kind: 'playing' }
- | { kind: 'error'; message: string };
+ | {kind: 'booting'}
+ | {kind: 'ready'}
+ | {kind: 'preparing'}
+ | {kind: 'loading'; progress: number}
+ | {kind: 'generating'}
+ | {kind: 'playing'}
+ | {kind: 'error'; message: string};
const MODELS = [
KittenModel.Nano,
@@ -44,49 +47,59 @@ const SPEEDS = [0.5, 0.75, 1, 1.25, 1.5, 2];
export default function App() {
const [tts, setTts] = useState(null);
const ttsRef = useRef(null);
- const [state, setState] = useState({ kind: 'booting' });
+ const [state, setState] = useState({kind: 'booting'});
const [model, setModel] = useState(KittenModel.Nano);
const [voice, setVoice] = useState(KittenVoice.Bella);
const [speed, setSpeed] = useState(1);
- const [text, setText] = useState('Hello from KittenTTS. This is running on device with Expo.');
+ const [text, setText] = useState(
+ 'Hello from KittenTTS. This is running on device with Expo.',
+ );
const [result, setResult] = useState(null);
const mountedRef = useRef(true);
- const busy = state.kind === 'booting' || state.kind === 'preparing' || state.kind === 'loading' || state.kind === 'generating' || state.kind === 'playing';
+ const busy =
+ state.kind === 'booting' ||
+ state.kind === 'preparing' ||
+ state.kind === 'loading' ||
+ state.kind === 'generating' ||
+ state.kind === 'playing';
const player = useMemo(() => createExpoAudioPlayer(ExpoAudio), []);
- const loadModel = useCallback(async (nextModel: KittenModel) => {
- setState({ kind: 'preparing' });
- setResult(null);
+ const loadModel = useCallback(
+ async (nextModel: KittenModel) => {
+ setState({kind: 'preparing'});
+ setResult(null);
- try {
- await ttsRef.current?.dispose();
- const instance = await KittenTTS.create(
- { model: nextModel, player },
- (progress, info) => {
- if (mountedRef.current && info?.stage === 'downloading') {
- setState({
- kind: 'loading',
- progress,
- });
- }
- },
- );
- if (!mountedRef.current) {
- if (!__DEV__) await instance.dispose();
- return;
+ try {
+ await ttsRef.current?.dispose();
+ const instance = await KittenTTS.create(
+ {model: nextModel, player},
+ (progress, info) => {
+ if (mountedRef.current && info?.stage === 'downloading') {
+ setState({
+ kind: 'loading',
+ progress,
+ });
+ }
+ },
+ );
+ if (!mountedRef.current) {
+ if (!__DEV__) await instance.dispose();
+ return;
+ }
+ ttsRef.current = instance;
+ setTts(instance);
+ setState({kind: 'ready'});
+ } catch (error) {
+ ttsRef.current = null;
+ if (mountedRef.current) {
+ setTts(null);
+ setState({kind: 'error', message: friendlyError(error)});
+ }
}
- ttsRef.current = instance;
- setTts(instance);
- setState({ kind: 'ready' });
- } catch (error) {
- ttsRef.current = null;
- if (mountedRef.current) {
- setTts(null);
- setState({ kind: 'error', message: friendlyError(error) });
- }
- }
- }, [player]);
+ },
+ [player],
+ );
useEffect(() => {
mountedRef.current = true;
@@ -104,32 +117,35 @@ export default function App() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- const selectModel = useCallback((nextModel: KittenModel) => {
- setModel(nextModel);
- loadModel(nextModel);
- }, [loadModel]);
+ const selectModel = useCallback(
+ (nextModel: KittenModel) => {
+ setModel(nextModel);
+ loadModel(nextModel);
+ },
+ [loadModel],
+ );
const generate = useCallback(async () => {
if (!tts || !text.trim()) return;
- setState({ kind: 'generating' });
+ setState({kind: 'generating'});
try {
const nextResult = await tts.generate(text, voice, speed);
setResult(nextResult);
- setState({ kind: 'ready' });
+ setState({kind: 'ready'});
} catch (error) {
- setState({ kind: 'error', message: friendlyError(error) });
+ setState({kind: 'error', message: friendlyError(error)});
}
}, [speed, text, tts, voice]);
const speak = useCallback(async () => {
if (!tts || !text.trim()) return;
- setState({ kind: 'playing' });
+ setState({kind: 'playing'});
try {
const nextResult = await tts.speak(text, voice, speed);
setResult(nextResult);
- setState({ kind: 'ready' });
+ setState({kind: 'ready'});
} catch (error) {
- setState({ kind: 'error', message: friendlyError(error) });
+ setState({kind: 'error', message: friendlyError(error)});
}
}, [speed, text, tts, voice]);
@@ -138,72 +154,121 @@ export default function App() {
- KittenTTS
- Expo on-device text to speech
+
+
+
+
+ KittenTTS Example
+
+ Expo example of the React Native SDK for KittenTTS
+
+
-
-
-
- Text
-
+
+
+ Model
+
+ {statusSummary(state)}
+
+
+
+
+ {modelDisplayName(model)}
+
+
+
+
+
+ Text
+
+
+
+
-
-
-
-
-
- `${value.toFixed(2).replace(/0$/, '')}x`}
- onSelect={setSpeed}
- />
-
-
-
-
-
+
- {result ? (
-
- Last Result
-
-
-
-
+
+
+
+ Playback
+
+
+
+
- ) : null}
+
+
+
+
+ This system is for demonstration purposes only and is not intended
+ to process sensitive or personal data.
+
+
+ {result ? (
+
+ Last Result
+
+
+
+
+
+ ) : null}
+
);
}
-function StatusPanel({ state }: { state: WorkState }) {
+function StatusPanel({state}: {state: WorkState}) {
if (state.kind === 'ready') return null;
if (state.kind === 'error') {
@@ -214,15 +279,16 @@ function StatusPanel({ state }: { state: WorkState }) {
);
}
- const text = state.kind === 'booting'
- ? 'Preparing...'
- : state.kind === 'preparing'
+ const text =
+ state.kind === 'booting'
+ ? 'Preparing...'
+ : state.kind === 'preparing'
? 'Preparing assets...'
- : state.kind === 'loading'
+ : state.kind === 'loading'
? `Downloading assets... ${Math.round(state.progress * 100)}%`
: state.kind === 'generating'
- ? 'Generating audio...'
- : 'Playing audio...';
+ ? 'Generating audio...'
+ : 'Playing audio...';
return (
@@ -251,16 +317,22 @@ function OptionGroup({
{label}
- {values.map((value) => {
+ {values.map(value => {
const active = value === selected;
return (
onSelect(value)}
- >
- {getLabel(value)}
+ onPress={() => onSelect(value)}>
+
+ {getLabel(value)}
+
);
})}
@@ -282,16 +354,22 @@ function ActionButton({
}) {
return (
- {label}
+ onPress={onPress}>
+
+ {label}
+
);
}
-function ResultRow({ label, value }: { label: string; value: string }) {
+function ResultRow({label, value}: {label: string; value: string}) {
return (
{label}
@@ -300,17 +378,51 @@ function ResultRow({ label, value }: { label: string; value: string }) {
);
}
+function statusSummary(state: WorkState): string {
+ switch (state.kind) {
+ case 'booting':
+ return 'Booting';
+ case 'ready':
+ return 'Ready';
+ case 'preparing':
+ return 'Preparing';
+ case 'loading':
+ return `${Math.round(state.progress * 100)}%`;
+ case 'generating':
+ return 'Generating';
+ case 'playing':
+ return 'Playing';
+ case 'error':
+ return 'Error';
+ }
+}
+
+function speedLabel(value: number): string {
+ return `${value.toFixed(2).replace(/0$/, '')}x`;
+}
+
function friendlyError(error: unknown): string {
const message = error instanceof Error ? error.message : String(error);
const lower = message.toLowerCase();
- if (lower.includes('onnxruntime') || lower.includes('react-native-fs') || lower.includes('nativemodule')) {
+ if (
+ lower.includes('onnxruntime') ||
+ lower.includes('react-native-fs') ||
+ lower.includes('nativemodule')
+ ) {
return 'This example needs an Expo development build because it uses native inference and filesystem modules.';
}
- if (lower.includes('download') || lower.includes('network') || lower.includes('http')) {
+ if (
+ lower.includes('download') ||
+ lower.includes('network') ||
+ lower.includes('http')
+ ) {
return 'Could not download the model assets. Check the network connection and try again.';
}
- if (lower.includes('unable to resolve') || lower.includes('cannot find module')) {
+ if (
+ lower.includes('unable to resolve') ||
+ lower.includes('cannot find module')
+ ) {
return 'The local KittenTTS package could not be loaded. Run npm install and restart Expo with a cleared cache.';
}
@@ -320,46 +432,116 @@ function friendlyError(error: unknown): string {
const styles = StyleSheet.create({
screen: {
flex: 1,
- backgroundColor: '#F4F5F8',
+ backgroundColor: '#FAFAFA',
},
content: {
- padding: 20,
+ alignSelf: 'center',
+ maxWidth: 430,
+ width: '100%',
+ paddingHorizontal: 16,
paddingTop: Platform.OS === 'android' ? 44 : 24,
paddingBottom: 40,
},
header: {
+ alignItems: 'flex-start',
+ flexDirection: 'row',
+ marginBottom: 20,
+ },
+ logoMark: {
alignItems: 'center',
- marginBottom: 24,
+ borderRadius: 8,
+ height: 48,
+ justifyContent: 'center',
+ marginRight: 12,
+ overflow: 'hidden',
+ width: 48,
+ },
+ logoImage: {
+ height: 48,
+ width: 48,
+ },
+ headerCopy: {
+ flex: 1,
},
title: {
- color: '#111111',
- fontSize: 34,
- fontWeight: '800',
+ color: '#09090B',
+ fontSize: 30,
+ fontWeight: '700',
+ lineHeight: 32,
},
subtitle: {
- color: '#6F7178',
- fontSize: 17,
- marginTop: 4,
+ color: '#71717A',
+ fontSize: 16,
+ lineHeight: 22,
+ marginTop: 6,
+ },
+ demoCard: {
+ backgroundColor: '#FFFFFF',
+ borderColor: '#E4E4E7',
+ borderRadius: 8,
+ borderWidth: 1,
+ padding: 16,
+ },
+ modelRow: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginBottom: 18,
+ },
+ modelRowLeft: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ flexShrink: 1,
+ gap: 8,
+ },
+ modelRowLabel: {
+ color: '#09090B',
+ fontSize: 14,
+ fontWeight: '600',
+ },
+ pill: {
+ backgroundColor: '#F4F4F5',
+ borderRadius: 8,
+ paddingHorizontal: 9,
+ paddingVertical: 4,
+ },
+ pillText: {
+ color: '#52525B',
+ fontSize: 12,
+ fontWeight: '700',
+ },
+ softBadge: {
+ backgroundColor: '#F4F4F5',
+ borderRadius: 8,
+ flexShrink: 1,
+ paddingHorizontal: 10,
+ paddingVertical: 6,
+ },
+ softBadgeText: {
+ color: '#09090B',
+ fontSize: 13,
+ fontWeight: '700',
},
group: {
marginBottom: 18,
},
label: {
- color: '#6D6F76',
- fontSize: 13,
+ color: '#52525B',
+ fontSize: 12,
fontWeight: '700',
letterSpacing: 0,
marginBottom: 8,
- textTransform: 'uppercase',
},
input: {
- minHeight: 120,
+ minHeight: 122,
+ borderColor: '#E4E4E7',
borderRadius: 8,
+ borderWidth: 1,
backgroundColor: '#FFFFFF',
- color: '#15171A',
- fontSize: 18,
- lineHeight: 25,
- padding: 16,
+ color: '#09090B',
+ fontSize: 15,
+ lineHeight: 22,
+ padding: 12,
textAlignVertical: 'top',
},
options: {
@@ -368,109 +550,123 @@ const styles = StyleSheet.create({
gap: 10,
},
option: {
- minHeight: 44,
- borderWidth: 1,
- borderColor: '#DADCE2',
+ backgroundColor: '#F4F4F5',
borderRadius: 8,
- backgroundColor: '#FFFFFF',
justifyContent: 'center',
- paddingHorizontal: 16,
+ minHeight: 44,
+ paddingHorizontal: 12,
},
optionActive: {
- borderColor: '#007AFF',
- backgroundColor: '#007AFF',
+ backgroundColor: '#D4D4D8',
},
optionText: {
- color: '#2B2D33',
- fontSize: 16,
+ color: '#52525B',
+ fontSize: 14,
fontWeight: '600',
},
optionTextActive: {
- color: '#FFFFFF',
+ color: '#09090B',
+ },
+ actionGroup: {
+ backgroundColor: '#F4F4F5',
+ borderRadius: 8,
+ marginTop: 2,
+ padding: 10,
+ },
+ actionGroupLabel: {
+ color: '#52525B',
+ fontSize: 12,
+ fontWeight: '700',
+ marginBottom: 8,
},
actions: {
flexDirection: 'row',
gap: 12,
- marginTop: 8,
- marginBottom: 18,
},
button: {
+ backgroundColor: '#FFFFFF',
flex: 1,
- minHeight: 54,
+ minHeight: 46,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
},
buttonPrimary: {
- backgroundColor: '#007AFF',
+ backgroundColor: '#18181B',
},
buttonSecondary: {
backgroundColor: '#FFFFFF',
- borderColor: '#007AFF',
- borderWidth: 1,
},
buttonPrimaryText: {
color: '#FFFFFF',
- fontSize: 17,
+ fontSize: 15,
fontWeight: '700',
},
buttonSecondaryText: {
- color: '#007AFF',
- fontSize: 17,
+ color: '#09090B',
+ fontSize: 15,
fontWeight: '700',
},
disabled: {
- opacity: 0.45,
+ opacity: 0.48,
},
status: {
- minHeight: 52,
- borderRadius: 8,
- backgroundColor: '#EAF2FF',
- flexDirection: 'row',
alignItems: 'center',
+ flexDirection: 'row',
gap: 10,
- paddingHorizontal: 14,
- marginBottom: 18,
+ marginTop: 16,
},
statusText: {
- color: '#0B63CE',
- fontSize: 15,
+ color: '#854D0E',
+ flex: 1,
+ fontSize: 13,
fontWeight: '600',
+ lineHeight: 18,
},
statusError: {
- backgroundColor: '#FFF0F0',
+ alignItems: 'flex-start',
},
statusErrorText: {
- color: '#D93025',
- fontSize: 15,
+ color: '#B42318',
+ flex: 1,
+ fontSize: 13,
fontWeight: '600',
- lineHeight: 21,
+ lineHeight: 18,
+ },
+ disclaimer: {
+ color: '#71717A',
+ fontSize: 12,
+ lineHeight: 17,
+ marginTop: 16,
},
result: {
+ backgroundColor: '#FAFAFA',
+ borderColor: '#E4E4E7',
borderRadius: 8,
- backgroundColor: '#FFFFFF',
- padding: 16,
+ borderWidth: 1,
+ marginTop: 18,
+ padding: 12,
},
resultTitle: {
- color: '#1F8F4D',
- fontSize: 17,
- fontWeight: '800',
- marginBottom: 10,
+ color: '#09090B',
+ fontSize: 16,
+ fontWeight: '700',
+ marginBottom: 12,
},
resultRow: {
flexDirection: 'row',
justifyContent: 'space-between',
- gap: 16,
- paddingVertical: 5,
+ gap: 12,
+ paddingVertical: 4,
},
resultLabel: {
- color: '#6F7178',
- fontSize: 15,
+ color: '#71717A',
+ fontSize: 14,
},
resultValue: {
- color: '#15171A',
+ color: '#09090B',
flexShrink: 1,
- fontSize: 15,
+ fontSize: 14,
fontWeight: '700',
textAlign: 'right',
},
diff --git a/examples/ExpoExample/assets/kittenml_logo.png b/examples/ExpoExample/assets/kittenml_logo.png
new file mode 100644
index 0000000..d1093df
Binary files /dev/null and b/examples/ExpoExample/assets/kittenml_logo.png differ
diff --git a/examples/ExpoWordTimingsExample/App.tsx b/examples/ExpoWordTimingsExample/App.tsx
index b80c80c..2915d70 100644
--- a/examples/ExpoWordTimingsExample/App.tsx
+++ b/examples/ExpoWordTimingsExample/App.tsx
@@ -1,6 +1,7 @@
-import { useEffect, useMemo, useRef, useState } from 'react';
+import {useEffect, useMemo, useRef, useState} from 'react';
import {
ActivityIndicator,
+ Image,
Pressable,
SafeAreaView,
ScrollView,
@@ -9,9 +10,10 @@ import {
TextInput,
View,
} from 'react-native';
-import { StatusBar } from 'expo-status-bar';
+import {StatusBar} from 'expo-status-bar';
import * as ExpoAudio from 'expo-audio';
import {
+ ALL_VOICES,
KittenModel,
KittenTTS,
KittenTTSResult,
@@ -22,22 +24,18 @@ import {
modelDisplayName,
voiceDisplayName,
} from '@kittentts/react-native';
-import type { KittenWordTiming } from '@kittentts/react-native';
+import type {KittenWordTiming} from '@kittentts/react-native';
+
+const LOGO = require('./assets/kittenml_logo.png');
type Status =
- | { kind: 'idle'; message: string }
- | { kind: 'preparing' }
- | { kind: 'loading'; progress: number }
- | { kind: 'working'; message: string }
- | { kind: 'error'; message: string };
+ | {kind: 'idle'; message: string}
+ | {kind: 'preparing'}
+ | {kind: 'loading'; progress: number}
+ | {kind: 'working'; message: string}
+ | {kind: 'error'; message: string};
const MODEL = KittenModel.NanoInt8;
-const VOICES = [
- KittenVoice.Bella,
- KittenVoice.Luna,
- KittenVoice.Jasper,
- KittenVoice.Leo,
-];
export default function App() {
const [text, setText] = useState(
@@ -54,7 +52,10 @@ export default function App() {
const highlightTimerRef = useRef | null>(null);
const player = useMemo(() => createExpoAudioPlayer(ExpoAudio), []);
- const busy = status.kind === 'preparing' || status.kind === 'loading' || status.kind === 'working';
+ const busy =
+ status.kind === 'preparing' ||
+ status.kind === 'loading' ||
+ status.kind === 'working';
useEffect(() => {
return () => {
@@ -68,13 +69,13 @@ export default function App() {
async function getTTS(): Promise {
if (ttsRef.current) return ttsRef.current;
- setStatus({ kind: 'preparing' });
- const cached = await KittenTTS.isModelDownloaded({ model: MODEL });
+ setStatus({kind: 'preparing'});
+ const cached = await KittenTTS.isModelDownloaded({model: MODEL});
const instance = await KittenTTS.create(
- { model: MODEL, defaultVoice: voice, player },
+ {model: MODEL, defaultVoice: voice, player},
(progress, info) => {
if (info?.stage === 'downloading') {
- setStatus({ kind: 'loading', progress });
+ setStatus({kind: 'loading', progress});
}
},
);
@@ -89,7 +90,7 @@ export default function App() {
async function speak() {
if (!text.trim()) {
- setStatus({ kind: 'error', message: 'Enter text before speaking.' });
+ setStatus({kind: 'error', message: 'Enter text before speaking.'});
return;
}
@@ -97,24 +98,27 @@ export default function App() {
setResult(null);
setActiveWordIndex(null);
const tts = await getTTS();
- setStatus({ kind: 'working', message: 'Generating audio...' });
+ setStatus({kind: 'working', message: 'Generating audio...'});
const nextResult = await tts.generate(text, voice);
setResult(nextResult);
- setStatus({ kind: 'working', message: 'Playing with word highlighting...' });
+ setStatus({
+ kind: 'working',
+ message: 'Playing with word highlighting...',
+ });
await tts.play(nextResult, {
onPlaybackStart: () => startWordHighlighting(nextResult),
});
stopWordHighlighting();
- setStatus({ kind: 'idle', message: 'Playback finished.' });
+ setStatus({kind: 'idle', message: 'Playback finished.'});
} catch (error) {
stopWordHighlighting();
- setStatus({ kind: 'error', message: friendlyError(error) });
+ setStatus({kind: 'error', message: friendlyError(error)});
}
}
async function generateOnly() {
if (!text.trim()) {
- setStatus({ kind: 'error', message: 'Enter text before generating.' });
+ setStatus({kind: 'error', message: 'Enter text before generating.'});
return;
}
@@ -122,12 +126,12 @@ export default function App() {
setResult(null);
setActiveWordIndex(null);
const tts = await getTTS();
- setStatus({ kind: 'working', message: 'Generating audio...' });
+ setStatus({kind: 'working', message: 'Generating audio...'});
const nextResult = await tts.generate(text, voice);
setResult(nextResult);
- setStatus({ kind: 'idle', message: 'Generated audio with word timings.' });
+ setStatus({kind: 'idle', message: 'Generated audio with word timings.'});
} catch (error) {
- setStatus({ kind: 'error', message: friendlyError(error) });
+ setStatus({kind: 'error', message: friendlyError(error)});
}
}
@@ -136,77 +140,105 @@ export default function App() {
- Word Timings
-
- Expo SDK 55 development-build demo. Expo Go will not work.
-
+
+
+
+
+ KittenTTS Example
+
+ Word timings example of the React Native SDK for KittenTTS
+
+
-
- Model
- {modelDisplayName(MODEL)}
-
+
+
+
+ Model
+
+ {statusSummary(status)}
+
+
+
+
+ {modelDisplayName(MODEL)}
+
+
+
-
- Text
-
-
+
+ Text
+
+
+
+
+ Voice
+
+ {ALL_VOICES.map(item => (
+ setVoice(item)}
+ style={[
+ styles.option,
+ voice === item && styles.optionSelected,
+ busy && styles.disabled,
+ ]}>
+
+ {voiceDisplayName(item)}
+
+
+ ))}
+
+
-
- Voice
-
- {VOICES.map((item) => (
+
+ Playback
+
setVoice(item)}
- style={[styles.option, voice === item && styles.optionSelected]}
- >
-
- {voiceDisplayName(item)}
+ onPress={generateOnly}
+ style={[styles.button, busy && styles.disabled]}>
+ Generate
+
+
+
+ Speak
- ))}
+
-
-
-
-
-
- Generate
-
-
-
- Speak
-
-
-
+
+
+ {result ? (
+
+ ) : null}
- {result ? (
-
- ) : null}
+
+ This system is for demonstration purposes only and is not intended
+ to process sensitive or personal data.
+
+
);
@@ -222,7 +254,8 @@ export default function App() {
highlightTimerRef.current = setInterval(() => {
const elapsedSeconds = (Date.now() - startedAt) / 1000;
const active = wordTimings.find(
- item => elapsedSeconds >= item.startTime && elapsedSeconds < item.endTime,
+ item =>
+ elapsedSeconds >= item.startTime && elapsedSeconds < item.endTime,
);
setActiveWordIndex(active?.wordIndex ?? null);
}, 50);
@@ -271,9 +304,9 @@ function ResultCard({
key={`transcript-${item.wordIndex}-${item.word}`}
style={[
styles.transcriptWord,
- activeWordIndex === item.wordIndex && styles.transcriptWordActive,
- ]}
- >
+ activeWordIndex === item.wordIndex &&
+ styles.transcriptWordActive,
+ ]}>
{item.word}
{index < transcriptWords.length - 1 ? ' ' : ''}
@@ -281,28 +314,27 @@ function ResultCard({
- {timings.map((item) => (
+ {timings.map(item => (
+ ]}>
+ activeWordIndex === item.wordIndex &&
+ styles.timingWordActive,
+ ]}>
{item.word}
+ activeWordIndex === item.wordIndex &&
+ styles.timingTimeActive,
+ ]}>
{item.startTime.toFixed(2)}s - {item.endTime.toFixed(2)}s
@@ -319,7 +351,22 @@ function ResultCard({
);
}
-function StatusView({ status }: { status: Status }) {
+function statusSummary(status: Status): string {
+ switch (status.kind) {
+ case 'idle':
+ return status.message.includes('Ready') ? 'Ready' : 'Loaded';
+ case 'preparing':
+ return 'Preparing';
+ case 'loading':
+ return `${Math.round(status.progress * 100)}%`;
+ case 'working':
+ return 'Working';
+ case 'error':
+ return 'Error';
+ }
+}
+
+function StatusView({status}: {status: Status}) {
if (status.kind === 'preparing') {
return (
@@ -341,13 +388,13 @@ function StatusView({ status }: { status: Status }) {
}
return (
-
+
+ ]}>
{status.message}
@@ -374,46 +421,112 @@ function friendlyError(error: unknown): string {
const styles = StyleSheet.create({
screen: {
flex: 1,
- backgroundColor: '#F6F7F9',
+ backgroundColor: '#FAFAFA',
},
content: {
- gap: 16,
- padding: 20,
+ alignSelf: 'center',
+ maxWidth: 430,
+ width: '100%',
+ paddingHorizontal: 16,
+ paddingTop: 24,
+ paddingBottom: 40,
},
header: {
- gap: 6,
- paddingTop: 12,
+ alignItems: 'flex-start',
+ flexDirection: 'row',
+ marginBottom: 20,
+ },
+ logoMark: {
+ alignItems: 'center',
+ borderRadius: 8,
+ height: 48,
+ justifyContent: 'center',
+ marginRight: 12,
+ overflow: 'hidden',
+ width: 48,
+ },
+ logoImage: {
+ height: 48,
+ width: 48,
+ },
+ headerCopy: {
+ flex: 1,
},
title: {
- color: '#111827',
- fontSize: 28,
+ color: '#09090B',
+ fontSize: 30,
fontWeight: '700',
+ lineHeight: 32,
},
subtitle: {
- color: '#5B6472',
- fontSize: 15,
- lineHeight: 21,
+ color: '#71717A',
+ fontSize: 16,
+ lineHeight: 22,
+ marginTop: 6,
},
- panel: {
- gap: 10,
+ demoCard: {
+ backgroundColor: '#FFFFFF',
+ borderColor: '#E4E4E7',
+ borderRadius: 8,
+ borderWidth: 1,
+ padding: 16,
},
- label: {
- color: '#374151',
+ modelRow: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginBottom: 18,
+ },
+ modelRowLeft: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ flexShrink: 1,
+ gap: 8,
+ },
+ modelRowLabel: {
+ color: '#09090B',
+ fontSize: 14,
+ fontWeight: '600',
+ },
+ pill: {
+ backgroundColor: '#F4F4F5',
+ borderRadius: 8,
+ paddingHorizontal: 9,
+ paddingVertical: 4,
+ },
+ pillText: {
+ color: '#52525B',
+ fontSize: 12,
+ fontWeight: '700',
+ },
+ softBadge: {
+ backgroundColor: '#F4F4F5',
+ borderRadius: 8,
+ flexShrink: 1,
+ paddingHorizontal: 10,
+ paddingVertical: 6,
+ },
+ softBadgeText: {
+ color: '#09090B',
fontSize: 13,
fontWeight: '700',
- textTransform: 'uppercase',
},
- value: {
- color: '#111827',
- fontSize: 17,
+ panel: {
+ marginBottom: 18,
+ },
+ label: {
+ color: '#52525B',
+ fontSize: 12,
+ fontWeight: '700',
+ marginBottom: 8,
},
input: {
- minHeight: 110,
- borderColor: '#D5DAE1',
+ minHeight: 122,
+ borderColor: '#E4E4E7',
borderRadius: 8,
borderWidth: 1,
- color: '#111827',
- fontSize: 16,
+ color: '#09090B',
+ fontSize: 15,
lineHeight: 22,
padding: 12,
textAlignVertical: 'top',
@@ -425,44 +538,50 @@ const styles = StyleSheet.create({
gap: 8,
},
option: {
- borderColor: '#CAD1DB',
+ backgroundColor: '#F4F4F5',
borderRadius: 8,
- borderWidth: 1,
+ minHeight: 38,
+ justifyContent: 'center',
paddingHorizontal: 12,
- paddingVertical: 9,
- backgroundColor: '#FFFFFF',
},
optionSelected: {
- borderColor: '#1F6FEB',
- backgroundColor: '#EAF2FF',
+ backgroundColor: '#D4D4D8',
},
optionText: {
- color: '#1F2937',
+ color: '#52525B',
fontSize: 14,
fontWeight: '600',
},
optionTextSelected: {
- color: '#174EA6',
+ color: '#09090B',
+ },
+ actionGroup: {
+ backgroundColor: '#F4F4F5',
+ borderRadius: 8,
+ marginTop: 2,
+ padding: 10,
+ },
+ actionGroupLabel: {
+ color: '#52525B',
+ fontSize: 12,
+ fontWeight: '700',
+ marginBottom: 8,
},
status: {
alignItems: 'center',
- borderColor: '#D5DAE1',
- borderRadius: 8,
- borderWidth: 1,
flexDirection: 'row',
gap: 10,
- minHeight: 48,
- padding: 12,
- backgroundColor: '#FFFFFF',
+ marginTop: 16,
},
errorStatus: {
- borderColor: '#F2B8B5',
- backgroundColor: '#FFF1F1',
+ alignItems: 'flex-start',
},
statusText: {
- color: '#344054',
+ color: '#854D0E',
flex: 1,
- fontSize: 14,
+ fontSize: 13,
+ fontWeight: '600',
+ lineHeight: 18,
},
errorStatusText: {
color: '#B42318',
@@ -473,72 +592,75 @@ const styles = StyleSheet.create({
},
button: {
alignItems: 'center',
- borderColor: '#1F2937',
+ backgroundColor: '#FFFFFF',
borderRadius: 8,
- borderWidth: 1,
flex: 1,
- minHeight: 48,
+ minHeight: 46,
justifyContent: 'center',
paddingHorizontal: 14,
- backgroundColor: '#FFFFFF',
},
primaryButton: {
- borderColor: '#1F6FEB',
- backgroundColor: '#1F6FEB',
- },
- buttonDisabled: {
- opacity: 0.55,
+ backgroundColor: '#18181B',
},
buttonText: {
- color: '#111827',
- fontSize: 16,
+ color: '#09090B',
+ fontSize: 15,
fontWeight: '700',
},
primaryButtonText: {
color: '#FFFFFF',
},
+ disabled: {
+ opacity: 0.48,
+ },
+ disclaimer: {
+ color: '#71717A',
+ fontSize: 12,
+ lineHeight: 17,
+ marginTop: 16,
+ },
result: {
- borderColor: '#D5DAE1',
+ backgroundColor: '#FAFAFA',
+ borderColor: '#E4E4E7',
borderRadius: 8,
borderWidth: 1,
gap: 14,
- padding: 14,
- backgroundColor: '#FFFFFF',
+ marginTop: 18,
+ padding: 12,
},
resultTitle: {
- color: '#111827',
- fontSize: 18,
- fontWeight: '800',
+ color: '#09090B',
+ fontSize: 16,
+ fontWeight: '700',
},
resultGrid: {
flexDirection: 'row',
gap: 28,
},
resultLabel: {
- color: '#5B6472',
+ color: '#71717A',
fontSize: 13,
},
resultValue: {
- color: '#111827',
- fontSize: 18,
+ color: '#09090B',
+ fontSize: 17,
fontWeight: '700',
},
timingsTitle: {
- color: '#374151',
- fontSize: 13,
+ color: '#52525B',
+ fontSize: 12,
fontWeight: '700',
- textTransform: 'uppercase',
},
transcript: {
- color: '#111827',
+ color: '#09090B',
fontSize: 16,
lineHeight: 30,
},
transcriptWord: {
- color: '#111827',
+ color: '#09090B',
},
transcriptWordActive: {
- backgroundColor: '#1F6FEB',
+ backgroundColor: '#18181B',
color: '#FFFFFF',
fontWeight: '800',
},
@@ -547,7 +669,7 @@ const styles = StyleSheet.create({
},
timingRow: {
alignItems: 'center',
- borderColor: '#E4E7EC',
+ borderColor: '#E4E4E7',
borderRadius: 8,
borderWidth: 1,
flexDirection: 'row',
@@ -557,29 +679,29 @@ const styles = StyleSheet.create({
paddingVertical: 8,
},
timingRowActive: {
- borderColor: '#1F6FEB',
- backgroundColor: '#EAF2FF',
+ borderColor: '#D4D4D8',
+ backgroundColor: '#F4F4F5',
},
timingWord: {
- color: '#111827',
+ color: '#09090B',
flex: 1,
fontSize: 14,
fontWeight: '700',
},
timingWordActive: {
- color: '#174EA6',
+ color: '#09090B',
},
timingTime: {
- color: '#475467',
+ color: '#52525B',
fontSize: 13,
fontVariant: ['tabular-nums'],
},
timingTimeActive: {
- color: '#174EA6',
+ color: '#09090B',
fontWeight: '700',
},
emptyTimings: {
- color: '#667085',
+ color: '#71717A',
fontSize: 14,
lineHeight: 20,
},
diff --git a/examples/ExpoWordTimingsExample/assets/kittenml_logo.png b/examples/ExpoWordTimingsExample/assets/kittenml_logo.png
new file mode 100644
index 0000000..d1093df
Binary files /dev/null and b/examples/ExpoWordTimingsExample/assets/kittenml_logo.png differ
diff --git a/examples/OfflineBundledAssetsExample/App.tsx b/examples/OfflineBundledAssetsExample/App.tsx
index 17d33e3..7039210 100644
--- a/examples/OfflineBundledAssetsExample/App.tsx
+++ b/examples/OfflineBundledAssetsExample/App.tsx
@@ -1,8 +1,9 @@
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import * as ExpoAudio from 'expo-audio';
-import { StatusBar } from 'expo-status-bar';
+import {StatusBar} from 'expo-status-bar';
import {
ActivityIndicator,
+ Image,
SafeAreaView,
ScrollView,
StyleSheet,
@@ -22,46 +23,53 @@ import {
} from '@kittentts/react-native';
import manifestJson from './assets/kittentts/manifest.json';
+const LOGO = require('./assets/kittenml_logo.png');
+
const manifest = manifestJson as KittenTTSBundledAssetsManifest;
type WorkState =
- | { kind: 'preparing' }
- | { kind: 'ready' }
- | { kind: 'speaking' }
- | { kind: 'error'; message: string };
+ | {kind: 'preparing'}
+ | {kind: 'ready'}
+ | {kind: 'speaking'}
+ | {kind: 'error'; message: string};
export default function App() {
const ttsRef = useRef(null);
const mountedRef = useRef(true);
const player = useMemo(() => createExpoAudioPlayer(ExpoAudio), []);
const models = useMemo(() => bundledAssetModels(manifest), []);
- const [model, setModel] = useState(models[0] ?? KittenModel.NanoInt8);
- const [state, setState] = useState({ kind: 'preparing' });
+ const [model, setModel] = useState(
+ models[0] ?? KittenModel.NanoInt8,
+ );
+ const [state, setState] = useState({kind: 'preparing'});
- const prepare = useCallback(async (nextModel: KittenModel) => {
- setState({ kind: 'preparing' });
- try {
- await ttsRef.current?.dispose();
- const config = await createBundledAssetConfig(manifest, {
- model: nextModel,
- defaultVoice: KittenVoice.Bella,
- });
- const tts = await KittenTTS.create({ ...config, player });
-
- if (!mountedRef.current) {
- await tts.dispose();
- return;
- }
+ const prepare = useCallback(
+ async (nextModel: KittenModel) => {
+ setState({kind: 'preparing'});
+ try {
+ await ttsRef.current?.dispose();
+ const config = await createBundledAssetConfig(manifest, {
+ model: nextModel,
+ defaultVoice: KittenVoice.Bella,
+ });
+ const tts = await KittenTTS.create({...config, player});
- ttsRef.current = tts;
- setState({ kind: 'ready' });
- } catch (error) {
- if (mountedRef.current) {
- ttsRef.current = null;
- setState({ kind: 'error', message: errorMessage(error) });
+ if (!mountedRef.current) {
+ await tts.dispose();
+ return;
+ }
+
+ ttsRef.current = tts;
+ setState({kind: 'ready'});
+ } catch (error) {
+ if (mountedRef.current) {
+ ttsRef.current = null;
+ setState({kind: 'error', message: errorMessage(error)});
+ }
}
- }
- }, [player]);
+ },
+ [player],
+ );
useEffect(() => {
mountedRef.current = true;
@@ -77,12 +85,12 @@ export default function App() {
const tts = ttsRef.current;
if (!tts) return;
- setState({ kind: 'speaking' });
+ setState({kind: 'speaking'});
try {
await tts.speak('KittenTTS is running from bundled app assets.');
- setState({ kind: 'ready' });
+ setState({kind: 'ready'});
} catch (error) {
- setState({ kind: 'error', message: errorMessage(error) });
+ setState({kind: 'error', message: errorMessage(error)});
}
}, []);
@@ -92,50 +100,117 @@ export default function App() {
- Bundled KittenTTS
- {statusText(state)}
-
- {busy ? : null}
-
-
- Bundled model
-
- {models.map((candidate) => (
- setModel(candidate)}
- >
-
+
+
+
+
+ KittenTTS Example
+
+ Offline bundled-assets example of the React Native SDK
+
+
+
+
+
+
+
+ Model
+
+ {statusSummary(state)}
+
+
+
+
+ {modelDisplayName(model)}
+
+
+
+
+
+ Bundled model
+
+ {models.map(candidate => (
+
- {modelDisplayName(candidate)}
-
-
- ))}
+ disabled={busy}
+ onPress={() => setModel(candidate)}>
+
+ {modelDisplayName(candidate)}
+
+
+ ))}
+
-
-
- Speak
-
+
+ Playback
+
+ Speak
+
+
+
+
+
+
+ This system is for demonstration purposes only and is not intended
+ to process sensitive or personal data.
+
+
);
}
+function StatusView({state}: {state: WorkState}) {
+ if (state.kind === 'ready') return null;
+
+ if (state.kind === 'error') {
+ return (
+
+
+ {state.message}
+
+
+ );
+ }
+
+ return (
+
+
+ {statusText(state)}
+
+ );
+}
+
+function statusSummary(state: WorkState): string {
+ switch (state.kind) {
+ case 'preparing':
+ return 'Preparing';
+ case 'ready':
+ return 'Ready';
+ case 'speaking':
+ return 'Speaking';
+ case 'error':
+ return 'Error';
+ }
+}
+
function statusText(state: WorkState): string {
switch (state.kind) {
case 'preparing':
@@ -156,31 +231,104 @@ function errorMessage(error: unknown): string {
const styles = StyleSheet.create({
screen: {
flex: 1,
- backgroundColor: '#F7F7F8',
+ backgroundColor: '#FAFAFA',
},
content: {
- flexGrow: 1,
+ alignSelf: 'center',
+ maxWidth: 430,
+ width: '100%',
+ paddingHorizontal: 16,
+ paddingTop: 24,
+ paddingBottom: 40,
+ },
+ header: {
+ alignItems: 'flex-start',
+ flexDirection: 'row',
+ marginBottom: 20,
+ },
+ logoMark: {
+ alignItems: 'center',
+ borderRadius: 8,
+ height: 48,
justifyContent: 'center',
- gap: 18,
- padding: 24,
+ marginRight: 12,
+ overflow: 'hidden',
+ width: 48,
+ },
+ logoImage: {
+ height: 48,
+ width: 48,
+ },
+ headerCopy: {
+ flex: 1,
},
title: {
- color: '#111827',
- fontSize: 28,
+ color: '#09090B',
+ fontSize: 30,
fontWeight: '700',
+ lineHeight: 32,
},
subtitle: {
- color: '#4B5563',
- lineHeight: 20,
+ color: '#71717A',
+ fontSize: 16,
+ lineHeight: 22,
+ marginTop: 6,
+ },
+ demoCard: {
+ backgroundColor: '#FFFFFF',
+ borderColor: '#E4E4E7',
+ borderRadius: 8,
+ borderWidth: 1,
+ padding: 16,
+ },
+ modelRow: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginBottom: 18,
+ },
+ modelRowLeft: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ flexShrink: 1,
+ gap: 8,
+ },
+ modelRowLabel: {
+ color: '#09090B',
+ fontSize: 14,
+ fontWeight: '600',
+ },
+ pill: {
+ backgroundColor: '#F4F4F5',
+ borderRadius: 8,
+ paddingHorizontal: 9,
+ paddingVertical: 4,
+ },
+ pillText: {
+ color: '#52525B',
+ fontSize: 12,
+ fontWeight: '700',
+ },
+ softBadge: {
+ backgroundColor: '#F4F4F5',
+ borderRadius: 8,
+ flexShrink: 1,
+ paddingHorizontal: 10,
+ paddingVertical: 6,
+ },
+ softBadgeText: {
+ color: '#09090B',
+ fontSize: 13,
+ fontWeight: '700',
},
section: {
- gap: 10,
+ marginBottom: 18,
},
label: {
- color: '#374151',
- fontSize: 13,
+ color: '#52525B',
+ fontSize: 12,
fontWeight: '700',
- textTransform: 'uppercase',
+ marginBottom: 8,
},
modelGrid: {
flexDirection: 'row',
@@ -188,35 +336,74 @@ const styles = StyleSheet.create({
gap: 10,
},
modelButton: {
- borderWidth: 1,
- borderColor: '#D1D5DB',
+ backgroundColor: '#F4F4F5',
borderRadius: 8,
- paddingHorizontal: 14,
- paddingVertical: 10,
+ justifyContent: 'center',
+ minHeight: 38,
+ paddingHorizontal: 12,
},
modelButtonSelected: {
- borderColor: '#2563EB',
- backgroundColor: '#DBEAFE',
+ backgroundColor: '#D4D4D8',
},
modelButtonText: {
- color: '#374151',
- fontWeight: '700',
+ color: '#52525B',
+ fontSize: 14,
+ fontWeight: '600',
},
modelButtonTextSelected: {
- color: '#1D4ED8',
+ color: '#09090B',
+ },
+ actionGroup: {
+ backgroundColor: '#F4F4F5',
+ borderRadius: 8,
+ marginTop: 2,
+ padding: 10,
+ },
+ actionGroupLabel: {
+ color: '#52525B',
+ fontSize: 12,
+ fontWeight: '700',
+ marginBottom: 8,
},
speakButton: {
alignItems: 'center',
+ backgroundColor: '#18181B',
borderRadius: 8,
- backgroundColor: '#2563EB',
- paddingHorizontal: 18,
- paddingVertical: 13,
+ justifyContent: 'center',
+ minHeight: 46,
+ paddingHorizontal: 14,
},
speakButtonText: {
color: '#FFFFFF',
+ fontSize: 15,
fontWeight: '700',
},
+ status: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ gap: 10,
+ marginTop: 16,
+ },
+ statusText: {
+ color: '#854D0E',
+ flex: 1,
+ fontSize: 13,
+ fontWeight: '600',
+ lineHeight: 18,
+ },
+ errorStatus: {
+ alignItems: 'flex-start',
+ },
+ errorStatusText: {
+ color: '#B42318',
+ },
+ disclaimer: {
+ color: '#71717A',
+ fontSize: 12,
+ lineHeight: 17,
+ marginTop: 16,
+ },
disabled: {
- opacity: 0.45,
+ opacity: 0.48,
},
});
diff --git a/examples/OfflineBundledAssetsExample/assets/kittenml_logo.png b/examples/OfflineBundledAssetsExample/assets/kittenml_logo.png
new file mode 100644
index 0000000..d1093df
Binary files /dev/null and b/examples/OfflineBundledAssetsExample/assets/kittenml_logo.png differ