diff --git a/dave/bot.mjs b/dave/bot.mjs
index e8e710c..6d7f994 100644
--- a/dave/bot.mjs
+++ b/dave/bot.mjs
@@ -177,6 +177,7 @@ let currentGuildId = null, currentChannelId = null, currentChannelName = null;
const speakingSet = new Set(); // userIds currently speaking (for participant list)
const activeSubs = new Set(); // userIds with an in-flight receive subscription
let listsByGuild = {}; // guildId -> {whitelistUsers, blacklistUsers, whitelistRoles, blacklistRoles}
+let botSettings = { bargeIn: true }; // behaviour toggles from the dashboard
// Attach the bot's audio player (so it can speak) to a fresh connection.
function setupPlayer(connection) {
@@ -201,6 +202,11 @@ function setupReceiver(connection) {
return;
}
}
+ // Barge-in: the moment an allowed user speaks, stop the bot's current TTS
+ // so it doesn't talk over them (toggleable from the dashboard, default on).
+ if (botSettings.bargeIn !== false && voicePlayer) {
+ try { voicePlayer.stop(true); } catch {}
+ }
activeSubs.add(userId);
if (!perUser.has(userId)) perUser.set(userId, { opusPackets: 0, pcmFrames: 0 });
const opusStream = receiver.subscribe(userId, {
@@ -314,6 +320,7 @@ async function reportLoop() {
j = await r.json();
} catch { return; } // dashboard down: keep running, retry next tick
if (j?.lists) listsByGuild = j.lists; // latest whitelist/blacklist config
+ if (j?.settings) botSettings = j.settings; // latest behaviour toggles
for (const cmd of (j?.commands || [])) {
try { await handleCommand(cmd); } catch (e) { log(`command ${cmd?.type} failed: ${e.message}`); }
}
diff --git a/wsai/bot_control.py b/wsai/bot_control.py
index 294be6d..08fe30b 100644
--- a/wsai/bot_control.py
+++ b/wsai/bot_control.py
@@ -32,6 +32,20 @@ class BotControl:
# Per-guild listen filter. Empty whitelist => listen to everyone;
# blacklist always excludes. Users and roles both supported.
self._lists: dict[str, dict[str, Any]] = {}
+ # Bot behaviour settings the dashboard toggles and the bot reads on each
+ # report. bargeIn: stop the bot's current TTS the moment a user speaks.
+ self._settings: dict[str, Any] = {"bargeIn": True}
+
+ # -- bot behaviour settings ------------------------------------------ #
+ def get_settings(self) -> dict[str, Any]:
+ with self._lock:
+ return dict(self._settings)
+
+ def set_settings(self, data: dict[str, Any]) -> dict[str, Any]:
+ with self._lock:
+ if "bargeIn" in data:
+ self._settings["bargeIn"] = bool(data["bargeIn"])
+ return dict(self._settings)
# -- whitelist / blacklist (per guild) ------------------------------- #
@staticmethod
diff --git a/wsai/dashboard.py b/wsai/dashboard.py
index 641668a..e42a1b3 100644
--- a/wsai/dashboard.py
+++ b/wsai/dashboard.py
@@ -85,6 +85,8 @@ def _make_handler(dash: "Dashboard"):
self._send_json({"ok": True, "guildId": gid, "lists": dash.bot.get_lists(gid)})
elif path == "/api/tts/settings":
self._handle_tts_settings_get()
+ elif path == "/api/bot/settings":
+ self._send_json({"ok": True, "settings": dash.bot.get_settings()})
elif path == "/events":
self._stream_events()
else:
@@ -115,6 +117,8 @@ def _make_handler(dash: "Dashboard"):
self._handle_tts_settings_post()
elif path == "/api/tts/preview":
self._handle_tts_preview()
+ elif path == "/api/bot/settings":
+ self._handle_bot_settings_post()
else:
self._send(404, b"not found", "text/plain; charset=utf-8")
@@ -244,10 +248,12 @@ def _make_handler(dash: "Dashboard"):
self._send_json({"ok": False, "error": "invalid JSON"}, 400)
return
dash.bot.report(data)
- # Hand the bot any queued commands + the current listen filters in the
- # same round trip so it does not have to poll extra endpoints.
+ # Hand the bot any queued commands + the current listen filters +
+ # behaviour settings in the same round trip so it does not have to
+ # poll extra endpoints.
self._send_json({"ok": True, "commands": dash.bot.drain(),
- "lists": dash.bot.all_lists()})
+ "lists": dash.bot.all_lists(),
+ "settings": dash.bot.get_settings()})
def _handle_bot_select(self) -> None:
"""UI picked a server/voice channel → queue a join (or leave) command."""
@@ -282,6 +288,19 @@ def _make_handler(dash: "Dashboard"):
monitor.log("info", f"청취 화이트/블랙리스트 업데이트 (guild={guild_id})")
self._send_json({"ok": True, "guildId": guild_id, "lists": saved})
+ def _handle_bot_settings_post(self) -> None:
+ """Save a bot behaviour toggle (e.g. bargeIn). The bot reads the new
+ value on its next report round-trip."""
+ raw = self._read_body()
+ try:
+ data = json.loads(raw.decode("utf-8")) if raw else {}
+ except (ValueError, AttributeError):
+ self._send_json({"ok": False, "error": "invalid JSON"}, 400)
+ return
+ settings = dash.bot.set_settings(data)
+ monitor.log("info", "봇 설정 변경: " + json.dumps(settings, ensure_ascii=False))
+ self._send_json({"ok": True, "settings": settings})
+
def _handle_tts_settings_get(self) -> None:
"""Return the live TTS controls (speed/word_gap/sentence_gap/pitch)
and their slider ranges so the page can render the panel."""
@@ -748,7 +767,10 @@ PAGE = r"""
.sttbox{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:14px 16px;margin:0 0 16px}
.sttbox h2{font-size:13px;margin:0 0 10px;font-weight:600;color:var(--fg)}
.sttbox h2.collap{cursor:pointer;user-select:none;margin:0}
- .sttbox h2.collap #ttsCaret{display:inline-block;width:12px;color:var(--muted)}
+ .sttbox h2.collap span{display:inline-block;width:12px;color:var(--muted)}
+ .cfgrow{display:flex;gap:9px;align-items:center;font-size:13px;color:var(--fg);cursor:pointer;padding:4px 0}
+ .cfgrow input[type=checkbox]{width:16px;height:16px;accent-color:#3aa0ff;cursor:pointer}
+ .cfghint{color:var(--muted);font-weight:400;font-size:12px}
.sttrow{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.btn{background:#173042;border:1px solid #234a63;color:var(--fg);border-radius:10px;padding:8px 14px;font-size:13px;cursor:pointer}
.btn:hover{background:#1d3d54}
@@ -877,20 +899,9 @@ PAGE = r"""
봇: 연결 안 됨
-
-
+
-
+ ▸ ⚙️ 봇 관련 설정
+
+ ▸ 🧹 로그 관련 설정
+
+