feat: add voice customization settings UI (#1017)
Some checks failed
Tests / lint (pull_request) Failing after 16s
Tests / test (pull_request) Has been skipped

- Add /voice/settings page with sliders for speed (WPM) and volume,
  voice model selector, preview text input, and preview/save actions
- Add /voice/settings/data (GET) and /voice/settings/save (POST) endpoints
  to voice.py; settings persisted to data/voice_settings.json and applied
  live to the pyttsx3 TTS engine with graceful degradation
- Add VS* CSS classes to mission-control.css (range slider, select, actions)
- Link "VOICE SETTINGS" in desktop dropdown and mobile nav in base.html
- Preview uses browser Web Speech API (pitch/rate/volume) for instant feedback

Fixes #1017

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Alexander Whitestone
2026-03-23 14:37:23 -04:00
parent 0c627f175b
commit f352932db5
4 changed files with 334 additions and 2 deletions

View File

@@ -2547,3 +2547,120 @@
.tower-adv-title { font-size: 0.85rem; font-weight: 600; color: var(--text-bright); }
.tower-adv-detail { font-size: 0.8rem; color: var(--text); margin-top: 2px; }
.tower-adv-action { font-size: 0.75rem; color: var(--green); margin-top: 4px; font-style: italic; }
/* ── Voice settings ───────────────────────────────────────── */
.voice-settings-page { max-width: 600px; margin: 0 auto; }
.vs-field { margin-bottom: 1.5rem; }
.vs-label {
display: block;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.1em;
color: var(--text-dim);
margin-bottom: 0.5rem;
}
.vs-value { color: var(--green); font-family: var(--font); }
.vs-slider {
width: 100%;
-webkit-appearance: none;
appearance: none;
height: 4px;
background: var(--border);
border-radius: 2px;
outline: none;
cursor: pointer;
}
.vs-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--purple);
cursor: pointer;
box-shadow: 0 0 6px rgba(124, 58, 237, 0.5);
transition: box-shadow 0.2s;
}
.vs-slider::-webkit-slider-thumb:hover { box-shadow: 0 0 12px rgba(124, 58, 237, 0.8); }
.vs-slider::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--purple);
cursor: pointer;
border: none;
box-shadow: 0 0 6px rgba(124, 58, 237, 0.5);
}
.vs-range-labels {
display: flex;
justify-content: space-between;
font-size: 0.7rem;
color: var(--text-dim);
margin-top: 0.25rem;
}
.vs-select,
.vs-input {
width: 100%;
padding: 0.5rem 0.75rem;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text);
font-family: var(--font);
font-size: 0.9rem;
}
.vs-select { cursor: pointer; }
.vs-select:focus,
.vs-input:focus {
outline: none;
border-color: var(--purple);
box-shadow: 0 0 0 2px rgba(124, 58, 237, 0.2);
}
.vs-unavailable {
font-size: 0.85rem;
color: var(--text-dim);
padding: 0.5rem 0.75rem;
border: 1px dashed var(--border);
border-radius: var(--radius-sm);
}
.vs-actions {
display: flex;
gap: 0.75rem;
margin-top: 1.5rem;
flex-wrap: wrap;
}
.vs-btn-preview,
.vs-btn-save {
flex: 1;
padding: 0.6rem 1.2rem;
border-radius: var(--radius-sm);
font-family: var(--font);
font-size: 0.85rem;
font-weight: 700;
letter-spacing: 0.08em;
cursor: pointer;
min-height: 44px;
transition: opacity 0.2s, box-shadow 0.2s, background 0.2s;
}
.vs-btn-preview {
background: transparent;
border: 1px solid var(--purple);
color: var(--purple);
}
.vs-btn-preview:hover {
background: rgba(124, 58, 237, 0.15);
box-shadow: 0 0 8px rgba(124, 58, 237, 0.3);
}
.vs-btn-save {
background: var(--green);
border: none;
color: var(--bg-deep);
}
.vs-btn-save:hover { opacity: 0.85; }