mirror of
https://github.com/R0m1k3/EveFlow.git
synced 2026-10-11 17:29:03 +02:00
É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>
76 lines
4.1 KiB
TypeScript
76 lines
4.1 KiB
TypeScript
/**
|
||
* Instant local commands: short French/English intents handled on the machine without a
|
||
* round trip to Hermes (lock, open app/url, volume, media, screenshot to Hermes).
|
||
* Anything unmatched goes to Hermes as usual.
|
||
*/
|
||
import type { SystemAction } from '../../shared/bridge';
|
||
import { bridge } from '../lib/bridge';
|
||
import { Log } from '../lib/log';
|
||
|
||
export interface LocalIntent {
|
||
kind: 'action' | 'screenshot';
|
||
action?: SystemAction;
|
||
/** Spoken confirmation. */
|
||
reply: string;
|
||
/** For screenshot: the question to send to Hermes with the image. */
|
||
question?: string;
|
||
}
|
||
|
||
const norm = (t: string) =>
|
||
t
|
||
.normalize('NFD')
|
||
.replace(/[̀-ͯ]/g, '')
|
||
.toLowerCase()
|
||
.replace(/[’']/g, ' ')
|
||
.replace(/[^a-z0-9 :/._-]+/g, ' ')
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
|
||
const LOCK = /^(verrouille|verrouiller|bloque|lock)( (la |ma )?(session|le pc|l ordinateur|the (pc|computer|screen)))?$/;
|
||
const VOL_UP = /^(monte|augmente|hausse|plus fort|volume plus|turn up|raise)( (le|the)? ?(son|volume))?( de \d+)?$/;
|
||
const VOL_DOWN = /^(baisse|diminue|moins fort|volume moins|turn down|lower)( (le|the)? ?(son|volume))?( de \d+)?$/;
|
||
const MUTE = /^(coupe|couper|mute|silence|desactive)( (le|the)? ?(son|volume|audio))?$/;
|
||
const PLAY = /^(pause|play|lecture|reprends|reprendre|mets en pause|met en pause|stop la musique|arrete la musique)$/;
|
||
const NEXT = /^(suivant|suivante|piste suivante|musique suivante|next|skip)$/;
|
||
const PREV = /^(precedent|precedente|piste precedente|previous)$/;
|
||
const OPEN_APP = /^(ouvre|ouvrir|lance|lancer|demarre|open|launch|start) ((l application|l appli|le logiciel|le programme|the app|le|la|les|l|un|une|the|moi) )*(.+)$/;
|
||
const OPEN_URL = /^(ouvre|ouvrir|va sur|open|go to) (https?:\/\/\S+|(www\.)?[a-z0-9-]+\.[a-z]{2,}(\/\S*)?)$/;
|
||
const SCREEN = /(regarde|regardes|analyse|decris|decrit|lis|explique|qu est ce qu il y a sur|que vois tu sur|what is on|look at|read) (mon |l |the )?(ecran|screen)|capture (d )?ecran|screenshot/;
|
||
|
||
export function parseLocalIntent(text: string): LocalIntent | null {
|
||
const t = norm(text).replace(/^(jarvis|hey jarvis|ok jarvis)[ ,]*/, '');
|
||
if (!t) return null;
|
||
if (LOCK.test(t)) return { kind: 'action', action: { type: 'lock' }, reply: 'Session verrouillée.' };
|
||
if (MUTE.test(t)) return { kind: 'action', action: { type: 'media', key: 'mute' }, reply: 'Son coupé.' };
|
||
if (VOL_UP.test(t)) return { kind: 'action', action: { type: 'media', key: 'volume-up' }, reply: 'Volume augmenté.' };
|
||
if (VOL_DOWN.test(t)) return { kind: 'action', action: { type: 'media', key: 'volume-down' }, reply: 'Volume baissé.' };
|
||
if (PLAY.test(t)) return { kind: 'action', action: { type: 'media', key: 'play-pause' }, reply: 'Lecture.' };
|
||
if (NEXT.test(t)) return { kind: 'action', action: { type: 'media', key: 'next' }, reply: 'Piste suivante.' };
|
||
if (PREV.test(t)) return { kind: 'action', action: { type: 'media', key: 'previous' }, reply: 'Piste précédente.' };
|
||
if (SCREEN.test(t)) return { kind: 'screenshot', reply: 'Je regarde votre écran.', question: text.trim() };
|
||
const url = OPEN_URL.exec(t);
|
||
if (url) {
|
||
const target = url[2].startsWith('http') ? url[2] : `https://${url[2]}`;
|
||
return { kind: 'action', action: { type: 'open-url', url: target }, reply: `J’ouvre ${url[2]}.` };
|
||
}
|
||
const app = OPEN_APP.exec(t);
|
||
if (app) {
|
||
const name = app[app.length - 1].trim();
|
||
if (name.length <= 40 && !/\s(et|puis|and)\s/.test(name)) return { kind: 'action', action: { type: 'open-app', name }, reply: `J’ouvre ${name}.` };
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/** Execute an action intent through the bridge. Returns the spoken outcome. */
|
||
export async function runLocalIntent(intent: LocalIntent): Promise<{ ok: boolean; message: string }> {
|
||
const api = bridge();
|
||
if (!api || !intent.action) return { ok: false, message: 'Actions locales indisponibles hors Electron.' };
|
||
try {
|
||
const result = await api.system.action(intent.action);
|
||
Log.info('local', `${intent.action.type}: ${result.ok ? 'ok' : result.message}`);
|
||
return { ok: result.ok, message: result.ok ? intent.reply : result.message || 'Échec de la commande locale.' };
|
||
} catch (err) {
|
||
return { ok: false, message: (err as Error).message };
|
||
}
|
||
}
|