diff --git a/CURRENT.md b/CURRENT.md index a75a9ad..8c4e6d5 100644 --- a/CURRENT.md +++ b/CURRENT.md @@ -40,6 +40,13 @@ mobile draft at 390px. - [x] C11 preview caching: Discord-hosted image/GIF previews (WebP 320px, hash-addressed, 5 MiB cap, one attempt) run after every walk in both modes. Live run `...-20260916090722`: `previewed: 2`; thumbs served over `/api/media///thumb` (200). + - [x] C63 Half-Life presets. The wrapper now implements `VESKSLOP_TTS_PRESET=vox|hev` (Keith + Bell profile: VOX pitch 3/rate 17/prominence 22/head 52/volume 70, HEV pitch 15/rate 24, + expression 59; other voices untouched; preset < profile < expression; the `vox` preset implies + VOX prosody unless `VESKSLOP_TTS_VOX` is set). The daemon carries `tts.preset` (vox/hev/null), + per-line overrides on `/api/audio/speak` and MCP `speak`, the `VESKSLOP_TTS_PRESET` export and + `{preset}` placeholder, and the TTS panel has a preset select that auto-enables VOX (and drops + the preset if VOX is turned off). Wrapper syntax checked; preferences round-trip verified. - [x] C62 VOX prosody controls. The last open Veskslop-side item from the MicroslopSam design: `tts.vox` and `tts.voxClean` preferences (per-line overrides on `/api/audio/speak` and MCP `speak`), `VESKSLOP_TTS_VOX` / `VESKSLOP_TTS_VOX_CLEAN` exports plus `{vox}` / `{vox_clean}` diff --git a/docs/voice.md b/docs/voice.md index 0774976..b72ca73 100644 --- a/docs/voice.md +++ b/docs/voice.md @@ -161,6 +161,10 @@ from the TTS panel: announcement energy; **clean** (`tts.voxClean`) drops the staccato wrapper. VOX outranks word mode and needs `python3` on the host. Both travel to the command provider as `VESKSLOP_TTS_VOX` / `VESKSLOP_TTS_VOX_CLEAN` (or the `{vox}` / `{vox_clean}` placeholders). + A **preset** select (`tts.preset`: factory, VOX, HEV) shifts the Keith Bell speaker profile to + the Half-Life approximation; `vox` also implies VOX prosody unless VOX is explicitly off, and + overrides travel as `VESKSLOP_TTS_PRESET` / `{preset}`. Precedence is preset < profile < + expression, so the sliders still win over the preset. - **Test lines** - Attention, Laboratory, Critical, Health, and Radiation, plus two custom lines kept in `localStorage`. Each button speaks the line with the current profile, so a change can be auditioned without touching the real queue. diff --git a/microslopsam/setup/veskslop-tts.sh b/microslopsam/setup/veskslop-tts.sh index 44fec36..efb1020 100755 --- a/microslopsam/setup/veskslop-tts.sh +++ b/microslopsam/setup/veskslop-tts.sh @@ -18,6 +18,7 @@ # VESKSLOP_TTS_ECHO mono echo amount 0-100 (default 0) # VESKSLOP_TTS_WORDS 1 = synthesize each word, merge (Flextalk) # VESKSLOP_TTS_WORD_GAP_MS silence between words in ms (0-500) +# VESKSLOP_TTS_PRESET vox | hev (Keith Bell profile preset; vox implies VOX) # VESKSLOP_TTS_VOX 1 = VOX prosody, NetTTS-style (Flextalk) # VESKSLOP_TTS_VOX_CLEAN 1 = VOX without the \!wH1/\!wH0 stutter # @@ -52,6 +53,12 @@ WORDS=${VESKSLOP_TTS_WORDS:-0} WORD_GAP=${VESKSLOP_TTS_WORD_GAP_MS:-120} VOX=${VESKSLOP_TTS_VOX:-0} VOX_CLEAN=${VESKSLOP_TTS_VOX_CLEAN:-0} +PRESET=${VESKSLOP_TTS_PRESET:-} + +# The vox preset implies VOX prosody unless the caller set VESKSLOP_TTS_VOX explicitly. +if [ "$PRESET" = "vox" ] && [ -z "${VESKSLOP_TTS_VOX+x}" ]; then + VOX=1 +fi case "$VOICE" in sam) ENGVOICE="Sam" ;; @@ -173,6 +180,15 @@ if [ -n "$SPEAKER_GUID" ]; then P_LOWER=40 P_EXPR="" + # Half-Life presets target Keith Bell only. Precedence: preset < VESKSLOP_TTS_PROFILE + # < VESKSLOP_TTS_EXPRESSION. + if [ "$ENGVOICE" = "Keith Bell" ]; then + case "$PRESET" in + vox) P_PITCH=3; P_RATE=17; P_PROM=22; P_HEAD=52; P_LOWER=40; P_VOLUME=70; P_EXPR=59 ;; + hev) P_PITCH=15; P_RATE=24; P_PROM=22; P_HEAD=52; P_LOWER=40; P_VOLUME=70; P_EXPR=59 ;; + esac + fi + if [ -n "$PROFILE" ]; then OLDIFS=$IFS IFS=',' diff --git a/poc/public/app.js b/poc/public/app.js index 93e3515..d170488 100644 --- a/poc/public/app.js +++ b/poc/public/app.js @@ -2027,6 +2027,7 @@ function fillProfileControls() { $("#tts-echo-value").textContent = Number($("#tts-echo").value) > 0 ? $("#tts-echo").value : "off"; $("#tts-words").checked = state.speech.words === true; + $("#tts-preset").value = state.speech.preset ?? ""; $("#tts-vox").checked = state.speech.vox === true; $("#tts-vox-clean").checked = state.speech.voxClean === true; $("#tts-vox-clean").disabled = state.speech.vox !== true; @@ -2064,7 +2065,8 @@ function effectOptions() { words: $("#tts-words").checked, wordGapMs: Number($("#tts-word-gap").value), vox: $("#tts-vox").checked, - voxClean: $("#tts-vox-clean").checked + voxClean: $("#tts-vox-clean").checked, + preset: $("#tts-preset").value || null }; } function syncVoiceControls(voices, selected) { @@ -2743,6 +2745,17 @@ function init() { $("#tts-vox").addEventListener("change", () => { $("#tts-vox-clean").disabled = !$("#tts-vox").checked; $("#tts-words").disabled = $("#tts-vox").checked; + // The vox preset implies VOX; turning VOX off means "factory/custom" again. + if (!$("#tts-vox").checked && $("#tts-preset").value === "vox") $("#tts-preset").value = ""; + void saveSpeech(); + }); + $("#tts-preset").addEventListener("change", () => { + // Picking the VOX preset turns on VOX prosody so the panel matches the engine. + if ($("#tts-preset").value === "vox") { + $("#tts-vox").checked = true; + $("#tts-vox-clean").disabled = false; + $("#tts-words").disabled = true; + } void saveSpeech(); }); $("#tts-vox-clean").addEventListener("change", () => void saveSpeech()); diff --git a/poc/public/index.html b/poc/public/index.html index 0688ef8..12d3f92 100644 --- a/poc/public/index.html +++ b/poc/public/index.html @@ -556,6 +556,15 @@ 120ms +
+ preset + + Keith Bell +