mirror of
https://github.com/R0m1k3/EveFlow.git
synced 2026-10-18 20:59:11 +02:00
feat: local offline voice models (sherpa-onnx) with model manager and wake word (v2.1.0)
- electron/voice: model catalog (Whisper base/small/turbo, SenseVoice, Kokoro v1.0, Piper fr), streaming tar.bz2 downloader with progress, sherpa-onnx worker running in an Electron utilityProcess (transcribe / synthesize / status / unload), IPC + bridge. - Renderer: 'local' providers for STT and TTS, Settings → Modèles locaux (download, progress, delete, activate), speaker selection, hands-free wake word with a tolerant matcher (accents, punctuation, edit distance) and attention window after a bare wake word. - Packaging: native addon and worker unpacked from the asar; release 2.1.0. Validated on Linux: Kokoro (fr) → Whisper base round trip, Piper (fr) → Whisper round trip. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Wn5VX9HNbJ7N54hR24u9Y
This commit is contained in:
22 files changed
+1361
-16
No files matched your search
@@ -16,6 +16,9 @@ export interface VoiceSettings extends SttConfig {
|
||||
silenceMs: number;
|
||||
sensitivity: number; // 1 (low) .. 5 (high)
|
||||
wakeChime: boolean;
|
||||
/** Hands-free: only react to utterances starting with this word (local STT recommended). */
|
||||
wakeWordEnabled: boolean;
|
||||
wakeWord: string;
|
||||
}
|
||||
|
||||
export interface SpeechSettings extends TtsConfig {
|
||||
@@ -77,7 +80,10 @@ export const DEFAULT_SETTINGS: Settings = {
|
||||
micDeviceId: '',
|
||||
silenceMs: 900,
|
||||
sensitivity: 3,
|
||||
wakeChime: true
|
||||
wakeChime: true,
|
||||
wakeWordEnabled: false,
|
||||
wakeWord: 'jarvis',
|
||||
localModel: 'whisper-base'
|
||||
},
|
||||
speech: {
|
||||
provider: 'openai-compatible',
|
||||
@@ -90,6 +96,8 @@ export const DEFAULT_SETTINGS: Settings = {
|
||||
systemVoice: '',
|
||||
language: 'fr-FR',
|
||||
volume: 1,
|
||||
localModel: 'kokoro-v1',
|
||||
localSpeaker: 30,
|
||||
autoSpeak: true,
|
||||
speakIncoming: true
|
||||
},
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { create } from 'zustand';
|
||||
import type { VoiceDownloadProgress, VoiceEngineStatus, VoiceModelStatus } from '../../shared/voice';
|
||||
import { bridge } from '../lib/bridge';
|
||||
import { Log } from '../lib/log';
|
||||
|
||||
interface VoiceModelsStore {
|
||||
models: VoiceModelStatus[];
|
||||
progress: Record<string, VoiceDownloadProgress>;
|
||||
engine: VoiceEngineStatus | null;
|
||||
error: string | null;
|
||||
refresh: () => Promise<void>;
|
||||
checkEngine: () => Promise<void>;
|
||||
download: (id: string) => Promise<void>;
|
||||
cancel: (id: string) => Promise<void>;
|
||||
remove: (id: string) => Promise<void>;
|
||||
subscribe: () => () => void;
|
||||
}
|
||||
|
||||
export const useVoiceModels = create<VoiceModelsStore>((set, get) => ({
|
||||
models: [],
|
||||
progress: {},
|
||||
engine: null,
|
||||
error: null,
|
||||
|
||||
refresh: async () => {
|
||||
const api = bridge();
|
||||
if (!api) return;
|
||||
try {
|
||||
set({ models: await api.voice.listModels(), error: null });
|
||||
} catch (err) {
|
||||
set({ error: (err as Error).message });
|
||||
}
|
||||
},
|
||||
checkEngine: async () => {
|
||||
const api = bridge();
|
||||
if (!api) return;
|
||||
try {
|
||||
set({ engine: await api.voice.status() });
|
||||
} catch (err) {
|
||||
set({ engine: { available: false, error: (err as Error).message, loaded: [], modelsDir: '' } });
|
||||
}
|
||||
},
|
||||
download: async (id) => {
|
||||
const api = bridge();
|
||||
if (!api) return;
|
||||
set((s) => ({ progress: { ...s.progress, [id]: { id, phase: 'download', received: 0, total: 0, percent: 0 } }, error: null }));
|
||||
try {
|
||||
await api.voice.downloadModel(id);
|
||||
} catch (err) {
|
||||
const message = (err as Error).message;
|
||||
Log.error('voice', `download ${id} failed: ${message}`);
|
||||
set({ error: message });
|
||||
} finally {
|
||||
await get().refresh();
|
||||
}
|
||||
},
|
||||
cancel: async (id) => {
|
||||
await bridge()?.voice.cancelDownload(id);
|
||||
},
|
||||
remove: async (id) => {
|
||||
const api = bridge();
|
||||
if (!api) return;
|
||||
try {
|
||||
set({ models: await api.voice.removeModel(id) });
|
||||
} catch (err) {
|
||||
set({ error: (err as Error).message });
|
||||
}
|
||||
},
|
||||
subscribe: () => {
|
||||
const api = bridge();
|
||||
if (!api) return () => undefined;
|
||||
return api.voice.onProgress((p) => {
|
||||
set((s) => {
|
||||
const progress = { ...s.progress, [p.id]: p };
|
||||
if (p.phase === 'done' || p.phase === 'error' || p.phase === 'cancelled') {
|
||||
setTimeout(() => {
|
||||
set((s2) => {
|
||||
const next = { ...s2.progress };
|
||||
delete next[p.id];
|
||||
return { progress: next };
|
||||
});
|
||||
void get().refresh();
|
||||
}, 1500);
|
||||
}
|
||||
return { progress };
|
||||
});
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
export const installedModels = (models: VoiceModelStatus[], kind: 'stt' | 'tts') => models.filter((m) => m.kind === kind && m.installed);
|
||||
Reference in new issue
Block a user