Add MicroslopSam TTS, Flextalk voice profiles, effects, and engine controls
This commit is contained in:
Executable
+112
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convert INF AddReg sections into a Windows/Wine .reg file.
|
||||
|
||||
Example:
|
||||
inf2reg.py --section APIReg --define 49000='C:\\windows\\speech' \
|
||||
spchapi/SPCHAPI.INF > spchapi.reg
|
||||
"""
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
|
||||
ROOTS = {
|
||||
"HKCR": "HKEY_CLASSES_ROOT",
|
||||
"HKLM": "HKEY_LOCAL_MACHINE",
|
||||
"HKCU": "HKEY_CURRENT_USER",
|
||||
"HKU": "HKEY_USERS",
|
||||
}
|
||||
|
||||
|
||||
def expand(text, defines):
|
||||
def repl(m):
|
||||
key = m.group(1)
|
||||
if key == "25":
|
||||
return defines.get("WINDOWS", r"C:\windows")
|
||||
return defines.get(key, m.group(0))
|
||||
|
||||
return re.sub(r"%(\w+)%", repl, text)
|
||||
|
||||
|
||||
def reg_type(flags, data):
|
||||
if not flags:
|
||||
return "REG_SZ"
|
||||
try:
|
||||
f = int(flags, 16) if flags.lower().startswith("0x") else int(flags)
|
||||
except ValueError:
|
||||
return "REG_SZ"
|
||||
if f & 0x00020000:
|
||||
return "REG_EXPAND_SZ"
|
||||
if f & 0x00010000:
|
||||
return "REG_MULTI_SZ"
|
||||
if f & 0x00000001:
|
||||
return "REG_BINARY"
|
||||
return "REG_SZ"
|
||||
|
||||
|
||||
def reg_escape(value):
|
||||
return value.replace("\\", "\\\\").replace('"', '\\"')
|
||||
|
||||
|
||||
def unquote(token):
|
||||
token = token.strip()
|
||||
if len(token) >= 2 and token[0] == '"' and token[-1] == '"':
|
||||
return token[1:-1]
|
||||
return token
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("inf")
|
||||
ap.add_argument("--section", action="append", required=True,
|
||||
help="AddReg section name (repeatable)")
|
||||
ap.add_argument("--define", action="append", default=[],
|
||||
help="ID=value, e.g. 49000=C:\\windows\\speech")
|
||||
args = ap.parse_args()
|
||||
|
||||
defines = {"WINDOWS": r"C:\windows"}
|
||||
for item in args.define:
|
||||
k, v = item.split("=", 1)
|
||||
defines[k] = v
|
||||
|
||||
wanted = set(args.section)
|
||||
section = None
|
||||
out = ["REGEDIT4", ""]
|
||||
|
||||
with open(args.inf, "r", encoding="latin-1") as fh:
|
||||
for raw in fh:
|
||||
line = raw.strip()
|
||||
if not line or line.startswith(";"):
|
||||
continue
|
||||
m = re.match(r"^\[([^\]]+)\]$", line)
|
||||
if m:
|
||||
section = m.group(1)
|
||||
continue
|
||||
if section not in wanted:
|
||||
continue
|
||||
parts = [p.strip() for p in line.split(",")]
|
||||
if len(parts) < 4:
|
||||
continue
|
||||
root, key, name, flags = parts[0], parts[1], parts[2], parts[3]
|
||||
data = ",".join(parts[4:]) if len(parts) > 4 else ""
|
||||
if root.upper() not in ROOTS:
|
||||
continue
|
||||
data = expand(unquote(data), defines)
|
||||
name = expand(unquote(name), defines)
|
||||
if "%InstallPrompt%" in name:
|
||||
continue
|
||||
if not data:
|
||||
continue
|
||||
rtype = reg_type(flags.strip(), data)
|
||||
keypath = "\\".join(
|
||||
p for p in (ROOTS[root.upper()], expand(key, defines)) if p
|
||||
)
|
||||
value = data if rtype == "REG_BINARY" else '"%s"' % reg_escape(data)
|
||||
out.append("[%s]" % keypath)
|
||||
out.append("%s=%s" % ('@' if not name else '"%s"' % name, value))
|
||||
out.append("")
|
||||
|
||||
sys.stdout.write("\n".join(out) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/bin/sh
|
||||
# Install Wine + 32-bit Winelib toolchain + helpers. Debian/Ubuntu.
|
||||
# Run as root: sudo setup/install-deps.sh
|
||||
set -eu
|
||||
|
||||
[ "$(id -u)" = "0" ] || { echo "run as root: sudo $0" >&2; exit 1; }
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
if ! dpkg --print-foreign-architectures | grep -qx i386; then
|
||||
echo "== enabling i386 architecture"
|
||||
dpkg --add-architecture i386
|
||||
fi
|
||||
|
||||
apt-get update
|
||||
|
||||
for p in \
|
||||
wine wine64 wine32:i386 \
|
||||
wine64-tools \
|
||||
libwine-dev libwine-dev:i386 \
|
||||
gcc-multilib libc6-dev-i386 \
|
||||
mingw-w64 \
|
||||
xvfb unshield
|
||||
do
|
||||
if apt-cache policy "$p" 2>/dev/null | grep -q "Candidate: [0-9]"; then
|
||||
echo "== install $p"
|
||||
apt-get install -y --no-install-recommends "$p" || echo "!! FAILED: $p"
|
||||
else
|
||||
echo "-- unavailable, skipped: $p"
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
echo "versions:"
|
||||
wine --version 2>/dev/null || echo "wine missing"
|
||||
winegcc --version 2>/dev/null | head -1 || echo "winegcc missing"
|
||||
i686-w64-mingw32-gcc --version 2>/dev/null | head -1 || echo "mingw missing"
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/bin/sh
|
||||
# One-shot installer for MicroslopSam. Runs all setup steps, idempotently:
|
||||
# 1. system dependencies (Wine, Winelib, helpers; needs sudo)
|
||||
# 2. wine prefix + SAPI4 runtime + MSTTS voices + registration
|
||||
# 3. Flextalk extraction + voices + registration
|
||||
# 4. host build (Winelib and PE)
|
||||
# 5. quick voice check
|
||||
#
|
||||
# Usage: setup/install.sh
|
||||
# Re-running is safe.
|
||||
set -eu
|
||||
|
||||
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
ROOT=$(CDPATH= cd -- "$HERE/.." && pwd)
|
||||
ASSETS=${ASSETS:-$ROOT/../Microsoft-Sam-Mary-Mike-TruVoice-WSAPI4}
|
||||
FLEXTALK_DIR=${FLEXTALK_DIR:-$ROOT/../flextalk}
|
||||
|
||||
log() { printf '\n== %s\n' "$*"; }
|
||||
|
||||
# 1) system dependencies -----------------------------------------------------
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
log "installing system dependencies"
|
||||
"$HERE/install-deps.sh"
|
||||
else
|
||||
log "installing system dependencies (sudo)"
|
||||
sudo "$HERE/install-deps.sh"
|
||||
fi
|
||||
|
||||
# 2) SAPI4 runtime + MSTTS voices --------------------------------------------
|
||||
log "setting up wine prefix, SAPI4 runtime and MSTTS voices"
|
||||
"$HERE/setup-prefix.sh" "$ASSETS"
|
||||
|
||||
# 3) Flextalk voices ----------------------------------------------------------
|
||||
EXTRACTED="$FLEXTALK_DIR/extracted/TTS_Executables"
|
||||
if [ ! -d "$EXTRACTED/EngineDLLs" ]; then
|
||||
CAB=""
|
||||
for c in "$FLEXTALK_DIR/installer/flextalk/data1.cab" \
|
||||
"$ASSETS/flextalk/data1.cab"; do
|
||||
if [ -f "$c" ]; then CAB="$c"; break; fi
|
||||
done
|
||||
if [ -z "$CAB" ]; then
|
||||
echo "Flextalk data1.cab not found; set FLEXTALK_DIR to the archive dir" >&2
|
||||
exit 1
|
||||
fi
|
||||
command -v unshield >/dev/null 2>&1 || {
|
||||
echo "unshield is required to extract the Flextalk installer" >&2
|
||||
exit 1
|
||||
}
|
||||
log "extracting Flextalk installer ($CAB)"
|
||||
rm -rf "$FLEXTALK_DIR/extracted"
|
||||
unshield -O -d "$FLEXTALK_DIR/extracted" x "$CAB" >/dev/null
|
||||
fi
|
||||
log "installing Flextalk voices"
|
||||
"$HERE/setup-flextalk.sh" "$EXTRACTED"
|
||||
|
||||
# 4) host build ---------------------------------------------------------------
|
||||
log "building the host"
|
||||
mkdir -p "$ROOT/build"
|
||||
winegcc -m32 -O2 -o "$ROOT/build/sapi4host-winlib" "$ROOT/host/sapi4host.c" \
|
||||
-I"$ROOT/../reference" -lole32 -luser32 -ladvapi32
|
||||
if command -v i686-w64-mingw32-gcc >/dev/null 2>&1; then
|
||||
i686-w64-mingw32-gcc -O2 -static -o "$ROOT/build/sapi4host.exe" \
|
||||
"$ROOT/host/sapi4host.c" -I"$ROOT/../reference" \
|
||||
-lole32 -luser32 -ladvapi32 -luuid
|
||||
fi
|
||||
|
||||
# 5) check --------------------------------------------------------------------
|
||||
log "installed voices"
|
||||
"$HERE/run.sh" -l || true
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Done.
|
||||
|
||||
Veskslop configuration:
|
||||
VESKSLOP_TTS_CMD="$HERE/veskslop-tts.sh {out} {text_file}"
|
||||
VESKSLOP_TTS_VOICES="\$($HERE/veskslop-tts.sh --voices)"
|
||||
|
||||
Manual test:
|
||||
$HERE/run.sh -v Sam -t "Hello." -o /tmp/hello.wav
|
||||
|
||||
Normalized 48 kHz output (what Veskslop hears):
|
||||
VESKSLOP_TTS_VOICE=keith-bell $HERE/veskslop-tts.sh /tmp/keith.wav /tmp/text.txt
|
||||
EOF
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/bin/sh
|
||||
# Run the SAPI4 host with the project's wine prefix and a working X display.
|
||||
# Usage: setup/run.sh [sapi4host args...]
|
||||
set -eu
|
||||
|
||||
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
ROOT=$(CDPATH= cd -- "$HERE/.." && pwd)
|
||||
export WINEPREFIX=${WINEPREFIX:-$HOME/.wine-microslopsam}
|
||||
export WINEARCH=win32
|
||||
|
||||
if [ -z "${DISPLAY:-}" ]; then
|
||||
if ! [ -S /tmp/.X11-unix/X77 ]; then
|
||||
Xvfb :77 -screen 0 1024x768x24 >/dev/null 2>&1 &
|
||||
sleep 1
|
||||
fi
|
||||
export DISPLAY=:77
|
||||
fi
|
||||
|
||||
HOST="$ROOT/build/sapi4host-winlib.exe"
|
||||
if [ ! -x "$HOST" ]; then
|
||||
HOST="$ROOT/build/sapi4host.exe"
|
||||
fi
|
||||
if [ ! -x "$HOST" ]; then
|
||||
echo "no host binary; build first (see README)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec "$HOST" "$@"
|
||||
Executable
+95
@@ -0,0 +1,95 @@
|
||||
#!/bin/sh
|
||||
# Install AT&T FlexTalk 2.1 (Keith Bell, Kerry Watson) into the wine prefix.
|
||||
#
|
||||
# Source: extracted InstallShield cabinet, e.g.
|
||||
# flextalk/extracted/TTS_Executables
|
||||
# Usage: setup/setup-flextalk.sh [source-dir]
|
||||
set -eu
|
||||
|
||||
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
ROOT=$(CDPATH= cd -- "$HERE/.." && pwd)
|
||||
SRC=${1:-$ROOT/../flextalk/extracted/TTS_Executables}
|
||||
|
||||
[ -d "$SRC/EngineDLLs" ] || { echo "source not found: $SRC" >&2; exit 1; }
|
||||
|
||||
export WINEPREFIX=${WINEPREFIX:-$HOME/.wine-microslopsam}
|
||||
export WINEARCH=win32
|
||||
|
||||
if [ -z "${DISPLAY:-}" ]; then
|
||||
WRUN="xvfb-run -a wine"
|
||||
else
|
||||
WRUN="wine"
|
||||
fi
|
||||
|
||||
DEST="$WINEPREFIX/drive_c/FlexTalk"
|
||||
echo "== files -> $DEST"
|
||||
mkdir -p "$DEST"
|
||||
cp -a "$SRC/." "$DEST/"
|
||||
|
||||
REG="$HERE/out-flextalk.reg"
|
||||
cat > "$REG" <<'EOF'
|
||||
REGEDIT4
|
||||
|
||||
[HKEY_LOCAL_MACHINE\Software\Voice\TextToSpeech\Engine]
|
||||
"AT&T FlexTalk"="{FFE78E00-C69C-11CE-AEC1-0000C0E9CB87}"
|
||||
|
||||
[HKEY_LOCAL_MACHINE\Software\Voice\TextToSpeech\Engine\{FFE78E00-C69C-11CE-AEC1-0000C0E9CB87}]
|
||||
"Dirty"=dword:00000001
|
||||
|
||||
[HKEY_CLASSES_ROOT\CLSID\{FFE78E00-C69C-11CE-AEC1-0000C0E9CB87}]
|
||||
@="AT&T FlexTalk"
|
||||
|
||||
[HKEY_CLASSES_ROOT\CLSID\{FFE78E00-C69C-11CE-AEC1-0000C0E9CB87}\InprocServer32]
|
||||
@="C:\\FlexTalk\\EngineDLLs\\FlexTalk.dll"
|
||||
"ThreadingModel"="Apartment"
|
||||
|
||||
[HKEY_CLASSES_ROOT\CLSID\{778EAAE2-499F-11CF-B0BA-444553540000}\InprocServer32]
|
||||
@="C:\\FlexTalk\\EngineDLLs\\FlexTalk.dll"
|
||||
"ThreadingModel"="Apartment"
|
||||
|
||||
[HKEY_LOCAL_MACHINE\Software\AT&T\Watson\2.0\FlexTalk\2.0\Directories]
|
||||
"RootDirectory"="C:\\FlexTalk\\"
|
||||
|
||||
[HKEY_LOCAL_MACHINE\Software\AT&T\Watson\2.0\FlexTalk\2.0\Dictionaries]
|
||||
"StandardDictionary"="2 ft_sdict.dll"
|
||||
"ExceptionDictionary"="0 ft_udict.dll"
|
||||
"HomographDictionary"="1 ft_hgrap.dll"
|
||||
"ProperNameDictionary"="3 ft_namsa.dll"
|
||||
"LetterToSoundDictionary"="4 ft_l2s.dll"
|
||||
|
||||
[HKEY_LOCAL_MACHINE\Software\AT&T\Watson\2.0\FlexTalk\2.0\Options]
|
||||
"Gender"="Male"
|
||||
"SampleRate"="11025"
|
||||
|
||||
[HKEY_LOCAL_MACHINE\Software\AT&T\Watson\2.0\FlexTalk\2.0\Speakers\{11260610-C84A-11ce-AEC4-0000C0E9CB87}]
|
||||
"Age"=dword:0000001e
|
||||
"Expression"=dword:0000003b
|
||||
"Gender"=""
|
||||
"Head Size"=dword:0000002e
|
||||
"Lowering"=dword:00000028
|
||||
"Mode Name"="Keith Bell"
|
||||
"Pitch"=dword:0000001b
|
||||
"Prominences"=dword:00000011
|
||||
"Speaker Name"="Keith Bell"
|
||||
"Speaking Rate"=dword:00000021
|
||||
"Volume"=dword:00000064
|
||||
|
||||
[HKEY_LOCAL_MACHINE\Software\AT&T\Watson\2.0\FlexTalk\2.0\Speakers\{000031CB-3699-551F-AED0-6B4893382D57}]
|
||||
"Age"=dword:0000001e
|
||||
"Expression"=dword:00000064
|
||||
"Gender"=""
|
||||
"Head Size"=dword:0000002d
|
||||
"Lowering"=dword:00000028
|
||||
"Mode Name"="Kerry Watson"
|
||||
"Pitch"=dword:00000041
|
||||
"Prominences"=dword:00000011
|
||||
"Speaker Name"="Kerry Watson"
|
||||
"Speaking Rate"=dword:00000020
|
||||
"Volume"=dword:00000064
|
||||
EOF
|
||||
|
||||
echo "== registry"
|
||||
$WRUN regedit /S "$REG" 2>/dev/null || true
|
||||
|
||||
echo "== done; list voices:"
|
||||
echo " $WRUN $ROOT/host/sapi4host.exe -l"
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/bin/sh
|
||||
# Build a 32-bit Wine prefix, install the SAPI4 runtime, the MSTTS engine and
|
||||
# the MSTTS voices, and register everything.
|
||||
#
|
||||
# Usage: setup/setup-prefix.sh [path-to-asset-repo]
|
||||
# Default asset repo: ../Microsoft-Sam-Mary-Mike-TruVoice-WSAPI4
|
||||
set -eu
|
||||
|
||||
HERE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
ROOT=$(CDPATH= cd -- "$HERE/.." && pwd)
|
||||
ASSETS=${1:-$ROOT/../Microsoft-Sam-Mary-Mike-TruVoice-WSAPI4}
|
||||
|
||||
[ -d "$ASSETS/spchapi" ] || { echo "asset dir not found: $ASSETS" >&2; exit 1; }
|
||||
|
||||
export WINEPREFIX=${WINEPREFIX:-$HOME/.wine-microslopsam}
|
||||
export WINEARCH=win32
|
||||
|
||||
if [ -z "${DISPLAY:-}" ]; then
|
||||
WRUN="xvfb-run -a wine"
|
||||
else
|
||||
WRUN="wine"
|
||||
fi
|
||||
|
||||
echo "== prefix: $WINEPREFIX"
|
||||
mkdir -p "$WINEPREFIX"
|
||||
$WRUN wineboot -u
|
||||
|
||||
SPEECH_DIR="$WINEPREFIX/drive_c/windows/speech"
|
||||
ENGINE_DIR="$WINEPREFIX/drive_c/Program Files/Common Files/microsoft shared/SpeechEngines/TTS"
|
||||
mkdir -p "$SPEECH_DIR" "$ENGINE_DIR"
|
||||
|
||||
echo "== runtime -> $SPEECH_DIR"
|
||||
cp "$ASSETS"/spchapi/SPEECH.DLL "$SPEECH_DIR/"
|
||||
|
||||
echo "== engine + voices -> $ENGINE_DIR"
|
||||
cp "$ASSETS"/msttsl/msttssyn.dll "$ENGINE_DIR/"
|
||||
cp "$ASSETS"/msttsl/*.vce "$ASSETS"/msttsl/*.cfg "$ENGINE_DIR/"
|
||||
|
||||
echo "== registry"
|
||||
python3 "$HERE/inf2reg.py" --section APIReg \
|
||||
--define "49000=C:\\windows\\speech" \
|
||||
"$ASSETS/spchapi/SPCHAPI.INF" > "$HERE/out-spchapi.reg"
|
||||
python3 "$HERE/inf2reg.py" --section TTSReg \
|
||||
--define "49000=C:\\Program Files\\Common Files\\microsoft shared\\SpeechEngines\\TTS" \
|
||||
"$ASSETS/msttsl/mstts.inf" > "$HERE/out-mstts.reg"
|
||||
|
||||
$WRUN regedit /S "$HERE/out-spchapi.reg"
|
||||
$WRUN regedit /S "$HERE/out-mstts.reg"
|
||||
|
||||
echo "== done"
|
||||
echo "list voices: $WRUN $ROOT/host/sapi4host.exe -l"
|
||||
Executable
+308
@@ -0,0 +1,308 @@
|
||||
#!/bin/sh
|
||||
# Veskslop TTS command-provider wrapper for MicroslopSam.
|
||||
#
|
||||
# Recommended Veskslop configuration:
|
||||
# VESKSLOP_TTS_CMD="<repo>/setup/veskslop-tts.sh {out} {text_file}"
|
||||
# VESKSLOP_TTS_VOICES="$(setup/veskslop-tts.sh --voices)"
|
||||
#
|
||||
# Inputs:
|
||||
# $1 {out} output WAV path
|
||||
# $2 {text_file} UTF-8 text file
|
||||
# $3... optional: voice id, speed multiplier, pitch semitones, expression
|
||||
# env VESKSLOP_TTS_VOICE, VESKSLOP_TTS_SPEED, VESKSLOP_TTS_PITCH,
|
||||
# VESKSLOP_TTS_EXPRESSION
|
||||
# VESKSLOP_TTS_PROFILE Flextalk profile: key=value,key=value
|
||||
# keys: pitch, rate, volume, expression,
|
||||
# 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_WORD_GAP_MS silence between words in ms (0-500)
|
||||
#
|
||||
# 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
|
||||
|
||||
# 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=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"
|
||||
|
||||
if [ "${1:-}" = "--voices" ]; then
|
||||
echo "$VOICES"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
OUT=${1:?missing out path}
|
||||
TEXTFILE=${2:?missing text file}
|
||||
VOICE=${VESKSLOP_TTS_VOICE:-${3:-sam}}
|
||||
SPEED=${VESKSLOP_TTS_SPEED:-${4:-}}
|
||||
PITCH=${VESKSLOP_TTS_PITCH:-${5:-}}
|
||||
EXPRESSION=${VESKSLOP_TTS_EXPRESSION:-${6:-}}
|
||||
PROFILE=${VESKSLOP_TTS_PROFILE:-}
|
||||
HIGHPASS=${VESKSLOP_TTS_HIGHPASS:-40}
|
||||
ECHO=${VESKSLOP_TTS_ECHO:-0}
|
||||
WORDS=${VESKSLOP_TTS_WORDS:-0}
|
||||
WORD_GAP=${VESKSLOP_TTS_WORD_GAP_MS:-120}
|
||||
|
||||
case "$VOICE" in
|
||||
sam) ENGVOICE="Sam" ;;
|
||||
mary) ENGVOICE="Mary" ;;
|
||||
mike) ENGVOICE="Mike" ;;
|
||||
keith-bell) ENGVOICE="Keith Bell" ;;
|
||||
kerry-watson) ENGVOICE="Kerry Watson" ;;
|
||||
mike-telephone) ENGVOICE="Mike (for Telephone)" ;;
|
||||
mary-telephone) ENGVOICE="Mary (for Telephone)" ;;
|
||||
mike-hall) ENGVOICE="Mike in Hall" ;;
|
||||
mike-stadium) ENGVOICE="Mike in Stadium" ;;
|
||||
mike-space) ENGVOICE="Mike in Space" ;;
|
||||
mary-hall) ENGVOICE="Mary in Hall" ;;
|
||||
mary-stadium) ENGVOICE="Mary in Stadium" ;;
|
||||
mary-space) ENGVOICE="Mary in Space" ;;
|
||||
*) ENGVOICE="Sam" ;;
|
||||
esac
|
||||
|
||||
# Flextalk speakers have a registry profile; MSTTS voices do not.
|
||||
case "$ENGVOICE" in
|
||||
"Keith Bell") SPEAKER_GUID="{11260610-C84A-11ce-AEC4-0000C0E9CB87}"; DEF_PITCH=27; DEF_RATE=33; DEF_HEAD=46 ;;
|
||||
"Kerry Watson") SPEAKER_GUID="{000031CB-3699-551F-AED0-6B4893382D57}"; DEF_PITCH=65; DEF_RATE=32; DEF_HEAD=45 ;;
|
||||
*) SPEAKER_GUID=""; DEF_PITCH=100; DEF_RATE=150; DEF_HEAD=45 ;;
|
||||
esac
|
||||
|
||||
# Engine units (measured). Speed is words/min, natural rate near 150 for every
|
||||
# voice. Pitch units differ per voice: the engine's natural pitch corresponds to
|
||||
# about 100 (MSTTS), 106 (Keith Bell) and 186 (Kerry Watson). Semitones scale
|
||||
# that natural value by 2^(st/12) and clamp to the engine range.
|
||||
case "$ENGVOICE" in
|
||||
"Keith Bell") SMAX=390; PMAX=250; NAT_PITCH=106 ;;
|
||||
"Kerry Watson") SMAX=390; PMAX=250; NAT_PITCH=186 ;;
|
||||
*) SMAX=450; PMAX=200; NAT_PITCH=100 ;;
|
||||
esac
|
||||
SET_ARGS=""
|
||||
if [ -n "$SPEED" ]; then
|
||||
S=$(awk -v m="$SPEED" 'BEGIN{printf "%d", 150*m+0.5}')
|
||||
[ "$S" -lt 50 ] && S=50
|
||||
[ "$S" -gt "$SMAX" ] && S=$SMAX
|
||||
SET_ARGS="$SET_ARGS -s $S"
|
||||
fi
|
||||
if [ -n "$PITCH" ]; then
|
||||
P=$(awk -v st="$PITCH" -v nat="$NAT_PITCH" 'BEGIN{printf "%d", nat*exp(log(2)*st/12)+0.5}')
|
||||
[ "$P" -lt 50 ] && P=50
|
||||
[ "$P" -gt "$PMAX" ] && P=$PMAX
|
||||
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
|
||||
# restores the default.
|
||||
if [ -n "$SPEAKER_GUID" ]; then
|
||||
P_PITCH=$DEF_PITCH
|
||||
P_RATE=$DEF_RATE
|
||||
P_VOLUME=100
|
||||
P_PROM=17
|
||||
P_HEAD=$DEF_HEAD
|
||||
P_LOWER=40
|
||||
P_EXPR=""
|
||||
|
||||
if [ -n "$PROFILE" ]; then
|
||||
OLDIFS=$IFS
|
||||
IFS=','
|
||||
for pair in $PROFILE; do
|
||||
IFS='='
|
||||
# shellcheck disable=SC2086
|
||||
set -- $pair
|
||||
IFS=','
|
||||
case "$1" in
|
||||
pitch) P_PITCH=$2 ;;
|
||||
rate) P_RATE=$2 ;;
|
||||
volume) P_VOLUME=$2 ;;
|
||||
expression) P_EXPR=$2 ;;
|
||||
prominences) P_PROM=$2 ;;
|
||||
head_size|headSize) P_HEAD=$2 ;;
|
||||
lowering) P_LOWER=$2 ;;
|
||||
esac
|
||||
done
|
||||
IFS=$OLDIFS
|
||||
fi
|
||||
|
||||
if [ -n "$EXPRESSION" ]; then P_EXPR=$EXPRESSION; fi
|
||||
if [ -z "$P_EXPR" ]; then
|
||||
case "$ENGVOICE" in
|
||||
"Keith Bell") P_EXPR=59 ;;
|
||||
*) P_EXPR=100 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
for value in "$P_PITCH" "$P_RATE" "$P_VOLUME" "$P_EXPR" "$P_PROM" "$P_HEAD" "$P_LOWER"; do
|
||||
case "$value" in
|
||||
*[!0-9]*|"")
|
||||
echo "profile values must be numbers" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
REG_KEY="HKLM\\Software\\AT&T\\Watson\\2.0\\FlexTalk\\2.0\\Speakers\\$SPEAKER_GUID"
|
||||
# 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.
|
||||
{
|
||||
printf 'Windows Registry Editor Version 5.00\n\n'
|
||||
printf '[%s]\n' "$REG_KEY"
|
||||
printf '"Pitch"=dword:%08x\n' "$P_PITCH"
|
||||
printf '"Speaking Rate"=dword:%08x\n' "$P_RATE"
|
||||
printf '"Volume"=dword:%08x\n' "$P_VOLUME"
|
||||
printf '"Expression"=dword:%08x\n' "$P_EXPR"
|
||||
printf '"Prominences"=dword:%08x\n' "$P_PROM"
|
||||
printf '"Head Size"=dword:%08x\n' "$P_HEAD"
|
||||
printf '"Lowering"=dword:%08x\n' "$P_LOWER"
|
||||
} > "$TMP/profile.reg"
|
||||
|
||||
# Guard against silent reg-import failures (a malformed key would corrupt
|
||||
# only the import, and reg.exe can still return 0).
|
||||
if LC_ALL=C grep -q '[[:cntrl:]]' "$TMP/profile.reg"; then
|
||||
echo "generated Flextalk profile is malformed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v 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
|
||||
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"
|
||||
fi
|
||||
|
||||
HOST="$ROOT/build/sapi4host-winlib.exe"
|
||||
if [ ! -x "$HOST" ]; then HOST="$ROOT/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
|
||||
|
||||
run_host() {
|
||||
# shellcheck disable=SC2086
|
||||
"$HOST" -v "$ENGVOICE" -t "$2" -o "$1" $SET_ARGS >/dev/null 2>"$TMP/err"
|
||||
}
|
||||
|
||||
# A stale private Xvfb can break audio-destination creation; retry once.
|
||||
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
|
||||
start_xvfb
|
||||
if run_host "$1" "$2"; then return 0; fi
|
||||
fi
|
||||
|
||||
head -c 400 "$TMP/err" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 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
|
||||
GAP=$WORD_GAP
|
||||
case "$GAP" in *[!0-9]*|"") GAP=120 ;; esac
|
||||
[ "$GAP" -gt 500 ] && GAP=500
|
||||
|
||||
: > "$TMP/list.txt"
|
||||
INDEX=0
|
||||
for word in $(cat "$TXT"); do
|
||||
INDEX=$((INDEX + 1))
|
||||
try_run_host "$TMP/w$INDEX.wav" "$word"
|
||||
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" \
|
||||
-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"
|
||||
fi
|
||||
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" \
|
||||
>/dev/null 2>&1; then
|
||||
echo "could not merge the words" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
try_run_host "$RAW" "$(cat "$TXT")"
|
||||
fi
|
||||
[ -s "$RAW" ] || { echo "engine produced no audio" >&2; exit 1; }
|
||||
|
||||
# Filter chain: adjustable high-pass, silence trim, fades, optional mono echo.
|
||||
case "$HIGHPASS" in *[!0-9]*|"") HIGHPASS=40 ;; esac
|
||||
[ "$HIGHPASS" -lt 40 ] && HIGHPASS=40
|
||||
[ "$HIGHPASS" -gt 400 ] && HIGHPASS=400
|
||||
|
||||
case "$ECHO" in *[!0-9]*|"") ECHO=0 ;; esac
|
||||
[ "$ECHO" -gt 100 ] && ECHO=100
|
||||
|
||||
SIL="silenceremove=start_periods=1:start_duration=0.05:start_silence=0.03:start_threshold=-50dB"
|
||||
PRE="highpass=f=$HIGHPASS,$SIL,afade=t=in:st=0:d=0.008,areverse,$SIL,afade=t=in:st=0:d=0.008,areverse"
|
||||
if [ "$ECHO" -gt 0 ]; then
|
||||
DECAY=$(awk -v e="$ECHO" 'BEGIN{printf "%.2f", 0.2 + e * 0.006}')
|
||||
PRE="$PRE,aecho=0.8:0.85:60:${DECAY}"
|
||||
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" \
|
||||
-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)
|
||||
if [ -n "$I" ] && [ -n "$TP" ]; then
|
||||
GAIN=$(awk -v i="$I" -v tp="$TP" 'BEGIN{
|
||||
g = -16 - i;
|
||||
lim = -3 - tp;
|
||||
if (lim < g) g = lim;
|
||||
if (g > 30) g = 30;
|
||||
if (g < -20) g = -20;
|
||||
printf "%.2f", g
|
||||
}')
|
||||
else
|
||||
GAIN=0
|
||||
fi
|
||||
|
||||
TMPOUT="$TMP/out.wav"
|
||||
if ! 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
|
||||
fi
|
||||
[ -s "$TMPOUT" ] || { echo "post-processing produced no audio" >&2; exit 1; }
|
||||
mv -f "$TMPOUT" "$OUT"
|
||||
Reference in New Issue
Block a user