From 29855bcd1f1a80dee7251ef22dbde3684c976964 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Fri, 19 Jun 2026 23:39:04 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=202=20=E2=80=94=20Audio=20STT/TTS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TTS: edge-tts 7.2.8 (fr-FR-DeniseNeural) — génération MP3 asynchrone - STT: Web Speech API navigateur (SpeechRecognition fr-FR) - Audio playback: base64 MP3 décodé et joué via Audio API - Bouton micro avec animation pulse pendant l'écoute - Chemin absolu edge-tts (systemd PATH) - Audio envoyé dans le message 'response' existant (champ audio) Coût: latence TTS ~7s, zéro dépendance serveur pour le STT. --- bot_direct.py | 80 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 9 deletions(-) diff --git a/bot_direct.py b/bot_direct.py index 1e5eca1..468196c 100644 --- a/bot_direct.py +++ b/bot_direct.py @@ -6,10 +6,12 @@ PoC minimal : WebSocket texte → API Hermes → réponse texte. Pas de Pipecat — intégration directe pour valider le concept. """ import asyncio +import base64 import json import os import subprocess import sys +import tempfile import httpx from websockets.asyncio.server import serve from websockets.http11 import Response, Headers @@ -42,6 +44,33 @@ SYSTEM_PROMPT = ( "Pas de markdown. Réponds comme si tu parlais à l'oral." ) +# ─── TTS (Text-to-Speech) ───────────────────────────────────── +EDGE_TTS_VOICE = "fr-FR-DeniseNeural" # Voix féminine française naturelle +EDGE_TTS_BIN = "/home/openclaw/workspace/jarvis-pipecat/venv/bin/edge-tts" + +async def generate_tts(text: str) -> bytes: + """Génère un MP3 à partir du texte avec edge-tts. Retourne les bytes audio.""" + with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f: + mp3_path = f.name + try: + proc = await asyncio.create_subprocess_exec( + EDGE_TTS_BIN, "--voice", EDGE_TTS_VOICE, + "--text", text, "--write-media", mp3_path, + stdout=asyncio.subprocess.DEVNULL, + stderr=asyncio.subprocess.DEVNULL, + ) + await asyncio.wait_for(proc.wait(), timeout=15) + with open(mp3_path, "rb") as f: + return f.read() + except Exception as e: + print(f"⚠️ TTS error: {e}") + return b"" + finally: + try: + os.unlink(mp3_path) + except: + pass + # ─── Page HTML ─────────────────────────────────────────────── HTML_PAGE = """ @@ -71,6 +100,9 @@ button{padding:10px 20px;border-radius:8px;border:none;background:var(--cyan); color:var(--bg);font-weight:bold;cursor:pointer;font-size:.95em} button:hover{opacity:.9} button:disabled{opacity:.5;cursor:default} +#mic{width:40px;height:40px;font-size:1.2em;flex-shrink:0;padding:0;display:flex;align-items:center;justify-content:center;border-radius:50%} +#mic.listening{background:var(--red);animation:pulse 1s infinite} +@keyframes pulse{0%,100%{box-shadow:0 0 0 0 var(--red)}50%{box-shadow:0 0 0 8px transparent}} @@ -78,14 +110,17 @@ button:disabled{opacity:.5;cursor:default}
🔴 Déconnecté
- + +
@@ -177,9 +234,14 @@ async def ws_handler(websocket): messages.append({"role": "assistant", "content": reply}) print(f"🤖 {reply[:80]}") + # Générer TTS en parallèle de l'envoi texte + audio_bytes = await generate_tts(reply) + audio_b64 = base64.b64encode(audio_bytes).decode() if audio_bytes else "" + await websocket.send(json.dumps({ "type": "response", "content": reply, + "audio": audio_b64, })) except Exception as e: