1
0
This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Timmy-time-dashboard/src/dashboard/templates/voice_settings.html

132 lines
4.6 KiB
HTML

{% extends "base.html" %}
{% from "macros.html" import panel %}
{% block title %}Voice Settings{% endblock %}
{% block extra_styles %}{% endblock %}
{% block content %}
<div class="voice-settings-page py-3">
{% call panel("VOICE SETTINGS") %}
<form id="voice-settings-form">
<div class="vs-field">
<label class="vs-label" for="rate-slider">
SPEED &mdash; <span class="vs-value" id="rate-val">{{ settings.rate }}</span> WPM
</label>
<input type="range" class="vs-slider" id="rate-slider" name="rate"
min="50" max="400" step="5" value="{{ settings.rate }}"
oninput="document.getElementById('rate-val').textContent=this.value">
<div class="vs-range-labels"><span>Slow</span><span>Fast</span></div>
</div>
<div class="vs-field">
<label class="vs-label" for="vol-slider">
VOLUME &mdash; <span class="vs-value" id="vol-val">{{ (settings.volume * 100)|int }}</span>%
</label>
<input type="range" class="vs-slider" id="vol-slider" name="volume"
min="0" max="100" step="5" value="{{ (settings.volume * 100)|int }}"
oninput="document.getElementById('vol-val').textContent=this.value">
<div class="vs-range-labels"><span>Quiet</span><span>Loud</span></div>
</div>
<div class="vs-field">
<label class="vs-label" for="voice-select">VOICE MODEL</label>
{% if voices %}
<select class="vs-select" id="voice-select" name="voice_id">
<option value="">&#8212; System Default &#8212;</option>
{% for v in voices %}
<option value="{{ v.id }}" {% if v.id == settings.voice_id %}selected{% endif %}>
{{ v.name }}
</option>
{% endfor %}
</select>
{% else %}
<div class="vs-unavailable">Server TTS (pyttsx3) unavailable &mdash; preview uses browser speech synthesis</div>
<input type="hidden" id="voice-select" name="voice_id" value="{{ settings.voice_id }}">
{% endif %}
</div>
<div class="vs-field">
<label class="vs-label" for="preview-text">PREVIEW TEXT</label>
<input type="text" class="vs-input" id="preview-text"
value="Hello, I am Timmy. Your local AI assistant."
placeholder="Enter text to preview...">
</div>
<div class="vs-actions">
<button type="button" class="vs-btn-preview" id="preview-btn" onclick="previewVoice()">
&#9654; PREVIEW
</button>
<button type="button" class="vs-btn-save" id="save-btn" onclick="saveSettings()">
SAVE SETTINGS
</button>
</div>
</form>
{% endcall %}
</div>
<script>
function previewVoice() {
var text = document.getElementById('preview-text').value.trim() ||
'Hello, I am Timmy. Your local AI assistant.';
var rate = parseInt(document.getElementById('rate-slider').value, 10);
var volume = parseInt(document.getElementById('vol-slider').value, 10) / 100;
if (!('speechSynthesis' in window)) {
McToast.show('Speech synthesis not supported in this browser', 'warn');
return;
}
window.speechSynthesis.cancel();
var utterance = new SpeechSynthesisUtterance(text);
// Web Speech API rate: 1.0 ≈ 175 WPM (default)
utterance.rate = rate / 175;
utterance.volume = volume;
// Best-effort voice match from server selection
var voiceSelect = document.getElementById('voice-select');
if (voiceSelect && voiceSelect.value) {
var selectedText = voiceSelect.options[voiceSelect.selectedIndex].text.toLowerCase();
var firstWord = selectedText.split(' ')[0];
var browserVoices = window.speechSynthesis.getVoices();
var matched = browserVoices.find(function(v) {
return v.name.toLowerCase().includes(firstWord);
});
if (matched) { utterance.voice = matched; }
}
window.speechSynthesis.speak(utterance);
McToast.show('Playing preview\u2026', 'info');
}
async function saveSettings() {
var rate = document.getElementById('rate-slider').value;
var volPct = parseInt(document.getElementById('vol-slider').value, 10);
var voiceId = document.getElementById('voice-select').value;
var body = new URLSearchParams({
rate: rate,
volume: (volPct / 100).toFixed(2),
voice_id: voiceId
});
try {
var resp = await fetch('/voice/settings/save', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString()
});
var data = await resp.json();
if (data.saved) {
McToast.show('Voice settings saved.', 'info');
} else {
McToast.show('Failed to save settings.', 'error');
}
} catch (e) {
McToast.show('Error saving settings.', 'error');
}
}
</script>
{% endblock %}