Sync MicroslopSam VOX mode and path configuration

This commit is contained in:
Fisto
2026-09-16 08:46:39 +02:00
parent 49fd28756f
commit 6b961ddcef
12 changed files with 1033 additions and 95 deletions
+104 -39
View File
@@ -18,14 +18,15 @@
# 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_VOX 1 = VOX prosody, NetTTS-style (Flextalk)
# VESKSLOP_TTS_VOX_CLEAN 1 = VOX without the \!wH1/\!wH0 stutter
#
# Output: 48 kHz mono s16le WAV, trimmed, faded, loudness-normalized.
set -eu
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
ROOT=$(CDPATH= cd -- "$HERE/.." && pwd)
export WINEPREFIX=${WINEPREFIX:-$HOME/.wine-microslopsam}
export WINEARCH=win32
. "$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
@@ -49,6 +50,8 @@ HIGHPASS=${VESKSLOP_TTS_HIGHPASS:-40}
ECHO=${VESKSLOP_TTS_ECHO:-0}
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}
case "$VOICE" in
sam) ENGVOICE="Sam" ;;
@@ -83,6 +86,66 @@ case "$ENGVOICE" in
"Kerry Watson") SMAX=390; PMAX=250; NAT_PITCH=186 ;;
*) SMAX=450; PMAX=200; NAT_PITCH=100 ;;
esac
# The daemon does not export DISPLAY; keep a private Xvfb for the engine.
start_xvfb() {
if ! [ -S "$MICROSLOP_XSOCK" ]; then
"$MICROSLOP_XVFB" "$MICROSLOP_DISPLAY" -screen 0 1024x768x24 >/dev/null 2>&1 &
sleep 1
fi
}
if [ -z "${DISPLAY:-}" ]; then
start_xvfb
export DISPLAY=$MICROSLOP_DISPLAY
fi
# NetTTS-style line controls, handled here so no caller/WebUI work is needed:
# /rate N and/or /pitch N at the start of the line.
# N is a percent (100 = normal); values <= 2.0 are multipliers (/rate 1.5 = 150%).
# They override the env speed/pitch for this line and are not spoken. A line that
# contains only commands yields 100 ms of silence, so the daemon still gets a WAV.
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
RAW="$TMP/raw.wav"
TXT="$TMP/text.cp1252"
TEXT_IN="$TEXTFILE"
if [ -f "$TEXTFILE" ]; then
set -f
# shellcheck disable=SC2046
set -- $(cat "$TEXTFILE")
set +f
CMD_SPEED=""
CMD_PITCH=""
while [ "$#" -ge 2 ]; do
case "$1" in
/rate|/pitch)
case "$2" in
''|*[!0-9.]*) break ;;
esac
if [ "$1" = "/rate" ]; then CMD_SPEED=$2; else CMD_PITCH=$2; fi
shift 2
;;
*) break ;;
esac
done
if [ -n "$CMD_SPEED" ] || [ -n "$CMD_PITCH" ]; then
if [ -n "$CMD_SPEED" ]; then
SPEED=$(awk -v v="$CMD_SPEED" 'BEGIN{ if (v<=2.0) v*=100; if (v<0) v=0; if (v>200) v=200; printf "%.4f", v/100 }')
fi
if [ -n "$CMD_PITCH" ]; then
PITCH=$(awk -v v="$CMD_PITCH" 'BEGIN{ if (v<=2.0) v*=100; if (v<0) v=0; if (v>200) v=200; printf "%.4f", 12*log(v/100)/log(2) }')
fi
if [ -z "$*" ]; then
"$MICROSLOP_FFMPEG" -hide_banner -loglevel error -f lavfi -i anullsrc=r=48000:cl=mono \
-t 0.1 -c:a pcm_s16le -y "$OUT" >/dev/null 2>&1 \
|| { echo "could not write silence" >&2; exit 1; }
exit 0
fi
printf '%s\n' "$*" > "$TMP/input.txt"
TEXT_IN="$TMP/input.txt"
fi
fi
# Build the engine arguments from the (possibly command-overridden) speed/pitch.
SET_ARGS=""
if [ -n "$SPEED" ]; then
S=$(awk -v m="$SPEED" 'BEGIN{printf "%d", 150*m+0.5}')
@@ -97,23 +160,6 @@ if [ -n "$PITCH" ]; then
SET_ARGS="$SET_ARGS -p $P"
fi
# The daemon does not export DISPLAY; keep a private Xvfb for the engine.
start_xvfb() {
if ! [ -S /tmp/.X11-unix/X77 ]; then
Xvfb :77 -screen 0 1024x768x24 >/dev/null 2>&1 &
sleep 1
fi
}
if [ -z "${DISPLAY:-}" ]; then
start_xvfb
export DISPLAY=:77
fi
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
RAW="$TMP/raw.wav"
TXT="$TMP/text.cp1252"
# 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
# engine defaults plus any VESKSLOP_TTS_PROFILE overrides. A missing value therefore
@@ -187,28 +233,46 @@ if [ -n "$SPEAKER_GUID" ]; then
exit 1
fi
if ! command -v wine >/dev/null 2>&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 ! wine reg import "$TMP/profile.reg" >/dev/null 2>&1; then
if ! "$MICROSLOP_WINE" reg import "$TMP/profile.reg" >/dev/null 2>&1; then
echo "could not write the Flextalk profile" >&2
exit 1
fi
fi
# SAPI4 is ANSI/CP1252; transliterate UTF-8 (emoji become '?').
if command -v iconv >/dev/null 2>&1; then
iconv -f UTF-8 -t WINDOWS-1252//TRANSLIT "$TEXTFILE" > "$TXT" 2>/dev/null \
|| cp "$TEXTFILE" "$TXT"
else
cp "$TEXTFILE" "$TXT"
# 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.
TEXT_SRC="$TEXT_IN"
if [ "$VOX" = "1" ] && [ -n "$SPEAKER_GUID" ]; 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"
else
"$MICROSLOP_PYTHON" "$MICROSLOP_ROOT/setup/vox_parser.py" < "$TEXT_IN" > "$TMP/vox.txt"
fi
TEXT_SRC="$TMP/vox.txt"
else
echo "vox mode needs python3 and setup/vox_parser.py" >&2
exit 1
fi
fi
HOST="$ROOT/build/sapi4host-winlib.exe"
if [ ! -x "$HOST" ]; then HOST="$ROOT/build/sapi4host.exe"; fi
# transliterate UTF-8 (emoji become '?')
if command -v iconv >/dev/null 2>&1; then
iconv -f UTF-8 -t WINDOWS-1252//TRANSLIT "$TEXT_SRC" > "$TXT" 2>/dev/null \
|| cp "$TEXT_SRC" "$TXT"
else
cp "$TEXT_SRC" "$TXT"
fi
HOST="$MICROSLOP_BUILD/sapi4host-winlib.exe"
if [ ! -x "$HOST" ]; then HOST="$MICROSLOP_BUILD/sapi4host.exe"; fi
if [ ! -x "$HOST" ]; then echo "engine host not built" >&2; exit 1; fi
if ! command -v ffmpeg >/dev/null 2>&1; then echo "ffmpeg not found" >&2; exit 1; fi
if ! command -v "$MICROSLOP_FFMPEG" >/dev/null 2>&1; then echo "ffmpeg not found" >&2; exit 1; fi
run_host() {
# shellcheck disable=SC2086
@@ -219,9 +283,9 @@ run_host() {
try_run_host() {
if run_host "$1" "$2"; then return 0; fi
if [ "${DISPLAY:-}" = ":77" ]; then
pkill -f "Xvfb :77" 2>/dev/null || true
rm -f /tmp/.X11-unix/X77
if [ "${DISPLAY:-}" = "$MICROSLOP_DISPLAY" ]; then
pkill -f "Xvfb $MICROSLOP_DISPLAY" 2>/dev/null || true
rm -f "$MICROSLOP_XSOCK"
start_xvfb
if run_host "$1" "$2"; then return 0; fi
fi
@@ -232,7 +296,8 @@ 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.
if [ "$WORDS" = "1" ] && [ -n "$SPEAKER_GUID" ]; then
# VOX mode takes precedence: it produces its own cadence with vendor tags.
if [ "$WORDS" = "1" ] && [ -n "$SPEAKER_GUID" ] && [ "$VOX" != "1" ]; then
GAP=$WORD_GAP
case "$GAP" in *[!0-9]*|"") GAP=120 ;; esac
[ "$GAP" -gt 500 ] && GAP=500
@@ -245,7 +310,7 @@ if [ "$WORDS" = "1" ] && [ -n "$SPEAKER_GUID" ]; then
echo "file '$TMP/w$INDEX.wav'" >> "$TMP/list.txt"
if [ "$GAP" -gt 0 ]; then
ffmpeg -hide_banner -loglevel error -f lavfi -i "anullsrc=r=11025:cl=mono" \
"$MICROSLOP_FFMPEG" -hide_banner -loglevel error -f lavfi -i "anullsrc=r=11025:cl=mono" \
-t "$(awk -v g="$GAP" 'BEGIN{printf "%.3f", g/1000}')" -c:a pcm_s16le -y "$TMP/sil.wav" \
>/dev/null 2>&1
echo "file '$TMP/sil.wav'" >> "$TMP/list.txt"
@@ -253,7 +318,7 @@ if [ "$WORDS" = "1" ] && [ -n "$SPEAKER_GUID" ]; then
done
[ "$INDEX" -gt 0 ] || { echo "no words to speak" >&2; exit 1; }
if ! ffmpeg -hide_banner -loglevel error -f concat -safe 0 -i "$TMP/list.txt" -c copy -y "$RAW" \
if ! "$MICROSLOP_FFMPEG" -hide_banner -loglevel error -f concat -safe 0 -i "$TMP/list.txt" -c copy -y "$RAW" \
>/dev/null 2>&1; then
echo "could not merge the words" >&2
exit 1
@@ -281,7 +346,7 @@ fi
# Fixed-gain normalization: single-pass loudnorm misbehaves on short lines
# (LRA=0 causes heavy attenuation), so measure EBU R128 integrated loudness and
# true peak, then apply one gain clamped by the -3 dBFS peak target.
M=$(ffmpeg -hide_banner -nostats -i "$RAW" -af "$PRE,ebur128=peak=true" \
M=$("$MICROSLOP_FFMPEG" -hide_banner -nostats -i "$RAW" -af "$PRE,ebur128=peak=true" \
-f null - 2>&1 || true)
I=$(printf '%s' "$M" | sed -n 's/ *I: *\(-\{0,1\}[0-9.]*\) *LUFS.*/\1/p' | tail -1)
TP=$(printf '%s' "$M" | sed -n 's/ *Peak: *\(-\{0,1\}[0-9.]*\) *dBFS.*/\1/p' | tail -1)
@@ -299,7 +364,7 @@ else
fi
TMPOUT="$TMP/out.wav"
if ! ffmpeg -hide_banner -nostats -y -i "$RAW" -af "$PRE,volume=${GAIN}dB" \
if ! "$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