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

  1. Skip synthesis when the text is empty: check before calling and treat it as a no-op, not an error.
  2. Strip whitespace/markers before the emptiness test so whitespace-only leftovers are also skipped.
  3. 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

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


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