feat(gateway): butler filler line while Tatlock works
Tatlock turns take 10-25s, which is a long silence after a request. Speak a canned "Let me check on that for you, sir" immediately (synthesized once and cached), then re-assert the thinking state so the device keeps its effort-face spinner up until the real reply arrives. On the device, guard the playback-done handler so the filler audio finishing doesn't drop the spinner back to idle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKPbR6DY2JygHbyLjxm7Uu
This commit is contained in:
@@ -19,6 +19,9 @@ class Settings(BaseSettings):
|
||||
tts_model: str = "speaches-ai/Kokoro-82M-v1.0-ONNX"
|
||||
tts_voice: str = "bm_george"
|
||||
|
||||
# Spoken immediately after a query is accepted, to fill the long Tatlock wait.
|
||||
filler_text: str = "Let me check on that for you, sir."
|
||||
|
||||
# embedded fallback only (requires the [speech] extra)
|
||||
embedded_stt_model: str = "small"
|
||||
embedded_stt_device: str = "cuda"
|
||||
|
||||
@@ -24,6 +24,21 @@ PCM_CHUNK_BYTES = 4096
|
||||
# device registry: every endpoint that ever connected, keyed by client IP
|
||||
DEVICES: dict[str, dict] = {}
|
||||
|
||||
# Butler filler audio (the "let me check…" line) is synthesized once, then replayed.
|
||||
_filler_pcm: bytes | None = None
|
||||
_filler_tried = False
|
||||
|
||||
|
||||
async def _ensure_filler() -> bytes | None:
|
||||
global _filler_pcm, _filler_tried
|
||||
if not _filler_tried:
|
||||
_filler_tried = True
|
||||
try:
|
||||
_filler_pcm = await asyncio.to_thread(tts.synthesize, settings.filler_text)
|
||||
except Exception:
|
||||
logger.exception("filler synth failed; continuing without it")
|
||||
return _filler_pcm
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds")
|
||||
@@ -111,6 +126,17 @@ async def _handle_utterance(ws: WebSocket, tatlock: TatlockClient, pcm: bytes) -
|
||||
await ws.send_json({"type": "state", "value": "idle"})
|
||||
return
|
||||
|
||||
# Acknowledge immediately with a canned line so the long Tatlock wait isn't dead
|
||||
# air, then re-assert "thinking" to keep the spinner up while it works.
|
||||
filler = await _ensure_filler()
|
||||
if filler:
|
||||
await ws.send_json({"type": "reply_text", "text": settings.filler_text})
|
||||
await ws.send_json({"type": "audio_start", "sample_rate": settings.sample_rate})
|
||||
for offset in range(0, len(filler), PCM_CHUNK_BYTES):
|
||||
await ws.send_bytes(filler[offset : offset + PCM_CHUNK_BYTES])
|
||||
await ws.send_json({"type": "audio_end"})
|
||||
await ws.send_json({"type": "state", "value": "thinking"})
|
||||
|
||||
reply = await tatlock.ask(transcript)
|
||||
await ws.send_json({"type": "reply_text", "text": reply})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user