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:
@@ -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 });
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user