Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 139 additions & 30 deletions apps/speech/screens/TextToSpeechScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,95 @@ import {
models,
useTextToSpeech,
TextToSpeechModelConfig,
TextToSpeechSupertonicLanguage,
} from 'react-native-executorch';
import { ModelPicker, ModelOption } from '../components/ModelPicker';

const tts = models.text_to_speech.kokoro;
// Supertonic 3: a single multilingual model with 10 built-in voices. Each voice
// works for any of the supported languages, and both the language and the
// number of flow-matching steps are set per synthesis call (no model reload) —
// see the LANGUAGES / STEPS pickers below.
const tts = models.text_to_speech.supertonic;

const VOICES: ModelOption<TextToSpeechModelConfig>[] = [
{ label: '🇺🇸 AF Heart', value: tts.en_us.heart() },
{ label: '🇺🇸 AF River', value: tts.en_us.river() },
{ label: '🇺🇸 AF Sarah', value: tts.en_us.sarah() },
{ label: '🇺🇸 AM Adam', value: tts.en_us.adam() },
{ label: '🇺🇸 AM Michael', value: tts.en_us.michael() },
{ label: '🇺🇸 AM Santa', value: tts.en_us.santa() },
{ label: '🇬🇧 BF Emma', value: tts.en_gb.emma() },
{ label: '🇬🇧 BM Daniel', value: tts.en_gb.daniel() },
{ label: '🇫🇷 FF Siwis', value: tts.fr.siwis() },
{ label: '🇪🇸 EF Dora', value: tts.es.dora() },
{ label: '🇪🇸 EM Alex', value: tts.es.alex() },
{ label: '🇮🇹 IF Sara', value: tts.it.sara() },
{ label: '🇮🇹 IM Nicola', value: tts.it.nicola() },
{ label: '🇵🇹 PF Dora', value: tts.pt.dora() },
{ label: '🇵🇹 PM Santa', value: tts.pt.santa() },
{ label: '🇩🇪 DF Anna', value: tts.de.anna() },
{ label: '🇵🇱 PM Mateusz', value: tts.pl.mateusz() },
{ label: '🇮🇳 HF Alpha', value: tts.hi.alpha() },
{ label: '🇮🇳 HM Omega', value: tts.hi.omega() },
{ label: '🇮🇳 HM Psi', value: tts.hi.psi() },
{ label: '👨 Male 1', value: tts.m1() },
{ label: '👨 Male 2', value: tts.m2() },
{ label: '👨 Male 3', value: tts.m3() },
{ label: '👨 Male 4', value: tts.m4() },
{ label: '👨 Male 5', value: tts.m5() },
{ label: '👩 Female 1', value: tts.f1() },
{ label: '👩 Female 2', value: tts.f2() },
{ label: '👩 Female 3', value: tts.f3() },
{ label: '👩 Female 4', value: tts.f4() },
{ label: '👩 Female 5', value: tts.f5() },
];

// A per-call hyperparameter: the `<lang>` token. 'na' = unknown/other.
const LANGUAGES: ModelOption<TextToSpeechSupertonicLanguage>[] = [
{ label: '🇺🇸 English', value: 'en' },
{ label: '🇪🇸 Spanish', value: 'es' },
{ label: '🇫🇷 French', value: 'fr' },
{ label: '🇩🇪 German', value: 'de' },
{ label: '🇮🇹 Italian', value: 'it' },
{ label: '🇵🇹 Portuguese', value: 'pt' },
{ label: '🇵🇱 Polish', value: 'pl' },
{ label: '🇷🇺 Russian', value: 'ru' },
{ label: '🇯🇵 Japanese', value: 'ja' },
{ label: '🇰🇷 Korean', value: 'ko' },
{ label: '🇮🇳 Hindi', value: 'hi' },
{ label: '🌐 Other (na)', value: 'na' },
];

// A per-call hyperparameter: number of flow-matching steps (quality vs. speed).
const STEPS: ModelOption<number>[] = [
{ label: '6 (fastest)', value: 6 },
{ label: '8 (default)', value: 8 },
{ label: '16 (best quality)', value: 16 },
];

// Kokoro: language-specific voices. Each voice bundles its own phonemizer
// config, so only the voice picker is shown (no language/steps controls).
const KOKORO_LANG_LABELS: Record<string, string> = {
en_us: '🇺🇸 US English',
en_gb: '🇬🇧 UK English',
fr: '🇫🇷 French',
es: '🇪🇸 Spanish',
it: '🇮🇹 Italian',
pt: '🇵🇹 Portuguese',
hi: '🇮🇳 Hindi',
pl: '🇵🇱 Polish',
de: '🇩🇪 German',
};

const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);

// `models.text_to_speech.kokoro.<lang>.<voice>` are factory functions
// (`() => TextToSpeechModelConfig`); call them to get the actual configs.
const kokoroVoicesByLang = models.text_to_speech.kokoro as unknown as Record<
string,
Record<string, () => TextToSpeechModelConfig>
>;

const KOKORO_VOICES: ModelOption<TextToSpeechModelConfig>[] = Object.entries(
kokoroVoicesByLang
).flatMap(([lang, voices]) =>
Object.entries(voices).map(([name, factory]) => ({
label: `${KOKORO_LANG_LABELS[lang] ?? lang} · ${capitalize(name)}`,
value: factory(),
}))
);

type TtsModelType = 'supertonic' | 'kokoro';

const TTS_MODEL_OPTIONS: ModelOption<TtsModelType>[] = [
{ label: 'Supertonic', value: 'supertonic' },
{ label: 'Kokoro', value: 'kokoro' },
];

