fix(tts): never truncate spoken replies — split long text and scale playback waits - #78234
Open
andrexibiza wants to merge 3 commits into
Open
andrexibiza wants to merge 3 commits into
andrexibiza wants to merge 3 commits into
Conversation
…ayback waits The streaming-TTS truncation class had four silent user-facing failure modes on main: 1. speak_text and cli._voice_speak_response pre-capped every reply at 4000 characters, so long answers were cut off mid-sentence (NousResearch#53587). 2. text_to_speech_tool truncated over-cap text to the provider limit (OpenAI 4096, xAI 15k, ...) with only a log line, dropping the tail of user-facing speech (NousResearch#17973). 3. The streaming speaker truncated a single over-cap sentence instead of splitting it, losing the end of run-on replies. 4. play_audio_file killed ffplay after a flat 300s, cutting long TTS files mid-playback. This change eliminates the class: - speak_text and _voice_speak_response now pass the full prepared text; length enforcement is deferred to the provider layer, which splits. - text_to_speech_tool splits over-cap text into provider-safe chunks (sentence-boundary aware, hard-slicing run-on words) and concatenates the per-chunk audio with ffmpeg into the single requested output file (salvaged from NousResearch#17973, authored by @TKCen). No content is dropped; without ffmpeg it degrades to the first chunk with a warning. - stream_tts_to_speaker splits an oversized sentence into cap-sized parts and prefetches each, so run-on replies are spoken in full. - play_audio_file probes the file duration (ffprobe) and waits duration + 30s slack instead of a flat 300s (salvaged from NousResearch#53589, authored by @Nomadcxx). - XAI streaming TTS now passes additional_headers for websockets v15 (extra_headers was removed upstream; salvaged from NousResearch#75201, authored by @pluton74mac). Regression tests cover splitting (no content loss, run-on words, concat + cleanup, single-chunk short path), speak_text no longer pre-capping at 4000, streaming oversized-sentence splitting, the duration-scaled ffplay wait, and the websockets v15 header kwarg. Fixes NousResearch#53587 Fixes NousResearch#17973 Signed-off-by: andrexibiza <84248988+andrexibiza@users.noreply.github.com>
This was referenced Aug 4, 2026
Open
The NousResearch#53587 duration probe added to voice_mode playback spawns via subprocess.run, whose internal Popen collides with tests that mock subprocess.Popen: the env-scrub test captured the probe's env-less call as the player call (env=None -> assertion fail), and the macOS policy test's fake proc lacks stdout/stderr, raising AttributeError that the outer handler misread as a failed player. - probe catches Exception broadly: unknown duration -> fallback wait; it can never raise or abort a successful playback - env-scrub test identifies the system-player call by its flags instead of assuming a single Popen call Fixes the two CI failures on this PR (slices 2/8 + 7/8). Pre-existing Windows-local failures (PulseSocket/WSL, hybrid-prefetch timing) verified identical on pristine main via stash-test. Signed-off-by: andrexibiza <84248988+andrexibiza@users.noreply.github.com>
Five test_voice_mode failures + one tts_streaming failure, all Windows- local, all in the platform-mocking/timer-resolution class documented in the repo's own test conventions: - TestPulseSocketReachable: AF_UNIX does not exist on Windows Python -> skipif(win32); the feature (PulseAudio socket reachability) is POSIX-only. - test_wsl_without_pulse_blocks_voice: on Windows, powershell.exe exists, so _wsl_powershell_tts_available() is True and the code (correctly) downgrades to a notice. The test simulates WSL without that fallback, so pin _wsl_powershell_tts_available to False regardless of host platform. - TestWSL2PowerShellFallback (both): the PowerShell fallback is gated on platform.system() == "Linux" (WSL IS Linux), but the tests only mocked _is_wsl2_env. On a Windows runner the gate never opens. Patch platform.system -> "Linux" (+ pin _import_audio ImportError in the temp-filename test so a host with sounddevice installed cannot take the playback path early) — the repo's documented sys.platform/platform.system patch-together pattern. - test_hybrid_prefetch_fires_http_immediately: time.monotonic() has ~15.6ms tick resolution on Windows; two prefetch-adjacent starts land on the same tick and break the strict-ordering assert. Record with time.perf_counter() (QPC, high-res) — ordering intent unchanged. Result: tests/tools/test_voice_mode.py + test_tts_streaming.py fully green on Windows (111 passed, 2 skipped). Full campaign voice surface: 300 passed, 2 skipped; the one remaining local failure (test_stderr_progress_extends_beyond_timeout) is fixed by NousResearch#78221 pending main-merge. Signed-off-by: andrexibiza <84248988+andrexibiza@users.noreply.github.com>
Contributor
Author
CI failures (slices 2/8 + 7/8) — root-caused and fixed (
|
| Test | Root cause | Fix |
|---|---|---|
TestPulseSocketReachable ×2 |
socket.AF_UNIX doesn't exist on Windows Python |
skipif(win32) — feature is POSIX-only |
test_wsl_without_pulse_blocks_voice |
Windows has real powershell.exe, so the PowerShell TTS fallback is "available" and the code correctly downgrades to a notice |
pin _wsl_powershell_tts_available → False (the scenario under test) |
TestWSL2PowerShellFallback ×2 |
fallback gated on platform.system() == "Linux" (WSL IS Linux) but tests only mocked _is_wsl2_env |
patch platform.system → "Linux" (repo's documented sys.platform/platform.system pattern) |
test_hybrid_prefetch_fires_http_immediately |
time.monotonic() ~15.6ms tick on Windows; prefetch-adjacent starts land on the same tick |
record with time.perf_counter() (QPC, high-res) |
Verification
- Both files: 111 passed, 2 skipped (the PulseSocket guards) on Windows.
- Full campaign voice surface (14 files, incl. transcription_tools, wake_word, stt, gateway streaming): 300 passed, 2 skipped.
- The one remaining local failure (
test_stderr_progress_extends_beyond_timeout) is the pre-existing timing test fixed by test(stt): stabilize idle-timeout progress test against spawn latency (pre-existing flake on Windows + loaded CI) #78221 (already open, CI-green) — this branch picks it up when it merges main.
CI is re-running on the new head (277693ee42).
This was referenced Aug 4, 2026
19 tasks
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related #17973 #53587 #53589 #75201
What changed and why
The streaming-TTS truncation class had four silent user-facing failure modes on main:
speak_textandcli._voice_speak_response— every voice-mode/TUI reply was sliced to 4000 characters before synthesis, so long answers were cut off mid-sentence (#53587).text_to_speech_tool— over-cap text was truncated to the per-provider request limit (OpenAI 4096, xAI 15000, MiniMax 10000, …) with only a log line, silently dropping the tail of user-facing speech (#17973).cleaned[:stream_max_len]) instream_tts_to_speaker, losing the end of run-on replies.play_audio_filekilled ffplay after 300 seconds, cutting long TTS files mid-playback even when synthesis succeeded.The fix
speak_textand_voice_speak_responsenow pass the full prepared text; length enforcement is deferred to the provider layer, which now splits instead of truncating.text_to_speech_toolsplits over-cap text into provider-safe chunks (sentence-boundary aware; run-on words hard-sliced so no content is ever dropped) and concatenates the per-chunk audio with ffmpeg into the single requested output file. Without ffmpeg it degrades to the first chunk with a loud warning (never worse than the old behavior).play_audio_fileprobes the file duration (ffprobe) and waitsduration + 30sslack instead of a flat 300s cap.additional_headers(theextra_headerskwarg was removed in websockets 14/15 — this path was crashing on every streaming xAI synthesis).How to test
Expected: all of the above pass except the known pre-existing timing-flaky
test_hybrid_prefetch_fires_http_immediately(fails on pristineorigin/maintoo — it's an environment-timing test, not related to this change; verified against a clean checkout).Platforms tested
Why this matters to users
Voice mode and TUI TTS used to silently stop reading long replies at 4000 characters, drop the tail of anything over the provider's request cap, cut playback at 300 seconds, and crash xAI streaming entirely under websockets v15. After this change, whatever the model says is spoken in full — split into provider-safe chunks and reassembled — and playback waits as long as the audio actually is.
Credits
additional_headersfix salvaged from #75201, authored by @pluton74mac.Fixes #53587
Fixes #17973
Part of #40010
Part of #78207
Part of #79890