{"record":{"id":"b08f14102eaa22e3","repo":"zeroclaw-labs/zeroclaw","slug":"tts-text-too-long-chars-max","errorCode":null,"errorMessage":"TTS text too long ({} chars, max {})","messagePattern":"TTS text too long \\((.+?) chars, max (.+?)\\)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/tts.rs","lineNumber":1130,"sourceCode":"            );\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,\n                        \"available\": available,\n                    })),\n                \"tts: provider not configured\"\n            );","sourceCodeStart":1112,"sourceCodeEnd":1148,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/tts.rs#L1112-L1148","documentation":"synthesize_with_provider counts Unicode chars (chars().count(), not bytes) and rejects text longer than max_text_length. The limit comes from config.tts.max_text_length and defaults to 4096 when unset or zero. The check runs before any provider call, so oversized input never reaches the upstream API.","triggerScenarios":"Synthesizing a long channel message, article, or transcript in a single call: any text over 4096 chars (or over a lowered tts.max_text_length) bails. Counting is by chars, so CJK/emoji text is measured per character, not per UTF-8 byte.","commonSituations":"Bridging a chat channel to voice notes without chunking: one long paste exceeds the cap. Operators who lower tts.max_text_length to protect API quotas then see previously-fine messages rejected.","solutions":["Chunk the text to at most max_text_length chars per call and synthesize sequentially (keep chunks at char boundaries).","Raise the cap in config if the provider tolerates it: [tts] max_text_length = 8192.","Trim boilerplate/markers from the text before measuring."],"exampleFix":"// before — one call, bails over the 4096-char default cap\nlet audio = mgr.synthesize(&long_post).await?;\n\n// after — split by Unicode chars, synthesize per chunk\nfn split_unicode_chars(text: &str, max: usize) -> Vec<String> {\n    let mut out = Vec::new();\n    let mut cur = String::new();\n    for ch in text.chars() {\n        cur.push(ch);\n        if cur.chars().count() == max {\n            out.push(std::mem::take(&mut cur));\n        }\n    }\n    if !cur.is_empty() { out.push(cur); }\n    out\n}\nfor chunk in split_unicode_chars(&long_post, 4000) {\n    let part = mgr.synthesize(&chunk).await?;\n    send_voice(&part).await?;\n}","handlingStrategy":"validation","validationCode":"// Split on Unicode char boundaries so every chunk satisfies the\n// chars()-based limit; keep chunks under the configured cap.\nfn split_for_tts(text: &str, max_chars: usize) -> Vec<String> {\n    let mut out: Vec<String> = Vec::new();\n    let mut cur = String::new();\n    let mut n = 0usize;\n    for ch in text.chars() {\n        cur.push(ch);\n        n += 1;\n        if n == max_chars {\n            out.push(std::mem::take(&mut cur));\n            n = 0;\n        }\n    }\n    if n > 0 { out.push(cur); }\n    out\n}\n\nfor chunk in split_for_tts(&text, 4000) {\n    let audio = mgr.synthesize(&chunk).await?;\n    send_voice(&audio).await?;\n}","typeGuard":"fn fits_tts_limit(text: &str, max_chars: usize) -> bool {\n    text.chars().count() <= max_chars\n}","tryCatchPattern":null,"preventionTips":["Chunk long text (paragraph or sentence boundaries beat hard char splits) before synthesis.","Set [tts] max_text_length explicitly in config so the limit is an intentional number, not the 4096 default.","Remember the limit counts Unicode chars, not bytes — CJK and emoji text is measured per character."],"tags":["tts","validation","input","length-limit"],"backgroundTag":"input-length-limit","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}