fix: voice input in Electron - local Whisper STT + network error handling
- Add local Whisper STT via Transformers.js with Web Worker (no UI freeze) - Default sttProvider to 'local' in Electron (browser STT unavailable) - Fix infinite toast loop: stop auto-restart on network errors - Add retry limit with exponential backoff for transient STT errors - Append voice transcript to input field (append-inline), not replace - Add model catalog with download/load button in Voice Settings
This commit is contained in:
@@ -50,8 +50,9 @@ interface OpenChamberDefaults {
|
||||
defaultFileViewerPreview?: boolean;
|
||||
zenModel?: string;
|
||||
messageStreamTransport?: 'auto' | 'ws' | 'sse';
|
||||
sttProvider?: 'browser' | 'server';
|
||||
sttProvider?: 'browser' | 'server' | 'wasm';
|
||||
sttServerUrl?: string;
|
||||
wasmSttModel?: string;
|
||||
sttModel?: string;
|
||||
sttLanguage?: string;
|
||||
sttSilenceThresholdDb?: number;
|
||||
@@ -77,7 +78,7 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
|
||||
data?.messageStreamTransport === 'ws' || data?.messageStreamTransport === 'sse' || data?.messageStreamTransport === 'auto'
|
||||
? data.messageStreamTransport
|
||||
: undefined;
|
||||
const sttProvider = data?.sttProvider === 'browser' || data?.sttProvider === 'server' ? data.sttProvider : undefined;
|
||||
const sttProvider = data?.sttProvider === 'browser' || data?.sttProvider === 'server' || data?.sttProvider === 'wasm' ? data.sttProvider : undefined;
|
||||
const sttServerUrl = typeof data?.sttServerUrl === 'string' ? data.sttServerUrl.trim() : undefined;
|
||||
const sttModel = typeof data?.sttModel === 'string' ? data.sttModel.trim() : undefined;
|
||||
const sttLanguage = typeof data?.sttLanguage === 'string' ? data.sttLanguage.trim() : undefined;
|
||||
@@ -552,9 +553,10 @@ interface ConfigStore {
|
||||
openaiCompatibleVoice: string;
|
||||
openaiCompatibleTtsModel: string;
|
||||
// STT (speech-to-text) settings
|
||||
sttProvider: 'browser' | 'server';
|
||||
sttProvider: 'browser' | 'server' | 'wasm';
|
||||
sttServerUrl: string;
|
||||
sttModel: string;
|
||||
wasmSttModel: string;
|
||||
sttLanguage: string;
|
||||
sttSilenceThresholdDb: number;
|
||||
sttSilenceHoldMs: number;
|
||||
@@ -576,9 +578,10 @@ interface ConfigStore {
|
||||
setOpenaiCompatibleUrl: (url: string) => void;
|
||||
setOpenaiCompatibleVoice: (voice: string) => void;
|
||||
setOpenaiCompatibleTtsModel: (model: string) => void;
|
||||
setSttProvider: (provider: 'browser' | 'server') => void;
|
||||
setSttProvider: (provider: 'browser' | 'server' | 'wasm') => void;
|
||||
setSttServerUrl: (url: string) => void;
|
||||
setSttModel: (model: string) => void;
|
||||
setWasmSttModel: (model: string) => void;
|
||||
setSttLanguage: (lang: string) => void;
|
||||
setSttSilenceThresholdDb: (db: number) => void;
|
||||
setSttSilenceHoldMs: (ms: number) => void;
|
||||
@@ -761,11 +764,15 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
}
|
||||
return 'kokoro';
|
||||
})(),
|
||||
// STT provider: 'browser' (Web Speech API) or 'server' (OpenAI-compat)
|
||||
// STT provider: 'browser' (Web Speech API), 'server' (OpenAI-compat), 'wasm' (local Whisper)
|
||||
sttProvider: (() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const saved = localStorage.getItem('sttProvider');
|
||||
if (saved === 'browser' || saved === 'server') return saved;
|
||||
if (saved === 'browser' || saved === 'server' || saved === 'wasm') return saved;
|
||||
// Electron/Chromium's Web Speech API requires Google API keys
|
||||
// not available in Electron, so default to WASM local Whisper.
|
||||
const electron = (window as unknown as { __OPENCHAMBER_ELECTRON__?: { runtime?: string } }).__OPENCHAMBER_ELECTRON__;
|
||||
if (electron?.runtime === 'electron') return 'wasm' as const;
|
||||
}
|
||||
return 'browser' as const;
|
||||
})(),
|
||||
@@ -783,6 +790,13 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
}
|
||||
return 'deepdml/faster-whisper-large-v3-turbo-ct2';
|
||||
})(),
|
||||
wasmSttModel: (() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const saved = localStorage.getItem('wasmSttModel');
|
||||
if (saved) return saved;
|
||||
}
|
||||
return 'Xenova/whisper-base.en';
|
||||
})(),
|
||||
sttLanguage: (() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const saved = localStorage.getItem('sttLanguage');
|
||||
@@ -1886,7 +1900,7 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
}
|
||||
},
|
||||
|
||||
setSttProvider: (provider: 'browser' | 'server') => {
|
||||
setSttProvider: (provider: 'browser' | 'server' | 'wasm') => {
|
||||
set({ sttProvider: provider });
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('sttProvider', provider);
|
||||
@@ -1910,6 +1924,14 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
updateDesktopSettings({ sttModel: model }).catch(() => {});
|
||||
},
|
||||
|
||||
setWasmSttModel: (model: string) => {
|
||||
set({ wasmSttModel: model });
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('wasmSttModel', model);
|
||||
}
|
||||
updateDesktopSettings({ wasmSttModel: model }).catch(() => {});
|
||||
},
|
||||
|
||||
setSttLanguage: (lang: string) => {
|
||||
set({ sttLanguage: lang });
|
||||
if (typeof window !== 'undefined') {
|
||||
|
||||
Reference in New Issue
Block a user