mirror of
https://github.com/R0m1k3/EveFlow.git
synced 2026-10-23 23:28:14 +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
@@ -4,11 +4,12 @@
|
||||
* and the legacy Google Translate endpoint (no key, online only).
|
||||
*/
|
||||
import { Log } from '../../lib/log';
|
||||
import { bridge } from '../../lib/bridge';
|
||||
import { chunkForSpeech, cleanForSpeech, extractSentences } from '../../lib/text';
|
||||
import { httpFetch } from '../../lib/transport';
|
||||
import { audioBus } from './audioBus';
|
||||
|
||||
export type TtsProvider = 'openai-compatible' | 'system' | 'google-free' | 'off';
|
||||
export type TtsProvider = 'openai-compatible' | 'system' | 'google-free' | 'local' | 'off';
|
||||
|
||||
export interface TtsConfig {
|
||||
provider: TtsProvider;
|
||||
@@ -21,6 +22,10 @@ export interface TtsConfig {
|
||||
systemVoice: string;
|
||||
language: string;
|
||||
volume: number;
|
||||
/** Catalog id of the local sherpa-onnx voice model (provider 'local'). */
|
||||
localModel: string;
|
||||
/** Speaker id inside the local model. */
|
||||
localSpeaker: number;
|
||||
}
|
||||
|
||||
export type TtsState = 'idle' | 'loading' | 'speaking';
|
||||
@@ -159,7 +164,8 @@ export class TtsEngine {
|
||||
}
|
||||
|
||||
private prefetch(text: string, gen: number): Promise<AudioBuffer | null> {
|
||||
const task = this.config.provider === 'google-free' ? this.fetchGoogle(text) : this.fetchOpenAi(text);
|
||||
const task =
|
||||
this.config.provider === 'google-free' ? this.fetchGoogle(text) : this.config.provider === 'local' ? this.fetchLocal(text) : this.fetchOpenAi(text);
|
||||
return task
|
||||
.then(async (bytes) => {
|
||||
if (gen !== this.generation || !bytes) return null;
|
||||
@@ -199,6 +205,20 @@ export class TtsEngine {
|
||||
return res.binary;
|
||||
}
|
||||
|
||||
private async fetchLocal(text: string): Promise<Uint8Array> {
|
||||
const api = bridge();
|
||||
if (!api) throw new Error('La synthèse locale nécessite l’application Electron.');
|
||||
if (!this.config.localModel) throw new Error('Aucun modèle de voix local sélectionné.');
|
||||
const result = await api.voice.synthesize({
|
||||
modelId: this.config.localModel,
|
||||
text,
|
||||
speaker: this.config.localSpeaker,
|
||||
speed: Math.max(0.5, Math.min(2, this.config.speed || 1))
|
||||
});
|
||||
Log.debug('tts', `local synthesis ${result.durationMs} ms for ${result.audioSec.toFixed(1)}s of audio`);
|
||||
return result.wav;
|
||||
}
|
||||
|
||||
private async fetchGoogle(text: string): Promise<Uint8Array> {
|
||||
const url = `https://translate.google.com/translate_tts?ie=UTF-8&tl=${encodeURIComponent(this.config.language.split('-')[0] || 'fr')}&client=tw-ob&q=${encodeURIComponent(text.slice(0, 200))}`;
|
||||
const res = await httpFetch({
|
||||
|
||||
Reference in new issue
Block a user