-
- {t('settings.voice.page.stt.wasmNotLoaded')}
-
-
+
+ Kokoro
+ 305 MB
+ {model.installed ? (
+ <>
+
+
+ >
+ ) : model.downloading ? (
+
+
+
+ {typeof model.downloadProgress === 'number' ? `${model.downloadProgress}%` : ''}
+
+
+ ) : (
+
+ )}
+ {model.downloadError ? (
+ {model.downloadError}
+ ) : null}
);
};
@@ -131,11 +389,6 @@ const OPENAI_VOICE_OPTIONS = [
export const VoiceSettings: React.FC = () => {
const { t } = useI18n();
const { isMobile } = useDeviceInfo();
- const {
- isSupported,
- language,
- setLanguage,
- } = useBrowserVoice();
const voiceProvider = useConfigStore((state) => state.voiceProvider);
const setVoiceProvider = useConfigStore((state) => state.setVoiceProvider);
const speechRate = useConfigStore((state) => state.speechRate);
@@ -146,6 +399,22 @@ export const VoiceSettings: React.FC = () => {
const setSpeechVolume = useConfigStore((state) => state.setSpeechVolume);
const sayVoice = useConfigStore((state) => state.sayVoice);
const setSayVoice = useConfigStore((state) => state.setSayVoice);
+ const localTtsVoiceId = useConfigStore((state) => state.localTtsVoiceId);
+ const setLocalTtsVoiceId = useConfigStore((state) => state.setLocalTtsVoiceId);
+ const { speak: speakLocalTts, stop: stopLocalTts, isPlaying: isLocalTtsPlaying, error: localTtsError } = useLocalTTS();
+
+ const previewLocalVoice = useCallback(() => {
+ if (isLocalTtsPlaying) {
+ stopLocalTts();
+ return;
+ }
+ const voiceLabel = KOKORO_VOICE_OPTIONS.find((v) => v.id === localTtsVoiceId)?.label
+ ?? String(localTtsVoiceId);
+ void speakLocalTts(t('settings.voice.page.preview.voiceLine', { voiceName: voiceLabel }), {
+ speakerId: localTtsVoiceId,
+ speed: useConfigStore.getState().speechRate,
+ });
+ }, [isLocalTtsPlaying, localTtsVoiceId, speakLocalTts, stopLocalTts, t]);
const browserVoice = useConfigStore((state) => state.browserVoice);
const setBrowserVoice = useConfigStore((state) => state.setBrowserVoice);
const openaiVoice = useConfigStore((state) => state.openaiVoice);
@@ -172,19 +441,13 @@ export const VoiceSettings: React.FC = () => {
const setSttApiKey = useConfigStore((state) => state.setSttApiKey);
const sttModel = useConfigStore((state) => state.sttModel);
const setSttModel = useConfigStore((state) => state.setSttModel);
- const wasmSttModel = useConfigStore((state) => state.wasmSttModel);
- const setWasmSttModel = useConfigStore((state) => state.setWasmSttModel);
+ const sttLocalModel = useConfigStore((state) => state.sttLocalModel);
+ const setSttLocalModel = useConfigStore((state) => state.setSttLocalModel);
const sttLanguage = useConfigStore((state) => state.sttLanguage);
const setSttLanguage = useConfigStore((state) => state.setSttLanguage);
- const sttSilenceThresholdDb = useConfigStore((state) => state.sttSilenceThresholdDb);
- const setSttSilenceThresholdDb = useConfigStore((state) => state.setSttSilenceThresholdDb);
- const sttSilenceHoldMs = useConfigStore((state) => state.sttSilenceHoldMs);
- const setSttSilenceHoldMs = useConfigStore((state) => state.setSttSilenceHoldMs);
- const sttTranscribeOnStop = useConfigStore((state) => state.sttTranscribeOnStop);
- const setSttTranscribeOnStop = useConfigStore((state) => state.setSttTranscribeOnStop);
const setShowMessageTTSButtons = useConfigStore((state) => state.setShowMessageTTSButtons);
- const voiceModeEnabled = useConfigStore((state) => state.voiceModeEnabled);
- const setVoiceModeEnabled = useConfigStore((state) => state.setVoiceModeEnabled);
+ const dictationEnabled = useConfigStore((state) => state.dictationEnabled);
+ const setDictationEnabled = useConfigStore((state) => state.setDictationEnabled);
const [isSayAvailable, setIsSayAvailable] = useState(false);
const [sayVoices, setSayVoices] = useState
>([]);
@@ -274,7 +537,7 @@ export const VoiceSettings: React.FC = () => {
}, [isBrowserPreviewPlaying]);
useEffect(() => {
- if (!voiceModeEnabled || (voiceProvider !== 'openai' && voiceProvider !== 'openai-compatible')) {
+ if (!showMessageTTSButtons || (voiceProvider !== 'openai' && voiceProvider !== 'openai-compatible')) {
setIsOpenAIAvailable(openaiApiKey.trim().length > 0);
return;
}
@@ -292,10 +555,10 @@ export const VoiceSettings: React.FC = () => {
};
checkOpenAIAvailability();
- }, [openaiApiKey, voiceModeEnabled, voiceProvider]);
+ }, [openaiApiKey, showMessageTTSButtons, voiceProvider]);
useEffect(() => {
- if (!voiceModeEnabled) {
+ if (!showMessageTTSButtons) {
setIsSayAvailable(false);
setSayVoices([]);
return;
@@ -317,7 +580,7 @@ export const VoiceSettings: React.FC = () => {
.catch(() => {
setIsSayAvailable(false);
});
- }, [voiceModeEnabled]);
+ }, [showMessageTTSButtons]);
const previewVoice = useCallback(async () => {
if (previewAudio) {
@@ -498,11 +761,11 @@ export const VoiceSettings: React.FC = () => {
return (
- {/* Voice Setup */}
-
+ {/* Playback (read messages aloud) */}
+
- {t('settings.voice.page.section.voiceSetup')}
+ {t('settings.voice.page.section.playbackAndSummary')}
@@ -512,15 +775,15 @@ export const VoiceSettings: React.FC = () => {
className="group flex cursor-pointer items-center gap-2 py-1.5"
role="button"
tabIndex={0}
- aria-pressed={voiceModeEnabled}
- onClick={() => setVoiceModeEnabled(!voiceModeEnabled)}
- onKeyDown={(e) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); setVoiceModeEnabled(!voiceModeEnabled); } }}
+ aria-pressed={showMessageTTSButtons}
+ onClick={() => setShowMessageTTSButtons(!showMessageTTSButtons)}
+ onKeyDown={(e) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); setShowMessageTTSButtons(!showMessageTTSButtons); } }}
>
-
-
{t('settings.voice.page.field.enableVoiceMode')}
+
+
{t('settings.voice.page.field.messageReadAloudButton')}
- {voiceModeEnabled && (
+ {showMessageTTSButtons && (
<>
@@ -533,6 +796,7 @@ export const VoiceSettings: React.FC = () => {
- {t('settings.voice.page.provider.browser')} {t('settings.voice.page.tooltip.browser')}
+ - {t('settings.voice.page.provider.local')} {t('settings.voice.page.tooltip.localTts')}
- OpenAI: {t('settings.voice.page.tooltip.openai')}
- {t('settings.voice.page.provider.custom')} {t('settings.voice.page.tooltip.custom')}
- {t('settings.voice.page.provider.say')} {t('settings.voice.page.tooltip.say')}
@@ -550,6 +814,15 @@ export const VoiceSettings: React.FC = () => {
>
{t('settings.voice.page.provider.browser')}
+
)}
+ {/* Local (Kokoro) TTS model status */}
+ {voiceProvider === 'local' &&
}
+
{/* Voice Selection */}
{t('settings.voice.page.field.voice')}
+ {voiceProvider === 'local' && (
+ <>
+
+
+ {localTtsError ? (
+
{localTtsError}
+ ) : null}
+ >
+ )}
+
{voiceProvider === 'openai' && isOpenAIAvailable && (
<>