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

  1. Confirm the uri includes the full path: [providers.tts.piper.<alias>].uri = "http://127.0.0.1:5000/v1/audio/speech".
  2. Check the Piper server logs for the matching request; a 500 there is a server-side synthesis failure.
  3. Make the configured voice match a model the server has loaded; list loaded voices on the server.
  4. 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

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


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/68d83563c0bd5a2c. Report an issue: GitHub.