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

@@ -4,11 +4,15 @@
*/
import { config } from "./config.ts";
export type BroadcastAction = "start" | "stop";
export interface ConverseResult {
transcript: string;
language?: string | null;
reply: string;
error?: string | null;
/** Broadcast directive the brain requested this turn (setBroadcast tool). */
broadcast_action?: BroadcastAction | null;
/** base64-encoded 16-bit PCM WAV of the spoken reply, or null if TTS off */
audio_b64?: string | null;
}
@@ -16,12 +20,16 @@ export interface ConverseResult {
export interface TextResult {
reply: string;
error?: string | null;
broadcast_action?: BroadcastAction | null;
audio_b64?: string | null;
}
/** Full voice turn: WAV in -> {transcript, reply, reply audio}. */
export async function converse(wav: Buffer): Promise<ConverseResult> {
const res = await fetch(`${config.bridgeUrl}/converse`, {
/** Full voice turn: WAV in -> {transcript, reply, reply audio}. ``broadcasting``
* is the current live screen-share state so the brain routes search (Chrome
* while live, Gemini when off). */
export async function converse(wav: Buffer, broadcasting?: boolean): Promise<ConverseResult> {
const qs = broadcasting === undefined ? "" : `?broadcasting=${broadcasting ? "1" : "0"}`;
const res = await fetch(`${config.bridgeUrl}/converse${qs}`, {
method: "POST",
headers: { "content-type": "audio/wav" },
body: wav,