{"record":{"id":"275769ae2b0bce32","repo":"jamiepine/voicebox","slug":"unable-to-split-unstable-tts-output-for-retry","errorCode":null,"errorMessage":"Unable to split unstable TTS output for retry","messagePattern":"Unable to split unstable TTS output for retry","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"backend/utils/chunked_tts.py","lineNumber":275,"sourceCode":"    ) -> tuple[np.ndarray, int]:\n        chunk_audio, chunk_sr = await backend.generate(\n            chunk_text,\n            voice_prompt,\n            language,\n            chunk_seed,\n            instruct,\n        )\n\n        if runaway_detector is not None and runaway_detector(chunk_audio, chunk_sr):\n            if retry_depth >= MAX_RUNAWAY_RETRIES or len(chunk_text) <= MIN_RUNAWAY_RETRY_CHARS:\n                raise RuntimeError(\n                    \"TTS output remained unstable after retrying smaller text chunks\"\n                )\n\n            retry_max_chars = max(MIN_RUNAWAY_RETRY_CHARS, len(chunk_text) // 2)\n            retry_chunks = split_text_into_chunks(chunk_text, retry_max_chars)\n            if len(retry_chunks) <= 1:\n                raise RuntimeError(\"Unable to split unstable TTS output for retry\")\n\n            logger.warning(\n                \"Detected unstable TTS output for %d chars; retrying as %d smaller chunks\",\n                len(chunk_text),\n                len(retry_chunks),\n            )\n            retry_audio: list[np.ndarray] = []\n            for i, retry_text in enumerate(retry_chunks):\n                retry_seed = (\n                    chunk_seed + ((retry_depth + 1) * 1000) + i\n                    if chunk_seed is not None\n                    else None\n                )\n                audio, sample_rate = await generate_one(\n                    retry_text,\n                    retry_seed,\n                    retry_depth + 1,\n                )","sourceCodeStart":257,"sourceCodeEnd":293,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/utils/chunked_tts.py#L257-L293","documentation":"Raised by generate_one() in backend/utils/chunked_tts.py when the runaway detector flagged a chunk, the retry budget and minimum-size checks passed, but split_text_into_chunks(chunk_text, retry_max_chars) returned only a single chunk. The retry strategy depends on splitting the unstable chunk into strictly smaller pieces; if the splitter cannot subdivide it (e.g. no sentence/whitespace boundaries within the budget), retry is impossible and the function aborts.","triggerScenarios":"A chunk whose text has no splittable boundary under retry_max_chars — a single very long word/URL/token run, or text without sentence punctuation/whitespace where the splitter cannot break it; chunk length just over MIN_RUNAWAY_RETRY_CHARS so halving still yields one piece per the splitter's rules.","commonSituations":"Inputs containing very long unbroken token strings (URLs, base64, CJK without spaces, code); text with no terminal punctuation that the sentence-aware splitter keys on; languages without whitespace word boundaries.","solutions":["Pre-split the input on whitespace or character count before passing to chunked TTS so no single chunk is unsplittable.","Strip or break up long unbroken tokens (URLs, base64) in the source text.","Provide a custom trim_fn or preprocessor that normalizes problematic text.","For CJK/no-space languages, configure the chunker to split on characters, not sentence/word boundaries."],"exampleFix":"// before: a chunk that is one long token run cannot be split\naudio, sr = await generate_chunked_tts(backend, very_long_token_run, voice_prompt, ...)\n// after: break long token runs before synthesis\ntext = re.sub(r'(\\S{200})', r'\\1 ', text)  // insert breakable spaces\naudio, sr = await generate_chunked_tts(backend, text, voice_prompt, ...)","handlingStrategy":"validation","validationCode":"import re\n\ndef is_splittable(text: str, max_chars: int) -> bool:\n    # ensure there is at least one breakable boundary within max_chars\n    broken = re.findall(r'.{1,' + str(max_chars) + r'}', text)\n    return len(broken) > 1","typeGuard":null,"tryCatchPattern":"try:\n    audio, sr = await generate_chunked_tts(backend, text, voice_prompt, ...)\nexcept RuntimeError as e:\n    if 'Unable to split unstable TTS output' in str(e):\n        # insert breakable spaces into long token runs and retry\n        text = re.sub(r'(\\S{120})', r'\\1 ', text)\n        audio, sr = await generate_chunked_tts(backend, text, voice_prompt, ...)\n    else:\n        raise","preventionTips":["Pre-process input to break long unbroken token runs (URLs, base64, CJK) before synthesis.","For languages without whitespace, configure the chunker for character-level splits.","Sanitize text to insert spaces after very long non-space sequences."],"tags":["tts","runaway","chunking","splitting","voicebox"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}