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:
@@ -165,8 +165,14 @@ def transcribe(wav_bytes: bytes) -> dict:
|
||||
return {"text": text, "language": getattr(info, "language", None)}
|
||||
|
||||
|
||||
def think(text: str, language: Optional[str] = None) -> dict:
|
||||
"""Run the Jarvis reply engine on a piece of text."""
|
||||
def think(text: str, language: Optional[str] = None, broadcasting: Optional[bool] = None) -> dict:
|
||||
"""Run the Jarvis reply engine on a piece of text.
|
||||
|
||||
``broadcasting`` is the bot's live screen-share state for this turn; it is
|
||||
stashed in request-scoped state so the search routing can pick Chrome vs
|
||||
Gemini. If the reply engine calls the setBroadcast tool, the recorded
|
||||
directive is returned as ``broadcast_action`` for the bot to act on.
|
||||
"""
|
||||
if not BRAIN_ENABLED:
|
||||
return {"reply": text, "error": "brain disabled (JARVIS_BRAIN_ENABLED=0)"}
|
||||
_ensure_brain()
|
||||
@@ -174,6 +180,10 @@ def think(text: str, language: Optional[str] = None) -> dict:
|
||||
return {"reply": "", "error": _brain_error or "brain unavailable"}
|
||||
try:
|
||||
from jarvis.reply.engine import run_reply_engine
|
||||
from jarvis.reply import turn_state
|
||||
|
||||
turn_state.reset()
|
||||
turn_state.set_broadcasting(broadcasting)
|
||||
|
||||
# tts=None: we do our own Discord-side synthesis, the engine must not
|
||||
# try to speak to a local speaker that doesn't exist in this process.
|
||||
@@ -183,11 +193,20 @@ def think(text: str, language: Optional[str] = None) -> dict:
|
||||
reply = (reply or "").strip()
|
||||
if reply:
|
||||
_dialogue_memory.add_interaction(text, reply)
|
||||
return {"reply": reply}
|
||||
return {"reply": reply, "broadcast_action": turn_state.get_broadcast_action()}
|
||||
except Exception as e: # pragma: no cover
|
||||
return {"reply": "", "error": f"{type(e).__name__}: {e}"}
|
||||
|
||||
|
||||
def _coerce_bool(value) -> Optional[bool]:
|
||||
"""Parse a broadcasting flag from JSON (bool) or a query string."""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
return str(value).strip().lower() in ("1", "true", "yes", "on")
|
||||
|
||||
|
||||
def synthesize(text: str) -> Optional[bytes]:
|
||||
"""Synthesize text to a 16-bit PCM WAV using Piper. Returns None if TTS off."""
|
||||
if not TTS_ENABLED or not text.strip():
|
||||
@@ -234,7 +253,7 @@ def http_text():
|
||||
text = (data.get("text") or "").strip()
|
||||
if not text:
|
||||
return jsonify({"error": "missing 'text'"}), 400
|
||||
result = think(text, data.get("language"))
|
||||
result = think(text, data.get("language"), _coerce_bool(data.get("broadcasting")))
|
||||
audio = synthesize(result.get("reply", ""))
|
||||
if audio:
|
||||
result["audio_b64"] = base64.b64encode(audio).decode("ascii")
|
||||
@@ -263,7 +282,8 @@ def http_converse():
|
||||
transcript = stt.get("text", "")
|
||||
if not transcript:
|
||||
return jsonify({"transcript": "", "reply": "", "audio_b64": None})
|
||||
result = think(transcript, stt.get("language"))
|
||||
broadcasting = _coerce_bool(request.args.get("broadcasting"))
|
||||
result = think(transcript, stt.get("language"), broadcasting)
|
||||
audio = synthesize(result.get("reply", ""))
|
||||
return jsonify(
|
||||
{
|
||||
@@ -271,6 +291,7 @@ def http_converse():
|
||||
"language": stt.get("language"),
|
||||
"reply": result.get("reply", ""),
|
||||
"error": result.get("error"),
|
||||
"broadcast_action": result.get("broadcast_action"),
|
||||
"audio_b64": base64.b64encode(audio).decode("ascii") if audio else None,
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user