Merge remote-tracking branch 'origin/main' into feat/frank-tts-engine
# Conflicts: # serverAppConfigs-docs/index.md # serverAppConfigs-docs/log.md
This commit is contained in:
Executable
+268
@@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env bats
|
||||
# Seam: the voice-pack builder (scripts/build-voice-pack.sh).
|
||||
#
|
||||
# The contract this pins is the one frank-tts's packs live or die by. A crop is
|
||||
# only usable voice material if it starts PAST the first 10 seconds of the
|
||||
# source file — the project-wide source-material invariant — and this builder is
|
||||
# the thing that enforces it, because nothing downstream looks at offsets again.
|
||||
# A pack is three files plus provenance: reference.wav at 24 kHz mono inside the
|
||||
# 18-25 s window, the engine's own speaker artifacts beside it, and a
|
||||
# manifest.json whose sha256 still matches the bytes the crop came from. Builds
|
||||
# refuse to clobber what is already on disk, and --force retires rather than
|
||||
# deletes.
|
||||
#
|
||||
# No engine binary, no model, no GPU, no network and no fleet host: the crop is
|
||||
# cut from a locally generated tone and the pinned extractor is stood in by a
|
||||
# stub that mirrors the real tool's I/O shape (no -o flag: outputs named next to
|
||||
# the input with the extension swapped, at the sizes the codec's config
|
||||
# implies), which is how frank_tts_unit.bats drives its backends too.
|
||||
|
||||
load helpers
|
||||
|
||||
setup() {
|
||||
BUILDER="$REPO_ROOT/scripts/build-voice-pack.sh"
|
||||
ENV_FILE="$REPO_ROOT/scripts/voice-packs.env"
|
||||
TEST_TMP="$(mktemp -d)"
|
||||
PACKS="$TEST_TMP/packs"
|
||||
mkdir -p "$PACKS"
|
||||
STUB_ARGV="$TEST_TMP/stub-argv.tsv"
|
||||
export STUB_ARGV
|
||||
|
||||
# Forty seconds of "recording": stereo 44.1 kHz, so a legal build has to
|
||||
# downmix and resample just as it does on a real audiobook. Every crop this
|
||||
# file takes starts at or past 12 s, well out of the forbidden region.
|
||||
ffmpeg -v error -y -f lavfi -i "sine=frequency=330:sample_rate=44100:duration=40" \
|
||||
-ac 2 -c:a pcm_s16le "$TEST_TMP/source.wav"
|
||||
SOURCE_SHA="$(sha256sum "$TEST_TMP/source.wav" | awk '{ print $1 }')"
|
||||
|
||||
write_stub_tool
|
||||
# The shipped tunables file, then the three machine-local overrides that
|
||||
# point the engine at the stub: the real env stays under test.
|
||||
cat "$ENV_FILE" > "$TEST_TMP/env"
|
||||
{
|
||||
printf 'VOICE_PACK_DIR=%s\n' "$PACKS"
|
||||
printf 'VOICE_PACK_TOOL=%s\n' "$TEST_TMP/qwen-codec"
|
||||
printf 'VOICE_PACK_CODEC_MODEL=%s\n' "$TEST_TMP/codec.gguf"
|
||||
printf 'VOICE_PACK_TALKER_MODEL=%s\n' "$TEST_TMP/talker.gguf"
|
||||
} >> "$TEST_TMP/env"
|
||||
printf 'stub-codec-weights\n' > "$TEST_TMP/codec.gguf"
|
||||
printf 'stub-talker-weights\n' > "$TEST_TMP/talker.gguf"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm -rf "$TEST_TMP"
|
||||
}
|
||||
|
||||
# write_stub_tool — stand-in for qwen-codec. Records what it was handed and
|
||||
# writes <input-stem>.spk / <input-stem>.rvq, exactly where the real tool would.
|
||||
# STUB_SPK_BYTES lets one test be an engine that changed its x-vector size.
|
||||
write_stub_tool() {
|
||||
cat > "$TEST_TMP/qwen-codec" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
input="" model="" talker=""
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--model) model="$2"; shift 2 ;;
|
||||
--talker) talker="$2"; shift 2 ;;
|
||||
--device) shift 2 ;;
|
||||
-i) input="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
printf 'model=%s\ttalker=%s\tinput=%s\n' "$model" "$talker" "$input" >> "$STUB_ARGV"
|
||||
[ -n "$input" ] || exit 2
|
||||
[ -s "$model" ] || exit 3
|
||||
[ -s "$talker" ] || exit 4
|
||||
dur="$(ffprobe -v error -select_streams a:0 -show_entries stream=duration -of default=nw=1:nk=1 "$input")"
|
||||
frames="$(awk -v d="$dur" 'BEGIN { printf "%d\n", int(int(d * 24000 + 0.5) / 1920) }')"
|
||||
head -c "${STUB_SPK_BYTES:-8192}" /dev/zero > "${input%.wav}.spk"
|
||||
head -c "$((22 * frames))" /dev/zero > "${input%.wav}.rvq"
|
||||
printf '[Stub] Wrote %s.rvq: %d packed bytes\n' "${input%.wav}" "$((22 * frames))" >&2
|
||||
STUB
|
||||
chmod +x "$TEST_TMP/qwen-codec"
|
||||
}
|
||||
|
||||
# build — a real (non-dry) run against the stub tool with the tone as source.
|
||||
build() {
|
||||
local voice="$1"
|
||||
shift
|
||||
run "$BUILDER" --env "$TEST_TMP/env" \
|
||||
--source "$TEST_TMP/source.wav" --sha256 "$SOURCE_SHA" "$@" "$voice" "$PACKS"
|
||||
}
|
||||
|
||||
pack_files() {
|
||||
ls "$PACKS/$1" 2>/dev/null | sort | tr '\n' ' '
|
||||
}
|
||||
|
||||
@test "a crop offset inside the first ten seconds is refused and names the invariant" {
|
||||
build narrator1 --offset 4.5 --duration 20
|
||||
[ "$status" -ne 0 ] || { echo "$output"; false; }
|
||||
[[ "$output" == *"source-material invariant"* ]]
|
||||
[[ "$output" == *"first 10 s"* ]]
|
||||
# Refused before anything was created, not cleaned up afterwards.
|
||||
[ ! -e "$PACKS/narrator1" ]
|
||||
[ ! -s "$STUB_ARGV" ]
|
||||
}
|
||||
|
||||
@test "the identical crop at a legal offset builds the whole pack" {
|
||||
build narrator1 --offset 12 --duration 20
|
||||
[ "$status" -eq 0 ] || { echo "$output"; false; }
|
||||
[[ "$output" == *"CROP 12 s .. 32 s"* ]]
|
||||
[[ "$output" == *"VOICE_PACK_BUILT narrator1 $PACKS/narrator1"* ]]
|
||||
[ -f "$PACKS/narrator1/reference.wav" ]
|
||||
[ -f "$PACKS/narrator1/reference.spk" ]
|
||||
[ -f "$PACKS/narrator1/reference.rvq" ]
|
||||
[ -f "$PACKS/narrator1/manifest.json" ]
|
||||
}
|
||||
|
||||
@test "reference.wav comes out 24 kHz mono pcm inside the 18-25 s window" {
|
||||
build narrator1 --offset 12 --duration 20
|
||||
[ "$status" -eq 0 ] || { echo "$output"; false; }
|
||||
local wav="$PACKS/narrator1/reference.wav"
|
||||
[ "$(ffprobe -v error -select_streams a:0 -show_entries stream=codec_name -of default=nw=1:nk=1 "$wav")" = "pcm_s16le" ]
|
||||
[ "$(ffprobe -v error -select_streams a:0 -show_entries stream=sample_rate -of default=nw=1:nk=1 "$wav")" = "24000" ]
|
||||
[ "$(ffprobe -v error -select_streams a:0 -show_entries stream=channels -of default=nw=1:nk=1 "$wav")" = "1" ]
|
||||
local dur
|
||||
dur="$(ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$wav")"
|
||||
awk -v d="$dur" 'BEGIN { exit !(d >= 18 && d <= 25) }'
|
||||
}
|
||||
|
||||
@test "the manifest round-trips through jq with offsets and a sha that matches the source" {
|
||||
build narrator2 --offset 15 --duration 22.5
|
||||
[ "$status" -eq 0 ] || { echo "$output"; false; }
|
||||
local manifest="$PACKS/narrator2/manifest.json"
|
||||
jq -e '
|
||||
.voice == "narrator2"
|
||||
and (.crop.offset_seconds == 15)
|
||||
and (.crop.end_seconds == 37.5)
|
||||
and (.reference.sample_rate == 24000)
|
||||
and (.reference.channels == 1)
|
||||
and (.reference.duration_seconds >= 18 and .reference.duration_seconds <= 25)
|
||||
and (.artifacts.engine_commit | test("^[0-9a-f]{40}$"))
|
||||
and (.artifacts.produced == true)
|
||||
and (.invariant.forbidden_source_seconds == 10)
|
||||
' "$manifest"
|
||||
# The recorded hash is the source's hash, not a hash of something else.
|
||||
[ "$(jq -r .source.sha256 "$manifest")" = "$(sha256sum "$TEST_TMP/source.wav" | awk '{ print $1 }')" ]
|
||||
# The pack's provenance names the tool that made the artifacts and the files
|
||||
# it wrote beside the WAV.
|
||||
[ "$(jq -r .artifacts.tool "$manifest")" = "$TEST_TMP/qwen-codec" ]
|
||||
grep -q "input=$PACKS/narrator2/staging-reference.wav" "$STUB_ARGV"
|
||||
}
|
||||
|
||||
@test "a wrong pinned sha stops the build before a crop is cut" {
|
||||
run "$BUILDER" --env "$TEST_TMP/env" --source "$TEST_TMP/source.wav" \
|
||||
--sha256 "$(printf '0%.0s' {1..64})" --offset 12 --duration 20 narrator1 "$PACKS"
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"sha256"* ]]
|
||||
[ ! -e "$PACKS/narrator1" ]
|
||||
}
|
||||
|
||||
@test "a crop that runs off the end of the source is refused" {
|
||||
build narrator1 --offset 30 --duration 20
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"crop would come out short"* ]]
|
||||
[ ! -e "$PACKS/narrator1" ]
|
||||
}
|
||||
|
||||
@test "a crop under the engine's artifact window is refused before the extractor runs" {
|
||||
# 18.5 s is inside the reference window but packs to 22 * 231 = 5,082 B of
|
||||
# .rvq, under the 5,456 B the pinned codec config produces — the builder
|
||||
# says so rather than running an extractor that would emit it anyway.
|
||||
build narrator1 --offset 12 --duration 18.5
|
||||
[ "$status" -ne 0 ] || { echo "$output"; false; }
|
||||
[[ "$output" == *"5456"* ]]
|
||||
[ ! -e "$PACKS/narrator1" ]
|
||||
[ ! -s "$STUB_ARGV" ]
|
||||
}
|
||||
|
||||
@test "an extractor whose artifact size changed does not get a published voice" {
|
||||
# The engine-bump case: a 4,096 B x-vector is not the 8,192 B the pinned
|
||||
# encoder emits, so the pack must not reach the state the shim reads.
|
||||
export STUB_SPK_BYTES=4096
|
||||
build narrator1 --offset 12 --duration 20
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"reference.spk"* ]]
|
||||
[ ! -e "$PACKS/narrator1/reference.wav" ]
|
||||
[ ! -e "$PACKS/narrator1/manifest.json" ]
|
||||
}
|
||||
|
||||
@test "re-running without --force refuses and leaves every byte alone" {
|
||||
build narrator1 --offset 12 --duration 20
|
||||
[ "$status" -eq 0 ] || { echo "$output"; false; }
|
||||
sha256sum "$PACKS"/narrator1/* > "$TEST_TMP/before.sha"
|
||||
|
||||
build narrator1 --offset 12 --duration 20
|
||||
[ "$status" -ne 0 ] || { echo "$output"; false; }
|
||||
[[ "$output" == *"--force"* ]]
|
||||
sha256sum -c --quiet "$TEST_TMP/before.sha"
|
||||
[ "$(find "$PACKS" -name .AGENTTRASH -prune -o -type f -newer "$TEST_TMP/before.sha" -print | wc -l)" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "--force retires the previous pack instead of deleting anything" {
|
||||
build narrator1 --offset 12 --duration 20
|
||||
[ "$status" -eq 0 ] || { echo "$output"; false; }
|
||||
cp "$PACKS/narrator1/reference.wav" "$TEST_TMP/first-reference.wav"
|
||||
|
||||
build narrator1 --offset 12 --duration 20 --force
|
||||
[ "$status" -eq 0 ] || { echo "$output"; false; }
|
||||
[[ "$output" == *"RETIRED"* ]]
|
||||
# The old bytes survive, whole, under the trash directory beside the pack.
|
||||
local retired
|
||||
retired="$(find "$PACKS/.AGENTTRASH" -name reference.wav | head -n1)"
|
||||
[ -n "$retired" ]
|
||||
cmp "$retired" "$TEST_TMP/first-reference.wav"
|
||||
[ -f "$PACKS/narrator1/reference.wav" ]
|
||||
}
|
||||
|
||||
@test "--dry-run needs no extractor at all" {
|
||||
# Everything up to the engine: the gate, the crop, the probe, the plan and
|
||||
# the manifest it would write — with the tool path pointed at nothing.
|
||||
run "$BUILDER" --env "$TEST_TMP/env" --tool "$TEST_TMP/no-such-tool" \
|
||||
--source "$TEST_TMP/source.wav" --sha256 "$SOURCE_SHA" \
|
||||
--offset 12 --duration 20 --dry-run narrator1 "$PACKS"
|
||||
[ "$status" -eq 0 ] || { echo "$output"; false; }
|
||||
[[ "$output" == *"not present"* ]]
|
||||
[ ! -e "$PACKS/narrator1" ]
|
||||
[ ! -s "$STUB_ARGV" ]
|
||||
printf '%s\n' "$output" | awk '/^MANIFEST_BEGIN$/{p=1;next} /^MANIFEST_END$/{p=0} p' \
|
||||
| jq -e '.voice == "narrator1" and .crop.offset_seconds == 12 and .artifacts.produced == false'
|
||||
}
|
||||
|
||||
@test "a name the façade could not resolve is refused" {
|
||||
# app.py matches the directory name verbatim while the engine lowercases
|
||||
# names, and a slash would escape $VOICES_DIR: both are refused here, at the
|
||||
# only moment anyone can still fix them.
|
||||
for bad in 'Narrator1' '../evil' 'voice name'; do
|
||||
run "$BUILDER" --env "$TEST_TMP/env" --source "$TEST_TMP/source.wav" \
|
||||
--offset 12 --duration 20 "$bad" "$PACKS"
|
||||
[ "$status" -ne 0 ] || { echo "accepted '$bad': $output"; false; }
|
||||
[[ "$output" == *"voice name"* ]]
|
||||
done
|
||||
[ ! -e "$PACKS/Narrator1" ]
|
||||
[ ! -e "$PACKS/evil" ]
|
||||
}
|
||||
|
||||
@test "every crop in the tracked pack list clears the invariant" {
|
||||
# The law applied to the values the repo actually ships: no pack may be
|
||||
# described by an offset inside the forbidden region, a length outside the
|
||||
# reference window, or a source with no digest to verify it against.
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
set +a
|
||||
local checked=0 pack upper
|
||||
read -r -a packs <<<"$VOICE_PACKS"
|
||||
for pack in "${packs[@]}"; do
|
||||
upper="$(printf '%s' "$pack" | tr '[:lower:]' '[:upper:]')"
|
||||
local offset_var="VOICE_PACK_${upper}_OFFSET" duration_var="VOICE_PACK_${upper}_DURATION"
|
||||
local sha_var="VOICE_PACK_${upper}_SHA256" source_var="VOICE_PACK_${upper}_SOURCE"
|
||||
[ -n "${!source_var:-}" ] || { echo "$pack has no source path"; false; }
|
||||
printf '%s' "${!sha_var}" | grep -Eq '^[0-9a-f]{64}$' || { echo "$pack has no pinned sha256"; false; }
|
||||
awk -v o="${!offset_var}" 'BEGIN { exit !(o >= 10) }' || { echo "$pack crops at ${!offset_var} s, inside the forbidden first 10 s"; false; }
|
||||
awk -v d="${!duration_var}" 'BEGIN { exit !(d >= 18 && d <= 25) }' || { echo "$pack crops ${!duration_var} s, outside 18-25 s"; false; }
|
||||
checked=$((checked + 1))
|
||||
done
|
||||
[ "$checked" -gt 0 ]
|
||||
}
|
||||
Reference in New Issue
Block a user