diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b3c0f6..b827650 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ incompatibles. Chaque ligne renvoie à un commit dédié (un artefact = un commi ## [Unreleased] +## [0.29.0] — 2026-06-23 — fin Phase 6 (voix v1) +### Added +- `useSpeech` : mode **mains-libres** + wake-word « CHLOVA » (`extractCommand`), + écoute en boucle, micro en pause pendant le TTS (anti auto-écoute). +- Chat : bouton "Libre" (mains-libres) ; en mains-libres les réponses sont lues + d'office. Indicateur d'écoute. Build OK. (README web : section voix.) + ## [0.28.0] — 2026-06-23 — début Phase 6 (voix) ### Added - `web/src/useSpeech.ts` : hook voix 100 % navigateur (Web Speech API), STT diff --git a/web/README.md b/web/README.md index 47571c2..fa4bfb5 100644 --- a/web/README.md +++ b/web/README.md @@ -23,5 +23,14 @@ Le backend doit tourner avec l'auth configurée (`CHLOVA_ADMIN_*`, voir | `src/pages/Chat.tsx` | Conversation agent (v0.23.0). | | `src/pages/Review.tsx` | Need-review : approuver/refuser (v0.24.0). | -## Périmètre v1 -Login → Chat → Review. Voix + app RN : phases ultérieures (API commune réutilisée). +## Voix (Phase 6) +100 % navigateur (Web Speech API), zéro backend/GPU : +- **Parler** : dictée push-to-talk (fr-FR) → envoyée à l'agent. +- **Voix ON/OFF** : lecture vocale des réponses (TTS), réglage persistant. +- **Libre** : mains-libres, déclenché par le wake-word « CHLOVA … » ; le micro se + met en pause pendant la synthèse pour éviter l'auto-écoute. + +STT = Chrome/Edge (webkit) ; TTS = large support. Dégrade proprement sinon. + +## Périmètre +Login → Chat (+ voix) → Review. App RN : phase ultérieure (API commune réutilisée). diff --git a/web/src/pages/Chat.tsx b/web/src/pages/Chat.tsx index 6d2f7a9..d5a2322 100644 --- a/web/src/pages/Chat.tsx +++ b/web/src/pages/Chat.tsx @@ -42,7 +42,7 @@ export function Chat() { try { const { reply } = await api.chat(token, t); setMessages((m) => [...m, { role: "assistant", text: reply }]); - if (speakReplies) speech.speak(reply); + if (speakReplies || speech.handsFree) speech.speak(reply); } catch (err) { if (err instanceof ApiError && err.status === 401) { logout(); @@ -62,13 +62,18 @@ export function Chat() { }; const mic = (): void => { - if (speech.listening) { + if (speech.listening && !speech.handsFree) { speech.stopListening(); return; } speech.listen((text) => void sendText(text)); // dicter → envoyer }; + const toggleHandsFree = (): void => { + if (speech.handsFree) speech.stopHandsFree(); + else speech.startHandsFree((text) => void sendText(text)); // « CHLOVA … » → envoyer + }; + return (
@@ -86,6 +91,9 @@ export function Chat() {
))} {busy &&

CHLOVA réfléchit…

} + {speech.handsFree && !busy && !speech.speaking && ( +

Mains libres — dis « CHLOVA … »

+ )} {speech.speaking &&

Lecture vocale…

} {error &&

{error}

}
@@ -110,7 +118,7 @@ export function Chat() { onChange={(e) => setInput(e.target.value)} disabled={busy} /> - {speech.sttSupported && ( + {speech.sttSupported && !speech.handsFree && ( + )}