|
|
|
@@ -135,8 +135,10 @@ def _make_handler(dash: "Dashboard"):
|
|
|
|
"application/json; charset=utf-8")
|
|
|
|
"application/json; charset=utf-8")
|
|
|
|
return
|
|
|
|
return
|
|
|
|
speaker = urllib.parse.unquote(self.headers.get("X-User-Name", "") or "")
|
|
|
|
speaker = urllib.parse.unquote(self.headers.get("X-User-Name", "") or "")
|
|
|
|
|
|
|
|
guild = urllib.parse.unquote(self.headers.get("X-Guild-Name", "") or "")
|
|
|
|
|
|
|
|
channel = urllib.parse.unquote(self.headers.get("X-Channel-Name", "") or "")
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
res = dash.voice_turn(raw, speaker=speaker)
|
|
|
|
res = dash.voice_turn(raw, speaker=speaker, guild=guild, channel=channel)
|
|
|
|
except Exception as exc: # noqa: BLE001
|
|
|
|
except Exception as exc: # noqa: BLE001
|
|
|
|
log.exception("voice-turn failed")
|
|
|
|
log.exception("voice-turn failed")
|
|
|
|
self._send(500, json.dumps({"ok": False, "error": f"{type(exc).__name__}: {exc}"},
|
|
|
|
self._send(500, json.dumps({"ok": False, "error": f"{type(exc).__name__}: {exc}"},
|
|
|
|
@@ -395,7 +397,8 @@ class Dashboard:
|
|
|
|
if self.tts is not None:
|
|
|
|
if self.tts is not None:
|
|
|
|
self._submit(self.tts._ensure())
|
|
|
|
self._submit(self.tts._ensure())
|
|
|
|
|
|
|
|
|
|
|
|
def voice_turn(self, audio_bytes: bytes, speaker: str = "") -> dict:
|
|
|
|
def voice_turn(self, audio_bytes: bytes, speaker: str = "",
|
|
|
|
|
|
|
|
guild: str = "", channel: str = "") -> dict:
|
|
|
|
"""One Discord voice turn: decode the uploaded utterance, recognise it
|
|
|
|
"""One Discord voice turn: decode the uploaded utterance, recognise it
|
|
|
|
on the GPU, think of a reply (Claude brain if wired, else echo),
|
|
|
|
on the GPU, think of a reply (Claude brain if wired, else echo),
|
|
|
|
synthesise it on the GPU, and return {heard, reply, wav} where wav is the
|
|
|
|
synthesise it on the GPU, and return {heard, reply, wav} where wav is the
|
|
|
|
@@ -416,6 +419,7 @@ class Dashboard:
|
|
|
|
turn = self.monitor.turn(source="discord")
|
|
|
|
turn = self.monitor.turn(source="discord")
|
|
|
|
if speaker:
|
|
|
|
if speaker:
|
|
|
|
turn.speaker = speaker # who spoke (for the "누가 말했는지" log)
|
|
|
|
turn.speaker = speaker # who spoke (for the "누가 말했는지" log)
|
|
|
|
|
|
|
|
turn.guild, turn.channel = guild, channel # for 서버별/채널별 필터
|
|
|
|
t0 = time.monotonic()
|
|
|
|
t0 = time.monotonic()
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
subprocess.run(
|
|
|
|
subprocess.run(
|
|
|
|
@@ -629,6 +633,14 @@ PAGE = r"""<!DOCTYPE html>
|
|
|
|
border-radius:999px;padding:3px 10px;font-size:12px}
|
|
|
|
border-radius:999px;padding:3px 10px;font-size:12px}
|
|
|
|
.part.spk{border-color:#1f5236;color:#9ff0bd}
|
|
|
|
.part.spk{border-color:#1f5236;color:#9ff0bd}
|
|
|
|
.speaker{color:var(--muted);font-size:11.5px}
|
|
|
|
.speaker{color:var(--muted);font-size:11.5px}
|
|
|
|
|
|
|
|
/* 대화 로그 검색 필터 (시간·유저·서버·채널·내용) */
|
|
|
|
|
|
|
|
.tfilter{display:flex;gap:8px;align-items:center;flex-wrap:wrap;background:var(--panel);
|
|
|
|
|
|
|
|
border:1px solid var(--line);border-radius:12px;padding:8px 12px;margin:0 0 12px}
|
|
|
|
|
|
|
|
.tfilter input,.tfilter select{background:var(--panel2);border:1px solid var(--line);color:var(--fg);
|
|
|
|
|
|
|
|
border-radius:8px;padding:6px 9px;font-size:12.5px}
|
|
|
|
|
|
|
|
.tfilter input{width:118px}
|
|
|
|
|
|
|
|
.tf-label{color:var(--muted);font-size:12px;font-weight:600}
|
|
|
|
|
|
|
|
.tf-count{color:var(--muted);font-size:11.5px;margin-left:auto}
|
|
|
|
/* Modal / popup (reused by 프롬프트, 화이트/블랙리스트 …) */
|
|
|
|
/* Modal / popup (reused by 프롬프트, 화이트/블랙리스트 …) */
|
|
|
|
.modal{position:fixed;inset:0;z-index:20;background:rgba(4,7,11,.66);
|
|
|
|
.modal{position:fixed;inset:0;z-index:20;background:rgba(4,7,11,.66);
|
|
|
|
display:flex;align-items:center;justify-content:center;padding:20px}
|
|
|
|
display:flex;align-items:center;justify-content:center;padding:20px}
|
|
|
|
@@ -736,6 +748,22 @@ PAGE = r"""<!DOCTYPE html>
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
<div class="demobar" id="demobar" style="display:none"></div>
|
|
|
|
<div class="demobar" id="demobar" style="display:none"></div>
|
|
|
|
<div class="comp" id="comp"></div>
|
|
|
|
<div class="comp" id="comp"></div>
|
|
|
|
|
|
|
|
<div class="tfilter" id="tfilter">
|
|
|
|
|
|
|
|
<span class="tf-label">대화 로그 검색</span>
|
|
|
|
|
|
|
|
<select id="tfTime">
|
|
|
|
|
|
|
|
<option value="0">전체 시간</option>
|
|
|
|
|
|
|
|
<option value="5">최근 5분</option>
|
|
|
|
|
|
|
|
<option value="30">최근 30분</option>
|
|
|
|
|
|
|
|
<option value="60">최근 1시간</option>
|
|
|
|
|
|
|
|
<option value="180">최근 3시간</option>
|
|
|
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
<input id="tfUser" placeholder="유저(발화자)">
|
|
|
|
|
|
|
|
<input id="tfGuild" placeholder="서버">
|
|
|
|
|
|
|
|
<input id="tfChannel" placeholder="채널">
|
|
|
|
|
|
|
|
<input id="tfText" placeholder="내용(들음/답변)">
|
|
|
|
|
|
|
|
<button id="tfClear" class="btn hbtn">초기화</button>
|
|
|
|
|
|
|
|
<span id="tfCount" class="tf-count"></span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
<div id="turns"></div>
|
|
|
|
<div id="turns"></div>
|
|
|
|
<div id="empty" class="empty">아직 대화가 없습니다. 사용자가 말하면 여기에 단계별로 나타납니다.</div>
|
|
|
|
<div id="empty" class="empty">아직 대화가 없습니다. 사용자가 말하면 여기에 단계별로 나타납니다.</div>
|
|
|
|
</main>
|
|
|
|
</main>
|
|
|
|
@@ -845,6 +873,7 @@ function turnEl(t){
|
|
|
|
'<div class="trow">'+badge
|
|
|
|
'<div class="trow">'+badge
|
|
|
|
+'<span class="badge">#'+t.id+' · '+esc(t.source||'voice')+'</span>'
|
|
|
|
+'<span class="badge">#'+t.id+' · '+esc(t.source||'voice')+'</span>'
|
|
|
|
+(t.speaker?'<span class="badge">🗣 '+esc(t.speaker)+'</span>':'')
|
|
|
|
+(t.speaker?'<span class="badge">🗣 '+esc(t.speaker)+'</span>':'')
|
|
|
|
|
|
|
|
+(t.channel?'<span class="badge">🔊 '+esc((t.guild?t.guild+' / ':'')+t.channel)+'</span>':'')
|
|
|
|
+'<span class="time">'+fmtTime(t.wall)+'</span></div>'
|
|
|
|
+'<span class="time">'+fmtTime(t.wall)+'</span></div>'
|
|
|
|
+'<div class="line"><span class="tag">들음</span><span class="heard">'+(t.heard?esc(t.heard):'<i style="color:var(--muted)">(수신 대기)</i>')+'</span></div>'
|
|
|
|
+'<div class="line"><span class="tag">들음</span><span class="heard">'+(t.heard?esc(t.heard):'<i style="color:var(--muted)">(수신 대기)</i>')+'</span></div>'
|
|
|
|
+'<div class="line"><span class="tag">생각</span><span class="thought">'+(t.thought?esc(t.thought):'<i style="color:var(--muted)">…</i>')+'</span></div>'
|
|
|
|
+'<div class="line"><span class="tag">생각</span><span class="thought">'+(t.thought?esc(t.thought):'<i style="color:var(--muted)">…</i>')+'</span></div>'
|
|
|
|
@@ -863,6 +892,29 @@ function upsertTurn(t){
|
|
|
|
const fresh = turnEl(t);
|
|
|
|
const fresh = turnEl(t);
|
|
|
|
if(existing){ existing.replaceWith(fresh); }
|
|
|
|
if(existing){ existing.replaceWith(fresh); }
|
|
|
|
else { cont.prepend(fresh); }
|
|
|
|
else { cont.prepend(fresh); }
|
|
|
|
|
|
|
|
applyTurnFilter();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// --- 대화 로그 검색: 시간·유저·서버·채널·내용 필터 ------------------------ #
|
|
|
|
|
|
|
|
function turnFilter(){
|
|
|
|
|
|
|
|
return { mins:+$('tfTime').value, user:$('tfUser').value.trim().toLowerCase(),
|
|
|
|
|
|
|
|
guild:$('tfGuild').value.trim().toLowerCase(), channel:$('tfChannel').value.trim().toLowerCase(),
|
|
|
|
|
|
|
|
text:$('tfText').value.trim().toLowerCase() };
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
function turnMatches(t, f){
|
|
|
|
|
|
|
|
if(f.mins && (Date.now()/1000 - (t.wall||0)) > f.mins*60) return false;
|
|
|
|
|
|
|
|
if(f.user && !((t.speaker||'').toLowerCase().includes(f.user))) return false;
|
|
|
|
|
|
|
|
if(f.guild && !((t.guild||'').toLowerCase().includes(f.guild))) return false;
|
|
|
|
|
|
|
|
if(f.channel && !((t.channel||'').toLowerCase().includes(f.channel))) return false;
|
|
|
|
|
|
|
|
if(f.text && !(((t.heard||'')+' '+(t.reply||'')+' '+(t.thought||'')).toLowerCase().includes(f.text))) return false;
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
function applyTurnFilter(){
|
|
|
|
|
|
|
|
const f=turnFilter(); let shown=0;
|
|
|
|
|
|
|
|
for(const [id,t] of turns){ const el=$('turn-'+id); if(!el) continue;
|
|
|
|
|
|
|
|
const ok=turnMatches(t,f); el.style.display=ok?'':'none'; if(ok) shown++; }
|
|
|
|
|
|
|
|
const active = f.mins||f.user||f.guild||f.channel||f.text;
|
|
|
|
|
|
|
|
$('tfCount').textContent = turns.size ? (active ? shown+' / '+turns.size+' 대화' : turns.size+' 대화') : '';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Bottom terminal log panel: store all events, render filtered ---------- #
|
|
|
|
// --- Bottom terminal log panel: store all events, render filtered ---------- #
|
|
|
|
@@ -1121,6 +1173,9 @@ async function openLists(){
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$('wlBtn').onclick = openLists; $('blBtn').onclick = openLists;
|
|
|
|
$('wlBtn').onclick = openLists; $('blBtn').onclick = openLists;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
['tfTime','tfUser','tfGuild','tfChannel','tfText'].forEach(id=>{ const e=$(id); if(e){ e.oninput=applyTurnFilter; e.onchange=applyTurnFilter; } });
|
|
|
|
|
|
|
|
$('tfClear').onclick=()=>{ $('tfTime').value='0'; $('tfUser').value=''; $('tfGuild').value=''; $('tfChannel').value=''; $('tfText').value=''; applyTurnFilter(); };
|
|
|
|
|
|
|
|
|
|
|
|
async function pollBot(){
|
|
|
|
async function pollBot(){
|
|
|
|
try{ renderBot(await (await fetch('/api/bot/state')).json()); }catch(e){}
|
|
|
|
try{ renderBot(await (await fetch('/api/bot/state')).json()); }catch(e){}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|