#!/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
#
# The spoken text is NOT written here. It is read out of
# cmd/mavsttd/testdata/golden_v1.json, which is the same file the test scores
# against. It used to live in both places, so editing this script and running
# make stt-fixtures left the manifest describing audio that no longer existed —
# and at a WER ceiling of 0.34 over a five-word reference, a one-word drift
# passed silently. Punctuation does not matter: normalizeTranscript strips it.
#
# 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=""

manifest="$root/cmd/mavsttd/testdata/golden_v1.json"
command -v jq >/dev/null || { echo "jq is required to read $manifest" >&2; exit 1; }
[ -f "$manifest" ] || { echo "missing $manifest" >&2; exit 1; }

# case_text <name> — the reference transcript for one manifest case.
case_text() {
	local name="$1" text
	text="$(jq -r --arg n "$name" '.cases[] | select(.name==$n) | .text' "$manifest")"
	[ -n "$text" ] && [ "$text" != "null" ] || { echo "no case named $name in $manifest" >&2; exit 1; }
	printf '%s' "$text"
}

# case_wav <name> — the file name the manifest expects for one case.
case_wav() {
	local name="$1" wav
	wav="$(jq -r --arg n "$name" '.cases[] | select(.name==$n) | .wav' "$manifest")"
	[ -n "$wav" ] && [ "$wav" != "null" ] || { echo "no case named $name in $manifest" >&2; exit 1; }
	printf '%s' "$wav"
}

# 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)"
}

for name in ru_reminder ru_fact ru_query; do
	synth "$ru" "$out/$(case_wav "$name")" "$(case_text "$name")"
done

if [ -n "$en" ]; then
	# Keep the English line in the manifest 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/$(case_wav en_act)" "$(case_text en_act)"
else
	echo "no english piper voice found — skipping en_act.wav" >&2
fi

echo "fixtures regenerated; expected transcripts live in $out/golden_v1.json"
