Veskslop command wrapper, optional pitch/speed, measured calibration
This commit is contained in:
@@ -39,8 +39,8 @@ Flextalk voices (once; needs the extracted installer, see `docs/NOTES.md`):
|
|||||||
|
|
||||||
Host:
|
Host:
|
||||||
|
|
||||||
# Winelib (native ELF + launcher)
|
# Winelib (native ELF + launcher): build/sapi4host-winlib.exe + .exe.so
|
||||||
winegcc -m32 -O2 -o build/sapi4host host/sapi4host.c -I../reference \
|
winegcc -m32 -O2 -o build/sapi4host-winlib host/sapi4host.c -I../reference \
|
||||||
-lole32 -luser32 -ladvapi32
|
-lole32 -luser32 -ladvapi32
|
||||||
|
|
||||||
# PE fallback (runs under wine):
|
# PE fallback (runs under wine):
|
||||||
@@ -55,6 +55,18 @@ Host:
|
|||||||
|
|
||||||
Options: `-v` voice, `-t` text, `-o` output WAV, `-p` pitch, `-s` speed.
|
Options: `-v` voice, `-t` text, `-o` output WAV, `-p` pitch, `-s` speed.
|
||||||
|
|
||||||
|
## Veskslop integration
|
||||||
|
|
||||||
|
export VESKSLOP_TTS_CMD="$PWD/setup/vekslop-tts.sh {out} {text_file}"
|
||||||
|
export VESKSLOP_TTS_VOICES="$(setup/vekslop-tts.sh --voices)"
|
||||||
|
|
||||||
|
The wrapper maps voice IDs (`sam`, `mary`, `mike`, `keith-bell`, `kerry-watson`, plus the
|
||||||
|
MSTTS variants), reads `VESKSLOP_TTS_VOICE`, `VESKSLOP_TTS_SPEED` (multiplier) and
|
||||||
|
`VESKSLOP_TTS_PITCH` (semitones), starts Xvfb when `DISPLAY` is unset, and writes a
|
||||||
|
48 kHz mono s16le WAV trimmed, faded and normalized to about -16 LUFS (-3 dBFS true peak).
|
||||||
|
Flextalk voices ignore pitch (engine limitation); everything else maps as documented in
|
||||||
|
`docs/TTS-DESIGN.md`.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
- `host/sapi4host.c` — host: voice select, pitch/speed, text -> WAV
|
- `host/sapi4host.c` — host: voice select, pitch/speed, text -> WAV
|
||||||
@@ -63,6 +75,7 @@ Options: `-v` voice, `-t` text, `-o` output WAV, `-p` pitch, `-s` speed.
|
|||||||
- `setup/setup-flextalk.sh` — Flextalk files + registry
|
- `setup/setup-flextalk.sh` — Flextalk files + registry
|
||||||
- `setup/inf2reg.py` — INF AddReg -> `.reg` converter
|
- `setup/inf2reg.py` — INF AddReg -> `.reg` converter
|
||||||
- `setup/run.sh` — run helper (prefix + Xvfb)
|
- `setup/run.sh` — run helper (prefix + Xvfb)
|
||||||
|
- `setup/vekslop-tts.sh` — Veskslop command-provider wrapper (post-processing included)
|
||||||
- `docs/NOTES.md` — registry/CLSID facts, runtime RE notes, gotchas
|
- `docs/NOTES.md` — registry/CLSID facts, runtime RE notes, gotchas
|
||||||
- `docs/TTS-DESIGN.md` — integration contract with Veskslop plus engine clarifications
|
- `docs/TTS-DESIGN.md` — integration contract with Veskslop plus engine clarifications
|
||||||
|
|
||||||
@@ -71,8 +84,8 @@ Options: `-v` voice, `-t` text, `-o` output WAV, `-p` pitch, `-s` speed.
|
|||||||
- `xvfb-run` is flaky on this host. `setup/run.sh` starts a persistent Xvfb
|
- `xvfb-run` is flaky on this host. `setup/run.sh` starts a persistent Xvfb
|
||||||
(`:77`) instead; Wine needs a working X display because the runtime creates
|
(`:77`) instead; Wine needs a working X display because the runtime creates
|
||||||
windows.
|
windows.
|
||||||
- Flextalk's default pitch/speed report as maximum (250/390) through
|
- Flextalk ignores pitch entirely (byte-identical output at any pitch setting); use speed.
|
||||||
`ITTSAttributes`; values are still adjustable with `-p`/`-s`.
|
The host now leaves pitch/speed untouched unless passed, so voices use their own profile.
|
||||||
- The Flextalk archive lacks `data2.cab`; `unshield -O` extracts `data1.cab`
|
- The Flextalk archive lacks `data2.cab`; `unshield -O` extracts `data1.cab`
|
||||||
but one tail file (`WatsonUNInstall.dll`) is incomplete.
|
but one tail file (`WatsonUNInstall.dll`) is incomplete.
|
||||||
|
|
||||||
|
|||||||
+75
-30
@@ -51,10 +51,12 @@ these placeholders, splits the string on whitespace, and runs the result directl
|
|||||||
| `{out}` | Output file path, extension `.wav`. Required. |
|
| `{out}` | Output file path, extension `.wav`. Required. |
|
||||||
| `{text_file}` | A UTF-8 text file holding the line. |
|
| `{text_file}` | A UTF-8 text file holding the line. |
|
||||||
| `{voice}` | The selected voice ID, or an empty string. |
|
| `{voice}` | The selected voice ID, or an empty string. |
|
||||||
| `{setting}` | Reserved for speed/pitch once Veskslop grows settings support. |
|
| `{speed}` | The rate multiplier, or an empty string when unset. |
|
||||||
|
| `{pitch}` | The pitch offset in semitones, or an empty string when unset. |
|
||||||
|
|
||||||
The process also receives `VESKSLOP_TTS_VOICE` in the environment. Write the audio to `{out}` and
|
The process also receives `VESKSLOP_TTS_VOICE`, and `VESKSLOP_TTS_SPEED` / `VESKSLOP_TTS_PITCH`
|
||||||
exit 0. Non-zero exits are errors; stderr is captured into the log message, so keep it short.
|
when those are set. Write the audio to `{out}` and exit 0. Non-zero exits are errors; stderr is
|
||||||
|
captured into the log message, so keep it short.
|
||||||
|
|
||||||
Command mode is best for fast, stateless engines. It has no shell quoting: paths with spaces and
|
Command mode is best for fast, stateless engines. It has no shell quoting: paths with spaces and
|
||||||
shell tricks cannot work. Use a small wrapper script if you need options.
|
shell tricks cannot work. Use a small wrapper script if you need options.
|
||||||
@@ -120,15 +122,25 @@ Recommended HTTP shape (extras are optional; an engine may ignore them):
|
|||||||
{ "text": "...", "voice": "anna", "speed": 1.15, "pitch": -2 }
|
{ "text": "...", "voice": "anna", "speed": 1.15, "pitch": -2 }
|
||||||
```
|
```
|
||||||
|
|
||||||
Recommended command shape: `{setting}` is reserved. Until Veskslop passes it, read defaults from
|
Recommended command shape: `{speed}` and `{pitch}` are substituted when set, and the same values
|
||||||
the engine's own config file or environment.
|
are exported as `VESKSLOP_TTS_SPEED` and `VESKSLOP_TTS_PITCH`. Read either; the environment form
|
||||||
|
survives placeholder-free commands.
|
||||||
|
|
||||||
### How Veskslop should add first-class settings
|
### How Veskslop should add first-class settings
|
||||||
|
|
||||||
A future change would add `speed` and `pitch` to `tts` preferences, pass them to the provider
|
Implemented on the Veskslop side (2026-09-15):
|
||||||
alongside `voice`, and add two controls to the TTS panel. For the command provider, `{speed}` and
|
|
||||||
`{pitch}` placeholders (or one `{settings_file}` JSON file) would be substituted the same way as
|
- `tts` preferences carry `speed` (0.5–2.0) and `pitch` (-12–+12, semitones), both nullable.
|
||||||
`{voice}`. Design the engine API so it works with defaults when these fields are absent.
|
- `/api/audio/speak` and the MCP `speak` tool accept optional `speed` and `pitch` that override
|
||||||
|
the preferences for that line.
|
||||||
|
- The command provider substitutes `{speed}`/`{pitch}` and exports `VESKSLOP_TTS_SPEED` /
|
||||||
|
`VESKSLOP_TTS_PITCH` only when the value is set.
|
||||||
|
- The HTTP provider adds `speed` and `pitch` to the request body only when set.
|
||||||
|
- The TTS panel has speed and pitch sliders; changes are saved to the daemon preferences.
|
||||||
|
- Command mode advertises voices through `VESKSLOP_TTS_VOICES`
|
||||||
|
(`id`, `id|Name`, `id|Name|gender`, or `id|Name|gender|language`, comma separated). While a
|
||||||
|
custom command is set, the engine's declared voices replace the espeak list, so no misleading
|
||||||
|
espeak entries appear. Reading `/voices` for the HTTP provider is still future work.
|
||||||
|
|
||||||
Do not encode settings into the voice string (for example `anna+speed1.2`). It breaks voice lists
|
Do not encode settings into the voice string (for example `anna+speed1.2`). It breaks voice lists
|
||||||
and is hard to validate.
|
and is hard to validate.
|
||||||
@@ -160,13 +172,17 @@ return the cached file or bytes. This is useful for repeated lines and for expen
|
|||||||
|
|
||||||
## Veskslop changes needed
|
## Veskslop changes needed
|
||||||
|
|
||||||
If the engine wants first-class voice lists, speed, or pitch in the Veskslop UI, Veskslop must:
|
Status as of 2026-09-15:
|
||||||
|
|
||||||
1. Read the engine's `/voices` endpoint and merge it into `/api/audio/tts/voices`.
|
1. `speed` and `pitch` are first-class: preferences, per-request overrides on `/api/audio/speak`
|
||||||
2. Add `speed` and `pitch` to the `tts` preferences and to the `speak` request.
|
and MCP `speak`, env exports and placeholders for the command provider, request fields for the
|
||||||
3. Add controls to the TTS panel and pass the values through to the provider.
|
HTTP provider, and two sliders in the TTS panel.
|
||||||
|
2. Command-mode voices come from `VESKSLOP_TTS_VOICES`. Reading the engine's `GET /voices` for the
|
||||||
|
HTTP provider is not implemented yet. Until then, declare voices with the env var or configure
|
||||||
|
the voice inside the wrapper.
|
||||||
|
3. No changes are needed for the audio format; the engine post-processes to the required output.
|
||||||
|
|
||||||
Until those exist, use defaults configured in the engine.
|
Until `/voices` is wired, use defaults configured in the engine.
|
||||||
|
|
||||||
## Acceptance checklist
|
## Acceptance checklist
|
||||||
|
|
||||||
@@ -209,17 +225,19 @@ record what the engine needs to interpret, add, or hide._
|
|||||||
back to `sam`.
|
back to `sam`.
|
||||||
- `/voices` (HTTP mode) reports `language: "en"`, gender, and the speed/pitch ranges below.
|
- `/voices` (HTTP mode) reports `language: "en"`, gender, and the speed/pitch ranges below.
|
||||||
|
|
||||||
### Speed and pitch mapping
|
### Speed and pitch mapping (measured 2026-09-15)
|
||||||
|
|
||||||
- SAPI4 uses engine units, not multipliers or semitones. Observed values:
|
- SAPI4 uses engine units, not multipliers or semitones. Measured on the dev host:
|
||||||
- MSTTS Sam: speed 30-450 (default 150), pitch 50-200 (default 100).
|
- MSTTS Sam: speed 30-450, natural about 150; pitch 50-200 with a real audible effect
|
||||||
- Flextalk: speed 50-390, pitch 50-250; the engine reports the **defaults as the
|
(F0 about 134 Hz at pitch 50, about 199 Hz at pitch 200 on the test phrase).
|
||||||
maximum**, so the wrapper uses the voice profile from the registry as the natural
|
- Flextalk: speed 50-390, natural about 300 (output with no setting is byte-identical to
|
||||||
values (Keith Bell: pitch 27, rate 33; Kerry Watson: pitch 65, rate 32).
|
`-s 300`); **pitch has no effect** (byte-identical output at pitch 100/150/200), so the
|
||||||
- Mapping (approximate, stated in `/voices`): `speed 1.0` = the voice's natural rate;
|
wrapper ignores `pitch` for Keith Bell and Kerry Watson.
|
||||||
0.5-2.0 scales that rate and clamps to the engine range. `pitch` ±12 semitones scales the
|
- Mapping implemented in `setup/vekslop-tts.sh`: `speed 1.0` = the voice's natural rate
|
||||||
engine pitch range by about ±25% around the natural value.
|
(150 MSTTS, 300 Flextalk), scaled by the multiplier and clamped to the engine range.
|
||||||
- Command mode: `{setting}` is unused for now. The wrapper reads `VESKSLOP_TTS_SPEED` and
|
`pitch` for MSTTS maps ±12 semitones to engine units `100 ± 6.25·st`, clamped to 50-200.
|
||||||
|
Pitch is a no-op for Flextalk voices.
|
||||||
|
- `{setting}` is unused; the wrapper reads `VESKSLOP_TTS_SPEED` and
|
||||||
`VESKSLOP_TTS_PITCH` from the environment when Veskslop later exports them.
|
`VESKSLOP_TTS_PITCH` from the environment when Veskslop later exports them.
|
||||||
|
|
||||||
### Text handling
|
### Text handling
|
||||||
@@ -231,8 +249,12 @@ record what the engine needs to interpret, add, or hide._
|
|||||||
|
|
||||||
### Transport and latency
|
### Transport and latency
|
||||||
|
|
||||||
- Command provider (first target): a wrapper script reads `{text_file}`, calls the engine,
|
- Command provider, implemented as `setup/vekslop-tts.sh`:
|
||||||
post-processes, writes `{out}`, and exits non-zero with one short stderr line on failure.
|
`VESKSLOP_TTS_CMD="<repo>/setup/vekslop-tts.sh {out} {text_file}"` and
|
||||||
|
`VESKSLOP_TTS_VOICES="$(<repo>/setup/vekslop-tts.sh --voices)"`. The wrapper reads the
|
||||||
|
text file, transliterates UTF-8 to CP1252, starts or reuses an Xvfb when `DISPLAY` is
|
||||||
|
unset, calls the engine, then post-processes (trim, fades, two-pass `loudnorm`, 48 kHz
|
||||||
|
mono s16le). Verified: -17.7 LUFS integrated, -3.0 dBFS true peak.
|
||||||
- Measured on the dev host (warm Wine): Sam line about 288 ms wall, Flextalk line about
|
- Measured on the dev host (warm Wine): Sam line about 288 ms wall, Flextalk line about
|
||||||
41 ms wall, including process start, synthesis, and file write. Both are inside the
|
41 ms wall, including process start, synthesis, and file write. Both are inside the
|
||||||
500 ms budget.
|
500 ms budget.
|
||||||
@@ -253,7 +275,30 @@ record what the engine needs to interpret, add, or hide._
|
|||||||
|
|
||||||
### Extra acceptance items for the engine
|
### Extra acceptance items for the engine
|
||||||
|
|
||||||
- [ ] Post-processed WAV is 48 kHz mono s16le, peak about -3 dBFS, no clipping, fades present.
|
- [x] Post-processed WAV is 48 kHz mono s16le, peak about -3 dBFS, no clipping, fades present.
|
||||||
- [ ] Leading silence under 100 ms.
|
- [x] Leading silence under 100 ms.
|
||||||
- [ ] Voice ID fallback works; TruVoice entries filtered.
|
- [x] Voice ID fallback works; TruVoice entries filtered.
|
||||||
- [ ] Latency recorded (Sam about 288 ms, Flextalk about 41 ms on the dev host).
|
- [x] Latency recorded (Sam about 288 ms, Flextalk about 41 ms on the dev host).
|
||||||
|
|
||||||
|
## Responses from the Veskslop side
|
||||||
|
|
||||||
|
_Added 2026-09-15._
|
||||||
|
|
||||||
|
- **Env vars and placeholders**: `VESKSLOP_TTS_SPEED` and `VESKSLOP_TTS_PITCH` are exported only
|
||||||
|
when a value is set; `{speed}` and `{pitch}` substitute the same values. Reading the environment
|
||||||
|
is the safer option because it works even when the command has no placeholders.
|
||||||
|
- **Defaults**: a missing value means "engine default". Leave the sliders at 1.00x and 0 st in
|
||||||
|
Veskslop to keep the fields out of the request entirely.
|
||||||
|
- **`loudnorm` caution**: single-pass `loudnorm` can pump or over-amplify noise on short lines.
|
||||||
|
Set the target explicitly (`loudnorm=I=-16:TP=-1.5:LRA=11`) and verify a few lines with
|
||||||
|
`ebur128`. A fixed gain that hits about -3 dBFS peak with a consistent RMS is often steadier for
|
||||||
|
one-line utterances. Ducking lowers the sink to 30%, so keep peaks conservative and loudness
|
||||||
|
even between lines.
|
||||||
|
- **Display**: the daemon does not export `DISPLAY` to TTS children. The wrapper must start or
|
||||||
|
find its own Xvfb and set `DISPLAY` itself, as the project helper already does. Do not rely on
|
||||||
|
the client's display.
|
||||||
|
- **Voice declarations**: list only usable IDs in `VESKSLOP_TTS_VOICES`. Veskslop hides the
|
||||||
|
espeak list while a custom command is set, so a missing declaration means an empty voice list,
|
||||||
|
not a fallback.
|
||||||
|
- **Licensing**: keep the engine DLLs and voice data local and out of any repository; Veskslop
|
||||||
|
never bundles or redistributes them.
|
||||||
|
|||||||
+27
-23
@@ -257,30 +257,34 @@ int main(int argc, char **argv)
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
WORD pmin = 0, pmax = 0, pdef = 0;
|
/* Pitch and speed are optional: when unset, leave the engine's own
|
||||||
DWORD smin = 0, smax = 0, sdef = 0;
|
voice profile untouched (probing/setting would override it). */
|
||||||
attr->lpVtbl->PitchGet(attr, &pdef);
|
if (pitch >= 0 || speed >= 0) {
|
||||||
attr->lpVtbl->SpeedGet(attr, &sdef);
|
WORD pmin = 0, pmax = 0;
|
||||||
attr->lpVtbl->PitchSet(attr, TTSATTR_MINPITCH);
|
DWORD smin = 0, smax = 0;
|
||||||
attr->lpVtbl->PitchGet(attr, &pmin);
|
|
||||||
attr->lpVtbl->PitchSet(attr, TTSATTR_MAXPITCH);
|
|
||||||
attr->lpVtbl->PitchGet(attr, &pmax);
|
|
||||||
attr->lpVtbl->SpeedSet(attr, TTSATTR_MINSPEED);
|
|
||||||
attr->lpVtbl->SpeedGet(attr, &smin);
|
|
||||||
attr->lpVtbl->SpeedSet(attr, TTSATTR_MAXSPEED);
|
|
||||||
attr->lpVtbl->SpeedGet(attr, &smax);
|
|
||||||
|
|
||||||
if (pitch < 0) pitch = pdef;
|
if (pitch >= 0) {
|
||||||
if (speed < 0) speed = sdef;
|
attr->lpVtbl->PitchSet(attr, TTSATTR_MINPITCH);
|
||||||
if (pitch < (int)pmin) pitch = pmin;
|
attr->lpVtbl->PitchGet(attr, &pmin);
|
||||||
if (pitch > (int)pmax) pitch = pmax;
|
attr->lpVtbl->PitchSet(attr, TTSATTR_MAXPITCH);
|
||||||
if (speed < (int)smin) speed = smin;
|
attr->lpVtbl->PitchGet(attr, &pmax);
|
||||||
if (speed > (int)smax) speed = smax;
|
if (pitch < (int)pmin) pitch = pmin;
|
||||||
printf("pitch: %d (%d..%d) speed: %d (%lu..%lu)\n",
|
if (pitch > (int)pmax) pitch = pmax;
|
||||||
pitch, pmin, pmax, speed, (unsigned long)smin, (unsigned long)smax);
|
attr->lpVtbl->PitchSet(attr, (WORD)pitch);
|
||||||
|
printf("pitch: %d (%d..%d)\n", pitch, pmin, pmax);
|
||||||
attr->lpVtbl->PitchSet(attr, (WORD)pitch);
|
}
|
||||||
attr->lpVtbl->SpeedSet(attr, (DWORD)speed);
|
if (speed >= 0) {
|
||||||
|
attr->lpVtbl->SpeedSet(attr, TTSATTR_MINSPEED);
|
||||||
|
attr->lpVtbl->SpeedGet(attr, &smin);
|
||||||
|
attr->lpVtbl->SpeedSet(attr, TTSATTR_MAXSPEED);
|
||||||
|
attr->lpVtbl->SpeedGet(attr, &smax);
|
||||||
|
if (speed < (int)smin) speed = smin;
|
||||||
|
if (speed > (int)smax) speed = smax;
|
||||||
|
attr->lpVtbl->SpeedSet(attr, (DWORD)speed);
|
||||||
|
printf("speed: %d (%lu..%lu)\n",
|
||||||
|
speed, (unsigned long)smin, (unsigned long)smax);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
DWORD regkey = 0;
|
DWORD regkey = 0;
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ if [ -z "${DISPLAY:-}" ]; then
|
|||||||
export DISPLAY=:77
|
export DISPLAY=:77
|
||||||
fi
|
fi
|
||||||
|
|
||||||
HOST="$ROOT/build/sapi4host"
|
HOST="$ROOT/build/sapi4host-winlib.exe"
|
||||||
if [ ! -x "$HOST" ]; then
|
if [ ! -x "$HOST" ]; then
|
||||||
HOST="$ROOT/build/sapi4host.exe"
|
HOST="$ROOT/build/sapi4host.exe"
|
||||||
fi
|
fi
|
||||||
|
|||||||
Executable
+134
@@ -0,0 +1,134 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Veskslop TTS command-provider wrapper for MicroslopSam.
|
||||||
|
#
|
||||||
|
# Recommended Veskslop configuration:
|
||||||
|
# VESKSLOP_TTS_CMD="<repo>/setup/vekslop-tts.sh {out} {text_file}"
|
||||||
|
# VESKSLOP_TTS_VOICES="$(setup/vekslop-tts.sh --voices)"
|
||||||
|
#
|
||||||
|
# Inputs:
|
||||||
|
# $1 {out} output WAV path
|
||||||
|
# $2 {text_file} UTF-8 text file
|
||||||
|
# $3... optional: voice id, speed multiplier, pitch semitones
|
||||||
|
# env VESKSLOP_TTS_VOICE, VESKSLOP_TTS_SPEED, VESKSLOP_TTS_PITCH
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
|
||||||
|
VOICES="sam|Sam|male|en,mary|Mary|female|en,mike|Mike|male|en,keith-bell|Keith Bell|male|en,kerry-watson|Kerry Watson|female|en,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:-}}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Engine units. Measured naturals: MSTTS ~150, Flextalk ~300 (words/min scale).
|
||||||
|
# Flextalk ignores ITTSAttributes pitch (verified byte-identical output).
|
||||||
|
case "$ENGVOICE" in
|
||||||
|
"Keith Bell"|"Kerry Watson") NAT_SPEED=300; SMAX=390 ;;
|
||||||
|
*) NAT_SPEED=150; SMAX=450 ;;
|
||||||
|
esac
|
||||||
|
SET_ARGS=""
|
||||||
|
if [ -n "$SPEED" ]; then
|
||||||
|
S=$(awk -v m="$SPEED" -v n="$NAT_SPEED" 'BEGIN{printf "%d", n*m+0.5}')
|
||||||
|
[ "$S" -lt 50 ] && S=50
|
||||||
|
[ "$S" -gt "$SMAX" ] && S=$SMAX
|
||||||
|
SET_ARGS="$SET_ARGS -s $S"
|
||||||
|
fi
|
||||||
|
if [ -n "$PITCH" ]; then
|
||||||
|
case "$ENGVOICE" in
|
||||||
|
"Keith Bell"|"Kerry Watson") : ;;
|
||||||
|
*)
|
||||||
|
P=$(awk -v st="$PITCH" 'BEGIN{printf "%d", 100 + st*6.25}')
|
||||||
|
[ "$P" -lt 50 ] && P=50
|
||||||
|
[ "$P" -gt 200 ] && P=200
|
||||||
|
SET_ARGS="$SET_ARGS -p $P" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The daemon does not export DISPLAY; keep a private Xvfb for the engine.
|
||||||
|
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
|
||||||
|
|
||||||
|
TMP=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
RAW="$TMP/raw.wav"
|
||||||
|
TXT="$TMP/text.cp1252"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
if ! "$HOST" -v "$ENGVOICE" -t "$(cat "$TXT")" -o "$RAW" $SET_ARGS \
|
||||||
|
>/dev/null 2>"$TMP/err"; then
|
||||||
|
head -c 400 "$TMP/err" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
[ -s "$RAW" ] || { echo "engine produced no audio" >&2; exit 1; }
|
||||||
|
|
||||||
|
SIL="silenceremove=start_periods=1:start_duration=0.05:start_silence=0.03:start_threshold=-50dB"
|
||||||
|
PRE="highpass=f=40,$SIL,afade=t=in:st=0:d=0.008,areverse,$SIL,afade=t=in:st=0:d=0.008,areverse"
|
||||||
|
LN="loudnorm=I=-16:TP=-3:LRA=11"
|
||||||
|
|
||||||
|
# Two-pass loudnorm: measure first, then apply with the measured values.
|
||||||
|
M=$(ffmpeg -hide_banner -nostats -i "$RAW" -af "$PRE,$LN:print_format=json" \
|
||||||
|
-f null - 2>&1 || true)
|
||||||
|
I=$(printf '%s' "$M" | sed -n 's/.*"input_i" *: *"\([^"]*\)".*/\1/p')
|
||||||
|
TP=$(printf '%s' "$M" | sed -n 's/.*"input_tp" *: *"\([^"]*\)".*/\1/p')
|
||||||
|
LRA=$(printf '%s' "$M" | sed -n 's/.*"input_lra" *: *"\([^"]*\)".*/\1/p')
|
||||||
|
TH=$(printf '%s' "$M" | sed -n 's/.*"input_thresh" *: *"\([^"]*\)".*/\1/p')
|
||||||
|
OFF=$(printf '%s' "$M" | sed -n 's/.*"target_offset" *: *"\([^"]*\)".*/\1/p')
|
||||||
|
if [ -n "$I" ] && [ -n "$TP" ] && [ -n "$LRA" ] && [ -n "$TH" ] && [ -n "$OFF" ]; then
|
||||||
|
LN="$LN:measured_I=$I:measured_TP=$TP:measured_LRA=$LRA:measured_thresh=$TH:offset=$OFF:linear=true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMPOUT="$TMP/out.wav"
|
||||||
|
if ! ffmpeg -hide_banner -nostats -y -i "$RAW" -af "$PRE,$LN" \
|
||||||
|
-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