Merge the MicroslopSam engine into the repository
This commit is contained in:
@@ -19,9 +19,13 @@
|
||||
# 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)
|
||||
# VESKSLOP_TTS_VOX_CLEAN 1 = VOX without the \!wH1/\!wH0 stutter
|
||||
# VESKSLOP_TTS_HUM_MS leading low hum in ms, 0 = off (default 80)
|
||||
# VESKSLOP_TTS_HUM_HZ hum frequency, 30-200 (default 75)
|
||||
# VESKSLOP_TTS_HUM_DB hum peak level, -80..-6 (default -48)
|
||||
# VESKSLOP_TTS_PRESET vox | hev (Half-Life profiles, Keith Bell;
|
||||
# vox also enables VOX prosody)
|
||||
#
|
||||
# Output: 48 kHz mono s16le WAV, trimmed, faded, loudness-normalized.
|
||||
set -eu
|
||||
@@ -30,17 +34,18 @@ HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
. "$HERE/config.sh"
|
||||
ROOT=$MICROSLOP_ROOT
|
||||
|
||||
# VESKSLOP_TTS_VOICES entries: id|Name|gender|language[|flags]
|
||||
# `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
|
||||
# it into the Wine registry before synthesis.
|
||||
# The Keith Bell defaults follow the active preset so the UI's "default" snap points match
|
||||
# the values the preset actually writes.
|
||||
# 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
|
||||
|
||||
# VESKSLOP_TTS_VOICES entries: id|Name|gender|language[|flags]
|
||||
# `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
|
||||
# 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=$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
|
||||
@@ -62,11 +67,14 @@ 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
|
||||
# The VOX preset is the Half-Life announcement voice: it implies VOX prosody
|
||||
# unless the caller explicitly disables it.
|
||||
if [ "$PRESET" = "vox" ] && [ -z "${VESKSLOP_TTS_VOX:-}" ]; then
|
||||
VOX=1
|
||||
fi
|
||||
HUM_MS=${VESKSLOP_TTS_HUM_MS:-80}
|
||||
HUM_HZ=${VESKSLOP_TTS_HUM_HZ:-75}
|
||||
HUM_DB=${VESKSLOP_TTS_HUM_DB:--48}
|
||||
|
||||
case "$VOICE" in
|
||||
sam) ENGVOICE="Sam" ;;
|
||||
@@ -160,20 +168,24 @@ if [ -f "$TEXTFILE" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# 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.
|
||||
# Build the engine controls from the (possibly command-overridden) speed/pitch.
|
||||
# Flextalk voices take sticky vendor tags relative to the registry profile, the
|
||||
# way NetTTS does it: \!R is a duration factor (smaller = faster, 1.0 neutral)
|
||||
# and \!% is a pitch multiplier (1.0 neutral). ITTSAttributes -s/-p would
|
||||
# override the profile and make presets sound fast and shallow, so tags are used
|
||||
# for Flextalk. MSTTS has no vendor tags and keeps -s/-p.
|
||||
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} "
|
||||
# A neutral multiplier must not emit a tag: \!R1.00 means a 1.0x rate,
|
||||
# which would override the profile's own speaking rate.
|
||||
R=$(awk -v m="$SPEED" 'BEGIN{ if (m<=0) m=1; if (m==1) { exit } f=1/m; if (f<0.25) f=0.25; if (f>4) f=4; printf "%.2f", f }')
|
||||
[ -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} "
|
||||
PCT=$(awk -v st="$PITCH" 'BEGIN{ if (st==0) { exit } f=exp(log(2)*st/12); if (f<0.5) f=0.5; if (f>2) f=2; printf "%.2f", f }')
|
||||
[ -n "$PCT" ] && TAG_PREFIX="$TAG_PREFIX\\!%$PCT "
|
||||
fi
|
||||
else
|
||||
if [ -n "$SPEED" ]; then
|
||||
@@ -203,14 +215,24 @@ 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
|
||||
# Half-Life presets for Keith Bell (the game's announcement voice).
|
||||
# "vox" is the approximated Valve VOX profile published by NetTTS
|
||||
# (scripts/winetricks/nettts_flextalk_vox_profile.reg); "hev" is a starting
|
||||
# point for the HEV suit voice, which has no published profile.
|
||||
case "$PRESET" in
|
||||
vox)
|
||||
if [ "$ENGVOICE" = "Keith Bell" ]; then
|
||||
P_PITCH=3; P_RATE=17; P_VOLUME=70
|
||||
P_PROM=22; P_HEAD=52; P_LOWER=40
|
||||
fi
|
||||
;;
|
||||
hev)
|
||||
if [ "$ENGVOICE" = "Keith Bell" ]; then
|
||||
P_PITCH=15; P_RATE=24; P_VOLUME=70
|
||||
P_PROM=22; P_HEAD=52; P_LOWER=40
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -n "$PROFILE" ]; then
|
||||
OLDIFS=$IFS
|
||||
@@ -250,21 +272,27 @@ if [ -n "$SPEAKER_GUID" ]; then
|
||||
esac
|
||||
done
|
||||
|
||||
IS_PRESET=0
|
||||
if [ "$ENGVOICE" = "Keith Bell" ]; then
|
||||
case "$PRESET" in
|
||||
vox|hev) IS_PRESET=1 ;;
|
||||
esac
|
||||
# NetTTS's VOX profile also carries Gender, Line Mode, the text-class
|
||||
# switches and the default-speaker pointer. Write the full set for presets,
|
||||
# exactly like its nettts_flextalk_vox_profile.reg, in both hives.
|
||||
FULL_PROFILE=0
|
||||
if [ "$PRESET" = "vox" ] || [ "$PRESET" = "hev" ]; then
|
||||
if [ "$ENGVOICE" = "Keith Bell" ]; then FULL_PROFILE=1; fi
|
||||
fi
|
||||
|
||||
REG_ROOT='Software\AT&T\Watson\2.0\FlexTalk\2.0'
|
||||
# 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.
|
||||
# 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'
|
||||
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"
|
||||
if [ "$FULL_PROFILE" = "1" ]; then
|
||||
printf '[HKEY_LOCAL_MACHINE\\%s]\n' "$REG_ROOT"
|
||||
printf '"Default Speaker"="%s"\n\n' "$SPEAKER_GUID"
|
||||
printf '[HKEY_CURRENT_USER\\%s]\n' "$REG_ROOT"
|
||||
printf '"Default Speaker"="%s"\n\n' "$SPEAKER_GUID"
|
||||
fi
|
||||
for hive in HKEY_LOCAL_MACHINE HKEY_CURRENT_USER; do
|
||||
printf '[%s\\%s\\Speakers\\%s]\n' "$hive" "$REG_ROOT" "$SPEAKER_GUID"
|
||||
printf '"Pitch"=dword:%08x\n' "$P_PITCH"
|
||||
printf '"Speaking Rate"=dword:%08x\n' "$P_RATE"
|
||||
printf '"Volume"=dword:%08x\n' "$P_VOLUME"
|
||||
@@ -272,19 +300,29 @@ if [ -n "$SPEAKER_GUID" ]; then
|
||||
printf '"Prominences"=dword:%08x\n' "$P_PROM"
|
||||
printf '"Head Size"=dword:%08x\n' "$P_HEAD"
|
||||
printf '"Lowering"=dword:%08x\n' "$P_LOWER"
|
||||
if [ "$IS_PRESET" = "1" ]; then
|
||||
if [ "$FULL_PROFILE" = "1" ]; then
|
||||
printf '"Abbreviation Expansion"=dword:00000001\n'
|
||||
printf '"Address Class"=dword:00000001\n'
|
||||
printf '"Age"=dword:0000001e\n'
|
||||
printf '"Alphabetic Hyphen"=dword:00000000\n'
|
||||
printf '"Case Significance"=dword:00000001\n'
|
||||
printf '"Date Class"=dword:00000001\n'
|
||||
printf '"Fraction Class"=dword:00000001\n'
|
||||
printf '"Gender"=dword:00000002\n'
|
||||
printf '"Line Mode"=dword:00000001\n'
|
||||
printf '"Math Mode"=dword:00000000\n'
|
||||
printf '"Measurements Class"=dword:00000001\n'
|
||||
printf '"Money Class"=dword:00000001\n'
|
||||
printf '"Numerical Hyphen"=dword:00000000\n'
|
||||
printf '"Ordinal Numbers Class"=dword:00000001\n'
|
||||
printf '"ProofRead Mode"=dword:00000000\n'
|
||||
printf '"Proper Names Class"=dword:00000001\n'
|
||||
printf '"Spell Mode"=dword:00000000\n'
|
||||
printf '"Telephone Numbers Class"=dword:00000001\n'
|
||||
printf '"Time Class"=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"
|
||||
|
||||
# Guard against silent reg-import failures (a malformed key would corrupt
|
||||
@@ -294,21 +332,34 @@ if [ -n "$SPEAKER_GUID" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v "$MICROSLOP_WINE" >/dev/null 2>&1; then
|
||||
echo "wine is required to set the Flextalk profile" >&2
|
||||
exit 1
|
||||
# Skip the wine round-trip when this voice's profile was already applied.
|
||||
# The cache lives inside the prefix, so recreating the prefix invalidates
|
||||
# it automatically.
|
||||
PROFILE_CACHE="$WINEPREFIX/.microslopsam-profile$SPEAKER_GUID.reg"
|
||||
if [ -f "$PROFILE_CACHE" ] && cmp -s "$TMP/profile.reg" "$PROFILE_CACHE"; then
|
||||
PROFILE_APPLIED=1
|
||||
else
|
||||
PROFILE_APPLIED=0
|
||||
fi
|
||||
if ! "$MICROSLOP_WINE" reg import "$TMP/profile.reg" >/dev/null 2>&1; then
|
||||
echo "could not write the Flextalk profile" >&2
|
||||
exit 1
|
||||
|
||||
if [ "$PROFILE_APPLIED" != "1" ]; then
|
||||
if ! command -v "$MICROSLOP_WINE" >/dev/null 2>&1; then
|
||||
echo "wine is required to set the Flextalk profile" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! "$MICROSLOP_WINE" reg import "$TMP/profile.reg" >/dev/null 2>&1; then
|
||||
echo "could not write the Flextalk profile" >&2
|
||||
exit 1
|
||||
fi
|
||||
cp -f "$TMP/profile.reg" "$PROFILE_CACHE" 2>/dev/null || true
|
||||
fi
|
||||
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. Word mode builds
|
||||
# its own cadence from single-word instances, so the parser is skipped there: the
|
||||
# VOX tags cannot survive a word split.
|
||||
# its own cadence from single-word instances, so the parser is skipped there: VOX
|
||||
# tags cannot survive a word split.
|
||||
TEXT_SRC="$TEXT_IN"
|
||||
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
|
||||
@@ -370,7 +421,7 @@ if [ "$WORDS" = "1" ] && [ -n "$SPEAKER_GUID" ]; then
|
||||
INDEX=0
|
||||
for word in $(cat "$TXT"); do
|
||||
INDEX=$((INDEX + 1))
|
||||
try_run_host "$TMP/w$INDEX.wav" "${TAG_PREFIX}${word}"
|
||||
try_run_host "$TMP/w$INDEX.wav" "$TAG_PREFIX$word"
|
||||
echo "file '$TMP/w$INDEX.wav'" >> "$TMP/list.txt"
|
||||
|
||||
if [ "$GAP" -gt 0 ]; then
|
||||
@@ -388,7 +439,7 @@ if [ "$WORDS" = "1" ] && [ -n "$SPEAKER_GUID" ]; then
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
try_run_host "$RAW" "${TAG_PREFIX}$(cat "$TXT")"
|
||||
try_run_host "$RAW" "$TAG_PREFIX$(cat "$TXT")"
|
||||
fi
|
||||
[ -s "$RAW" ] || { echo "engine produced no audio" >&2; exit 1; }
|
||||
|
||||
@@ -428,7 +479,31 @@ else
|
||||
fi
|
||||
|
||||
TMPOUT="$TMP/out.wav"
|
||||
if ! "$MICROSLOP_FFMPEG" -hide_banner -nostats -y -i "$RAW" -af "$PRE,volume=${GAIN}dB" \
|
||||
# Leading hum: a quiet low tone in the pre-speech padding, to prime Discord's
|
||||
# audio pipeline so the first syllable is not gated. Level/frequency/duration
|
||||
# are configurable; 0 ms disables it.
|
||||
case "$HUM_MS" in *[!0-9]*|"") HUM_MS=80 ;; esac
|
||||
[ "$HUM_MS" -gt 2000 ] && HUM_MS=2000
|
||||
case "$HUM_HZ" in *[!0-9.]*|"") HUM_HZ=75 ;; esac
|
||||
case "$HUM_DB" in *[!0-9.-]*|""|"."|"-") HUM_DB=-48 ;; esac
|
||||
if [ "$HUM_MS" -gt 0 ]; then
|
||||
HUM_S=$(awk -v ms="$HUM_MS" 'BEGIN{printf "%.3f", ms/1000}')
|
||||
HUM_HZ=$(awk -v v="$HUM_HZ" 'BEGIN{ if (v<30) v=30; if (v>200) v=200; printf "%.1f", v }')
|
||||
HUM_DB=$(awk -v v="$HUM_DB" 'BEGIN{ if (v>-6) v=-6; if (v<-80) v=-80; printf "%.1f", v }')
|
||||
HUM_AMP=$(awk -v db="$HUM_DB" 'BEGIN{printf "%.6f", 10^(db/20)}')
|
||||
HUM_FADE=$(awk -v s="$HUM_S" 'BEGIN{ f=s/4; if (f>0.02) f=0.02; if (f<0.005) f=0.005; printf "%.3f", f }')
|
||||
HUM_FADE_OUT=$(awk -v s="$HUM_S" -v f="$HUM_FADE" 'BEGIN{printf "%.3f", s-f}')
|
||||
# The hum is mixed on its own chain and deliberately bypasses $PRE (highpass,
|
||||
# trim, fades): VESKSLOP_TTS_HIGHPASS must never remove it.
|
||||
if ! "$MICROSLOP_FFMPEG" -hide_banner -nostats -y \
|
||||
-f lavfi -i "aevalsrc=exprs='$HUM_AMP*sin(2*PI*$HUM_HZ*t)':s=48000:d=$HUM_S" \
|
||||
-i "$RAW" \
|
||||
-filter_complex "[0:a]afade=t=in:st=0:d=$HUM_FADE,afade=t=out:st=$HUM_FADE_OUT:d=$HUM_FADE,aformat=sample_fmts=s16:channel_layouts=mono:sample_rates=48000[h];[1:a]$PRE,volume=${GAIN}dB,aresample=48000,aformat=sample_fmts=s16:channel_layouts=mono[a];[h][a]concat=n=2:v=0:a=1[out]" \
|
||||
-map "[out]" -c:a pcm_s16le "$TMPOUT" >/dev/null 2>"$TMP/ff"; then
|
||||
head -c 400 "$TMP/ff" >&2
|
||||
exit 1
|
||||
fi
|
||||
elif ! "$MICROSLOP_FFMPEG" -hide_banner -nostats -y -i "$RAW" -af "$PRE,volume=${GAIN}dB" \
|
||||
-ar 48000 -ac 1 -c:a pcm_s16le "$TMPOUT" >/dev/null 2>"$TMP/ff"; then
|
||||
head -c 400 "$TMP/ff" >&2
|
||||
exit 1
|
||||
|
||||
Reference in New Issue
Block a user