feat: couple broadcast to voice + voice-controlled broadcast toggle

Completes the STREAM_BROWSER=true behaviour:
- handleJoin auto-starts the broadcast on voice join and wires the session to
  the guild streamer; each turn reports the live state to the brain so search
  routes Chrome (live) vs Gemini (off).
- New setBroadcast tool lets the user toggle the broadcast by voice ("방송
  켜줘/꺼줘") via the LLM (no hardcoded phrases); it refuses when
  STREAM_BROWSER=false. The directive flows brain -> bridge (broadcast_action)
  -> bot streamer.start/stop, guarded by isActive() so it's idempotent.
- Per-turn IPC uses a thread-local (reply/turn_state.py) instead of threading
  params through the whole engine chain: bridge sets broadcasting in, tool
  records the directive out; Tool.execute exposes broadcasting on ToolContext.

Bot typecheck clean; brain covered by tests/test_set_broadcast.py (+ existing
routing tests). Specs + docs updated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
javis-bot
2026-06-11 10:08:30 +09:00
parent 35e754d6ee
commit ca86390407
11 changed files with 316 additions and 14 deletions

View File

@@ -71,6 +71,11 @@ export class VoiceSession {
private playQueue: Buffer[] = [];
/** Optional callback to surface transcripts/replies to a text channel. */
onTurn?: (info: { user: string; transcript: string; reply: string }) => void;
/** Live screen-share state, sent with each turn so the brain routes search
* (Chrome while broadcasting, Gemini when off). */
getBroadcasting?: () => boolean;
/** Apply a broadcast directive the brain requested (start/stop the stream). */
onBroadcastAction?: (action: "start" | "stop") => void | Promise<void>;
constructor(channel: VoiceBasedChannel) {
this.guildId = channel.guild.id;
@@ -123,10 +128,19 @@ export class VoiceSession {
const wav = pcm16MonoToWav(mono, DISCORD_RATE);
try {
const result = await converse(wav);
const result = await converse(wav, this.getBroadcasting?.());
if (result.transcript) {
this.onTurn?.({ user: userId, transcript: result.transcript, reply: result.reply });
}
// Apply any broadcast directive the brain requested (e.g. user said
// "방송 켜줘 / 꺼줘") before playing the reply.
if (result.broadcast_action && this.onBroadcastAction) {
try {
await this.onBroadcastAction(result.broadcast_action);
} catch (e) {
console.error("[voice] broadcast action failed:", e);
}
}
const audio = decodeWav(result.audio_b64);
if (audio) this.play(audio);
} catch (err) {