Feature: Agent Voice Customization UI #1045
@@ -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")
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user