mirror of
https://github.com/R0m1k3/EveFlow.git
synced 2026-10-22 06:38:14 +02:00
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:
1 parent
4f6074183f
commit
af8bdc5e9d
24 files changed
+691
-47
No files matched your search
@@ -12,6 +12,8 @@ import { useSettings } from '../state/settings';
|
||||
import { executeLocalTool, LOCAL_TOOL_DEFINITIONS } from './hermes/localTools';
|
||||
import type { HermesStreamEvent, SendHandle } from './hermes/types';
|
||||
import { speech } from './voice/speech';
|
||||
import { bridge } from '../lib/bridge';
|
||||
import { parseLocalIntent, runLocalIntent } from './localCommands';
|
||||
|
||||
let active: SendHandle | null = null;
|
||||
let activeMessageId: string | null = null;
|
||||
@@ -165,9 +167,49 @@ function addPending(request: Omit<PendingRequest, 'id' | 'createdAt'>): void {
|
||||
speech.say(request.kind === 'approval' ? 'Autorisation requise.' : request.kind === 'clarify' ? request.description : 'Saisie requise.', { interrupt: false });
|
||||
}
|
||||
|
||||
/** Screenshot of the primary display as a data URL (Electron only). */
|
||||
export async function captureScreen(): Promise<string | null> {
|
||||
const api = bridge();
|
||||
if (!api) return null;
|
||||
try {
|
||||
return await api.system.captureScreen(1600);
|
||||
} catch (err) {
|
||||
useChat.getState().setError(`Capture d’écran : ${(err as Error).message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Short system intents handled on the machine without a Hermes round trip. Returns true when consumed. */
|
||||
async function tryLocalIntent(text: string, source: string): Promise<{ handled: boolean; images?: string[]; text?: string }> {
|
||||
const settings = useSettings.getState().settings;
|
||||
if (!settings.voice.localCommands || !bridge()) return { handled: false };
|
||||
const intent = parseLocalIntent(text);
|
||||
if (!intent) return { handled: false };
|
||||
if (intent.kind === 'screenshot') {
|
||||
const shot = await captureScreen();
|
||||
if (!shot) return { handled: false };
|
||||
return { handled: false, images: [shot], text: intent.question || text };
|
||||
}
|
||||
const chat = useChat.getState();
|
||||
chat.addMessage({ role: 'user', content: text, source, status: 'done' });
|
||||
const result = await runLocalIntent(intent);
|
||||
chat.addMessage({ role: 'assistant', content: result.message, source: 'local', status: result.ok ? 'done' : 'error' });
|
||||
chat.setHud(result.ok ? 'idle' : 'error');
|
||||
if (settings.speech.autoSpeak) speech.say(result.message, { interrupt: true });
|
||||
return { handled: true };
|
||||
}
|
||||
|
||||
export async function sendMessage(text: string, images: string[] = [], source = 'eveflow'): Promise<void> {
|
||||
const trimmed = text.trim();
|
||||
let trimmed = text.trim();
|
||||
if ((!trimmed && images.length === 0) || active) return;
|
||||
if (trimmed && images.length === 0) {
|
||||
const local = await tryLocalIntent(trimmed, source);
|
||||
if (local.handled) return;
|
||||
if (local.images) {
|
||||
images = local.images;
|
||||
trimmed = (local.text ?? trimmed).trim();
|
||||
}
|
||||
}
|
||||
const chat = useChat.getState();
|
||||
const hermes = useHermes.getState();
|
||||
const settings = useSettings.getState().settings;
|
||||
|
||||
Reference in new issue
Block a user