diff --git a/src/dashboard/routes/voice.py b/src/dashboard/routes/voice.py index 10ea95ad..3d505f38 100644 --- a/src/dashboard/routes/voice.py +++ b/src/dashboard/routes/voice.py @@ -1,3 +1,5 @@ +import json +from pathlib import Path """Voice routes — /voice/* and /voice/enhanced/* endpoints. Provides NLU intent detection, TTS control, the full voice-to-action @@ -152,3 +154,51 @@ async def process_voice_input( "error": error, "spoken": speak_response and response_text is not None, } + +@router.get("/settings") +async def get_voice_settings(): + """Get current voice settings.""" + settings_path = Path("data/voice_settings.json") + if settings_path.exists(): + try: + return json.loads(settings_path.read_text()) + except Exception: + pass + + # Default settings + return { + "rate": 175, + "volume": 0.9, + "voice_id": None + } + +@router.post("/settings") +async def update_voice_settings( + rate: int = Form(...), + volume: float = Form(...), + voice_id: str = Form(None) +): + """Update and persist voice settings.""" + settings = { + "rate": rate, + "volume": volume, + "voice_id": voice_id + } + + # Persist to file + settings_path = Path("data/voice_settings.json") + settings_path.parent.mkdir(parents=True, exist_ok=True) + settings_path.write_text(json.dumps(settings)) + + # Update engine + try: + from timmy_serve.voice_tts import voice_tts + if voice_tts.available: + voice_tts.set_rate(rate) + voice_tts.set_volume(volume) + if voice_id: + voice_tts.set_voice(voice_id) + except Exception as exc: + logger.error("Failed to update voice engine: %s", exc) + + return {"status": "ok", "settings": settings} diff --git a/src/dashboard/templates/voice_button.html b/src/dashboard/templates/voice_button.html index 5f79247b..a6e916a1 100644 --- a/src/dashboard/templates/voice_button.html +++ b/src/dashboard/templates/voice_button.html @@ -40,6 +40,29 @@
  • "Emergency stop"
  • + +
    +

    // SETTINGS

    + +
    + + +
    + +
    + + +
    + +
    + + +
    + + +
    @@ -120,6 +143,71 @@ async function processVoiceCommand(text) { } setTimeout(resetButton, 2000); + +async function loadVoiceSettings() { + try { + const [settingsRes, statusRes] = await Promise.all([ + fetch('/voice/settings'), + fetch('/voice/tts/status') + ]); + + const settings = await settingsRes.json(); + const status = await statusRes.json(); + + // Populate voices + const select = document.getElementById('voice-select'); + if (status.voices) { + status.voices.forEach(v => { + const opt = document.createElement('option'); + opt.value = v.id; + opt.textContent = v.name; + if (v.id === settings.voice_id) opt.selected = true; + select.appendChild(opt); + }); + } + + // Set sliders + document.getElementById('voice-rate').value = settings.rate; + document.getElementById('rate-val').textContent = settings.rate; + document.getElementById('voice-volume').value = settings.volume * 100; + document.getElementById('volume-val').textContent = Math.round(settings.volume * 100); + } catch (e) { + console.error('Failed to load voice settings:', e); + } +} + +async function saveVoiceSettings() { + const rate = document.getElementById('voice-rate').value; + const volume = document.getElementById('voice-volume').value / 100; + const voice_id = document.getElementById('voice-select').value; + + try { + const response = await fetch('/voice/settings', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: `rate=${rate}&volume=${volume}&voice_id=${encodeURIComponent(voice_id)}` + }); + + if (response.ok) { + document.getElementById('voice-status').textContent = 'Settings saved!'; + setTimeout(resetButton, 2000); + } + } catch (e) { + console.error('Failed to save voice settings:', e); + } +} + +// Update display values on slide +document.getElementById('voice-rate').oninput = function() { + document.getElementById('rate-val').textContent = this.value; +}; +document.getElementById('voice-volume').oninput = function() { + document.getElementById('volume-val').textContent = this.value; +}; + +// Load on start +loadVoiceSettings(); + } {% endblock %}