// Supertonic renders at 44.1 kHz, Kokoro at 24 kHz.
const SUPERTONIC_SAMPLE_RATE = 44100;
const KOKORO_SAMPLE_RATE = 24000;

import FontAwesome from '@expo/vector-icons/FontAwesome';
import {
AudioManager,
Expand All @@ -56,13 +117,13 @@ import ErrorBanner from '../components/ErrorBanner';
* Converts an audio vector (Float32Array) to an AudioBuffer for playback
* @param audioVector - The generated audio samples from the model
* @param audioContext - An optional AudioContext to create the buffer in. If not provided, a new one will be created.
* @param sampleRate - The sample rate (default: 24000 Hz for Kokoro)
* @param sampleRate - The sample rate (default: 44100 Hz for Supertonic)
* @returns AudioBuffer ready for playback
*/
const createAudioBufferFromVector = (
audioVector: Float32Array,
audioContext: AudioContext | null = null,
sampleRate: number = 24000
sampleRate: number = SUPERTONIC_SAMPLE_RATE
): AudioBuffer => {
if (audioContext == null) audioContext = new AudioContext({ sampleRate });

Expand All @@ -78,11 +139,29 @@ const createAudioBufferFromVector = (
};

export const TextToSpeechScreen = ({ onBack }: { onBack: () => void }) => {
const [selectedTtsModel, setSelectedTtsModel] =
useState<TtsModelType>('supertonic');
const [selectedSpeaker, setSelectedSpeaker] =
useState<TextToSpeechModelConfig>(tts.en_us.heart());
useState<TextToSpeechModelConfig>(tts.m1());
const [selectedLang, setSelectedLang] =
useState<TextToSpeechSupertonicLanguage>('en');
const [totalSteps, setTotalSteps] = useState<number>(8);

const model = useTextToSpeech(selectedSpeaker);

const sampleRate =
selectedTtsModel === 'supertonic'
? SUPERTONIC_SAMPLE_RATE
: KOKORO_SAMPLE_RATE;

const handleSelectTtsModel = (type: TtsModelType) => {
if (type === selectedTtsModel) return;
setSelectedTtsModel(type);
setSelectedSpeaker(
type === 'supertonic' ? VOICES[0].value : KOKORO_VOICES[0].value
);
};

const [inputText, setInputText] = useState('');
const [isPlaying, setIsPlaying] = useState(false);
const [readyToGenerate, setReadyToGenerate] = useState(false);
Expand All @@ -99,7 +178,7 @@ export const TextToSpeechScreen = ({ onBack }: { onBack: () => void }) => {
iosOptions: ['defaultToSpeaker'],
});

const context = new AudioContext({ sampleRate: 24000 });
const context = new AudioContext({ sampleRate });
audioContextRef.current = context;
context.suspend();

Expand All @@ -114,7 +193,7 @@ export const TextToSpeechScreen = ({ onBack }: { onBack: () => void }) => {
audioContextRef.current = null;
gainNodeRef.current = null;
};
}, []);
}, [sampleRate]);

useEffect(() => {
setReadyToGenerate(!model.isGenerating && model.isReady && !isPlaying);
Expand All @@ -141,7 +220,7 @@ export const TextToSpeechScreen = ({ onBack }: { onBack: () => void }) => {
const audioBuffer = createAudioBufferFromVector(
audioVec,
audioContext,
24000
sampleRate
);

const source = (sourceRef.current =
Expand All @@ -167,8 +246,10 @@ export const TextToSpeechScreen = ({ onBack }: { onBack: () => void }) => {

await model.stream({
text: inputText,
speed: 0.9,
phonemize: true,
speed: 1.0,
...(selectedTtsModel === 'supertonic'
? { totalSteps, lang: selectedLang }
: {}),
onNext,
onEnd,
});
Expand Down Expand Up @@ -209,14 +290,42 @@ export const TextToSpeechScreen = ({ onBack }: { onBack: () => void }) => {
</View>
<ErrorBanner message={error} onDismiss={() => setError(null)} />

<ModelPicker
label="Model"
models={TTS_MODEL_OPTIONS}
selectedModel={selectedTtsModel}
disabled={model.isGenerating || isPlaying}
onSelect={handleSelectTtsModel}
/>

<ModelPicker
label="Voice"
models={VOICES}
models={selectedTtsModel === 'supertonic' ? VOICES : KOKORO_VOICES}
selectedModel={selectedSpeaker}
disabled={model.isGenerating}
onSelect={(m) => setSelectedSpeaker(m)}
/>

{selectedTtsModel === 'supertonic' && (
<>
<ModelPicker
label="Language"
models={LANGUAGES}
selectedModel={selectedLang}
disabled={isPlaying}
onSelect={(l) => setSelectedLang(l)}
/>

<ModelPicker
label="Steps"
models={STEPS}
selectedModel={totalSteps}
disabled={isPlaying}
onSelect={(s) => setTotalSteps(s)}
/>
</>
)}

<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>Enter text to synthesize</Text>
<TextInput
Expand Down
5 changes: 1 addition & 4 deletions apps/speech/screens/VoiceActivityDetectionScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@ import {
Platform,
} from 'react-native';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import {
models,
useVAD
} from 'react-native-executorch';
import { models, useVAD } from 'react-native-executorch';
import FontAwesome from '@expo/vector-icons/FontAwesome';
import { AudioManager, AudioRecorder } from 'react-native-audio-api';
import SWMIcon from '../assets/swm_icon.svg';
Expand Down
Loading
Loading