feat(voice): configurable user voice-recognition time (bargeInMs, default 700ms)

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 <noreply@anthropic.com>
This commit is contained in:
EJClaw
2026-08-27 11:35:01 +09:00
parent d5501d0b8a
commit 0e4e7c6bb2
3 changed files with 41 additions and 4 deletions

View File

@@ -180,8 +180,13 @@ let listsByGuild = {}; // guildId -> {whitelistUsers, blacklistUsers,
let botSettings = { bargeIn: true }; // behaviour toggles from the dashboard let botSettings = { bargeIn: true }; // behaviour toggles from the dashboard
// Barge-in only fires when the Discord "speaking" (green ring) stays on for at // 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 // 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); 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 const bargeTimers = new Map(); // userId -> pending stop timer
// Attach the bot's audio player (so it can speak) to a fresh connection. // 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.set(userId, setTimeout(() => {
bargeTimers.delete(userId); bargeTimers.delete(userId);
try { voicePlayer.stop(true); } catch {} try { voicePlayer.stop(true); } catch {}
}, BARGE_IN_MS)); }, bargeInMs()));
} }
activeSubs.add(userId); activeSubs.add(userId);
if (!perUser.has(userId)) perUser.set(userId, { opusPackets: 0, pcmFrames: 0 }); if (!perUser.has(userId)) perUser.set(userId, { opusPackets: 0, pcmFrames: 0 });

View File

@@ -37,7 +37,13 @@ class BotControl:
self._lists: dict[str, dict[str, Any]] = st.get("lists") or {} self._lists: dict[str, dict[str, Any]] = st.get("lists") or {}
# Bot behaviour settings the dashboard toggles and the bot reads on each # 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. # 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 ------------------------------------------ # # -- bot behaviour settings ------------------------------------------ #
def get_settings(self) -> dict[str, Any]: def get_settings(self) -> dict[str, Any]:
@@ -48,6 +54,13 @@ class BotControl:
with self._lock: with self._lock:
if "bargeIn" in data: if "bargeIn" in data:
self._settings["bargeIn"] = bool(data["bargeIn"]) 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) out = dict(self._settings)
from . import state_store from . import state_store
state_store.patch("botSettings", out) state_store.patch("botSettings", out)

View File

@@ -1162,6 +1162,10 @@ PAGE = r"""<!DOCTYPE html>
<div id="botcfgBody" style="display:none"> <div id="botcfgBody" style="display:none">
<label class="cfgrow"><input type="checkbox" id="cfgBargeIn" checked> <label class="cfgrow"><input type="checkbox" id="cfgBargeIn" checked>
<span>유저 목소리 들을 때 봇이 말하던 것 중지 <b class="cfghint">(기본: 켜짐)</b></span></label> <span>유저 목소리 들을 때 봇이 말하던 것 중지 <b class="cfghint">(기본: 켜짐)</b></span></label>
<label class="cfgrow"><span>유저 음성 인식 시간
<input type="number" id="cfgBargeInMs" min="0" max="5000" step="50" value="700"
style="width:80px;margin:0 4px"> ms
<b class="cfghint">(이 시간 이상 말해야 봇이 멈춤 · 기본: 700)</b></span></label>
</div> </div>
</section> </section>
<section class="sttbox" id="logcfgbox"> <section class="sttbox" id="logcfgbox">
@@ -1483,8 +1487,23 @@ function wireCollapse(toggleId, bodyId, caretId){
body:JSON.stringify({bargeIn:$('cfgBargeIn').checked})}); body:JSON.stringify({bargeIn:$('cfgBargeIn').checked})});
toast('봇 설정 저장됨'); }catch(e){} 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=>{ 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(()=>{}); }).catch(()=>{});
// 로그 설정: 잡음 로그 표시 안 함 — 클라이언트 필터(로컬 저장). // 로그 설정: 잡음 로그 표시 안 함 — 클라이언트 필터(로컬 저장).
$('cfgHideNoise').checked = hideNoise; $('cfgHideNoise').checked = hideNoise;