feat(dashboard): browser STT recognition test on the GPU

The status page was view-only, so there was no way to actually verify Korean
recognition end-to-end. Add a live test: record from the mic (localhost/https)
or upload an audio file (works over LAN http, where browsers block getUserMedia),
POST it to a new /api/stt endpoint that ffmpeg-normalises the blob to 16 kHz
mono and runs the real GPU faster-whisper, then shows the recognised text +
latency + device. Results also land in the live turn feed.

The dashboard now optionally holds a WhisperSTT and drives it from a private
asyncio loop thread. New `python -m wsai --stt-test` serves the page with STT
enabled and pre-warms the GPU worker so the first recognition is instant.
WhisperSTT.resolved_device is exposed for the UI.

Verified: wav and browser-style webm/opus uploads both return the correct
Korean text on device=cuda in ~240-280ms. 12 tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
EJClaw
2026-08-18 21:49:03 +09:00
parent 77d7cd8b56
commit b9c929a73f
3 changed files with 233 additions and 4 deletions

View File

@@ -76,6 +76,7 @@ class WhisperSTT:
self._proc: asyncio.subprocess.Process | None = None
self._lock = asyncio.Lock()
self.load_ms: int | None = None
self.resolved_device: str | None = None # "cuda" | "cpu", known after start
# Keep the worker's most recent stderr so a crash reports its real cause
# instead of a bare JSONDecodeError. Bounded so it can't grow unbounded.
self._stderr_tail: collections.deque[str] = collections.deque(maxlen=40)
@@ -153,6 +154,7 @@ class WhisperSTT:
f"whisper worker failed to start: {info}.{self._stderr_hint()}"
)
self.load_ms = info.get("ms")
self.resolved_device = info.get("device")
log.info(
"whisper worker ready in %s ms on %s (model %s)",
self.load_ms, info.get("device"), info.get("model"),