Apply TTS presets with vendor tags and both hives

This commit is contained in:
Fisto
2026-09-21 11:43:13 +02:00
parent ac23ee712a
commit 7ad0a691db
5 changed files with 194 additions and 24 deletions
+8
View File
@@ -40,6 +40,14 @@ mobile draft at 390px.
- [x] C11 preview caching: Discord-hosted image/GIF previews (WebP 320px, hash-addressed, 5 MiB - [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`: cap, one attempt) run after every walk in both modes. Live run `...-20260916090722`:
`previewed: 2`; thumbs served over `/api/media/<scrape>/<item>/thumb` (200). `previewed: 2`; thumbs served over `/api/media/<scrape>/<item>/thumb` (200).
- [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
to both registry hives with Gender/Line Mode plus a Default Speaker pointer. `--voices`
reflects the active preset in the Keith Bell `defaults=` flags so the panel snap points match.
New `microslopsam/setup/test-wrapper.sh` verifies the whole contract with stubbed wine and
engine binaries (tags vs attributes, registry shape, preset extras, implied VOX, metadata);
all checks pass.
- [x] C63 Half-Life presets. The wrapper now implements `VESKSLOP_TTS_PRESET=vox|hev` (Keith - [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, 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 expression 59; other voices untouched; preset < profile < expression; the `vox` preset implies
+7 -1
View File
@@ -164,7 +164,13 @@ from the TTS panel:
A **preset** select (`tts.preset`: factory, VOX, HEV) shifts the Keith Bell speaker profile to 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 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 < overrides travel as `VESKSLOP_TTS_PRESET` / `{preset}`. Precedence is preset < profile <
expression, so the sliders still win over the preset. expression, so the sliders still win over the preset. The wrapper writes the audible profile to
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
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 - **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 kept in `localStorage`. Each button speaks the line with the current profile, so a change can be
auditioned without touching the real queue. auditioned without touching the real queue.
+2
View File
@@ -22,6 +22,8 @@ pure-native build, and keep a path to a fully portable engine.
- AT&T Flextalk: Keith Bell, Kerry Watson - AT&T Flextalk: Keith Bell, Kerry Watson
- Test output: `build/test_sam.wav` (22.05 kHz), `build/test_Keith_Bell.wav`, - Test output: `build/test_sam.wav` (22.05 kHz), `build/test_Keith_Bell.wav`,
`build/test_Kerry_Watson.wav` (11.025 kHz). `build/test_Kerry_Watson.wav` (11.025 kHz).
- Wrapper contract test (no Wine needed, stubs wine and the engine host):
`sh setup/test-wrapper.sh`.
## Build ## Build
+110
View File
@@ -0,0 +1,110 @@
#!/bin/sh
# Regression test for the Veskslop TTS wrapper contract. No Wine needed: the wine
# and engine-host binaries are stubbed, so this checks the arguments the wrapper
# builds, the registry import it generates, and the voice metadata it publishes.
#
# The bug this guards against: rate/pitch sent as ITTSAttributes (-s/-p) override
# the Flextalk speaker profile, so the VOX/HEV presets sounded too fast and not
# deep enough. Flextalk must use the sticky vendor tags (\!R / \!%), and neutral
# sliders must emit neither.
set -eu
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
WRAP="$HERE/veskslop-tts.sh"
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
mkdir -p "$WORK/build" "$WORK/bin" "$WORK/out"
"${MICROSLOP_FFMPEG:-ffmpeg}" -hide_banner -loglevel error -f lavfi \
-i "sine=frequency=200:duration=0.3" -ar 11025 -ac 1 -c:a pcm_s16le -y "$WORK/sine.wav"
cat > "$WORK/build/sapi4host.exe" <<'STUB'
#!/bin/sh
{ printf 'ARGV:'; for a in "$@"; do printf ' [%s]' "$a"; done; printf '\n'; } >> "$STUB_LOG"
OUT=""
while [ "$#" -ge 2 ]; do
[ "$1" = "-o" ] && OUT=$2
shift
done
cp "$STUB_SINE" "$OUT"
STUB
cat > "$WORK/bin/wine" <<'STUB'
#!/bin/sh
[ "${1:-}" = "reg" ] && [ "${2:-}" = "import" ] && cp "$3" "$STUB_REG"
exit 0
STUB
chmod +x "$WORK/build/sapi4host.exe" "$WORK/bin/wine"
printf 'Attention all personnel.\n' > "$WORK/text.txt"
export MICROSLOP_BUILD="$WORK/build"
export MICROSLOP_WINE="$WORK/bin/wine"
export MICROSLOP_FFMPEG="${MICROSLOP_FFMPEG:-ffmpeg}"
export DISPLAY=:99
export STUB_LOG="$WORK/host.log"
export STUB_REG="$WORK/profile.reg"
export STUB_SINE="$WORK/sine.wav"
FAIL=0
check() {
if [ "$2" = "$3" ]; then
echo "ok - $1"
else
echo "FAIL - $1: expected [$2], got [$3]"
FAIL=1
fi
}
run_wrapper() {
: > "$STUB_LOG"
sh "$WRAP" "$WORK/out.wav" "$WORK/text.txt" >/dev/null
}
# The spoken text is the -t argument; the engine host writes to the wrapper's temp file.
text_for() {
sed -n '1s/.*\[-t\] \[\(.*\)\] \[-o\].*/\1/p' "$STUB_LOG"
}
attr_count() {
grep -oE '\[-s\]|\[-p\]' "$STUB_LOG" | wc -l | tr -d ' '
}
# Neutral Flextalk sliders emit neither attributes nor tags.
VESKSLOP_TTS_VOICE=keith-bell run_wrapper
check "neutral flextalk text" "Attention all personnel." "$(text_for)"
check "neutral flextalk has no attributes" "0" "$(attr_count)"
# Adjustments ride as tags; attributes stay out even at non-neutral values.
VESKSLOP_TTS_VOICE=keith-bell VESKSLOP_TTS_VOX=0 \
VESKSLOP_TTS_SPEED=1.3 VESKSLOP_TTS_PITCH=3 run_wrapper
check "flextalk rate tag" "\!R0.769 \!%1.189 Attention all personnel." "$(text_for)"
check "flextalk has no attributes" "0" "$(attr_count)"
# MSTTS has no vendor tags and keeps the attributes.
VESKSLOP_TTS_VOICE=sam VESKSLOP_TTS_SPEED=1.3 VESKSLOP_TTS_PITCH=3 run_wrapper
check "mstts text has no tags" "Attention all personnel." "$(text_for)"
check "mstts keeps attributes" "2" "$(attr_count)"
# The vox preset implies VOX prosody when VESKSLOP_TTS_VOX is unset.
VESKSLOP_TTS_VOICE=keith-bell VESKSLOP_TTS_PRESET=vox run_wrapper
check "vox preset implies VOX parser" "1" "$(grep -c 'wH1' "$STUB_LOG")"
# The preset writes the full audible profile to both hives.
VESKSLOP_TTS_VOICE=keith-bell VESKSLOP_TTS_PRESET=vox run_wrapper
check "preset writes both hives" "2" "$(grep -c 'Speakers\\{11260610' "$STUB_REG")"
check "preset pitch (both hives)" "2" "$(grep -c '"Pitch"=dword:00000003' "$STUB_REG")"
check "preset rate (both hives)" "2" "$(grep -c '"Speaking Rate"=dword:00000011' "$STUB_REG")"
check "preset gender and line mode" "4" "$(grep -cE '"Gender"=dword:00000002|"Line Mode"=dword:00000001' "$STUB_REG")"
check "preset default speaker" "2" "$(grep -c '"Default Speaker"' "$STUB_REG")"
# A plain Flextalk run keeps the defaults and writes no preset extras.
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")"
# 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')"
[ "$FAIL" = "0" ] && echo "wrapper tests passed" || { echo "wrapper tests failed"; exit 1; }
+49 -5
View File
@@ -33,7 +33,14 @@ ROOT=$MICROSLOP_ROOT
# `expression=NN` marks a Flextalk voice with an Expression profile parameter and # `expression=NN` marks a Flextalk voice with an Expression profile parameter and
# its registry default. Expression is applied at voice load, so the wrapper writes # its registry default. Expression is applied at voice load, so the wrapper writes
# it into the Wine registry before synthesis. # it into the Wine registry before synthesis.
VOICES="sam|Sam|male|en,mary|Mary|female|en,mike|Mike|male|en,keith-bell|Keith Bell|male|en|expression=59+defaults=pitch:27:rate:33:prominences:17:head_size:46:lowering:40:expression:59,kerry-watson|Kerry Watson|female|en|expression=100+defaults=pitch:65:rate:32:prominences:17:head_size:45:lowering:40:expression:100,mike-telephone|Mike (for Telephone)|male|en,mary-telephone|Mary (for Telephone)|female|en,mike-hall|Mike in Hall|male|en,mike-stadium|Mike in Stadium|male|en,mike-space|Mike in Space|male|en,mary-hall|Mary in Hall|female|en,mary-stadium|Mary in Stadium|female|en,mary-space|Mary in Space|female|en" # The Keith Bell defaults follow the active preset so the UI's "default" snap points match
# the values the preset actually writes.
case "${VESKSLOP_TTS_PRESET:-}" in
vox) KB_DEFAULTS="pitch:3:rate:17:prominences:22:head_size:52:lowering:40:expression:59" ;;
hev) KB_DEFAULTS="pitch:15:rate:24:prominences:22:head_size:52:lowering:40:expression:59" ;;
*) KB_DEFAULTS="pitch:27:rate:33:prominences:17:head_size:46:lowering:40:expression:59" ;;
esac
VOICES="sam|Sam|male|en,mary|Mary|female|en,mike|Mike|male|en,keith-bell|Keith Bell|male|en|expression=59+defaults=$KB_DEFAULTS,kerry-watson|Kerry Watson|female|en|expression=100+defaults=pitch:65:rate:32:prominences:17:head_size:45:lowering:40:expression:100,mike-telephone|Mike (for Telephone)|male|en,mary-telephone|Mary (for Telephone)|female|en,mike-hall|Mike in Hall|male|en,mike-stadium|Mike in Stadium|male|en,mike-space|Mike in Space|male|en,mary-hall|Mary in Hall|female|en,mary-stadium|Mary in Stadium|female|en,mary-space|Mary in Space|female|en"
if [ "${1:-}" = "--voices" ]; then if [ "${1:-}" = "--voices" ]; then
echo "$VOICES" echo "$VOICES"
@@ -153,7 +160,21 @@ if [ -f "$TEXTFILE" ]; then
fi fi
# Build the engine arguments from the (possibly command-overridden) speed/pitch. # Build the engine arguments from the (possibly command-overridden) speed/pitch.
# Flextalk: rate and pitch ride as sticky vendor tags. Setting them through
# ITTSAttributes (-s/-p) overrides the speaker profile, which is the bug that made
# presets sound "too fast and not deep enough". MSTTS has no tags, so it keeps -s/-p.
SET_ARGS="" SET_ARGS=""
TAG_PREFIX=""
if [ -n "$SPEAKER_GUID" ]; then
if [ -n "$SPEED" ]; then
R=$(awk -v m="$SPEED" 'BEGIN{ if (m==1) exit; v=1/m; if (v<0.25) v=0.25; if (v>4.0) v=4.0; printf "%.3f", v }')
[ -n "$R" ] && TAG_PREFIX="${TAG_PREFIX}\\!R${R} "
fi
if [ -n "$PITCH" ]; then
P=$(awk -v st="$PITCH" 'BEGIN{ if (st==0) exit; v=exp(log(2)*st/12); if (v<0.5) v=0.5; if (v>2.0) v=2.0; printf "%.3f", v }')
[ -n "$P" ] && TAG_PREFIX="${TAG_PREFIX}\\!%${P} "
fi
else
if [ -n "$SPEED" ]; then if [ -n "$SPEED" ]; then
S=$(awk -v m="$SPEED" 'BEGIN{printf "%d", 150*m+0.5}') S=$(awk -v m="$SPEED" 'BEGIN{printf "%d", 150*m+0.5}')
[ "$S" -lt 50 ] && S=50 [ "$S" -lt 50 ] && S=50
@@ -166,6 +187,7 @@ if [ -n "$PITCH" ]; then
[ "$P" -gt "$PMAX" ] && P=$PMAX [ "$P" -gt "$PMAX" ] && P=$PMAX
SET_ARGS="$SET_ARGS -p $P" SET_ARGS="$SET_ARGS -p $P"
fi fi
fi
# Apply the Flextalk profile before synthesis. Profiles are read at voice load and # Apply the Flextalk profile before synthesis. Profiles are read at voice load and
# the host loads the engine per request, so the values are written every call: the # the host loads the engine per request, so the values are written every call: the
@@ -227,12 +249,21 @@ if [ -n "$SPEAKER_GUID" ]; then
esac esac
done done
REG_KEY="HKLM\\Software\\AT&T\\Watson\\2.0\\FlexTalk\\2.0\\Speakers\\$SPEAKER_GUID" IS_PRESET=0
if [ "$ENGVOICE" = "Keith Bell" ]; then
case "$PRESET" in
vox|hev) IS_PRESET=1 ;;
esac
fi
# Use printf, not echo: dash's echo interprets \2 in the key path as an # Use printf, not echo: dash's echo interprets \2 in the key path as an
# escape (STX), which corrupts the key and makes the import a silent no-op. # escape (STX), which corrupts the key and makes the import a silent no-op.
# NetTTS parity: the audible profile goes to both hives. Presets also write the
# informational keys from Valve's setup, and point Default Speaker at Keith Bell.
{ {
printf 'Windows Registry Editor Version 5.00\n\n' printf 'Windows Registry Editor Version 5.00\n\n'
printf '[%s]\n' "$REG_KEY" for HIVE in 'HKEY_LOCAL_MACHINE' 'HKEY_CURRENT_USER'; do
printf '[%s\\Software\\AT&T\\Watson\\2.0\\FlexTalk\\2.0\\Speakers\\%s]\n' "$HIVE" "$SPEAKER_GUID"
printf '"Pitch"=dword:%08x\n' "$P_PITCH" printf '"Pitch"=dword:%08x\n' "$P_PITCH"
printf '"Speaking Rate"=dword:%08x\n' "$P_RATE" printf '"Speaking Rate"=dword:%08x\n' "$P_RATE"
printf '"Volume"=dword:%08x\n' "$P_VOLUME" printf '"Volume"=dword:%08x\n' "$P_VOLUME"
@@ -240,6 +271,19 @@ if [ -n "$SPEAKER_GUID" ]; then
printf '"Prominences"=dword:%08x\n' "$P_PROM" printf '"Prominences"=dword:%08x\n' "$P_PROM"
printf '"Head Size"=dword:%08x\n' "$P_HEAD" printf '"Head Size"=dword:%08x\n' "$P_HEAD"
printf '"Lowering"=dword:%08x\n' "$P_LOWER" printf '"Lowering"=dword:%08x\n' "$P_LOWER"
if [ "$IS_PRESET" = "1" ]; then
printf '"Gender"=dword:00000002\n'
printf '"Line Mode"=dword:00000001\n'
fi
printf '\n'
done
if [ "$IS_PRESET" = "1" ]; then
for HIVE in 'HKEY_LOCAL_MACHINE' 'HKEY_CURRENT_USER'; do
printf '[%s\\Software\\AT&T\\Watson\\2.0\\FlexTalk\\2.0\\Speakers]\n' "$HIVE"
printf '"Default Speaker"="%s"\n\n' "$SPEAKER_GUID"
done
fi
} > "$TMP/profile.reg" } > "$TMP/profile.reg"
# Guard against silent reg-import failures (a malformed key would corrupt # Guard against silent reg-import failures (a malformed key would corrupt
@@ -322,7 +366,7 @@ if [ "$WORDS" = "1" ] && [ -n "$SPEAKER_GUID" ] && [ "$VOX" != "1" ]; then
INDEX=0 INDEX=0
for word in $(cat "$TXT"); do for word in $(cat "$TXT"); do
INDEX=$((INDEX + 1)) INDEX=$((INDEX + 1))
try_run_host "$TMP/w$INDEX.wav" "$word" try_run_host "$TMP/w$INDEX.wav" "${TAG_PREFIX}${word}"
echo "file '$TMP/w$INDEX.wav'" >> "$TMP/list.txt" echo "file '$TMP/w$INDEX.wav'" >> "$TMP/list.txt"
if [ "$GAP" -gt 0 ]; then if [ "$GAP" -gt 0 ]; then
@@ -340,7 +384,7 @@ if [ "$WORDS" = "1" ] && [ -n "$SPEAKER_GUID" ] && [ "$VOX" != "1" ]; then
exit 1 exit 1
fi fi
else else
try_run_host "$RAW" "$(cat "$TXT")" try_run_host "$RAW" "${TAG_PREFIX}$(cat "$TXT")"
fi fi
[ -s "$RAW" ] || { echo "engine produced no audio" >&2; exit 1; } [ -s "$RAW" ] || { echo "engine produced no audio" >&2; exit 1; }