{"record":{"id":"4a32d9e04426a1ed","repo":"zeroclaw-labs/zeroclaw","slug":"tts-returned-empty-audio","errorCode":null,"errorMessage":"TTS returned empty audio","messagePattern":"TTS returned empty audio","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/telegram.rs","lineNumber":1477,"sourceCode":"    async fn synthesize_and_send_voice(\n        api_base: &str,\n        bot_token: &str,\n        chat_id: &str,\n        thread_id: Option<&str>,\n        text: &str,\n        tts_manager: &crate::tts::TtsManager,\n    ) -> anyhow::Result<()> {\n        let audio_bytes = tts_manager.synthesize_opus(text).await?;\n        let audio_len = audio_bytes.len();\n        ::zeroclaw_log::record!(\n            INFO,\n            ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)\n                .with_attrs(::serde_json::json!({\"audio_len\": audio_len})),\n            \"synthesized bytes of audio\"\n        );\n\n        if audio_bytes.is_empty() {\n            anyhow::bail!(\"TTS returned empty audio\");\n        }\n\n        // synthesize_opus already transcodes to OGG/Opus via ffmpeg internally\n        let (method, field, filename, mime) = telegram_audio_send_spec(\"opus\")?;\n\n        let url = format!(\"{api_base}/bot{bot_token}/{method}\");\n        let client = zeroclaw_config::schema::build_runtime_proxy_client(\"channel.telegram\");\n\n        let mut form = reqwest::multipart::Form::new()\n            .text(\"chat_id\", chat_id.to_string())\n            .part(\n                field,\n                reqwest::multipart::Part::bytes(audio_bytes)\n                    .file_name(filename)\n                    .mime_str(mime)?,\n            );\n\n        if let Some(tid) = thread_id {","sourceCodeStart":1459,"sourceCodeEnd":1495,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/telegram.rs#L1459-L1495","documentation":"synthesize_and_send_voice() calls TtsManager::synthesize_opus (which transcodes to OGG/Opus via ffmpeg internally) and guards against a zero-byte result before uploading a voice note. Empty audio means the TTS provider or the ffmpeg transcode stage produced nothing, and the library refuses to send an empty file.","triggerScenarios":"TTS provider returns HTTP 200 with an empty body (quota exhausted, invalid API key coerced to empty upstream, unsupported/emoji-only input text); ffmpeg missing from PATH or failing silently so the transcode writes 0 bytes; empty reply text after markdown stripping.","commonSituations":"Expired TTS provider credentials; ffmpeg absent in a slim Docker image; agent reply reduced to whitespace before voice synthesis; provider model change returning audio under a new field that parsing misses.","solutions":["Verify ffmpeg is installed and on PATH (`ffmpeg -version`); synthesize_opus depends on it for the OGG/Opus transcode.","Check the TTS provider credentials and quota in config, and test tts_manager.synthesize_opus standalone with plain text.","Skip voice and fall back to a text reply when input text is empty or synthesis yields nothing — the caller already logs-and-continues at the TTS error site.","Add provider-response logging upstream so the real error surfaces instead of an empty buffer."],"exampleFix":"// before\nlet audio = tts_manager.synthesize_opus(text).await?;\n\n// after\nif text.trim().is_empty() {\n    return send_text_chunks(text, chat_id, thread_id).await;\n}\nlet audio = tts_manager.synthesize_opus(text).await?;\nif audio.is_empty() {\n    tracing::warn!(\"TTS produced empty audio; falling back to text\");\n    return send_text_chunks(text, chat_id, thread_id).await;\n}","handlingStrategy":"fallback","validationCode":"if text.trim().is_empty() {\n    // no point synthesizing: send text (or skip) instead\n    return send_text_chunks(text, chat_id, thread_id).await;\n}\nlet audio = tts_manager.synthesize_opus(text).await?;","typeGuard":"fn is_speakable(text: &str) -> bool {\n    !text.trim().is_empty()\n}","tryCatchPattern":"match synthesize_and_send_voice(...).await {\n    Err(e) if e.to_string().contains(\"TTS returned empty audio\") => {\n        tracing::warn!(\"voice synthesis empty; sending text instead\");\n        send_text_chunks(&text, chat_id, thread_id).await?;\n    }\n    other => other?,\n}","preventionTips":["Check `ffmpeg -version` in container healthchecks since synthesize_opus depends on it.","Alert on TTS provider auth/quota errors instead of letting them surface as empty buffers.","Guard the pipeline: skip voice for empty/whitespace replies."],"tags":["telegram","tts","ffmpeg","opus","voice-note"],"backgroundTag":"tts-empty-audio","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}