diff --git a/app/src/components/settings/panels/VoicePanel.tsx b/app/src/components/settings/panels/VoicePanel.tsx
index f512200db..46cc055f9 100644
--- a/app/src/components/settings/panels/VoicePanel.tsx
+++ b/app/src/components/settings/panels/VoicePanel.tsx
@@ -416,8 +416,9 @@ const VoicePanel = ({ embedded = false }: VoicePanelProps = {}) => {
Choose where transcription and synthesis run. Use the Install locally buttons to
- download the binaries and models into your workspace — no manual{' '}
- WHISPER_BIN or PIPER_BIN setup required.
+ download the binaries and models into your workspace. Local providers can be saved
+ before the install finishes — no manual WHISPER_BIN or{' '}
+ PIPER_BIN setup required.
@@ -433,7 +434,7 @@ const VoicePanel = ({ embedded = false }: VoicePanelProps = {}) => {
onChange={e => onSttProviderChange(e.target.value as 'cloud' | 'whisper')}
className="w-full rounded-md border border-stone-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 px-3 py-2 text-sm text-stone-900 dark:text-neutral-100 focus:outline-none focus:ring-1 focus:ring-primary-400">
Cloud (Whisper proxy)
-
+
Local Whisper{whisperReady ? '' : ' (install required)'}
@@ -523,7 +524,7 @@ const VoicePanel = ({ embedded = false }: VoicePanelProps = {}) => {
onChange={e => onTtsProviderChange(e.target.value as 'cloud' | 'piper')}
className="w-full rounded-md border border-stone-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 px-3 py-2 text-sm text-stone-900 dark:text-neutral-100 focus:outline-none focus:ring-1 focus:ring-primary-400">
Cloud (ElevenLabs proxy)
-
+
Local Piper{piperReady ? '' : ' (install required)'}
diff --git a/app/src/components/settings/panels/__tests__/VoicePanel.test.tsx b/app/src/components/settings/panels/__tests__/VoicePanel.test.tsx
index 49e0484b2..18d85ab2e 100644
--- a/app/src/components/settings/panels/__tests__/VoicePanel.test.tsx
+++ b/app/src/components/settings/panels/__tests__/VoicePanel.test.tsx
@@ -332,7 +332,7 @@ describe('VoicePanel', () => {
expect(screen.getByTestId('whisper-install-state')).toHaveTextContent('Not installed');
});
- it('disables the Local Whisper STT option when the engine is missing', async () => {
+ it('keeps the Local Whisper STT option selectable when the engine is missing', async () => {
runtime.whisperStatus = makeInstallStatus('whisper');
renderWithProviders( , { initialEntries: ['/settings/voice'] });
@@ -341,8 +341,15 @@ describe('VoicePanel', () => {
'option[value="whisper"]'
) as HTMLOptionElement | null;
expect(whisperOption).not.toBeNull();
- expect(whisperOption!.disabled).toBe(true);
+ expect(whisperOption!.disabled).toBe(false);
expect(whisperOption!.textContent).toMatch(/install required/i);
+
+ fireEvent.change(sttSelect, { target: { value: 'whisper' } });
+ await waitFor(() =>
+ expect(vi.mocked(openhumanVoiceSetProviders)).toHaveBeenCalledWith(
+ expect.objectContaining({ stt_provider: 'whisper' })
+ )
+ );
});
it('shows a Reinstall label once Whisper is installed', async () => {
@@ -390,7 +397,7 @@ describe('VoicePanel', () => {
expect(screen.getByTestId('piper-install-state')).toHaveTextContent('Not installed');
});
- it('disables the Local Piper TTS option when the engine is missing', async () => {
+ it('keeps the Local Piper TTS option selectable when the engine is missing', async () => {
runtime.piperStatus = makeInstallStatus('piper');
renderWithProviders( , { initialEntries: ['/settings/voice'] });
@@ -399,8 +406,15 @@ describe('VoicePanel', () => {
'option[value="piper"]'
) as HTMLOptionElement | null;
expect(piperOption).not.toBeNull();
- expect(piperOption!.disabled).toBe(true);
+ expect(piperOption!.disabled).toBe(false);
expect(piperOption!.textContent).toMatch(/install required/i);
+
+ fireEvent.change(ttsSelect, { target: { value: 'piper' } });
+ await waitFor(() =>
+ expect(vi.mocked(openhumanVoiceSetProviders)).toHaveBeenCalledWith(
+ expect.objectContaining({ tts_provider: 'piper' })
+ )
+ );
});
it('triggers installPiper when the user clicks Install', async () => {