feat(dashboard): default STT=medium, apply feedback+loading, log categories+filters

- Default STT model changed small -> medium (WhisperSTT default + service env;
  medium pre-cached). UI labels/hints updated.
- Model 적용 buttons: if the picked model == current, flash "변경사항 없음" for 3s
  then show the current model again; if it actually changes, STT shows a live
  loading panel that polls /api/models until the worker is ready and logs
  completion to the event log (set_stt_model/set_llm_model now return `changed`).
- Event/error log gains categories (READY, CONNECT, MODEL, TTS, VOICE, FILTER,
  SETTING, TURN, BRAIN, VISION, PIPELINE, …): Monitor.log takes an optional `cat`,
  call sites tagged, and each line shows a coloured category chip. A bot
  (re)connection now logs a CONNECT event.
- Log search extended: filter by 레벨(type), 종류(category, auto-populated), and
  시간(time range) in addition to free text.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
EJClaw
2026-08-26 23:51:58 +09:00
parent d164630bb8
commit 1ac45214ce
5 changed files with 136 additions and 44 deletions

View File

@@ -65,7 +65,7 @@ class Pipeline:
except Exception as exc: # a single bad frame must not kill the loop
log.exception("vision.describe failed")
if self.monitor is not None:
self.monitor.log("error", f"화면 이해 실패: {exc}")
self.monitor.log("error", f"화면 이해 실패: {exc}", cat="VISION")
continue
await self.context.update(obs)
log.debug("screen: %s", obs.text[:120])
@@ -122,7 +122,7 @@ class Pipeline:
return
if self.monitor is not None:
self.monitor.set_status(listening=True)
self.monitor.log("info", "음성 수신 시작 — 발화 대기 중")
self.monitor.log("info", "음성 수신 시작 — 발화 대기 중", cat="PIPELINE")
try:
async for utt in self.stt.utterances():
await self._handle(utt)
@@ -157,7 +157,7 @@ class Pipeline:
except Exception as exc: # a warm failure must not abort startup
log.warning("prewarm %s failed: %s", name, exc)
if self.monitor is not None:
self.monitor.log("error", f"{name} 예열 실패: {exc}")
self.monitor.log("error", f"{name} 예열 실패: {exc}", cat="READY")
async def run(self) -> None:
# A TaskGroup (not bare gather) so that if ONE loop raises, the others
@@ -167,7 +167,7 @@ class Pipeline:
# still-live loop (close-during-use).
if self.monitor is not None:
self.monitor.set_status(running=True)
self.monitor.log("info", "파이프라인 시작")
self.monitor.log("info", "파이프라인 시작", cat="PIPELINE")
await self._prewarm()
try:
async with asyncio.TaskGroup() as tg:
@@ -177,12 +177,12 @@ class Pipeline:
except* Exception as eg:
if self.monitor is not None:
for exc in eg.exceptions:
self.monitor.log("error", f"루프 예외: {type(exc).__name__}: {exc}")
self.monitor.log("error", f"루프 예외: {type(exc).__name__}: {exc}", cat="PIPELINE")
raise
finally:
if self.monitor is not None:
self.monitor.set_status(running=False, listening=False)
self.monitor.log("info", "파이프라인 종료")
self.monitor.log("info", "파이프라인 종료", cat="PIPELINE")
await self.aclose()
async def aclose(self) -> None: