{"record":{"id":"676081ce20f683a3","repo":"zeroclaw-labs/zeroclaw","slug":"tts-text-must-not-be-empty","errorCode":null,"errorMessage":"TTS text must not be empty","messagePattern":"TTS text must not be empty","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/tts.rs","lineNumber":1126,"sourceCode":"            bail!(\n                \"Agent has no tts_provider configured. Set \\\n                 `agent.<alias>.tts_provider = \\\"<type>.<alias>\\\"` referencing a \\\n                 [providers.tts.<type>.<alias>] entry.\"\n            );\n        }\n        self.synthesize_with_provider(text, provider_alias, voice)\n            .await\n    }\n\n    /// Synthesize text using a specific dotted-alias model_provider and voice.\n    pub async fn synthesize_with_provider(\n        &self,\n        text: &str,\n        provider_alias: &str,\n        voice: &str,\n    ) -> Result<Vec<u8>> {\n        if text.is_empty() {\n            bail!(\"TTS text must not be empty\");\n        }\n        let char_count = text.chars().count();\n        if char_count > self.max_text_length {\n            bail!(\n                \"TTS text too long ({} chars, max {})\",\n                char_count,\n                self.max_text_length\n            );\n        }\n\n        let tts = self.tts_providers.get(provider_alias).ok_or_else(|| {\n            let available = self.available_providers().join(\", \");\n            ::zeroclaw_log::record!(\n                ERROR,\n                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)\n                    .with_outcome(::zeroclaw_log::EventOutcome::Failure)\n                    .with_attrs(::serde_json::json!({\n                        \"tts_provider\": provider_alias,","sourceCodeStart":1108,"sourceCodeEnd":1144,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/tts.rs#L1108-L1144","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before — forwards whatever survives marker stripping\nlet audio = mgr.synthesize(&cleaned).await?;\n\n// after — skip empties (and whitespace-only) before synthesis\nif cleaned.trim().is_empty() {\n    return Ok(());\n}\nlet audio = mgr.synthesize(&cleaned).await?;","handlingStrategy":"validation","validationCode":"// Guard every call site; also catches whitespace-only leftovers that\n// would pass the provider's byte-emptiness check but waste an API call.\nfn has_speech_content(text: &str) -> bool {\n    !text.trim().is_empty()\n}\n\nif !has_speech_content(&text) {\n    return Ok(()); // nothing to say — skip synthesis\n}","typeGuard":"fn has_speech_text(t: &str) -> bool {\n    !t.trim().is_empty()\n}","tryCatchPattern":null,"preventionTips":["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."],"tags":["tts","validation","input"],"backgroundTag":"input-validation-failed","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}