62cc072f8c
Four committed WAV fixtures go through the real whisper.cpp binding in cmd/mavsttd, so a wrong model, a wrong language hint, a broken resample or a regressed silence gate fails `make test` instead of surfacing as Maven mishearing him. The fixtures are piper-synthesised, not recorded: scripts/gen-stt-fixtures.sh drives the vendored piper with the ru_RU-irina voice Maven already speaks with, so nothing of the owner's voice is committed and every fixture is reproducible. 360K total for three Russian clips and one English. Matching is tolerant on purpose. Golden transcripts move with the model, so each case asserts intent-carrying keywords (prefix match, so Russian inflection does not fail it) plus a word error rate ceiling, not an exact string. The matcher is unit-tested on its own and needs no model. TestGoldenAudioTranscription skips when models/stt/ggml-small.bin is absent, so `make test` still passes on a box without models. TestGoldenFixturesAreCanonical runs everywhere and checks the WAVs are 16k mono s16le and would clear mavsttd's own silence gate.
68 lines
2.8 KiB
Bash
Executable File
68 lines
2.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gen-stt-fixtures.sh — regenerate the golden STT audio fixtures.
|
|
#
|
|
# The fixtures in cmd/mavsttd/testdata/*.wav are SYNTHESISED, not recorded.
|
|
# They come out of the same piper voices maven speaks with, so nothing of the
|
|
# owner's voice is committed and every fixture is reproducible from this
|
|
# script plus the voice model. They are also small: 16 kHz mono s16le, a
|
|
# couple of seconds each.
|
|
#
|
|
# Usage:
|
|
# scripts/gen-stt-fixtures.sh
|
|
#
|
|
# Voices are picked up from, in order, $PIPER_VOICE_RU / $PIPER_VOICE_EN, then
|
|
# the repo's models/tts, then ~/esp-server/voices. The English voice is not
|
|
# vendored; if it is missing the English fixture is skipped and the existing
|
|
# one is left alone.
|
|
set -euo pipefail
|
|
|
|
root="$(cd "$(dirname "$0")/.." && pwd)"
|
|
out="$root/cmd/mavsttd/testdata"
|
|
piper="${PIPER_BIN:-$root/deps/piper/piper}"
|
|
espeak="${PIPER_ESPEAK:-$root/deps/piper/espeak-ng-data}"
|
|
|
|
pick_voice() {
|
|
for c in "$@"; do
|
|
[ -f "$c" ] && { echo "$c"; return 0; }
|
|
done
|
|
return 1
|
|
}
|
|
|
|
ru="$(pick_voice "${PIPER_VOICE_RU:-}" "$root/models/tts/ru_RU-irina-medium.onnx" "$HOME/esp-server/voices/ru_RU-irina-medium.onnx")" || {
|
|
echo "no russian piper voice found" >&2
|
|
exit 1
|
|
}
|
|
en="$(pick_voice "${PIPER_VOICE_EN:-}" "$root/models/tts/en_US-lessac-medium.onnx" "$HOME/esp-server/voices/en_US-lessac-medium.onnx")" || en=""
|
|
|
|
# synth <voice> <out.wav> <text>
|
|
# piper emits raw 22050 Hz s16le on stdout; ffmpeg resamples to the canonical
|
|
# 16 kHz mono and writes a plain 44-byte-header WAV (-fflags bitexact keeps
|
|
# ffmpeg's encoder LIST chunk out, so the bytes are stable across ffmpeg
|
|
# builds and internal/audio.PCMFromWAV reads them without scanning).
|
|
synth() {
|
|
local voice="$1" dest="$2" text="$3"
|
|
printf '%s' "$text" | LD_LIBRARY_PATH="$(dirname "$piper")" "$piper" \
|
|
--model "$voice" --config "$voice.json" \
|
|
--espeak_data "$espeak" --output_raw --quiet |
|
|
ffmpeg -hide_banner -loglevel error -y \
|
|
-f s16le -ar 22050 -ac 1 -i - \
|
|
-af "adelay=200,apad=pad_dur=0.2" \
|
|
-ar 16000 -ac 1 -c:a pcm_s16le -fflags bitexact "$dest"
|
|
echo "wrote $dest ($(stat -c%s "$dest") bytes)"
|
|
}
|
|
|
|
synth "$ru" "$out/ru_reminder.wav" "Напомни мне через час позвонить маме."
|
|
synth "$ru" "$out/ru_fact.wav" "Отметь, что я выпил воды."
|
|
synth "$ru" "$out/ru_query.wav" "Что у меня сегодня по календарю?"
|
|
|
|
if [ -n "$en" ]; then
|
|
# Keep the English line free of words piper spells out letter by letter —
|
|
# "nginx" comes out of lessac as "engine X", which is a TTS artefact and
|
|
# would make the fixture assert on the wrong thing.
|
|
synth "$en" "$out/en_act.wav" "Restart the web server and check the disk space."
|
|
else
|
|
echo "no english piper voice found — skipping en_act.wav" >&2
|
|
fi
|
|
|
|
echo "fixtures regenerated; expected transcripts live in $out/golden_v1.json"
|