diff --git a/README.md b/README.md index 4f25841..9179ca4 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ La version 2 est une réécriture complète : plus de robot 3D, un pipeline voca * **Heures calmes et priorités** : plage horaire pendant laquelle les messages poussés s'affichent sans être lus ni faire clignoter le noyau (badge « non lus » à la place), thème nuit automatique, mots prioritaires lus quand même, résumé vocal des rapports longs (les premières phrases seulement). * **Mode mission** : un bouton dans la barre de commande bascule sur un second modèle Hermes (plus puissant) pour les tâches longues ; le modèle rapide reste utilisé pour la conversation courante. * **Widget compact « glanceable »** : état (veille, écoute, réflexion, parle), dernière phrase de l'assistant, badge de non-lus, indicateurs heures calmes et mission. +* **Voix masculine ou féminine** : un réglage unique (Paramètres → Voix) appliqué à tous les moteurs. En local, Piper Tom ou Pierre (UPMC) pour le masculin, téléchargé automatiquement si aucune voix masculine n'est installée ; onyx / nova pour les API compatibles OpenAI ; Paul / Hortense pour les voix Windows. * **Barge-in** : en mains libres, parler par-dessus l'assistant coupe sa voix ; le seuil est relevé pendant qu'il parle pour ignorer l'écho du haut-parleur. * **Écoute permanente** : un détecteur de mot-clé de 3 Mo (sherpa-onnx, keyword spotting) tourne en continu sur le micro, quasi gratuit en CPU. « Jarvis » (ou n'importe quel mot-clé) ouvre l'écoute, « Jarvis, allume… » envoie directement la commande, et le mot coupe la voix en cours. Alternative : filtre du mot après transcription en mains libres. * **STT externe** : n'importe quelle API `/v1/audio/transcriptions` compatible OpenAI (Qwen3-ASR, Whisper, Speaches, faster-whisper-server, LocalAI, OpenAI). Repli sur la reconnaissance Chromium. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 5aad28c..27b74ff 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -77,6 +77,7 @@ Sources : [jarvis-desktop-ai](https://github.com/ccarloshenri/jarvis-desktop-ai) | Silero VAD sur phrase Kokoro de 2,3 s (2.3.0) | un seul segment, début et fin détectés, 6,7 s d'audio traités en 190 ms | | Capture d'écran → Hermes (2.3.0) | JPEG de 107 ko reçu côté Hermes (mock chat completions) | | Intention locale « coupe le son » (2.3.0) | traitée sans Hermes, résultat affiché dans le fil | +| Correctifs 2.4.0.1 | réponse vide en chat completions désormais expliquée (JSON non streamé ou erreur HTTP 200), la liaison ne passe plus en « dégradé » quand seule l'API des crons échoue, transcriptions parasites (« (cliquant) », « *Claire* ») ignorées, préférence de voix masculine/féminine | | Serveur MCP (2.4.0) | initialize, tools/list (14 outils), tools/call côté principal (presse-papiers, capture image) et côté renderer (état, message dans le fil) | Sur un PC à 28 cœurs les temps sont nettement plus courts. Whisper small est maintenant recommandé pour le français. diff --git a/electron/voice/catalog.ts b/electron/voice/catalog.ts index 62f7fa1..7fa51b7 100644 --- a/electron/voice/catalog.ts +++ b/electron/voice/catalog.ts @@ -164,3 +164,12 @@ export const VOICE_CATALOG: VoiceModelSpec[] = [ export function findModel(id: string): VoiceModelSpec | undefined { return VOICE_CATALOG.find((m) => m.id === id); } + +// Speaker gender from the label ("(homme, …)", "(femme, …)", known first names) so the UI and the +// voice preference can pick a masculine or feminine voice without a lookup table per model. +const MALE = /\b(homme|tom|pierre|adam|michael|eric|liam|george|lewis|daniel|fenrir|puck|onyx|echo|santa)\b/i; +for (const spec of VOICE_CATALOG) { + for (const sp of spec.speakers ?? []) { + if (!sp.gender) sp.gender = /\b(femme|female)\b/i.test(sp.name) ? 'f' : MALE.test(sp.name) ? 'm' : /\bfemme\b/i.test(spec.name) ? 'f' : /\bhomme\b/i.test(spec.name) ? 'm' : undefined; + } +} diff --git a/package.json b/package.json index e47627b..62c0cdc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "eveflow", "version": "2.4.0", - "releaseVersion": "2.4.0", + "releaseVersion": "2.4.0.1", "description": "JARVIS-style desktop HUD for Hermes Agent: voice, streaming runs, scheduled jobs, skills and telemetry", "main": "dist-electron/main.js", "private": true, diff --git a/shared/voice.ts b/shared/voice.ts index 7485b0c..a17ac4e 100644 --- a/shared/voice.ts +++ b/shared/voice.ts @@ -7,6 +7,8 @@ export interface VoiceSpeaker { id: number; name: string; lang: string; + /** m = masculine, f = feminine (from the catalog label). */ + gender?: 'm' | 'f'; } export interface VoiceModelSpec { diff --git a/src/App.tsx b/src/App.tsx index 19ce752..1bcdbeb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,7 @@ import { bridge } from './lib/bridge'; import { Log } from './lib/log'; import { handlePush, localToolContext, stopGeneration } from './services/conversation'; import { initMcpBridge } from './services/mcpBridge'; +import { ensurePreferredVoice } from './services/voice/voicePreference'; import { isQuietTime } from './lib/quietHours'; import { speech } from './services/voice/speech'; import { voiceController } from './services/voice/voiceController'; @@ -45,7 +46,7 @@ function useBoot(): boolean { } if (api) { disposers.push(useVoiceModels.getState().subscribe()); - void useVoiceModels.getState().refresh(); + void useVoiceModels.getState().refresh().then(() => ensurePreferredVoice()); if (settings.voice.wakeMode === 'kws') void voiceController.startWakeMode(); let lastWake = JSON.stringify([settings.voice.wakeMode, settings.voice.wakeWord, settings.voice.kwsSensitivity, settings.voice.micDeviceId]); disposers.push( diff --git a/src/components/panels/OpsPanel.tsx b/src/components/panels/OpsPanel.tsx index 642ac18..b790ed8 100644 --- a/src/components/panels/OpsPanel.tsx +++ b/src/components/panels/OpsPanel.tsx @@ -1,6 +1,7 @@ import { Fragment, useEffect, useState } from 'react'; import { useShallow } from 'zustand/react/shallow'; import { + AlertTriangle, Wrench, CalendarClock, Sparkles, Layers, History, Play, Pause, Trash2, Pencil, Plus, Save, X, RefreshCw, CheckCircle2, XCircle, Loader2, Bot, Cpu, GitBranch, Webhook, FolderOpen } from 'lucide-react'; @@ -41,6 +42,7 @@ function ActivityTab() { function JobsTab() { const jobs = useHermes((s) => s.jobs); + const jobsError = useHermes((s) => s.jobsError); const busy = useHermes((s) => s.busy); const lastSyncAt = useHermes((s) => s.lastSyncAt); const refreshJobs = useHermes((s) => s.refreshJobs); @@ -95,8 +97,9 @@ function JobsTab() { )} + {jobsError &&
Crons indisponibles : {jobsError}
} {jobs.length === 0 ? ( -
Aucun cron Hermes. Créez une mission planifiée en langage naturel.
+
{jobsError ? 'La liaison Hermes reste utilisable ; seule la planification est indisponible.' : 'Aucun cron Hermes. Créez une mission planifiée en langage naturel.'}
) : (
{jobs.map((job) => { diff --git a/src/components/settings/SettingsDrawer.tsx b/src/components/settings/SettingsDrawer.tsx index 75a664e..04723d2 100644 --- a/src/components/settings/SettingsDrawer.tsx +++ b/src/components/settings/SettingsDrawer.tsx @@ -4,6 +4,7 @@ import { bridge } from '../../lib/bridge'; import { HermesClient } from '../../services/hermes/client'; import { listSystemVoices } from '../../services/voice/tts'; import { speech } from '../../services/voice/speech'; +import { ensurePreferredVoice } from '../../services/voice/voicePreference'; import { listMicrophones } from '../../services/voice/capture'; import { transcribeWav } from '../../services/voice/stt'; import { encodeWav } from '../../services/voice/wav'; @@ -357,6 +358,14 @@ export function SettingsDrawer({ onClose }: Props) { {section === 'speech' && (
+
+ +
+ + +
+ S’applique à tous les moteurs : voix locale (Piper Tom ou Pierre pour le masculin, téléchargée automatiquement si besoin), voix OpenAI (onyx / nova), voix système Windows (Paul / Hortense). +