zeroclaw-labs/zeroclaw · error · anyhow::Error
Piper TTS API error ({}): {}
Error message
Piper TTS API error ({}): {} What it means
Raised by PiperTtsProvider::synthesize when the local OpenAI-compatible endpoint (default http://127.0.0.1:5000/v1/audio/speech, overridable via [providers.tts.piper.<alias>].uri) answers non-2xx. The request sends {model: "tts-1", input, voice} with no auth; the message extracts error.message from the body, defaulting to "unknown error" for non-conforming bodies such as HTML 404 pages.
Source
Thrown at crates/zeroclaw-channels/src/tts.rs:890
let resp = self
.client
.post(&self.api_url)
.json(&body)
.send()
.await
.context("Failed to send Piper TTS request")?;
let status = resp.status();
if !status.is_success() {
let error_body: serde_json::Value = resp
.json()
.await
.unwrap_or_else(|_| serde_json::json!({"error": "unknown"}));
let msg = error_body["error"]["message"]
.as_str()
.unwrap_or("unknown error");
bail!("Piper TTS API error ({}): {}", status, msg);
}
let bytes = resp
.bytes()
.await
.context("Failed to read Piper TTS response body")?;
Ok(bytes.to_vec())
}
fn supported_voices(&self) -> Vec<String> {
// Piper voices depend on installed models; return empty (dynamic).
Vec::new()
}
fn supported_formats(&self) -> Vec<String> {
["mp3", "wav", "opus"]
.iter()
.map(|s| (*s).to_string())View on GitHub (pinned to 88bb9c8533)
Solutions
- Confirm the uri includes the full path: [providers.tts.piper.<alias>].uri = "http://127.0.0.1:5000/v1/audio/speech".
- Check the Piper server logs for the matching request; a 500 there is a server-side synthesis failure.
- Make the configured voice match a model the server has loaded; list loaded voices on the server.
- For "unknown error" with 404, the endpoint path is wrong — fix uri; the body was not an OpenAI-style JSON error.
Example fix
# before — base URL only; server answers 404 HTML [providers.tts.piper.main] uri = "http://127.0.0.1:5000" # after — full OpenAI-compatible speech path [providers.tts.piper.main] uri = "http://127.0.0.1:5000/v1/audio/speech"
Defensive patterns
Strategy: validation
Validate before calling
// Pre-flight: the local Piper server must be reachable before synthesis.
async fn piper_endpoint_reachable(uri: &str) -> bool {
let host_port = uri
.strip_prefix("http://")
.and_then(|rest| rest.split('/').next())
.unwrap_or("127.0.0.1:5000");
tokio::net::TcpStream::connect(host_port).await.is_ok()
} Try / catch
if let Err(err) = mgr.synthesize(text).await {
let msg = err.to_string();
if msg.contains("Piper TTS API error") {
// local server answered — inspect its logs; "unknown error" means non-OpenAI body (wrong path)
if msg.contains("unknown error") {
return Err(err.context("check [providers.tts.piper.<alias>].uri includes /v1/audio/speech"));
}
}
return Err(err);
} Prevention
- Configure uri as the full path http://127.0.0.1:5000/v1/audio/speech, not the bare host.
- Start and health-check the Piper server (and its loaded voices) as a supervised dependency before the runtime.
- Keep the configured voice aligned with the models the Piper server actually loaded.
When it happens
Trigger: The Piper server runs but rejects the request: requested voice is not a loaded model (400/404), uri points to the wrong path so the framework returns an HTML 404, or the server throws a 500 during synthesis. A fully down server instead fails earlier with "Failed to send Piper TTS request" (connection refused).
Common situations: uri configured as the base URL (http://127.0.0.1:5000) without the /v1/audio/speech path. Starting the Piper server with a different voice/model than the configured voice. Reverse proxies in front of the server rewriting paths and returning HTML error pages.
Related errors
- OpenAI TTS API error ({}): {}
- ElevenLabs TTS API error ({}): {}
- Google TTS API error ({}): {}
- Failed to initiate call: {}
- Failed to speak: {}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/68d83563c0bd5a2c.
Report an issue: GitHub.