feat: fin de phrase Silero, vision d'écran et actions locales (v2.3.0) (#13)

Écoute permanente
- Silero VAD dans le worker sherpa-onnx (vad.start/audio/stop) ; le renderer
  envoie des trames de 128 ms pendant la commande et reçoit le segment WAV
  complet (speech-start / segment). Repli sur le VAD énergétique si le modèle
  n'est pas installé ou si l'option est désactivée.
- Modèle « silero-vad » (0,6 Mo) dans le catalogue, section « Fin de phrase »
  dans Modèles locaux, réglage dans Paramètres → Micro.

Vision d'écran
- IPC system:screen-capture (desktopCapturer, JPEG 1600 px) ; bouton dans la
  barre de commande ; « regarde mon écran… » joint la capture à la question
  envoyée à Hermes (transport chat completions pour les images).

Actions locales
- IPC system:action à liste blanche : verrouiller la session, touches média
  (volume, mute, lecture, piste), ouvrir une application connue ou une URL
  http(s), presse-papiers, recherche de fichiers dans les dossiers utilisateur.
- Routeur d'intentions FR/EN exécuté avant Hermes (« coupe le son », « ouvre
  Spotify », « verrouille la session »…), résultat affiché et lu ; désactivable.

Correctif
- Chat completions : les premiers fragments SSE arrivés avant l'événement de
  démarrage étaient perdus (premier mot manquant) ; ils sont maintenant rejoués.

Tests : 26 tests unitaires (intentions locales ajoutées), e2e Electron sous
Xvfb avec les vrais modèles (Silero : un segment par phrase, 6,7 s d'audio
traités en 190 ms ; capture 109 ko reçue par Hermes ; intention locale
traitée sans Hermes).


Claude-Session: https://claude.ai/code/session_017Wn5VX9HNbJ7N54hR24u9Y

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
LogiFlowandClaude authored and GitHub committed 2026-09-03 20:46:26 +02:00
1 parent 4f6074183f
commit af8bdc5e9d
24 files changed
+691 -47

No files matched your search

+20 -2
View File
@@ -113,8 +113,21 @@ class VoiceController {
this.unsubscribeKws?.();
this.unsubscribeKws = api.voice.onKwsDetected((d) => this.onWakeDetected(d.keyword));
const sensitivity = Math.min(5, Math.max(1, Math.round(settings.sensitivity))) as 1 | 2 | 3 | 4 | 5;
let neuralVad = false;
if (settings.neuralVad) {
try {
const installed = (await api.voice.listModels()).some((m) => m.id === 'silero-vad' && m.installed);
if (installed) {
await api.voice.vadStart({ modelId: 'silero-vad', silenceMs: Math.max(300, Math.min(1500, settings.silenceMs - 200)), threshold: 0.5, maxUtteranceSec: 25 });
neuralVad = true;
}
} catch (err) {
Log.warn('voice', `silero unavailable, energy VAD fallback: ${(err as Error).message}`);
}
}
await this.wake.start({
deviceId: settings.micDeviceId || undefined,
neuralVad,
vad: { silenceMs: settings.silenceMs, speechRatio: SENSITIVITY_RATIO[sensitivity], minRms: SENSITIVITY_MIN_RMS[sensitivity] },
callbacks: {
onPhase: (phase) => {
@@ -141,7 +154,8 @@ class VoiceController {
}
});
voice.setWake('spotting', result.accepted.map((k) => k.replace(/_/g, ' ')));
Log.info('voice', `wake mode on: ${result.accepted.join(', ')}${result.rejected.length ? ` (rejetés : ${result.rejected.join(', ')})` : ''}`);
voice.setNeuralVad(neuralVad);
Log.info('voice', `wake mode on: ${result.accepted.join(', ')}${result.rejected.length ? ` (rejetés : ${result.rejected.join(', ')})` : ''}${neuralVad ? ' · Silero' : ''}`);
} catch (err) {
const message = (err as Error).message;
voice.setWake('error');
@@ -149,6 +163,7 @@ class VoiceController {
useChat.getState().setError(`Mot d’activation : ${message}`);
Log.error('voice', `wake mode failed: ${message}`);
await api.voice.kwsStop().catch(() => undefined);
await api.voice.vadStop().catch(() => undefined);
}
}
@@ -157,7 +172,10 @@ class VoiceController {
this.unsubscribeKws = null;
this.wake.stop();
useVoice.getState().setWake('off');
await bridge()?.voice.kwsStop().catch(() => undefined);
useVoice.getState().setNeuralVad(false);
const api = bridge();
await api?.voice.kwsStop().catch(() => undefined);
await api?.voice.vadStop().catch(() => undefined);
}
/** Restart spotting with the current settings (wake word, sensitivity, microphone). */