@@ -60,8 +60,48 @@ const STEPS: ModelOption<number>[] = [
6060 { label : '16 (best quality)' , value : 16 } ,
6161] ;
6262
63- // Supertonic renders audio at 44.1 kHz.
64- const SAMPLE_RATE = 44100 ;
63+ // Kokoro: language-specific voices. Each voice bundles its own phonemizer
64+ // config, so only the voice picker is shown (no language/steps controls).
65+ const KOKORO_LANG_LABELS : Record < string , string > = {
66+ en_us : '🇺🇸 US English' ,
67+ en_gb : '🇬🇧 UK English' ,
68+ fr : '🇫🇷 French' ,
69+ es : '🇪🇸 Spanish' ,
70+ it : '🇮🇹 Italian' ,
71+ pt : '🇵🇹 Portuguese' ,
72+ hi : '🇮🇳 Hindi' ,
73+ pl : '🇵🇱 Polish' ,
74+ de : '🇩🇪 German' ,
75+ } ;
76+
77+ const capitalize = ( s : string ) => s . charAt ( 0 ) . toUpperCase ( ) + s . slice ( 1 ) ;
78+
79+ // `models.text_to_speech.kokoro.<lang>.<voice>` are factory functions
80+ // (`() => TextToSpeechModelConfig`); call them to get the actual configs.
81+ const kokoroVoicesByLang = models . text_to_speech . kokoro as unknown as Record <
82+ string ,
83+ Record < string , ( ) => TextToSpeechModelConfig >
84+ > ;
85+
86+ const KOKORO_VOICES : ModelOption < TextToSpeechModelConfig > [ ] = Object . entries (
87+ kokoroVoicesByLang
88+ ) . flatMap ( ( [ lang , voices ] ) =>
89+ Object . entries ( voices ) . map ( ( [ name , factory ] ) => ( {
90+ label : `${ KOKORO_LANG_LABELS [ lang ] ?? lang } · ${ capitalize ( name ) } ` ,
91+ value : factory ( ) ,
92+ } ) )
93+ ) ;
94+
95+ type TtsModelType = 'supertonic' | 'kokoro' ;
96+
97+ const TTS_MODEL_OPTIONS : ModelOption < TtsModelType > [ ] = [
98+ { label : 'Supertonic' , value : 'supertonic' } ,
99+ { label : 'Kokoro' , value : 'kokoro' } ,
100+ ] ;
101+
102+ // Supertonic renders at 44.1 kHz, Kokoro at 24 kHz.
103+ const SUPERTONIC_SAMPLE_RATE = 44100 ;
104+ const KOKORO_SAMPLE_RATE = 24000 ;
65105
66106import FontAwesome from '@expo/vector-icons/FontAwesome' ;
67107import {
@@ -83,7 +123,7 @@ import ErrorBanner from '../components/ErrorBanner';
83123const createAudioBufferFromVector = (
84124 audioVector : Float32Array ,
85125 audioContext : AudioContext | null = null ,
86- sampleRate : number = SAMPLE_RATE
126+ sampleRate : number = SUPERTONIC_SAMPLE_RATE
87127) : AudioBuffer => {
88128 if ( audioContext == null ) audioContext = new AudioContext ( { sampleRate } ) ;
89129
@@ -99,6 +139,8 @@ const createAudioBufferFromVector = (
99139} ;
100140
101141export const TextToSpeechScreen = ( { onBack } : { onBack : ( ) => void } ) => {
142+ const [ selectedTtsModel , setSelectedTtsModel ] =
143+ useState < TtsModelType > ( 'supertonic' ) ;
102144 const [ selectedSpeaker , setSelectedSpeaker ] =
103145 useState < TextToSpeechModelConfig > ( tts . m1 ( ) ) ;
104146 const [ selectedLang , setSelectedLang ] =
@@ -107,6 +149,19 @@ export const TextToSpeechScreen = ({ onBack }: { onBack: () => void }) => {
107149
108150 const model = useTextToSpeech ( selectedSpeaker ) ;
109151
152+ const sampleRate =
153+ selectedTtsModel === 'supertonic'
154+ ? SUPERTONIC_SAMPLE_RATE
155+ : KOKORO_SAMPLE_RATE ;
156+
157+ const handleSelectTtsModel = ( type : TtsModelType ) => {
158+ if ( type === selectedTtsModel ) return ;
159+ setSelectedTtsModel ( type ) ;
160+ setSelectedSpeaker (
161+ type === 'supertonic' ? VOICES [ 0 ] . value : KOKORO_VOICES [ 0 ] . value
162+ ) ;
163+ } ;
164+
110165 const [ inputText , setInputText ] = useState ( '' ) ;
111166 const [ isPlaying , setIsPlaying ] = useState ( false ) ;
112167 const [ readyToGenerate , setReadyToGenerate ] = useState ( false ) ;
@@ -123,7 +178,7 @@ export const TextToSpeechScreen = ({ onBack }: { onBack: () => void }) => {
123178 iosOptions : [ 'defaultToSpeaker' ] ,
124179 } ) ;
125180
126- const context = new AudioContext ( { sampleRate : SAMPLE_RATE } ) ;
181+ const context = new AudioContext ( { sampleRate } ) ;
127182 audioContextRef . current = context ;
128183 context . suspend ( ) ;
129184
@@ -138,7 +193,7 @@ export const TextToSpeechScreen = ({ onBack }: { onBack: () => void }) => {
138193 audioContextRef . current = null ;
139194 gainNodeRef . current = null ;
140195 } ;
141- } , [ ] ) ;
196+ } , [ sampleRate ] ) ;
142197
143198 useEffect ( ( ) => {
144199 setReadyToGenerate ( ! model . isGenerating && model . isReady && ! isPlaying ) ;
@@ -165,7 +220,7 @@ export const TextToSpeechScreen = ({ onBack }: { onBack: () => void }) => {
165220 const audioBuffer = createAudioBufferFromVector (
166221 audioVec ,
167222 audioContext ,
168- SAMPLE_RATE
223+ sampleRate
169224 ) ;
170225
171226 const source = ( sourceRef . current =
@@ -192,8 +247,9 @@ export const TextToSpeechScreen = ({ onBack }: { onBack: () => void }) => {
192247 await model . stream ( {
193248 text : inputText ,
194249 speed : 1.0 ,
195- totalSteps,
196- lang : selectedLang ,
250+ ...( selectedTtsModel === 'supertonic'
251+ ? { totalSteps, lang : selectedLang }
252+ : { } ) ,
197253 onNext,
198254 onEnd,
199255 } ) ;
@@ -234,29 +290,41 @@ export const TextToSpeechScreen = ({ onBack }: { onBack: () => void }) => {
234290 </ View >
235291 < ErrorBanner message = { error } onDismiss = { ( ) => setError ( null ) } />
236292
293+ < ModelPicker
294+ label = "Model"
295+ models = { TTS_MODEL_OPTIONS }
296+ selectedModel = { selectedTtsModel }
297+ disabled = { model . isGenerating || isPlaying }
298+ onSelect = { handleSelectTtsModel }
299+ />
300+
237301 < ModelPicker
238302 label = "Voice"
239- models = { VOICES }
303+ models = { selectedTtsModel === 'supertonic' ? VOICES : KOKORO_VOICES }
240304 selectedModel = { selectedSpeaker }
241305 disabled = { model . isGenerating }
242306 onSelect = { ( m ) => setSelectedSpeaker ( m ) }
243307 />
244308
245- < ModelPicker
246- label = "Language"
247- models = { LANGUAGES }
248- selectedModel = { selectedLang }
249- disabled = { isPlaying }
250- onSelect = { ( l ) => setSelectedLang ( l ) }
251- />
309+ { selectedTtsModel === 'supertonic' && (
310+ < >
311+ < ModelPicker
312+ label = "Language"
313+ models = { LANGUAGES }
314+ selectedModel = { selectedLang }
315+ disabled = { isPlaying }
316+ onSelect = { ( l ) => setSelectedLang ( l ) }
317+ />
252318
253- < ModelPicker
254- label = "Steps"
255- models = { STEPS }
256- selectedModel = { totalSteps }
257- disabled = { isPlaying }
258- onSelect = { ( s ) => setTotalSteps ( s ) }
259- />
319+ < ModelPicker
320+ label = "Steps"
321+ models = { STEPS }
322+ selectedModel = { totalSteps }
323+ disabled = { isPlaying }
324+ onSelect = { ( s ) => setTotalSteps ( s ) }
325+ />
326+ </ >
327+ ) }
260328
261329 < View style = { styles . inputContainer } >
262330 < Text style = { styles . inputLabel } > Enter text to synthesize</ Text >
0 commit comments