zeroclaw-labs/zeroclaw · error · anyhow::Error

TTS returned empty audio

Error message

TTS returned empty audio

What it means

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.

Source

Thrown at crates/zeroclaw-channels/src/telegram.rs:1477

    async fn synthesize_and_send_voice(
        api_base: &str,
        bot_token: &str,
        chat_id: &str,
        thread_id: Option<&str>,
        text: &str,
        tts_manager: &crate::tts::TtsManager,
    ) -> anyhow::Result<()> {
        let audio_bytes = tts_manager.synthesize_opus(text).await?;
        let audio_len = audio_bytes.len();
        ::zeroclaw_log::record!(
            INFO,
            ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
                .with_attrs(::serde_json::json!({"audio_len": audio_len})),
            "synthesized bytes of audio"
        );

        if audio_bytes.is_empty() {
            anyhow::bail!("TTS returned empty audio");
        }

        // synthesize_opus already transcodes to OGG/Opus via ffmpeg internally
        let (method, field, filename, mime) = telegram_audio_send_spec("opus")?;

        let url = format!("{api_base}/bot{bot_token}/{method}");
        let client = zeroclaw_config::schema::build_runtime_proxy_client("channel.telegram");

        let mut form = reqwest::multipart::Form::new()
            .text("chat_id", chat_id.to_string())
            .part(
                field,
                reqwest::multipart::Part::bytes(audio_bytes)
                    .file_name(filename)
                    .mime_str(mime)?,
            );

        if let Some(tid) = thread_id {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Verify ffmpeg is installed and on PATH (`ffmpeg -version`); synthesize_opus depends on it for the OGG/Opus transcode.
  2. Check the TTS provider credentials and quota in config, and test tts_manager.synthesize_opus standalone with plain text.
  3. 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.
  4. Add provider-response logging upstream so the real error surfaces instead of an empty buffer.

Example fix

// before
let audio = tts_manager.synthesize_opus(text).await?;

// after
if text.trim().is_empty() {
    return send_text_chunks(text, chat_id, thread_id).await;
}
let audio = tts_manager.synthesize_opus(text).await?;
if audio.is_empty() {
    tracing::warn!("TTS produced empty audio; falling back to text");
    return send_text_chunks(text, chat_id, thread_id).await;
}
Defensive patterns

Strategy: fallback

Validate before calling

if text.trim().is_empty() {
    // no point synthesizing: send text (or skip) instead
    return send_text_chunks(text, chat_id, thread_id).await;
}
let audio = tts_manager.synthesize_opus(text).await?;

Type guard

fn is_speakable(text: &str) -> bool {
    !text.trim().is_empty()
}

Try / catch

match synthesize_and_send_voice(...).await {
    Err(e) if e.to_string().contains("TTS returned empty audio") => {
        tracing::warn!("voice synthesis empty; sending text instead");
        send_text_chunks(&text, chat_id, thread_id).await?;
    }
    other => other?,
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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