From 0e4e7c6bb2d743b36c3663017df19bca93f05fed Mon Sep 17 00:00:00 2001 From: EJClaw Date: Thu, 27 Aug 2026 11:35:01 +0900 Subject: [PATCH] feat(voice): configurable user voice-recognition time (bargeInMs, default 700ms) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose the barge-in sustained-speech threshold as a dashboard bot setting "유저 음성 인식 시간" (ms). Server clamps 0..5000, persists via state_store, and the bot reads it each report round-trip; WSAI_BARGE_IN_MS/700 stays the fallback default. Co-Authored-By: Claude Opus 4.7 --- dave/bot.mjs | 9 +++++++-- wsai/bot_control.py | 15 ++++++++++++++- wsai/dashboard.py | 21 ++++++++++++++++++++- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/dave/bot.mjs b/dave/bot.mjs index 42493ce..a056768 100644 --- a/dave/bot.mjs +++ b/dave/bot.mjs @@ -180,8 +180,13 @@ let listsByGuild = {}; // guildId -> {whitelistUsers, blacklistUsers, let botSettings = { bargeIn: true }; // behaviour toggles from the dashboard // Barge-in only fires when the Discord "speaking" (green ring) stays on for at // least this long — a brief blip (keyboard click, cough) shouldn't cut the bot -// off, but a genuine ~0.7s of speech should. Tunable via WSAI_BARGE_IN_MS. +// off, but a genuine ~0.7s of speech should. The dashboard's "유저 음성 인식 시간" +// (botSettings.bargeInMs) wins when set; WSAI_BARGE_IN_MS / 700 is the fallback. const BARGE_IN_MS = Number(process.env.WSAI_BARGE_IN_MS || 700); +function bargeInMs() { + const v = Number(botSettings.bargeInMs); + return Number.isFinite(v) && v >= 0 ? v : BARGE_IN_MS; +} const bargeTimers = new Map(); // userId -> pending stop timer // Attach the bot's audio player (so it can speak) to a fresh connection. @@ -214,7 +219,7 @@ function setupReceiver(connection) { bargeTimers.set(userId, setTimeout(() => { bargeTimers.delete(userId); try { voicePlayer.stop(true); } catch {} - }, BARGE_IN_MS)); + }, bargeInMs())); } activeSubs.add(userId); if (!perUser.has(userId)) perUser.set(userId, { opusPackets: 0, pcmFrames: 0 }); diff --git a/wsai/bot_control.py b/wsai/bot_control.py index fa05463..03a3372 100644 --- a/wsai/bot_control.py +++ b/wsai/bot_control.py @@ -37,7 +37,13 @@ class BotControl: self._lists: dict[str, dict[str, Any]] = st.get("lists") or {} # 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, **(st.get("botSettings") or {})} + # bargeInMs: how long (ms) a user must keep speaking before that barge-in + # fires — the "유저 음성 인식 시간" the dashboard exposes (default 700). + self._settings: dict[str, Any] = { + "bargeIn": True, + "bargeInMs": 700, + **(st.get("botSettings") or {}), + } # -- bot behaviour settings ------------------------------------------ # def get_settings(self) -> dict[str, Any]: @@ -48,6 +54,13 @@ class BotControl: with self._lock: if "bargeIn" in data: self._settings["bargeIn"] = bool(data["bargeIn"]) + if "bargeInMs" in data: + try: + ms = int(data["bargeInMs"]) + except (TypeError, ValueError): + ms = 700 + # Keep it sane: 0ms = instant, cap at 5s so a typo can't wedge it. + self._settings["bargeInMs"] = max(0, min(5000, ms)) out = dict(self._settings) from . import state_store state_store.patch("botSettings", out) diff --git a/wsai/dashboard.py b/wsai/dashboard.py index 2a91864..60e0f33 100644 --- a/wsai/dashboard.py +++ b/wsai/dashboard.py @@ -1162,6 +1162,10 @@ PAGE = r"""
@@ -1483,8 +1487,23 @@ function wireCollapse(toggleId, bodyId, caretId){ body:JSON.stringify({bargeIn:$('cfgBargeIn').checked})}); toast('봇 설정 저장됨'); }catch(e){} }; + // 유저 음성 인식 시간(ms): 서버가 0~5000으로 클램프, 봇이 다음 report에서 읽어감. + $('cfgBargeInMs').onchange = async ()=>{ + let ms = Math.round(Number($('cfgBargeInMs').value)); + if(!Number.isFinite(ms)) ms = 700; + ms = Math.max(0, Math.min(5000, ms)); + $('cfgBargeInMs').value = ms; + try{ const r=await fetch('/api/bot/settings',{method:'POST',headers:{'Content-Type':'application/json'}, + body:JSON.stringify({bargeInMs:ms})}); + const j=await r.json(); + if(j&&j.ok&&j.settings&&j.settings.bargeInMs!=null) $('cfgBargeInMs').value=j.settings.bargeInMs; + toast('봇 설정 저장됨'); }catch(e){} + }; fetch('/api/bot/settings').then(r=>r.json()).then(j=>{ - if(j&&j.ok&&j.settings) $('cfgBargeIn').checked = j.settings.bargeIn !== false; + if(j&&j.ok&&j.settings){ + $('cfgBargeIn').checked = j.settings.bargeIn !== false; + if(j.settings.bargeInMs!=null) $('cfgBargeInMs').value = j.settings.bargeInMs; + } }).catch(()=>{}); // 로그 설정: 잡음 로그 표시 안 함 — 클라이언트 필터(로컬 저장). $('cfgHideNoise').checked = hideNoise;