feat: live status dashboard for the voice loop

Add a stdlib-only observability site so you can open a browser and watch,
step by step: whether it is listening, what it heard, what the brain thought
and answered, how long each stage took, and whether anything errored.

- wsai/monitor.py: thread-safe telemetry hub (per-turn timed steps, status
  header, error log) with a pub/sub for live push.
- wsai/dashboard.py: stdlib http.server serving a self-contained page plus an
  SSE (/events) live stream; /api/state snapshot fallback.
- Pipeline emits step-by-step turn telemetry (화면 맥락 → 두뇌 → 응답) and
  listening/running status; optional monitor, so existing paths are untouched.
- `python -m wsai --dashboard` starts the site (0.0.0.0:8787, WSAI_DASHBOARD_PORT)
  and loops the mock voice demo so there is always live activity to watch.
- Tests cover turn recording, per-step timing, error marking, and live push.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
EJClaw
2026-08-16 20:24:13 +09:00
parent 3d76cd6c52
commit 6b0755e1ff
7 changed files with 780 additions and 21 deletions

View File

@@ -63,18 +63,27 @@ class MockVision:
class MockSTT:
"""Feeds a scripted set of user utterances, then goes quiet."""
def __init__(self, script: list[str] | None = None, interval: float = 2.0) -> None:
def __init__(
self,
script: list[str] | None = None,
interval: float = 2.0,
loop: bool = False,
) -> None:
self.script = script or [
"지금 화면에 뭐 보여?",
"저 에러 왜 나는 거야?",
"고마워",
]
self.interval = interval
self.loop = loop
async def utterances(self) -> AsyncIterator[Utterance]:
for line in self.script:
await asyncio.sleep(self.interval)
yield Utterance(text=line, ts=time.monotonic(), source="voice")
while True:
for line in self.script:
await asyncio.sleep(self.interval)
yield Utterance(text=line, ts=time.monotonic(), source="voice")
if not self.loop:
return
async def aclose(self) -> None:
return