package main import ( "context" "testing" "time" ) // fakeGate fires on demand instead of running three ONNX models. The gate's // own arithmetic is measured on real audio in docs/evals; what these tests // cover is the thing that decides whether an utterance is shipped. type fakeGate struct { fireOn int // fire when this many frames have been fed, 0 never fires fed int resets int } func (g *fakeGate) Feed(_ []int16) bool { g.fed++ return g.fireOn > 0 && g.fed == g.fireOn } func (g *fakeGate) Reset() { g.resets++ } func (g *fakeGate) Score() float64 { return 1 } // wakingSession wires a session whose gate fires on the first frame it sees. func wakingSession(fireOn int, window time.Duration) (*session, *fakePlayer, *fakeSender, *fakeGate) { sess, p, snd := newTestSession(bargeInConfig{}) g := &fakeGate{fireOn: fireOn} sess.UseWakeWord(g, window) return sess, p, snd, g } func TestKeywordlessSpeechNeverReachesSTT(t *testing.T) { sess, p, snd, g := wakingSession(0, 8*time.Second) speakThenPause(t, sess) if len(snd.sent) != 0 { t.Fatalf("sent %d utterances, want 0 — this is the whole point of V-487", len(snd.sent)) } if sess.ignored != 1 { t.Errorf("ignored = %d, want 1", sess.ignored) } if p.plays != 0 { t.Errorf("plays = %d, want 0", p.plays) } if g.fed == 0 { t.Error("the gate was never fed a frame") } } func TestKeywordOpensTheGate(t *testing.T) { sess, p, snd, _ := wakingSession(1, 8*time.Second) speakThenPause(t, sess) if len(snd.sent) != 1 { t.Fatalf("sent %d utterances, want 1", len(snd.sent)) } if sess.wakes != 1 { t.Errorf("wakes = %d, want 1", sess.wakes) } if sess.ignored != 0 { t.Errorf("ignored = %d, want 0", sess.ignored) } if p.plays != 1 { t.Errorf("plays = %d, want 1", p.plays) } } // One keyword buys one turn. Without this the microphone stays open for as // long as he keeps talking, which is the state the gate exists to end. func TestOneKeywordBuysOneTurn(t *testing.T) { sess, p, snd, _ := wakingSession(1, 8*time.Second) speakThenPause(t, sess) p.Stop() // she finished her reply sess.discard = 0 // the backlog drain is not what this measures speakThenPause(t, sess) if len(snd.sent) != 1 { t.Fatalf("sent %d utterances, want 1: the second had no keyword", len(snd.sent)) } if sess.ignored != 1 { t.Errorf("ignored = %d, want 1", sess.ignored) } } // The keyword is heard, then he says nothing for longer than the window. What // he says after that is not addressed to her. func TestTheKeywordExpires(t *testing.T) { sess, _, snd, _ := wakingSession(1, 500*time.Millisecond) now := time.Unix(1750000000, 0) sess.now = func() time.Time { return now } if err := sess.feed(context.Background(), silentBytes()); err != nil { t.Fatalf("feed: %v", err) } if sess.wakes != 1 { t.Fatalf("wakes = %d, want 1", sess.wakes) } now = now.Add(2 * time.Second) speakThenPause(t, sess) if len(snd.sent) != 0 { t.Fatalf("sent %d utterances, want 0 — the keyword had expired", len(snd.sent)) } } // Barge-in cuts her off whether or not the keyword was heard. What he says // after cutting her off still has to carry it. func TestBargeInStillInterruptsHer(t *testing.T) { sess, p, _, g := wakingSession(0, 8*time.Second) sess.barge = bargeInConfig{RMS: 0.2, Frames: 3} p.playing = true loud := frameAt(0.35) for i := 0; i < 4; i++ { if err := sess.feed(context.Background(), loud); err != nil { t.Fatalf("feed %d: %v", i, err) } } if sess.bargeIns != 1 { t.Fatalf("bargeIns = %d, want 1", sess.bargeIns) } if p.stops != 1 { t.Errorf("stops = %d, want 1", p.stops) } if g.resets == 0 { t.Error("barge-in left pre-playback audio in the gate") } } // Her own reply must not wake her. Frames captured while the player runs never // reach the gate, and the gate is cleared when playback ends. func TestHerOwnVoiceNeverReachesTheGate(t *testing.T) { sess, p, _, g := wakingSession(1, 8*time.Second) p.playing = true for i := 0; i < 10; i++ { if err := sess.feed(context.Background(), frameAt(0.35)); err != nil { t.Fatalf("feed: %v", err) } } if g.fed != 0 { t.Fatalf("gate was fed %d frames while she was speaking, want 0", g.fed) } if sess.wakes != 0 { t.Errorf("wakes = %d, want 0", sess.wakes) } } // No model, no gate: the daemon behaves exactly as it did before V-487 stage // two. An operator with a missing file gets yesterday's mavwaked, not one that // refuses to hear anything. func TestNoGateShipsEveryUtterance(t *testing.T) { sess, _, snd := newTestSession(bargeInConfig{}) speakThenPause(t, sess) if len(snd.sent) != 1 { t.Fatalf("sent %d utterances, want 1", len(snd.sent)) } if sess.ignored != 0 { t.Errorf("ignored = %d, want 0", sess.ignored) } } // A nil *wakeWord is the closed gate, not a crash and not an open one. func TestNilWakeWordHearsNothing(t *testing.T) { var w *wakeWord if w.Feed([]int16{0, 0, 0}) { t.Error("a nil wake word reported the keyword") } if w.Score() != 0 { t.Error("a nil wake word reported a score") } w.Reset() w.Close() }