zeroclaw-labs/zeroclaw · error · anyhow::Error
TTS text must not be empty
Error message
TTS text must not be empty
What it means
synthesize_with_provider rejects an empty text before any provider is consulted. All provider backends (OpenAI, ElevenLabs, Google, Edge subprocess, Piper) funnel through this single guard, so an empty input fails fast without burning an API call or spawning a subprocess.
Source
Thrown at crates/zeroclaw-channels/src/tts.rs:1126
bail!(
"Agent has no tts_provider configured. Set \
`agent.<alias>.tts_provider = \"<type>.<alias>\"` referencing a \
[providers.tts.<type>.<alias>] entry."
);
}
self.synthesize_with_provider(text, provider_alias, voice)
.await
}
/// Synthesize text using a specific dotted-alias model_provider and voice.
pub async fn synthesize_with_provider(
&self,
text: &str,
provider_alias: &str,
voice: &str,
) -> Result<Vec<u8>> {
if text.is_empty() {
bail!("TTS text must not be empty");
}
let char_count = text.chars().count();
if char_count > self.max_text_length {
bail!(
"TTS text too long ({} chars, max {})",
char_count,
self.max_text_length
);
}
let tts = self.tts_providers.get(provider_alias).ok_or_else(|| {
let available = self.available_providers().join(", ");
::zeroclaw_log::record!(
ERROR,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
.with_outcome(::zeroclaw_log::EventOutcome::Failure)
.with_attrs(::serde_json::json!({
"tts_provider": provider_alias,View on GitHub (pinned to 88bb9c8533)
Solutions
- Skip synthesis when the text is empty: check before calling and treat it as a no-op, not an error.
- Strip whitespace/markers before the emptiness test so whitespace-only leftovers are also skipped.
- If it surfaces in production, log the source message id to find which producer forwards empty payloads.
Example fix
// before — forwards whatever survives marker stripping
let audio = mgr.synthesize(&cleaned).await?;
// after — skip empties (and whitespace-only) before synthesis
if cleaned.trim().is_empty() {
return Ok(());
}
let audio = mgr.synthesize(&cleaned).await?; Defensive patterns
Strategy: validation
Validate before calling
// Guard every call site; also catches whitespace-only leftovers that
// would pass the provider's byte-emptiness check but waste an API call.
fn has_speech_content(text: &str) -> bool {
!text.trim().is_empty()
}
if !has_speech_content(&text) {
return Ok(()); // nothing to say — skip synthesis
} Type guard
fn has_speech_text(t: &str) -> bool {
!t.trim().is_empty()
} Prevention
- Skip synthesis for empty/whitespace text instead of relying on the error to surface it.
- After stripping attachment markers ([IMAGE:...] etc.), re-check emptiness before TTS.
- Treat this error in production logs as a producer bug: find and fix the caller forwarding empty payloads.
When it happens
Trigger: Passing "" to synthesize/synthesize_with_voice/synthesize_with_provider: channel message content that is empty after attachment-marker stripping, an upstream tool returning an empty string, or a test fixture feeding "". Note the check is byte-emptiness only — a whitespace-only string passes this guard and is sent to the provider.
Common situations: A channel handler strips [IMAGE:...] markers and forwards the now-empty remainder to TTS. Voice-note pipelines that synthesize every inbound message hit it whenever a message is attachments-only.
Related errors
- TTS text too long ({} chars, max {})
- ElevenLabs voice ID contains invalid characters: {voice}
- Edge TTS binary_path must be a bare command name without pat
- Edge TTS binary_path must be one of {:?}, got: {raw_path}
- TTS returned empty audio
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/676081ce20f683a3.
Report an issue: GitHub.