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