Add a --deep probe to check_tts so a live TTS failure isn't invisible

Reported: the pet was speaking in the offline pyttsx3 voice instead of the
server-synthesized one. speak()'s fallback chain is silent by design (a
TtsError just logs and falls through to pyttsx3), and the shallow doctor
check only ever verified config presence, not that a real /desk/tts call
actually succeeds — so "misconfigured" and "configured but the server
rejects it" looked identical from the outside.

check_tts(deep=True) now posts a couple of words to /desk/tts for real and
reports the exact HTTP status and server error message (a 404 means the
voice id isn't real or isn't owned by this key - the likely shape of a
Voicebox-clone ownership mismatch; 402 means the account is out of
credits; 401 means DESK_API_KEY is wrong). Wired through the same
CHECKS/run() deep-parameter dance check_streaming_endpoint already uses.

5 new tests, including one pinning that run()'s TypeError-fallback
actually reaches check_tts's deep parameter rather than silently running
it shallow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CBvympe6SuHdhf9q1VQqS
This commit is contained in:
2026-09-13 16:35:01 -06:00
co-authored by Claude Sonnet 5
parent 2a2cf38399
commit ad9925da02
2 changed files with 121 additions and 10 deletions
+48 -10
View File
@@ -152,18 +152,56 @@ def check_stt() -> Check:
return Check("speech-to-text", OK, f"server relay, {mode}")
def check_tts() -> Check:
def check_tts(deep: bool = False) -> Check:
"""TTS is the server's /desk/tts — no local ElevenLabs account needed for
the normal reply voice, just a voice id for it to request."""
if config.is_configured() and config.ELEVENLABS_VOICE_ID:
the normal reply voice, just a voice id for it to request.
Shallow only checks config presence, same as the other shallow checks —
it can't see a live failure (bad voice id, an expired key, a Voicebox
clone owned by someone else). --deep actually calls /desk/tts with a
couple of words and reports the real HTTP status/error, which is the
only way to tell "misconfigured" apart from "configured but broken"
(e.g. speak() silently falling back to the offline voice)."""
if not (config.is_configured() and config.ELEVENLABS_VOICE_ID):
if _module("pyttsx3"):
return Check("text-to-speech", WARN, "server/voice not configured — offline voice only",
"set BOLT_SERVER_URL/DESK_API_KEY and ELEVENLABS_VOICE_ID for the real voice")
return Check("text-to-speech", FAIL, "no server/voice config and no pyttsx3 fallback",
"set BOLT_SERVER_URL/DESK_API_KEY/ELEVENLABS_VOICE_ID, "
"or pip install pyttsx3 for an offline voice")
if not deep:
return Check("text-to-speech", OK,
f"server relay, voice …{config.ELEVENLABS_VOICE_ID[-6:]}")
if _module("pyttsx3"):
return Check("text-to-speech", WARN, "server/voice not configured — offline voice only",
"set BOLT_SERVER_URL/DESK_API_KEY and ELEVENLABS_VOICE_ID for the real voice")
return Check("text-to-speech", FAIL, "no server/voice config and no pyttsx3 fallback",
"set BOLT_SERVER_URL/DESK_API_KEY/ELEVENLABS_VOICE_ID, "
"or pip install pyttsx3 for an offline voice")
f"server relay, voice …{config.ELEVENLABS_VOICE_ID[-6:]} "
"(not probed; --deep to actually synthesize)")
try:
import requests
response = requests.post(
f"{config.SERVER_URL}/desk/tts",
headers={"X-Desk-Api-Key": config.API_KEY},
json={"session_id": config.SESSION_ID, "text": "testing",
"voice_id": config.ELEVENLABS_VOICE_ID},
timeout=20,
)
except Exception as exc:
return Check("text-to-speech", FAIL, f"probe failed: {exc}",
"check BOLT_SERVER_URL is reachable from this machine")
if response.status_code == 200 and response.content:
return Check("text-to-speech", OK,
f"server synthesized {len(response.content)} bytes "
f"for voice …{config.ELEVENLABS_VOICE_ID[-6:]}")
detail = ""
try:
detail = str(response.json().get("error") or "")
except Exception:
pass
return Check(
"text-to-speech", FAIL,
f"server returned {response.status_code}{': ' + detail if detail else ''} "
f"for voice …{config.ELEVENLABS_VOICE_ID[-6:]}",
"a 404 means this voice id isn't yours (or doesn't exist); 402 means "
"the account is out of credits; 401 means DESK_API_KEY is wrong",
)
def check_dialogue() -> Check: