Address review of the noise/broadcast fixes: - STT now refuses to run Whisper on non-speech. transcribe() runs the Silero VAD (bundled with faster-whisper, no new dep) BEFORE the model, so noise or a brief loud blip with no real speech never reaches STT and can't be hallucinated into a transcript. The no_speech_prob/avg_logprob post-filter stays as a second line of defence (a clap the VAD lets through is still killed by Whisper's own no_speech_prob). VAD is env-tunable (VAD_THRESHOLD, VAD_MIN_SPEECH_MS, VAD_ENABLED) and fail-open so a VAD error never swallows a real utterance. Validated on real audio: synthesised Korean speech passes; silence, a 50ms blip and white noise are rejected. - Broadcast auto-start no longer blocks the voice join and no longer silently swallows failures: wiring is synchronous, the Go-Live start runs in the background with a bounded retry and a loud final-failure log. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
94 lines
2.8 KiB
TypeScript
94 lines
2.8 KiB
TypeScript
import { test, expect } from "bun:test";
|
|
|
|
// broadcast.ts pulls in the runtime `config`, which requires DISCORD_GUILD_ID.
|
|
// Set it before the dynamic import so the module loads without a real .env.
|
|
process.env.DISCORD_GUILD_ID ||= "test-guild";
|
|
const { wireBroadcast, autoStartBroadcast } = await import("./broadcast.ts");
|
|
|
|
/** Minimal in-memory ScreenStreamer that records start/stop calls. */
|
|
function fakeStreamer(active = false) {
|
|
const calls: string[] = [];
|
|
let on = active;
|
|
return {
|
|
calls,
|
|
kind: "selfbot" as const,
|
|
isActive: () => on,
|
|
async start() {
|
|
calls.push("start");
|
|
on = true;
|
|
return "ok";
|
|
},
|
|
async stop() {
|
|
calls.push("stop");
|
|
on = false;
|
|
},
|
|
};
|
|
}
|
|
|
|
const ctx = { guildId: "g", voiceChannelId: "c" };
|
|
|
|
test("wireBroadcast reports live state to the brain", () => {
|
|
const s = fakeStreamer(false);
|
|
const session: any = {};
|
|
wireBroadcast(session, s as any, ctx);
|
|
expect(session.getBroadcasting()).toBe(false);
|
|
});
|
|
|
|
test("wireBroadcast lets the brain toggle the stream by voice", async () => {
|
|
const s = fakeStreamer(false);
|
|
const session: any = {};
|
|
wireBroadcast(session, s as any, ctx);
|
|
await session.onBroadcastAction("start"); // idle -> start
|
|
expect(session.getBroadcasting()).toBe(true);
|
|
await session.onBroadcastAction("start"); // already live -> no-op
|
|
await session.onBroadcastAction("stop"); // stop
|
|
expect(session.getBroadcasting()).toBe(false);
|
|
expect(s.calls).toEqual(["start", "stop"]);
|
|
});
|
|
|
|
test("autoStartBroadcast starts the broadcast when idle", async () => {
|
|
const s = fakeStreamer(false);
|
|
const ok = await autoStartBroadcast(s as any, ctx, 2, 0);
|
|
expect(ok).toBe(true);
|
|
expect(s.calls).toEqual(["start"]);
|
|
});
|
|
|
|
test("autoStartBroadcast does not restart an already-live broadcast", async () => {
|
|
const s = fakeStreamer(true);
|
|
const ok = await autoStartBroadcast(s as any, ctx, 2, 0);
|
|
expect(ok).toBe(true);
|
|
expect(s.calls).toEqual([]);
|
|
});
|
|
|
|
test("autoStartBroadcast retries a transient failure and then succeeds", async () => {
|
|
let on = false;
|
|
const calls: string[] = [];
|
|
const s: any = {
|
|
isActive: () => on,
|
|
async start() {
|
|
calls.push("start");
|
|
if (calls.length === 1) throw new Error("transient: voice not ready");
|
|
on = true;
|
|
},
|
|
async stop() {},
|
|
};
|
|
const ok = await autoStartBroadcast(s, ctx, 2, 0);
|
|
expect(ok).toBe(true);
|
|
expect(calls).toEqual(["start", "start"]);
|
|
});
|
|
|
|
test("autoStartBroadcast returns false after exhausting retries (no throw)", async () => {
|
|
let attempts = 0;
|
|
const s: any = {
|
|
isActive: () => false,
|
|
async start() {
|
|
attempts++;
|
|
throw new Error("boom");
|
|
},
|
|
async stop() {},
|
|
};
|
|
const ok = await autoStartBroadcast(s, ctx, 2, 0);
|
|
expect(ok).toBe(false);
|
|
expect(attempts).toBe(2); // retried, then gave up without throwing
|
|
});
|