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
- Check the preceding log line — `synthesized 0 bytes` confirms a provider-side issue rather than WhatsApp upload failure.
- Skip TTS for empty/blank text and send plain text instead.
- 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
- Skip voice conversion for blank/whitespace text before triggering the flow.
- Monitor the `TTS: synthesized N bytes` log line; a stream of `0 bytes` means provider trouble, not WhatsApp trouble.
- Smoke-test the TTS provider config (voice, language, quota) in CI.
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
- Recipient cannot be empty
- Recipient `{trimmed}` does not contain a valid phone number
- WhatsApp marker target {} is empty
- Audio file too large ({} bytes, max {MAX_AUDIO_BYTES})
- ElevenLabs voice ID contains invalid characters: {voice}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/d928884545653913.
Report an issue: GitHub.