Feature: Agent Voice Customization UI #1045

Closed
gemini wants to merge 2 commits from feature/voice-customization-ui into main
2 changed files with 95 additions and 1 deletions

View File

@@ -66,10 +66,51 @@ async def tts_speak(text: str = Form(...)):
# ── Voice button page ────────────────────────────────────────────────────
@router.post("/settings")
async def update_voice_settings(
voice_id: str = Form(None),
rate: int = Form(None),
volume: float = Form(None),
):
"""Update TTS settings."""
try:
from timmy_serve.voice_tts import voice_tts
if voice_id:
voice_tts.set_voice(voice_id)
if rate:
voice_tts.set_rate(rate)
if volume is not None:
voice_tts.set_volume(volume)
return {"status": "ok", "message": "Voice settings updated"}
except Exception as exc:
return {"status": "error", "message": str(exc)}
@router.get("/button", response_class=HTMLResponse)
async def voice_button_page(request: Request):
"""Render the voice button UI."""
return templates.TemplateResponse(request, "voice_button.html")
try:
from timmy_serve.voice_tts import voice_tts
voices = voice_tts.get_voices() if voice_tts.available else []
current_voice = voice_tts._engine.getProperty("voice") if voice_tts._engine else None
current_rate = voice_tts._rate
current_volume = voice_tts._volume
except Exception:
voices = []
current_voice = None
current_rate = 175
current_volume = 0.9
return templates.TemplateResponse(
request,
"voice_button.html",
{
"voices": voices,
"current_voice": current_voice,
"current_rate": current_rate,
"current_volume": current_volume
}
)
@router.post("/command")

View File

@@ -30,6 +30,27 @@
</div>
</div>
<div class="voice-settings mc-panel" style="margin-top: 1rem; border-top: 1px solid var(--border-color); padding-top: 1rem;">
<h3 style="font-size: 0.9rem; color: var(--text-dim); margin-bottom: 0.5rem;">// VOICE CUSTOMIZATION</h3>
<div class="form-group">
<label for="voice-select">Voice:</label>
<select id="voice-select" class="form-control" onchange="updateVoiceSettings()">
{% for voice in voices %}
<option value="{{ voice.id }}" {% if voice.id == current_voice %}selected{% endif %}>{{ voice.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group" style="margin-top: 0.5rem;">
<label for="rate-slider">Rate: <span id="rate-val">{{ current_rate }}</span></label>
<input type="range" id="rate-slider" min="50" max="300" value="{{ current_rate }}" class="form-control" oninput="updateRateLabel(this.value)" onchange="updateVoiceSettings()">
</div>
<div class="form-group" style="margin-top: 0.5rem;">
<label for="volume-slider">Volume: <span id="volume-val">{{ current_volume }}</span></label>
<input type="range" id="volume-slider" min="0" max="1" step="0.1" value="{{ current_volume }}" class="form-control" oninput="updateVolumeLabel(this.value)" onchange="updateVoiceSettings()">
</div>
<button class="btn btn-sm btn-outline-secondary" style="margin-top: 0.5rem;" onclick="testVoice()">Test Voice</button>
</div>
<div class="voice-tips">
<h3>Try saying:</h3>
<ul>
@@ -87,6 +108,38 @@ function startListening() {
function stopListening() {
if (recognition && isListening) { recognition.stop(); }
}
function updateRateLabel(val) { document.getElementById('rate-val').textContent = val; }
function updateVolumeLabel(val) { document.getElementById('volume-val').textContent = val; }
async function updateVoiceSettings() {
const voice_id = document.getElementById('voice-select').value;
const rate = document.getElementById('rate-slider').value;
const volume = document.getElementById('volume-slider').value;
try {
await fetch('/voice/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'voice_id=' + encodeURIComponent(voice_id) + '&rate=' + rate + '&volume=' + volume
});
} catch (e) {
console.error('Failed to update voice settings:', e);
}
}
async function testVoice() {
const text = "Hello, I am Timmy. How can I help you today?";
try {
await fetch('/voice/tts/speak', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'text=' + encodeURIComponent(text)
});
} catch (e) {
console.error('Failed to test voice:', e);
}
}
function resetButton() {
document.getElementById('voice-status').textContent = 'Tap and hold to speak';
document.getElementById('voice-btn').classList.remove('listening');