mirror of
https://github.com/R0m1k3/EveFlow.git
synced 2026-10-23 15:18:13 +02:00
feat!: rebuild EveFlow as a JARVIS-style voice HUD for Hermes Agent (v2.0.0)
Complete rewrite of the application: - Toolchain: Electron 44, Vite 8, React 19, TypeScript 5.9, Zustand, Vitest; main process rewritten in TypeScript (electron/), typed IPC contract (shared/), sandboxed renderer with webSecurity on and an HTTP/SSE proxy in the main process. - Voice: AudioWorklet microphone capture at 16 kHz with adaptive energy VAD and auto-stop, WAV encoder, OpenAI-compatible STT, TTS queue with sentence-level streaming, prefetch and Web Audio playback feeding an analyser; hands-free mode, global push-to-talk hotkey, system-voice fallback. - Hermes: full API client (capabilities, health, models, skills, toolsets, sessions, jobs) with automatic transport selection: runs API (SSE lifecycle, approvals, steer, stop) > sessions stream > chat completions with local tools; tolerant event normalisation; webhook receiver hardened (secret, size limit). - UI: JARVIS arc-reactor core on Canvas 2D reacting to the real audio signal, holographic HUD layout (transcript, core, Hermes ops panel with tools/crons/ skills/sessions/link tabs, telemetry), settings drawer, approval modals, compact floating widget, four themes, tray icon and global shortcuts. - Removed the 3D Eve robot, three.js and legacy assets; migrated 1.x settings. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Wn5VX9HNbJ7N54hR24u9Y
This commit is contained in:
239 files changed
+11351
-86495
No files matched your search
+154
@@ -0,0 +1,154 @@
|
||||
/** Text utilities shared by the chat renderer and the speech pipeline. */
|
||||
|
||||
const PHONETIC: Record<string, string> = {
|
||||
api: 'a-pé-i',
|
||||
cors: 'korss',
|
||||
svg: 'ess-vé-gé',
|
||||
cpu: 'cé-pé-u',
|
||||
gpu: 'gé-pé-u',
|
||||
fps: 'eff-pé-ess',
|
||||
tts: 'té-té-ess',
|
||||
stt: 'ess-té-té',
|
||||
ui: 'u-i',
|
||||
json: 'djé-zone',
|
||||
url: 'u-err-el',
|
||||
html: 'ach-té-em-el',
|
||||
css: 'cé-ess-ess',
|
||||
js: 'ji-ess',
|
||||
github: 'guite-heub',
|
||||
git: 'guite',
|
||||
npm: 'enne-pé-em',
|
||||
cli: 'cé-el-i',
|
||||
ipc: 'i-pé-cé',
|
||||
hermes: 'hermès',
|
||||
ok: 'oké'
|
||||
};
|
||||
|
||||
/** Convert `MEDIA:/path` tokens (Hermes gateway shorthand) to markdown images. */
|
||||
export function preprocessMedia(content: string): string {
|
||||
if (!content) return content;
|
||||
return content.replace(/MEDIA:\s*(\S+)/g, (_m, p: string) => `\n\n\n\n`);
|
||||
}
|
||||
|
||||
/** Strip markdown, code, links, URLs and emojis so the text can be spoken naturally. */
|
||||
export function cleanForSpeech(raw: string): string {
|
||||
if (!raw) return '';
|
||||
let text = raw.replace(/MEDIA:\s*\S+/g, '');
|
||||
text = text.replace(/```[\s\S]*?```/g, ' ');
|
||||
text = text.replace(/<think>[\s\S]*?<\/think>/gi, ' ');
|
||||
text = text.replace(/`([^`]+)`/g, '$1');
|
||||
text = text.replace(/!\[[^\]]*\]\([^)]*\)/g, '');
|
||||
text = text.replace(/\[([^\]]+)\]\((?:[^)(]+|\([^)(]*\))*\)/g, '$1');
|
||||
text = text.replace(/(?:https?|ftp|file):\/\/\S+/gi, '');
|
||||
text = text.replace(/www\.\S+/gi, '');
|
||||
text = text.replace(/^\s{0,3}#{1,6}\s+/gm, '');
|
||||
text = text.replace(/^\s*[-*+]\s+/gm, ', ');
|
||||
text = text.replace(/^\s*\d+[.)]\s+/gm, ', ');
|
||||
text = text.replace(/^\s*>\s?/gm, '');
|
||||
text = text.replace(/\|/g, ' ');
|
||||
text = text.replace(/:\s*(?:\r?\n)/g, '. ');
|
||||
text = text.replace(/(?:\r?\n){2,}/g, '. ');
|
||||
text = text.replace(/\r?\n/g, ' ');
|
||||
text = text
|
||||
.split(/\s+/)
|
||||
.filter((word) => !(word.includes('/') && word.length > 3) && !/^[\d.]+:\d+$/.test(word) && !/^\d{1,3}(\.\d{1,3}){3}$/.test(word))
|
||||
.join(' ');
|
||||
text = text
|
||||
.replace(/[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}\u{FE00}-\u{FEFF}\u{200D}]/gu, '')
|
||||
.replace(/(\*\*|__|~~|[*_#`~])/g, '')
|
||||
.replace(/\[[^\]]*\]/g, '')
|
||||
.replace(/(-{2,}|={2,}|~{2,})/g, ' ')
|
||||
.replace(/\.{3,}/g, '...')
|
||||
.replace(/[<>{}\\^]/g, '')
|
||||
.replace(/\s{2,}/g, ' ')
|
||||
.trim();
|
||||
return applyPhonetics(text);
|
||||
}
|
||||
|
||||
function applyPhonetics(text: string): string {
|
||||
let out = text;
|
||||
for (const [key, value] of Object.entries(PHONETIC)) {
|
||||
const escaped = key.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
out = out.replace(new RegExp(`(?<=^|\\s|\\p{P})${escaped}(?=$|\\s|\\p{P})`, 'giu'), value);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull complete sentences out of a streaming buffer.
|
||||
* Returns the sentences ready to be spoken and the remainder to keep buffering.
|
||||
*/
|
||||
export function extractSentences(buffer: string, minLength = 12): { sentences: string[]; rest: string } {
|
||||
const sentences: string[] = [];
|
||||
let rest = buffer;
|
||||
// Never split inside an unfinished code block.
|
||||
const fences = (buffer.match(/```/g) ?? []).length;
|
||||
if (fences % 2 === 1) return { sentences, rest };
|
||||
const regex = /[^.!?\n\r]+(?:[.!?]+(?=\s|$)|[\n\r]+)/g;
|
||||
let consumed = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
let pending = '';
|
||||
while ((match = regex.exec(buffer)) !== null) {
|
||||
if (match.index !== consumed) break; // gap: stop at first non-contiguous match
|
||||
const candidate = (pending + match[0]).trim();
|
||||
consumed = match.index + match[0].length;
|
||||
if (candidate.length < minLength || /\b(?:\d+|[A-Z]|M|Mme|Dr|etc|ex)\.$/.test(candidate)) {
|
||||
pending = pending + match[0];
|
||||
continue;
|
||||
}
|
||||
sentences.push(candidate);
|
||||
pending = '';
|
||||
}
|
||||
rest = pending + buffer.slice(consumed);
|
||||
return { sentences, rest };
|
||||
}
|
||||
|
||||
/** Split a finished text into speakable chunks of bounded size. */
|
||||
export function chunkForSpeech(text: string, maxLength = 220): string[] {
|
||||
const { sentences, rest } = extractSentences(text + '\n', 1);
|
||||
const all = [...sentences, rest.trim()].filter(Boolean);
|
||||
const out: string[] = [];
|
||||
for (const sentence of all) {
|
||||
if (sentence.length <= maxLength) {
|
||||
out.push(sentence);
|
||||
continue;
|
||||
}
|
||||
let current = '';
|
||||
for (const word of sentence.split(/\s+/)) {
|
||||
if ((current + ' ' + word).trim().length > maxLength) {
|
||||
if (current) out.push(current.trim());
|
||||
current = word;
|
||||
} else {
|
||||
current = current ? `${current} ${word}` : word;
|
||||
}
|
||||
}
|
||||
if (current) out.push(current.trim());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function previewText(value: string, max = 96): string {
|
||||
const flat = value.replace(/\s+/g, ' ').trim();
|
||||
return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
|
||||
}
|
||||
|
||||
export function formatTime(date: Date | string | number): string {
|
||||
const d = date instanceof Date ? date : new Date(date);
|
||||
return Number.isNaN(d.getTime()) ? '' : d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
|
||||
}
|
||||
|
||||
export function formatDateTime(date: Date | string | number | undefined): string {
|
||||
if (!date) return '—';
|
||||
const d = date instanceof Date ? date : new Date(date);
|
||||
return Number.isNaN(d.getTime()) ? String(date) : d.toLocaleString([], { dateStyle: 'short', timeStyle: 'short' });
|
||||
}
|
||||
|
||||
export function formatDuration(seconds: number): string {
|
||||
const s = Math.max(0, Math.floor(seconds));
|
||||
const d = Math.floor(s / 86400);
|
||||
const h = Math.floor((s % 86400) / 3600);
|
||||
const m = Math.floor((s % 3600) / 60);
|
||||
if (d > 0) return `${d}j ${h}h`;
|
||||
if (h > 0) return `${h}h ${m.toString().padStart(2, '0')}m`;
|
||||
return `${m}m ${(s % 60).toString().padStart(2, '0')}s`;
|
||||
}
|
||||
Reference in new issue
Block a user