bastion 0.53.2
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m4s
ci / fuzz (push) Successful in 3m45s

This commit is contained in:
Privatae
2026-10-20 04:42:14 +00:00
commit e4c9b69b46
471 changed files with 159794 additions and 0 deletions
+916
View File
@@ -0,0 +1,916 @@
//go:build !windows
// Tagged to match anchor_test.go, where setupBox and the pairing fixtures live.
// contextstt.go itself is platform-neutral and builds everywhere; only these
// gates borrow the anchored-box fixture.
package main
// Gates for speech to text (contextstt.go).
//
// THE BAR THESE MEET: real whisper-cli, real audio with known words in it, over
// a real ctx_extract management frame, on the PULL transport — which is the one
// that matters here, because 8 MiB of inline frame is 4.4 minutes of 16 kHz PCM
// and a recording worth transcribing is usually longer than that. A test of the
// argv builder would prove nothing about any of it.
//
// THE FIXTURE. The audio is named by PRIVATAE_STT_TEST_AUDIO (a wav with speech
// in it) and PRIVATAE_STT_TEST_WORDS (a comma-separated list of words that are
// actually said in it). It is not committed: a speech clip long enough to be a
// real test is hundreds of kilobytes, and the point of these checks is the real
// engine reading real words rather than a fixture round-tripping through a
// parser. The unit gates below run everywhere and need neither.
import (
"encoding/binary"
"encoding/json"
"fmt"
"math"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"testing"
"time"
)
// sttFixture is the wav and the words in it, or a skip.
func sttFixture(t *testing.T) (string, []string) {
t.Helper()
path := strings.TrimSpace(os.Getenv("PRIVATAE_STT_TEST_AUDIO"))
if path == "" {
t.Skip("PRIVATAE_STT_TEST_AUDIO is not set — no speech fixture to read")
}
if _, err := os.Stat(path); err != nil {
t.Skipf("PRIVATAE_STT_TEST_AUDIO %q is not readable: %v", path, err)
}
words := []string{}
for _, w := range strings.Split(os.Getenv("PRIVATAE_STT_TEST_WORDS"), ",") {
if w = strings.TrimSpace(w); w != "" {
words = append(words, w)
}
}
if len(words) == 0 {
t.Skip("PRIVATAE_STT_TEST_WORDS is not set — a transcript with nothing known " +
"to check against is not a test of transcription")
}
return path, words
}
// requireEngine skips unless this machine can actually transcribe. The reason is
// carried through so a skip says which of the two halves is missing — the engine
// or a model — which is the same distinction the capability itself makes.
func requireEngine(t *testing.T) {
t.Helper()
if _, err := exec.LookPath(sttBin); err != nil {
t.Skipf("%s is not on PATH", sttBin)
}
if ok, why := sttProve(); !ok {
t.Skipf("this machine cannot transcribe: %s", why)
}
}
// decodeSTT reads a successful reply.
func decodeSTT(t *testing.T, out, errOut string, code int) ctxExtractResp {
t.Helper()
if code != 0 {
t.Fatalf("ctx_extract refused the job: %s", strings.TrimSpace(errOut))
}
var resp ctxExtractResp
if err := json.Unmarshal([]byte(out), &resp); err != nil {
t.Fatalf("reply is not the shape this lane returns: %v (%q)", err, out)
}
return resp
}
// ── THE ONE THAT MATTERS: real audio, real engine, over the pull transport ──
func TestTranscribeRealAudioOverThePull(t *testing.T) {
setupBox(t)
requireEngine(t)
wav, words := sttFixture(t)
resetPrepCache()
doc, err := os.ReadFile(wav)
if err != nil {
t.Fatal(err)
}
h, hostName := startFakeHost(t, "bearer-abc")
_ = hostName
ticket := h.issue(doc)
req, err := json.Marshal(ctxExtractReq{
Tool: sttTool, Mime: "audio/wav", Ticket: ticket,
Size: int64(len(doc)), MaxBytes: ctxPullMaxBytes,
PullBudget: 60,
ExtractBudget: sttCeiling().Seconds(),
})
if err != nil {
t.Fatal(err)
}
out, errOut, code, root := driveExtractIn(t, tAnchor, []string{string(req)})
resp := decodeSTT(t, out, errOut, code)
if resp.Source != "pull" {
t.Fatalf("the job went over %q, not the pull transport this test exists to "+
"exercise", resp.Source)
}
low := strings.ToLower(resp.Text)
for _, w := range words {
if !strings.Contains(low, strings.ToLower(w)) {
t.Fatalf("the transcript is missing %q, a word that is said in this "+
"recording — got: %q", w, firstLine(resp.Text))
}
}
// THE MODEL IS ON THE RECORD. Without it a transcript cannot be judged
// afterwards: base.en and large-v3-turbo are 3x apart in error rate on hard
// audio and the words look identical either way.
if resp.Model == "" {
t.Fatal("the reply does not say which model read this recording — the host " +
"writes that onto every row, and a transcript with no model on it claims " +
"an accuracy nobody can check")
}
if resp.Model != sttFindModels()[0].Name {
t.Fatalf("a job naming no model ran %q, not this box's best %q",
resp.Model, sttFindModels()[0].Name)
}
// THE TIMESTAMPS ARE THE FEATURE. A transcript without them is a wall of
// text; with them a search result is a moment in the recording.
if len(resp.Segments) == 0 {
t.Fatal("no segments — there is nothing to file as media_offset_ms, so a " +
"result could never be jumped to")
}
var last int64 = -1
for i, s := range resp.Segments {
if s.T0 < last {
t.Fatalf("segment %d starts at %dms, before segment %d ended at %dms — "+
"offsets that go backwards would file the wrong moment", i, s.T0, i-1, last)
}
if s.T1 < s.T0 {
t.Fatalf("segment %d ends before it starts (%d..%d)", i, s.T0, s.T1)
}
last = s.T1
}
if resp.DoneMS != resp.Segments[len(resp.Segments)-1].T1 {
t.Fatalf("transcribed_ms is %d but the last segment ends at %d — the number "+
"the user is shown must be the one the engine actually reached",
resp.DoneMS, resp.Segments[len(resp.Segments)-1].T1)
}
// The fixture is a wav, so its own header states the duration exactly and the
// box must have read it rather than leaving it unknown.
if resp.AudioMS <= 0 {
t.Fatal("audio_ms is unset for a wav — the RIFF header states the length, " +
"so 'how much of your recording is in the corpus' is answerable and was " +
"not answered")
}
if resp.Blank {
t.Fatal("a recording with words in it came back flagged as silence")
}
// NOTHING OF THE USER'S IS LEFT BEHIND, checked after the terminal frame the
// way the rest of this lane checks it.
leftovers, _ := os.ReadDir(root)
for _, e := range leftovers {
t.Fatalf("the user's recording is still on their disk after the job "+
"reported done: %s", e.Name())
}
}
// ── THE TRAP: the engine exits 0 on audio it could not read ─────────────────
//
// THIS IS THE RED-FIRST CHECK. whisper-cli prints "error: failed to read audio
// file" to STDERR and EXITS ZERO on an mp4 (measured against the real binary).
// A runner that believed the exit code returns a successful job with no text in
// it, and the host — which has three carefully separated sentences for an empty
// answer — picks the wrong one and tells the user their recording is silence.
//
// The user's fix for "this is silence" is nothing. The user's fix for this is to
// install ffmpeg. Getting it wrong sends them nowhere.
func TestAContainerRefusedByTheEngineIsNotSilence(t *testing.T) {
setupBox(t)
requireEngine(t)
wav, _ := sttFixture(t)
if _, err := exec.LookPath("ffmpeg"); err != nil {
t.Skip("ffmpeg is not installed — no way to build a container to refuse")
}
resetPrepCache()
mp4 := filepath.Join(t.TempDir(), "clip.mp4")
if err := exec.Command("ffmpeg", "-nostdin", "-v", "error", "-y",
"-i", wav, "-c:a", "aac", mp4).Run(); err != nil {
t.Fatalf("could not build the mp4 fixture: %v", err)
}
doc, err := os.ReadFile(mp4)
if err != nil {
t.Fatal(err)
}
h, _ := startFakeHost(t, "bearer-abc")
// The PLAIN transcribe job, pointed at a container it cannot read.
req, err := json.Marshal(ctxExtractReq{
Tool: sttTool, Mime: "video/mp4", Ticket: h.issue(doc),
Size: int64(len(doc)), MaxBytes: ctxPullMaxBytes,
PullBudget: 60, ExtractBudget: sttCeiling().Seconds(),
})
if err != nil {
t.Fatal(err)
}
out, errOut, code := driveExtract(t, tAnchor, []string{string(req)})
if code == 0 {
var resp ctxExtractResp
_ = json.Unmarshal([]byte(out), &resp)
t.Fatalf("a recording the engine COULD NOT DECODE came back as a successful "+
"job (text=%q blank=%v). whisper-cli exits 0 on a file it failed to read, "+
"so this reply is an empty transcript presented as a finished one — the "+
"host will tell the user their recording is silent and they will fix "+
"nothing", resp.Text, resp.Blank)
}
low := strings.ToLower(errOut)
if !strings.Contains(low, "ffmpeg") {
t.Fatalf("the refusal does not name ffmpeg, which is the thing that would "+
"make this work: %q", strings.TrimSpace(errOut))
}
}
// ── AND THE SAME CONTAINER, ON THE JOB THAT HAS ffmpeg ──────────────────────
//
// Transcribing an mp4 DOES require ffmpeg. This is the half that proves the
// other half of that sentence: with ffmpeg present, av-transcribe gets the same
// words out of the container that transcribe gets out of the wav.
func TestAnMP4IsTranscribedWhenFFmpegIsThere(t *testing.T) {
setupBox(t)
requireEngine(t)
wav, words := sttFixture(t)
if _, err := exec.LookPath("ffmpeg"); err != nil {
t.Skip("ffmpeg is not installed — av-transcribe is correctly unavailable here")
}
resetPrepCache()
mp4 := filepath.Join(t.TempDir(), "clip.mp4")
if err := exec.Command("ffmpeg", "-nostdin", "-v", "error", "-y",
"-i", wav, "-c:a", "aac", mp4).Run(); err != nil {
t.Fatalf("could not build the mp4 fixture: %v", err)
}
doc, err := os.ReadFile(mp4)
if err != nil {
t.Fatal(err)
}
h, _ := startFakeHost(t, "bearer-abc")
req, err := json.Marshal(ctxExtractReq{
Tool: sttAVTool, Mime: "video/mp4", Ticket: h.issue(doc),
Size: int64(len(doc)), MaxBytes: ctxPullMaxBytes,
PullBudget: 60, ExtractBudget: sttCeiling().Seconds(),
})
if err != nil {
t.Fatal(err)
}
out, errOut, code, root := driveExtractIn(t, tAnchor, []string{string(req)})
resp := decodeSTT(t, out, errOut, code)
if !resp.Demuxed {
t.Fatal("the reply does not record that ffmpeg unpacked the audio track — " +
"'this needed ffmpeg' has to be visible in the record of the job that " +
"needed it")
}
low := strings.ToLower(resp.Text)
for _, w := range words {
if !strings.Contains(low, strings.ToLower(w)) {
t.Fatalf("the mp4's transcript is missing %q: %q", w, firstLine(resp.Text))
}
}
if resp.AudioMS <= 0 {
t.Fatal("the demux produced a wav and its duration was still not read")
}
// The demux writes a second file of the user's audio into the pairing root.
// It has to be gone too, and by the same deadline.
leftovers, _ := os.ReadDir(root)
for _, e := range leftovers {
t.Fatalf("the unpacked audio track is still on the user's disk after the job "+
"reported done: %s", e.Name())
}
}
// ── THE THIRD ANSWER: silence is a finished job, not a failure ──────────────
func TestSilenceIsAFinishedJobAndSaysSo(t *testing.T) {
setupBox(t)
requireEngine(t)
resetPrepCache()
h, _ := startFakeHost(t, "bearer-abc")
doc := sttProbeWAV()
req, err := json.Marshal(ctxExtractReq{
Tool: sttTool, Mime: "audio/wav", Ticket: h.issue(doc),
Size: int64(len(doc)), MaxBytes: ctxPullMaxBytes,
PullBudget: 60, ExtractBudget: sttCeiling().Seconds(),
})
if err != nil {
t.Fatal(err)
}
out, errOut, code := driveExtract(t, tAnchor, []string{string(req)})
resp := decodeSTT(t, out, errOut, code)
// THE ASSERTION IS DELIBERATELY THE WEAK ONE, and the reason is a measurement
// rather than a compromise. On the same one second of digital silence,
// tiny.en returns [BLANK_AUDIO] and base.en returns the word "You" — Whisper's
// documented habit of inventing short filler where there is nothing. No
// whisper-cli flag fixes it without losing the signal: `-sns` suppresses the
// non-speech tokens and returns NOTHING AT ALL, which makes "silence"
// indistinguishable from "the engine produced no output", collapsing two of
// the three answers this lane exists to keep apart.
//
// So what is pinned is what is true of every model: silence is a FINISHED job
// that names its model, and it never comes back as a wall of invented text.
// Which of the two it is depends on the model, and that is the model's
// behaviour, not this code's.
if !resp.Blank && len(resp.Text) > 40 {
t.Fatalf("one second of digital silence produced %d characters: %q — that is "+
"invented text going into the user's corpus, not a transcript",
len(resp.Text), resp.Text)
}
if resp.Model == "" {
t.Fatal("even a silent recording must say which model read it")
}
}
// ── the model is CHOSEN, never substituted ──────────────────────────────────
func TestANamedModelIsUsedOrRefusedWithTheList(t *testing.T) {
requireEngine(t)
models := sttFindModels()
if len(models) == 0 {
t.Skip("no models on this machine")
}
got, err := sttPickModel(models[len(models)-1].Name)
if err != nil {
t.Fatalf("a model this box holds was refused: %v", err)
}
if got.Name != models[len(models)-1].Name {
t.Fatalf("asked for %q and got %q", models[len(models)-1].Name, got.Name)
}
// THE SUBSTITUTION THAT MUST NOT HAPPEN. Quietly running a different model
// would put a number on the record the run never earned.
_, err = sttPickModel("definitely-not-a-model-9f3c")
if err == nil {
t.Fatal("a model this machine does not have was accepted — the job must " +
"refuse rather than silently read the recording with something else")
}
for _, m := range models {
if !strings.Contains(err.Error(), m.Name) {
t.Fatalf("the refusal does not list %q, so the caller cannot tell what "+
"to ask for instead: %v", m.Name, err)
}
}
}
// ── the declaration is the enforcement ──────────────────────────────────────
//
// The ceiling for this job is the only one in ctxExtractTimeoutFor that is not a
// literal — it is measured on the machine, because an hour of audio is 75
// seconds of work at base.en and fifteen minutes at large-v3-turbo. That makes
// it the one most able to drift from what is advertised, so it is pinned.
func TestTranscribeCeilingIsBoundedAndStable(t *testing.T) {
for _, tool := range []string{sttTool, sttAVTool} {
got := ctxExtractTimeoutFor(tool)
if got < sttMinCeiling {
t.Fatalf("%s declares %s, under the floor %s", tool, got, sttMinCeiling)
}
if got > sttMaxCeiling {
t.Fatalf("%s declares %s, over the guard %s — the host clamps it anyway, "+
"but a box that declares more than it will be granted is a box whose "+
"advertisement is not its behaviour", tool, got, sttMaxCeiling)
}
// DECLARED == ENFORCED, across the float round trip the wire makes. The
// advertisement is a float64 of seconds and the grant comes back the same
// way; a ceiling with sub-second precision would survive neither.
if got != got.Round(time.Second) {
t.Fatalf("%s declares %s, which is not a whole number of seconds — it "+
"cannot survive the round trip through ceiling_s and extract_budget",
tool, got)
}
if again := ctxExtractTimeoutFor(tool); again != got {
t.Fatalf("%s declared %s and then %s — the gate that asserts the "+
"advertisement equals the enforcement compares two separate calls",
tool, got, again)
}
}
}
// ── reading what the engine printed ─────────────────────────────────────────
func TestSTTParseReadsTheEnginesOwnFormat(t *testing.T) {
// Copied from a real run, byte for byte.
const out = `
[00:00:00.000 --> 00:00:07.940] And so my fellow Americans ask not what your country can do for you
[00:00:07.940 --> 00:01:10.760] ask what you can do for your country.
whisper_print_timings: something the engine said about itself
`
segs, blank, _ := sttParse(out)
if blank {
t.Fatal("a transcript with words in it was read as silence")
}
if len(segs) != 2 {
t.Fatalf("want 2 segments, got %d: %+v", len(segs), segs)
}
if segs[0].T0 != 0 || segs[0].T1 != 7940 {
t.Fatalf("first segment spans %d..%d, want 0..7940", segs[0].T0, segs[0].T1)
}
if segs[1].T1 != 70760 {
t.Fatalf("second segment ends at %d, want 70760 (1m10.760s)", segs[1].T1)
}
// A LINE THAT IS NOT A SEGMENT IS NOT TRANSCRIPT. The engine writes its own
// remarks to stdout under some builds, and a parser that took an unrecognised
// line as text would put the engine's words into the user's corpus.
if strings.Contains(sttText(segs), "whisper_print_timings") {
t.Fatal("the engine's own output about itself ended up in the transcript")
}
}
func TestBlankAudioIsSilenceAndNeverText(t *testing.T) {
segs, blank, nonSpeech := sttParse("[00:00:00.000 --> 00:00:10.000] [BLANK_AUDIO]\n")
if nonSpeech {
t.Fatal("silence was reported as a sound the engine could not name — [BLANK_AUDIO] is bracket-wrapped like every annotation, so the specific answer has to be taken before the general one")
}
if !blank {
t.Fatal("the engine's own silence token was not read as silence")
}
if len(segs) != 1 {
t.Fatalf("the silent stretch was dropped entirely: %+v", segs)
}
if got := sttText(segs); got != "" {
t.Fatalf("[BLANK_AUDIO] reached the user's corpus as %q", got)
}
}
func TestSTTClockRejectsWhatIsNotATimestamp(t *testing.T) {
for _, s := range []string{"00:00:01.500", " 01:02:03.004 "} {
if _, ok := sttClock(s); !ok {
t.Fatalf("a real timestamp %q was rejected", s)
}
}
for _, s := range []string{"", "nope", "1:2", "aa:bb:cc.ddd", "00:00"} {
if _, ok := sttClock(s); ok {
t.Fatalf("%q was read as a timestamp", s)
}
}
got, _ := sttClock("01:02:03.004")
if got != 3723004 {
t.Fatalf("01:02:03.004 came to %dms, want 3723004", got)
}
}
// ── the wav header, which is where an unknown duration stops being unknown ──
func TestWavDurationIsReadFromTheHeader(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "one.wav")
if err := os.WriteFile(p, sttProbeWAV(), 0o600); err != nil {
t.Fatal(err)
}
ms, ok := wavDurationMS(p)
if !ok {
t.Fatal("a wav this file wrote could not be read back")
}
if ms != 1000 {
t.Fatalf("one second of 16 kHz mono came back as %dms", ms)
}
// NOT A WAV IS NOT A DURATION. Reporting 0 as "zero seconds" rather than as
// "unknown" would tell a user their whole recording was transcribed when
// nothing was known about its length at all.
q := filepath.Join(dir, "not.wav")
if err := os.WriteFile(q, []byte("this is not a RIFF file"), 0o600); err != nil {
t.Fatal(err)
}
if _, ok := wavDurationMS(q); ok {
t.Fatal("a non-wav reported a duration")
}
}
// ── the text cap must not leave offsets pointing past the text ──────────────
func TestSegmentsAreTrimmedWithTheText(t *testing.T) {
segs := []sttSegment{
{T0: 0, T1: 1000, Text: "aaaa"},
{T0: 1000, T1: 2000, Text: "bbbb"},
{T0: 2000, T1: 3000, Text: "cccc"},
}
// Room for "aaaa\nbbbb" and not for the third.
got := sttSegmentsWithin(segs, 9)
if len(got) != 2 {
t.Fatalf("want 2 segments inside a 9-char cap, got %d: %+v", len(got), got)
}
if len(sttSegmentsWithin(segs, 0)) != 0 {
t.Fatal("a zero cap kept segments, which would give the host offsets for " +
"words it did not receive")
}
}
// ── model ranking ───────────────────────────────────────────────────────────
func TestModelsRankBestFirst(t *testing.T) {
names := []string{"tiny.en", "base.en", "small.en", "large-v3-turbo", "medium.en"}
best := ""
bestRank := -1
for _, n := range names {
if r := sttRank(n); r > bestRank {
bestRank, best = r, n
}
}
if best != "large-v3-turbo" {
t.Fatalf("the best-ranked model is %q, not large-v3-turbo — which is the "+
"one the benchmark put at 3.84%% on hard audio against base.en's 12.55%%",
best)
}
// A quantization ranks with its parent, so a box holding only base.en-q5_1
// does not fall to the bottom of the order behind a tiny.
if sttRank("base.en-q5_1") != sttRank("base.en") {
t.Fatal("a quantized base ranks apart from base — it is the same model class")
}
if sttRank("something-nobody-has-heard-of") != 0 {
t.Fatal("an unknown model outranked a measured one")
}
}
// ── the prep advertisement carries the models ───────────────────────────────
func TestTranscribeAdvertisesItsModels(t *testing.T) {
setupBox(t)
requireEngine(t)
resetPrepCache()
defer resetPrepCache()
tools, _, _ := prepCapable(tAnchor)
found := false
for _, tool := range tools {
if tool.Name != sttTool {
continue
}
found = true
if len(tool.Models) == 0 {
t.Fatal("transcribe is advertised with no models — 'this box has a speech " +
"engine' is not an answer for a recording, because which model reads " +
"it decides how accurate the transcript is")
}
if tool.CeilingS <= 0 {
t.Fatal("transcribe is advertised with no ceiling")
}
}
if !found {
t.Fatal("this machine can transcribe and did not advertise it")
}
}
// The blocked sentence is what a user in the commonest state actually reads:
// engine installed, no model, because the distribution package ships none.
func TestNoModelNamesWhereToPutOne(t *testing.T) {
why := sttNoModelReason()
if !strings.Contains(why, sttModelEnv) {
t.Fatalf("the reason does not name the override %s: %q", sttModelEnv, why)
}
dirs := sttModelDirs()
if len(dirs) == 0 {
t.Fatal("no directories are searched for a model at all")
}
for _, d := range dirs {
if strings.Contains(d, string(os.PathSeparator)) && !strings.Contains(why, d) {
t.Fatalf("the reason does not name %q, one of the directories that was "+
"actually searched — a user told 'no model' with no list has nowhere "+
"to put one: %q", d, why)
}
}
_ = fmt.Sprint()
}
// ── the engine's annotations are not the user's words ──────────────────────
// sttMusicWAV is ten seconds of a plain synthesised melody — four harmonics per
// note with a decaying envelope, the cheapest thing that is unmistakably music
// and unmistakably not speech. Built here for the same reason sttProbeWAV is:
// nobody should inherit a fixture they cannot regenerate.
func sttMusicWAV() []byte {
const rate = 16000
notes := []float64{261.63, 329.63, 392.00, 523.25, 392.00,
329.63, 261.63, 196.00, 261.63, 329.63}
data := make([]byte, 0, rate*2*len(notes))
for _, f := range notes {
for k := 0; k < rate; k++ {
tt := float64(k) / rate
env := math.Exp(-tt * 1.5)
if tt*20 < 1 {
env *= tt * 20
}
v := 0.0
for h, a := range map[int]float64{1: 1.0, 2: 0.5, 3: 0.25, 4: 0.12} {
v += a * math.Sin(2*math.Pi*f*float64(h)*tt)
}
v *= env * 0.18
if v > 1 {
v = 1
} else if v < -1 {
v = -1
}
data = binary.LittleEndian.AppendUint16(data, uint16(int16(v*32767)))
}
}
b := make([]byte, 0, 44+len(data))
b = append(b, "RIFF"...)
b = binary.LittleEndian.AppendUint32(b, uint32(36+len(data)))
b = append(b, "WAVEfmt "...)
b = binary.LittleEndian.AppendUint32(b, 16)
b = binary.LittleEndian.AppendUint16(b, 1)
b = binary.LittleEndian.AppendUint16(b, 1)
b = binary.LittleEndian.AppendUint32(b, rate)
b = binary.LittleEndian.AppendUint32(b, rate*2)
b = binary.LittleEndian.AppendUint16(b, 2)
b = binary.LittleEndian.AppendUint16(b, 16)
b = append(b, "data"...)
b = binary.LittleEndian.AppendUint32(b, uint32(len(data)))
return append(b, data...)
}
// THE MEASUREMENT THAT DECIDES THE SHAPE OF THE FIX, pinned so it cannot quietly
// stop being true. Every line below came out of the real whisper-cli
// on real synthesised audio:
//
// ten seconds of a melody base.en -> "[Music]"
// the same ten seconds tiny.en -> "(music)"
// eight seconds of a 440 Hz tone tiny.en -> "(phone ringing)"
// eight seconds of white noise base.en -> "(machine whirring)"
//
// Four spellings, two bracket styles, two cases, from two models on four inputs.
// A real song produced "[♪♪♪]". So the vocabulary is
// open-ended and a word list is a list of whatever somebody happened to hit —
// which is why the fix is the SHAPE and this test pins the shape.
func TestTheEnginesNonSpeechAnnotationsAreNeverTranscript(t *testing.T) {
observed := []string{"[Music]", "(music)", "(phone ringing)", "(machine whirring)",
"[♪♪♪]", "(applause)", "[MUSIC PLAYING]"}
for _, tok := range observed {
segs, blank, nonSpeech := sttParse(
"[00:00:00.000 --> 00:00:10.000] " + tok + "\n")
if got := sttText(segs); got != "" {
t.Fatalf("the engine's annotation %s reached the user's corpus as %q — "+
"those are the engine's words about a sound, never words anybody said, "+
"and a song filed as text is a false hit in every search that matches it",
tok, got)
}
if len(segs) != 1 {
t.Fatalf("%s was dropped entirely (%d segments) — it has to stay a "+
"timestamped segment, or \"the engine listened and there was no speech\" "+
"becomes indistinguishable from \"the engine returned nothing\"",
tok, len(segs))
}
if !nonSpeech {
t.Fatalf("%s was not reported as a sound the engine named", tok)
}
if blank {
t.Fatalf("%s was reported as SILENCE — it is not. Telling a user their "+
"music upload is silent is a false statement about their own file",
tok)
}
}
}
// THE THREE ANSWERS SURVIVE THE FIX, which is the constraint that ruled out
// --suppress-nst. Silence, a named sound and real speech have to stay three
// different replies; a fix that made any two of them look alike would have
// traded one wrong answer for a worse one.
func TestSilenceAndANamedSoundAndSpeechStayThreeAnswers(t *testing.T) {
cases := []struct {
name string
line string
blank, nonSpeech bool
text string
}{
{"silence", "[BLANK_AUDIO]", true, false, ""},
{"a named sound", "[Music]", false, true, ""},
{"speech", "ask what you can do for your country.", false, false,
"ask what you can do for your country."},
}
for _, c := range cases {
segs, blank, nonSpeech := sttParse("[00:00:00.000 --> 00:00:10.000] " + c.line + "\n")
if blank != c.blank || nonSpeech != c.nonSpeech || sttText(segs) != c.text {
t.Fatalf("%s came back blank=%v nonSpeech=%v text=%q; want %v/%v/%q",
c.name, blank, nonSpeech, sttText(segs), c.blank, c.nonSpeech, c.text)
}
}
// AND MUSIC BEFORE A MEETING IS A MEETING. Neither flag is set when real
// words were found; the transcript is the words and the annotation is gone.
segs, blank, nonSpeech := sttParse(
"[00:00:00.000 --> 00:00:05.000] [Music]\n" +
"[00:00:05.000 --> 00:00:09.000] right, shall we start\n")
if blank || nonSpeech {
t.Fatalf("a recording with speech in it was flagged blank=%v nonSpeech=%v",
blank, nonSpeech)
}
if got := sttText(segs); got != "right, shall we start" {
t.Fatalf("transcript is %q — the annotation was kept or the speech was lost", got)
}
}
// SPEECH THAT MERELY CONTAINS A BRACKET IS SPEECH. The rule is the WHOLE segment
// being one wrapped group; anything wider would start deleting things people
// actually said.
func TestSpeechContainingBracketsIsKeptIntact(t *testing.T) {
for _, line := range []string{
"the rate (about four percent) is fixed",
"(yes) and (no) are both answers",
"see clause [4] of the agreement",
"(",
"()x",
} {
segs, _, nonSpeech := sttParse("[00:00:00.000 --> 00:00:02.000] " + line + "\n")
if nonSpeech || sttText(segs) != line {
t.Fatalf("%q was treated as an engine annotation and dropped — that is a "+
"word the user said", line)
}
}
}
// THE SAME THING THROUGH THE REAL ENGINE. The unit gate above pins the shape;
// this proves the shape is the one the binary on this machine actually produces.
func TestRealMusicNeverBecomesTextInTheCorpus(t *testing.T) {
requireEngine(t)
dir := t.TempDir()
wav := filepath.Join(dir, "music.wav")
if err := os.WriteFile(wav, sttMusicWAV(), 0o600); err != nil {
t.Fatal(err)
}
res, err := runTranscribe(dir, wav, sttTool, "", "", time.Now().Add(sttCeiling()))
if err != nil {
t.Fatalf("transcribing music failed: %v", err)
}
if strings.TrimSpace(res.Text) != "" {
t.Fatalf("ten seconds of music produced %q in the user's searchable corpus "+
"(model %s) — nobody said any of that", res.Text, res.Model)
}
if res.Blank {
t.Fatalf("music was reported as silence by %s — the recording is not silent, "+
"and saying it is is a false statement about the user's file", res.Model)
}
if !res.NonSpeech && len(res.Segments) > 0 {
t.Logf("note: %s produced %d segments and named no sound; nothing reached "+
"the corpus, which is the assertion that matters", res.Model, len(res.Segments))
}
}
// ── the deadline sentence a user reads ─────────────────────────────────────
// A NEGATIVE DURATION IN A SENTENCE SHOWN TO A PERSON. The message said "still
// working after %s" and filled it with time.Until(deadline) — the time REMAINING,
// evaluated after the deadline had already passed, so what reached the user was
// "after -4s". The defect was never the sign: remaining time was the wrong
// quantity for a sentence about elapsed time, and clamping the display would
// have left the reply saying something it had not measured.
func TestTheDeadlineSentenceReportsElapsedTimeNotTimeRemaining(t *testing.T) {
requireEngine(t)
dir := t.TempDir()
wav := filepath.Join(dir, "probe.wav")
if err := os.WriteFile(wav, sttProbeWAV(), 0o600); err != nil {
t.Fatal(err)
}
// Short enough that the engine is killed while the model is still loading,
// which is the only path that reaches this sentence: it needs zero segments.
// MEASURED on this box, base.en is killed inside 400 ms and the overshoot past
// the deadline is 3-84 ms across six timed-out runs — so the OLD expression
// rendered "0s" here. It is non-positive BY CONSTRUCTION at every magnitude;
// a larger overshoot renders a negative, which is what was reported.
const budget = 300 * time.Millisecond
start := time.Now()
_, err := runTranscribe(dir, wav, sttTool, "", "", time.Now().Add(budget))
ran := time.Since(start)
if err == nil {
t.Skip("this machine loaded a model and transcribed inside the budget — " +
"there is no deadline to report on")
}
msg := err.Error()
m := regexp.MustCompile(`after (-?[0-9.]+)(ms|s|m|h)`).FindStringSubmatch(msg)
if m == nil {
t.Skipf("a different failure came back, so the deadline sentence was not "+
"reached: %v", err)
}
d, perr := time.ParseDuration(m[1] + m[2])
if perr != nil {
t.Fatalf("the duration in the deadline sentence does not parse: %q", msg)
}
if strings.HasPrefix(m[1], "-") {
t.Fatalf("the user is shown a NEGATIVE duration %q in a sentence about how "+
"long something took: %s", m[1]+m[2], msg)
}
if d <= 0 {
t.Fatalf("the engine ran for %v and the sentence says \"after %s\" — the "+
"reply is reporting the time REMAINING, which is <= 0 by the time this "+
"branch is reached, rather than the time that elapsed.\nFull sentence: %s",
ran.Round(time.Millisecond), m[1]+m[2], msg)
}
if d < budget/2 {
t.Fatalf("the engine ran for %v and the sentence names %s — that is not the "+
"elapsed time it claims to be.\nFull sentence: %s",
ran.Round(time.Millisecond), m[1]+m[2], msg)
}
}
// Sub-second runs must be expressible. Rounding to whole seconds turned every
// fast failure into the literal words "after 0s", which is as unbelievable to a
// reader as a negative was, and on this box it is the common case.
func TestAFastFailureReportsAFastTimeNotZero(t *testing.T) {
if got := sttReadableDuration(311 * time.Millisecond); got.String() != "311ms" {
t.Fatalf("311ms rendered as %q", got)
}
if got := sttReadableDuration(4200 * time.Millisecond); got.String() != "4s" {
t.Fatalf("4.2s rendered as %q", got)
}
}
// ── the language is recorded, and never silently substituted ───────────────
// NOTHING ON THE ROW SAID WHICH LANGUAGE A RECORDING WAS DECODED AS. The reply
// has carried a `lang` field all along — the host writes it onto the row and the
// OCR lanes fill it — and the transcript lane left it empty, so a recording
// decoded under an English prior had no first thing to check.
func TestEveryTranscriptRecordsTheLanguageItWasDecodedIn(t *testing.T) {
requireEngine(t)
dir := t.TempDir()
wav := filepath.Join(dir, "probe.wav")
if err := os.WriteFile(wav, sttProbeWAV(), 0o600); err != nil {
t.Fatal(err)
}
res, err := runTranscribe(dir, wav, sttTool, "", "", time.Now().Add(sttCeiling()))
if err != nil {
t.Fatalf("transcribe failed: %v", err)
}
if res.Lang != sttDefaultLang {
t.Fatalf("lang=%q on a transcript run with no language named; want %q — the "+
"engine's own help states its default is [en], and a row that does not "+
"say which language it was decoded in hides its own first failure mode",
res.Lang, sttDefaultLang)
}
}
// AN ENGLISH-ONLY MODEL MUST REFUSE, NOT PRETEND. MEASURED against the real
// binary: ggml-base.en handed `-l de` prints "main: WARNING: model is not
// multilingual, ignoring language and translation options", transcribes the
// audio as ENGLISH, and exits 0. A box that passed the flag through would file an
// English transcript on a row labelled German — the same silent substitution
// sttPickModel refuses one field over.
func TestAnEnglishOnlyModelRefusesAnotherLanguage(t *testing.T) {
en := sttModel{Name: "base.en"}
if _, err := sttLang(en, "de"); err == nil {
t.Fatal("an English-only model accepted a request to transcribe German — the " +
"engine ignores the flag and hands back English, so the row would carry a " +
"language the run never used")
}
if got, err := sttLang(en, ""); err != nil || got != sttDefaultLang {
t.Fatalf("an unnamed language on an English-only model gave (%q, %v); want "+
"(%q, nil)", got, err, sttDefaultLang)
}
if got, err := sttLang(en, "EN"); err != nil || got != sttDefaultLang {
t.Fatalf("English named explicitly on an English-only model was refused: (%q, %v)",
got, err)
}
multi := sttModel{Name: "large-v3-turbo"}
for _, want := range []string{"de", "auto", "ja"} {
if got, err := sttLang(multi, want); err != nil || got != want {
t.Fatalf("a multilingual model refused %q: (%q, %v)", want, got, err)
}
}
// The quantized English-only variants carry the suffix and are still English.
for _, n := range []string{"base.en-q5_1", "small.en", "tiny.en"} {
if !sttModelIsEnglishOnly(n) {
t.Fatalf("%s was read as multilingual", n)
}
}
for _, n := range []string{"large-v3-turbo", "base", "small-q5_1"} {
if sttModelIsEnglishOnly(n) {
t.Fatalf("%s was read as English-only", n)
}
}
// And the backstop reads the engine's own sentence, which is the authority
// over this build's reading of a filename.
if !sttEnglishOnlyRefusal(
"main: WARNING: model is not multilingual, ignoring language and translation options") {
t.Fatal("the engine's own not-multilingual warning was not recognised")
}
if sttEnglishOnlyRefusal("whisper_model_load: n_langs = 100") {
t.Fatal("a multilingual model's own load line was read as a refusal")
}
}
// THE ARGV CARRIES THE LANGUAGE ONLY WHEN THERE IS ONE TO CARRY, so an ordinary
// English job runs exactly the argv it ran before this field existed.
func TestLanguageArgvIsUnchangedForAnOrdinaryEnglishJob(t *testing.T) {
base := sttArgv("/m.bin", "/in.wav", 0, sttDefaultLang)
if strings.Join(base, " ") != "-m /m.bin -f /in.wav -np" {
t.Fatalf("the default argv changed: %v", base)
}
withLang := sttArgv("/m.bin", "/in.wav", 0, "de")
if strings.Join(withLang, " ") != "-m /m.bin -f /in.wav -np -l de" {
t.Fatalf("a named language did not reach the engine: %v", withLang)
}
}