{"record":{"id":"579f71527902b405","repo":"jamiepine/voicebox","slug":"tts-output-remained-unstable-after-retrying-smalle","errorCode":null,"errorMessage":"TTS output remained unstable after retrying smaller text chunks","messagePattern":"TTS output remained unstable after retrying smaller text chunks","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"backend/utils/chunked_tts.py","lineNumber":268,"sourceCode":"    -------\n    (audio, sample_rate) : Tuple[np.ndarray, int]\n    \"\"\"\n    async def generate_one(\n        chunk_text: str,\n        chunk_seed: int | None,\n        retry_depth: int = 0,\n    ) -> 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","sourceCodeStart":250,"sourceCodeEnd":286,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/utils/chunked_tts.py#L250-L286","documentation":"Raised by generate_one() in backend/utils/chunked_tts.py when the runaway detector still flags a chunk's audio as unstable after the retry budget is exhausted — specifically when retry_depth >= MAX_RUNAWAY_RETRIES (2) or the chunk is already at/below MIN_RUNAWAY_RETRY_CHARS (100). It means the TTS backend keeps producing speech-then-silence-then-hallucination shapes even on small inputs, so further splitting is futile.","triggerScenarios":"A pathological input chunk that triggers model EOS-miss/hallucination on every generation; a voice prompt that destabilizes the model; an engine bug producing runaway output; very long single sentences that cannot be split below 100 chars; deterministic seed always yielding runaway for the given text.","commonSituations":"Chatterbox/tada engines hallucinating after long silences; certain emoji/special-character inputs confusing the model; a cloned voice prompt with bad reference audio; model checkpoint regression; chunk that is one giant unbreakable token run.","solutions":["Shorten or rephrase the input text around the failing region and retry.","Change the seed (per-request seed) so a different decoding path is taken.","Inspect the voice prompt / reference audio quality; re-clone from cleaner samples if the cloned voice destabilizes generation.","If recurring, report the failing text + engine + model version as a model bug; consider disabling the runaway_detector only if you accept unbounded output risk.","Increase MAX_RUNAWAY_RETRIES in chunked_tts.py only as a last resort (it raises latency, not quality)."],"exampleFix":"// before: long input always trips runaway after retries\naudio, sr = await generate_chunked_tts(backend, long_text, voice_prompt, ...)\n// after: split the input externally and use a different seed\nfor part in manually_split(long_text):\n    audio, sr = await generate_chunked_tts(backend, part, voice_prompt, seed=new_seed, ...)","handlingStrategy":"fallback","validationCode":"from backend.utils.chunked_tts import MIN_RUNAWAY_RETRY_CHARS, DEFAULT_MAX_CHUNK_CHARS\nimport re\n\ndef chunk_is_safe_to_retry(text: str) -> bool:\n    # avoid feeding chunks that are already at the retry floor or unsplittable\n    if len(text) <= MIN_RUNAWAY_RETRY_CHARS:\n        return False\n    if len(text) > DEFAULT_MAX_CHUNK_CHARS:\n        return False\n    # must contain at least one breakable boundary so a retry split yields >1 piece\n    return len(re.findall(r'\\S{1,120}', text)) > 1 or ' ' in text","typeGuard":null,"tryCatchPattern":"try:\n    audio, sr = await generate_chunked_tts(backend, text, voice_prompt, ...)\nexcept RuntimeError as e:\n    if 'remained unstable' in str(e):\n        # fall back to a different seed or shorter pre-split text\n        audio, sr = await generate_chunked_tts(backend, shorten(text), voice_prompt, seed=alt_seed, ...)\n    else:\n        raise","preventionTips":["Keep input chunks reasonably sized and avoid pathological long sentences.","Vary the seed per request so a deterministic bad decoding path is not always hit.","Use clean reference audio for cloned voices; bad prompts destabilize generation.","Do not disable the runaway detector — it prevents unbounded output cost."],"tags":["tts","runaway","chunking","model-stability","voicebox"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}