Let word mode combine with the VOX preset

This commit is contained in:
Fisto
2026-09-21 11:47:38 +02:00
parent 7ad0a691db
commit 51d42b3a16
6 changed files with 44 additions and 16 deletions
+6
View File
@@ -40,6 +40,12 @@ 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/<scrape>/<item>/thumb` (200).
- [x] C65 Word mode with the Half-Life preset. Word mode no longer yields to VOX prosody: with
a preset it synthesizes each word with the preset profile (per-word `\!R`/`\!%` tags), and the
VOX parser is skipped because its tags cannot survive a word split. The daemon always exports
`VESKSLOP_TTS_VOX` (0/1) so an explicit off is not overridden by the preset implication, and
the TTS panel allows preset + VOX + word mode together (word mode wins for delivery). Wrapper
tests cover preset+word mode with tags, the implied-VOX case, and the profile.
- [x] C64 Correct preset application. The design's bug-fix section landed in the wrapper:
Flextalk rate/pitch now ride as sticky `\!R` / `\!%` vendor tags (attributes only for MSTTS),
neutral sliders emit neither, word mode tags every word, and presets write the audible profile
+8 -4
View File
@@ -158,8 +158,10 @@ from the TTS panel:
default 0), and word mode (`tts.words`) with a gap in ms (`tts.wordGapMs`) for the radio/robot
character. **VOX prosody** (`tts.vox`) routes the line through the NetTTS-style encoder first -
sentence breaks, lead-ins, weighted pauses, and Flextalk vendor tags - for Black Mesa
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
announcement energy; **clean** (`tts.voxClean`) drops the staccato wrapper. Word mode **combines
with the presets** and takes precedence over VOX prosody: each word is its own engine instance,
so the parser is skipped and the vendor tags go on every word (`\!R` rate, `\!%` pitch). VOX
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
@@ -168,8 +170,10 @@ from the TTS panel:
both registry hives (plus the preset's Gender/Line Mode and a Default Speaker pointer), and for
Flextalk it sends rate/pitch as the sticky `\!R` / `\!%` vendor tags rather than
`ITTSAttributes`; attributes only apply to MSTTS voices. Neutral sliders emit neither, so the
preset is reproduced exactly. `sh microslopsam/setup/test-wrapper.sh` checks this contract with
stubbed wine and engine binaries, no Wine install needed. The preset values also show up in the
preset is reproduced exactly. The daemon always exports `VESKSLOP_TTS_VOX` (0 or 1) so an
explicit off is honored, and the panel lets a preset, VOX, and word mode be combined.
`sh microslopsam/setup/test-wrapper.sh` checks this contract with stubbed wine and engine
binaries, no Wine install needed. The preset values also show up in the
voice metadata defaults, so the panel's "default" snap points match what the engine gets.
- **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
+16
View File
@@ -103,6 +103,22 @@ VESKSLOP_TTS_VOICE=keith-bell run_wrapper
check "plain run writes both hives" "2" "$(grep -c 'Speakers\\{11260610' "$STUB_REG")"
check "plain run has no preset extras" "0" "$(grep -cE 'Gender|Line Mode|Default Speaker' "$STUB_REG")"
# Word mode combines with a preset and skips the VOX parser, with tags per word.
VESKSLOP_TTS_VOICE=keith-bell VESKSLOP_TTS_PRESET=vox VESKSLOP_TTS_VOX=1 \
VESKSLOP_TTS_WORDS=1 VESKSLOP_TTS_WORD_GAP_MS=0 VESKSLOP_TTS_SPEED=1.25 run_wrapper
check "word mode with preset synthesizes every word" "3" "$(grep -c '^ARGV:' "$STUB_LOG")"
check "word mode skips the VOX parser" "0" "$(grep -c 'wH1' "$STUB_LOG")"
check "word mode tags every word" "3" \
"$(sed -n 's/.*\[-t\] \[\(.*\)\] \[-o\].*/\1/p' "$STUB_LOG" | grep -c '^\\!R0.800 ')"
check "word mode keeps the preset profile" "2" "$(grep -c '"Pitch"=dword:00000003' "$STUB_REG")"
check "word mode emits no attributes" "0" "$(attr_count)"
# The vox preset implies VOX when the variable is unset, but word mode still wins.
VESKSLOP_TTS_VOICE=keith-bell VESKSLOP_TTS_PRESET=vox VESKSLOP_TTS_WORDS=1 \
VESKSLOP_TTS_WORD_GAP_MS=0 run_wrapper
check "implied VOX yields to word mode" "3" "$(grep -c '^ARGV:' "$STUB_LOG")"
check "implied VOX skipped for words" "0" "$(grep -c 'wH1' "$STUB_LOG")"
# Voice metadata follows the active preset.
check "voices reflect hev" "1" \
"$(VESKSLOP_TTS_PRESET=hev sh "$WRAP" --voices | tr ',' '\n' | grep -c 'keith-bell|Keith Bell|male|en|expression=59+defaults=pitch:15:rate:24')"
+9 -5
View File
@@ -16,7 +16,8 @@
# prominences, head_size, lowering
# VESKSLOP_TTS_HIGHPASS high-pass Hz (40-400, default 40)
# VESKSLOP_TTS_ECHO mono echo amount 0-100 (default 0)
# VESKSLOP_TTS_WORDS 1 = synthesize each word, merge (Flextalk)
# VESKSLOP_TTS_WORDS 1 = synthesize each word, merge (Flextalk);
# combines with a preset and wins over VOX prosody
# 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)
@@ -305,9 +306,11 @@ fi
# SAPI4 is ANSI/CP1252. VOX mode (Flextalk only) first transforms the text into
# vendor tags (\!wH1/\!wH0, \!br, \!sfNN) with setup/vox_parser.py; the parser
# works on UTF-8, so it runs before the CP1252 transliteration.
# works on UTF-8, so it runs before the CP1252 transliteration. Word mode builds
# its own cadence from single-word instances, so the parser is skipped there: the
# VOX tags cannot survive a word split.
TEXT_SRC="$TEXT_IN"
if [ "$VOX" = "1" ] && [ -n "$SPEAKER_GUID" ]; then
if [ "$VOX" = "1" ] && [ -n "$SPEAKER_GUID" ] && [ "$WORDS" != "1" ]; then
if command -v "$MICROSLOP_PYTHON" >/dev/null 2>&1 && [ -f "$MICROSLOP_ROOT/setup/vox_parser.py" ]; then
if [ "$VOX_CLEAN" = "1" ]; then
"$MICROSLOP_PYTHON" "$MICROSLOP_ROOT/setup/vox_parser.py" --clean < "$TEXT_IN" > "$TMP/vox.txt"
@@ -356,8 +359,9 @@ try_run_host() {
# Word mode (Flextalk): synthesize each word and merge with silence gaps, which
# gives the staccato radio cadence the Half-Life VOX/HEV voices are known for.
# VOX mode takes precedence: it produces its own cadence with vendor tags.
if [ "$WORDS" = "1" ] && [ -n "$SPEAKER_GUID" ] && [ "$VOX" != "1" ]; then
# It combines with a preset (the profile is already written) and takes precedence
# over VOX prosody, which cannot survive a word split.
if [ "$WORDS" = "1" ] && [ -n "$SPEAKER_GUID" ]; then
GAP=$WORD_GAP
case "$GAP" in *[!0-9]*|"") GAP=120 ;; esac
[ "$GAP" -gt 500 ] && GAP=500
+2 -6
View File
@@ -2031,7 +2031,6 @@ function fillProfileControls() {
$("#tts-vox").checked = state.speech.vox === true;
$("#tts-vox-clean").checked = state.speech.voxClean === true;
$("#tts-vox-clean").disabled = state.speech.vox !== true;
$("#tts-words").disabled = state.speech.vox === true;
const gap = Number(state.speech.wordGapMs);
$("#tts-word-gap").value = Number.isFinite(gap) && state.speech.wordGapMs !== null ? gap : 120;
$("#tts-word-gap-value").textContent = `${$("#tts-word-gap").value}ms`;
@@ -2744,17 +2743,14 @@ function init() {
$("#tts-words").addEventListener("change", () => void saveSpeech());
$("#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.
// Picking the VOX preset turns on VOX prosody as a default. Word mode still wins
// for word-by-word delivery, and turning VOX off keeps the preset.
if ($("#tts-preset").value === "vox") {
$("#tts-vox").checked = true;
$("#tts-vox-clean").disabled = false;
$("#tts-words").disabled = true;
}
void saveSpeech();
});
+3 -1
View File
@@ -381,7 +381,9 @@ export class Tts {
...(options.wordGapMs !== undefined
? { VESKSLOP_TTS_WORD_GAP_MS: String(options.wordGapMs) }
: {}),
...(options.vox ? { VESKSLOP_TTS_VOX: "1" } : {}),
// Always defined, so an explicit "off" is not overridden by the wrapper's
// preset implication (which only fires when the variable is unset).
VESKSLOP_TTS_VOX: options.vox ? "1" : "0",
...(options.voxClean ? { VESKSLOP_TTS_VOX_CLEAN: "1" } : {}),
...(options.preset ? { VESKSLOP_TTS_PRESET: options.preset } : {})
},