zeroclaw-labs/zeroclaw · error

TTS returned empty audio

Error message

TTS returned empty audio

What it means

Before uploading synthesized speech, the WhatsApp Web channel verifies the TTS step produced at least one byte and bails on an empty result. The immediately preceding log line (`TTS: synthesized N bytes of audio`) tells you whether the provider really returned zero bytes, distinguishing a provider problem from an upload problem.

Source

Thrown at crates/zeroclaw-channels/src/whatsapp_web.rs:1706

    /// Synthesize text to speech and send as a WhatsApp voice note (static version for spawned tasks).
    #[cfg(feature = "whatsapp-web")]
    async fn synthesize_voice_static(
        client: &whatsapp_rust::Client,
        to: &wacore_binary::jid::Jid,
        text: &str,
        tts_manager: &super::tts::TtsManager,
    ) -> 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),
            &format!("TTS: synthesized {} bytes of audio", audio_len)
        );

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

        use wacore::download::MediaType;
        use whatsapp_rust::upload::UploadOptions;
        let upload = client
            .upload(audio_bytes, MediaType::Audio, UploadOptions::default())
            .await
            .map_err(|e| {
                ::zeroclaw_log::record!(
                    ERROR,
                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)
                        .with_outcome(::zeroclaw_log::EventOutcome::Failure)
                        .with_attrs(::serde_json::json!({"error": format!("{}", e)})),
                    "Failed to upload TTS audio"
                );
                anyhow::Error::msg(format!("Failed to upload TTS audio: {e}"))
            })?;

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Check the preceding log line — `synthesized 0 bytes` confirms a provider-side issue rather than WhatsApp upload failure.
  2. Skip TTS for empty/blank text and send plain text instead.
  3. Verify the TTS provider configuration (voice, language, API key, quota) and test it directly with the same text.

Example fix

// before: hand any text straight to the voice flow
channel.send(&SendMessage::new(voice_marker_for(&text), to)).await?;

// after: only voice non-trivial text
if text.trim().is_empty() {
    return Ok(()); // nothing worth synthesizing
}
channel.send(&SendMessage::new(voice_marker_for(&text), to)).await?;
Defensive patterns

Strategy: fallback

Validate before calling

if text.trim().is_empty() {
    // nothing to synthesize: skip the voice flow entirely
    return Ok(());
}
channel.send(&SendMessage::new(voice_message_for(&text), to)).await

Type guard

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

Try / catch

match send_voice(&channel, &text, to).await {
    Err(e) if e.to_string().contains("TTS returned empty audio") => {
        // degrade to plain text instead of dropping the message
        channel.send(&SendMessage::new(text, to)).await
    }
    other => other,
}

Prevention

When it happens

Trigger: The TTS provider returns success with an empty body/zero bytes for the requested text — silent or whitespace-only input text, an unsupported voice/language combination, or a provider outage/quota error swallowed upstream.

Common situations: Empty message text routed into a voice flow; misconfigured TTS voice or language code; provider API change returning 200 with empty audio; quota exhausted mid-campaign.

Related errors


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