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"> - @@ -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"> - 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 () => {