feat(voice-bridge): /voice/ws Piper fallback

The /tts HTTP handler already chained cache -> F5 -> Piper Tower.
The /voice/ws path was missing the Piper branch — on F5 timeout
or load failure it sent an empty speak_end and the firmware got
silence.

Wire the same _piper_fallback() helper after the F5 path, mirror
the wav-to-pcm conversion done on the cache hit branch. Best
effort: any error in the fallback leaves pcm=None and the WS
behaves as before. backend tag distinguishes 'f5' / 'cache' /
'piper_fallback' in the ws_tts_* log events.

Note: Piper on Tower is EN-only today (per root CLAUDE.md), so
a FR utterance falls back to EN pronunciation. Deploying a Piper
FR model is the real follow-up; this just plugs the silence.
This commit is contained in:
L'électron rare
2026-05-24 01:21:26 +02:00
parent 43b5ddc8c5
commit a5b00aa38d
+19
View File
@@ -1628,6 +1628,25 @@ async def voice_ws(ws: WebSocket) -> None:
_jlog("ws_tts_f5_not_loaded", request_id=request_id,
load_err=_F5_LOAD_ERR)
# 5c. Piper fallback on F5 failure — mirrors the /tts handler.
# Same wav-to-pcm conversion; we resample on output only if the
# Piper sample-rate diverges from the WS contract (24 kHz). The
# fallback is intentionally best-effort: any error here just
# leaves pcm=None and the WS sends an empty speak_end below.
if pcm is None and speak_text:
wav_fallback = await _piper_fallback(speak_text)
if wav_fallback is not None:
try:
pcm = _wav_to_pcm16(wav_fallback)
tts_backend_used = "piper_fallback"
tts_err = None
_jlog("ws_tts_piper_fallback",
request_id=request_id, bytes=len(pcm))
except Exception as exc: # noqa: BLE001
_jlog("ws_tts_piper_decode_err",
request_id=request_id,
err=type(exc).__name__, msg=str(exc))
tts_stage_latency_ms = int(
(time.monotonic() - tts_started) * 1000
)