{"record":{"id":"68d83563c0bd5a2c","repo":"zeroclaw-labs/zeroclaw","slug":"piper-tts-api-error","errorCode":null,"errorMessage":"Piper TTS API error ({}): {}","messagePattern":"Piper TTS API error \\((.+?)\\): (.+?)","errorType":"http","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/tts.rs","lineNumber":890,"sourceCode":"\n        let resp = self\n            .client\n            .post(&self.api_url)\n            .json(&body)\n            .send()\n            .await\n            .context(\"Failed to send Piper TTS request\")?;\n\n        let status = resp.status();\n        if !status.is_success() {\n            let error_body: serde_json::Value = resp\n                .json()\n                .await\n                .unwrap_or_else(|_| serde_json::json!({\"error\": \"unknown\"}));\n            let msg = error_body[\"error\"][\"message\"]\n                .as_str()\n                .unwrap_or(\"unknown error\");\n            bail!(\"Piper TTS API error ({}): {}\", status, msg);\n        }\n\n        let bytes = resp\n            .bytes()\n            .await\n            .context(\"Failed to read Piper TTS response body\")?;\n        Ok(bytes.to_vec())\n    }\n\n    fn supported_voices(&self) -> Vec<String> {\n        // Piper voices depend on installed models; return empty (dynamic).\n        Vec::new()\n    }\n\n    fn supported_formats(&self) -> Vec<String> {\n        [\"mp3\", \"wav\", \"opus\"]\n            .iter()\n            .map(|s| (*s).to_string())","sourceCodeStart":872,"sourceCodeEnd":908,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/tts.rs#L872-L908","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"# before — base URL only; server answers 404 HTML\n[providers.tts.piper.main]\nuri = \"http://127.0.0.1:5000\"\n\n# after — full OpenAI-compatible speech path\n[providers.tts.piper.main]\nuri = \"http://127.0.0.1:5000/v1/audio/speech\"","handlingStrategy":"validation","validationCode":"// Pre-flight: the local Piper server must be reachable before synthesis.\nasync fn piper_endpoint_reachable(uri: &str) -> bool {\n    let host_port = uri\n        .strip_prefix(\"http://\")\n        .and_then(|rest| rest.split('/').next())\n        .unwrap_or(\"127.0.0.1:5000\");\n    tokio::net::TcpStream::connect(host_port).await.is_ok()\n}","typeGuard":null,"tryCatchPattern":"if let Err(err) = mgr.synthesize(text).await {\n    let msg = err.to_string();\n    if msg.contains(\"Piper TTS API error\") {\n        // local server answered — inspect its logs; \"unknown error\" means non-OpenAI body (wrong path)\n        if msg.contains(\"unknown error\") {\n            return Err(err.context(\"check [providers.tts.piper.<alias>].uri includes /v1/audio/speech\"));\n        }\n    }\n    return Err(err);\n}","preventionTips":["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."],"tags":["piper","tts","http","local-server"],"backgroundTag":"openai-compatible-api-error","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}