fix(frank-tts): make the boot warm pass survive both readers of its env
`WARM_TEXT` reaches the container **with its quotes**. The unit reads the env file through podman's `--env-file`, which takes the literal remainder of the line — no shell parsing — while bash `source` strips one layer. The hook interpolated the raw value into the synthesis body, so with the quotes intact the JSON was invalid and every voice logged "registered, but the warm synthesis failed" while a hand-run hook (bash shape) succeeded. Reproduced both ways against a real `podman run --env-file`, not by reading the man page. The hook now strips exactly one matched pair — only when both ends are the same quote AND the inner content carries another quote or a backslash, so a value that legitimately contains quotes is never mangled — and then JSON-escapes what is left, which is what makes a sentence like `He said "hi\" once"` arrive as itself instead of corrupting the body. The seam gains a test that reads `scripts/frank-tts-engine.env` BOTH ways — `source` and the way `podman --env-file` does — and asserts the engine receives the same sentence either way, in both shapes. A test that only sources the file cannot see this bug, which is how it survived the first deploy.
This commit is contained in:
@@ -394,6 +394,51 @@ assert {s["response_format"] for s in speech} == {"wav"}, speech
|
||||
PY
|
||||
}
|
||||
|
||||
@test "the warm sentence survives both readers of the env file intact" {
|
||||
# scripts/frank-tts-engine.env is read two ways and they disagree about
|
||||
# quotes. bash (`set -a; source`, what the seams and a human do) strips the
|
||||
# quotes around a value with spaces. podman's --env-file — how the unit really
|
||||
# reads it — does NOT: measured 2026-10-18 against the image's pinned base,
|
||||
# the value lands in the container WITH its quotes. Interpolated raw into the
|
||||
# hook's JSON body that is `""Warm up…"",` — the engine answers 400, no voice
|
||||
# warms, and the boot still reads healthy. So the hook unwraps one
|
||||
# shell-style layer and then escapes, and BOTH shapes must arrive as the
|
||||
# sentence the operator wrote.
|
||||
start_engine_stub
|
||||
|
||||
# Shape A: what podman delivers for `WARM_TEXT="Warm up, one two three."`.
|
||||
run env ENGINE_URL="http://127.0.0.1:$STUB_PORT" VOICES_DIR="$VOICES" \
|
||||
WARM_TEXT='"Warm up, one two three."' bash "$WARM"
|
||||
[ "$status" -eq 0 ] || { echo "$output"; false; }
|
||||
[[ "$output" == *"voice narrator1 cold="*"ms warm="*"ms"* ]] || { echo "$output"; false; }
|
||||
python3 - "$TEST_TMP/journal.jsonl" <<'PY'
|
||||
import json, sys
|
||||
entries = [json.loads(l) for l in open(sys.argv[1])]
|
||||
assert not [e for e in entries if e.get("invalid_json")], entries
|
||||
speech = [e for e in entries if e["path"] == "/v1/audio/speech"]
|
||||
assert len(speech) == 2, entries
|
||||
assert {s["input"] for s in speech} == {"Warm up, one two three."}, speech
|
||||
PY
|
||||
|
||||
# Shape B: a value that starts AND ends with a quote but carries more quotes
|
||||
# INSIDE — exactly the shape an unwrap rule must refuse, because peeling the
|
||||
# outer pair would delete characters the operator wrote. It must arrive
|
||||
# character for character (trailing quote included), which also proves the
|
||||
# escaping does not mangle what it is protecting.
|
||||
: > "$TEST_TMP/journal.jsonl"
|
||||
run env ENGINE_URL="http://127.0.0.1:$STUB_PORT" VOICES_DIR="$VOICES" \
|
||||
WARM_TEXT='"He said "hi"\ once"' bash "$WARM"
|
||||
[ "$status" -eq 0 ] || { echo "$output"; false; }
|
||||
python3 - "$TEST_TMP/journal.jsonl" <<'PY'
|
||||
import json, sys
|
||||
entries = [json.loads(l) for l in open(sys.argv[1])]
|
||||
assert not [e for e in entries if e.get("invalid_json")], entries
|
||||
speech = [e for e in entries if e["path"] == "/v1/audio/speech"]
|
||||
assert len(speech) == 2, entries
|
||||
assert {s["input"] for s in speech} == {'"He said "hi"\\ once"'}, speech
|
||||
PY
|
||||
}
|
||||
|
||||
@test "a directory without reference.wav is not a voice, and a mixed-case name is refused" {
|
||||
# `justamanifest` must be invisible; `Narrator2` must be loud. The engine
|
||||
# lowercases every voice name (tts_voice_name) while frank-tts matches the
|
||||
|
||||
Reference in New Issue
Block a user