Skip to content

fix(voice): remove 4000-char speak cap and scale ffplay wait to file duration - #53589

Open
Nomadcxx wants to merge 4 commits into
NousResearch:mainfrom
Nomadcxx:fix/voice-tts-playback-truncation
Open

Nomadcxx wants to merge 4 commits into
NousResearch:mainfrom
Nomadcxx:fix/voice-tts-playback-truncation

Conversation

@Nomadcxx

Copy link
Copy Markdown

What does this PR do?

Voice mode stopped reading long replies early for two stock Hermes bugs: speak_text and cli._voice_speak_response sliced input to 4000 characters before text_to_speech_tool, and play_audio_file killed ffplay after 300 seconds on MP3/OGG files.

This PR removes the hard 4000-char cap, centralizes markdown prep in prepare_voice_tts_text(), adds optional voice.max_tts_chars (default null = defer to provider limits), and scales the ffplay wait from ffprobe duration.

Fixing #50082 alone does not address these voice-layer limits.

Related Issue

Fixes #53587

Related: #50081 (synthesis subprocess timeout, separate layer)

Related PR

Related: #50082 (command-provider timeout mitigation; not a substitute for this change). No overlap in tools/tts_tool.py in this branch.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature
  • Security fix
  • Documentation update
  • Tests (adding or improving test coverage)
  • Refactor
  • New skill

Changes Made

  • hermes_cli/voice.py — voice_max_tts_chars(), strip_markdown_for_voice_tts(), prepare_voice_tts_text(); speak_text uses shared prep
  • cli.py — _voice_speak_response calls prepare_voice_tts_text
  • hermes_cli/config.py — default voice.max_tts_chars: null
  • cli-config.yaml.example — document voice.max_tts_chars
  • tools/voice_mode.py — _audio_file_duration_seconds(); play_audio_file wait = probed duration + 30s (3600s fallback)
  • scripts/repro_voice_tts_playback.py — minimal repro (FAIL on main, PASS on this branch)
  • tests/hermes_cli/test_voice_wrapper.py — TestVoiceMaxTtsChars, updated speak cap regression
  • tests/tools/test_voice_cli_integration.py — test_long_text_not_pretruncated
  • tests/tools/test_voice_mode.py — TestPlayAudioFfplayWaitTimeout

Explicitly excluded: tools/tts_tool.py / _run_command_tts idle timeout (#50082)

How to Test

  1. python scripts/repro_voice_tts_playback.py — expect PASS on this branch, FAIL on main
  2. scripts/run_tests.sh tests/hermes_cli/test_voice_wrapper.py tests/tools/test_voice_mode.py tests/tools/test_voice_cli_integration.py
  3. Manual: tts.provider: edge, /voice on, /voice tts, reply >5000 chars — confirm no 4000-char slice before text_to_speech_tool
  4. Manual: synthesize >5 min MP3, play_audio_file(path) — confirm playback reaches EOF

Checklist

Code

Documentation & Housekeeping

  • Updated cli-config.yaml.example for voice.max_tts_chars
  • Cross-platform impact considered (ffprobe/ffplay via shutil.which; WAV path unchanged)

Platforms tested

  • Linux (Arch)

@alt-glitch alt-glitch added type/bug Something isn't working tool/tts Text-to-speech and transcription comp/cli CLI entry point, hermes_cli/, setup wizard P3 Low — cosmetic, nice to have labels Jun 27, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the focused voice-layer fix. The reported regressions remain on current main: cli.py:11375 and hermes_cli/voice.py:784 pre-truncate to 4000 characters, while tools/voice_mode.py:1102 applies the 300-second system-player wait. The provider-aware downstream limit already exists at tools/tts_tool.py:2188.

Problems

  • voice.max_tts_chars is user-facing configuration, but the canonical voice configuration block at website/docs/user-guide/features/voice-mode.md:386-394 is not updated. cli-config.yaml.example is useful, but it is not the feature reference users are directed to.

Suggested changes

  • Document voice.max_tts_chars in that Voice Mode config block, including its null/omitted behavior and positive-value semantics.

This is an automated hermes-sweeper review.

Comment thread hermes_cli/config.py Outdated
@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users labels Jul 15, 2026
Nomadcxx added 2 commits July 24, 2026 22:46
Voice mode applied a hard 4000-character limit before text_to_speech_tool
and killed ffplay after 300s on MP3/OGG playback. Defer length limits to
provider caps unless voice.max_tts_chars is set; probe file duration for
ffplay wait. Adds regression tests and repro script.

Fixes NousResearch#53587
Preserve the provider-deferred text limit and duration-aware playback while integrating the newer normalization, streaming, and config-default paths.
@andrexibiza

Copy link
Copy Markdown
Contributor

Verification comment (Vox Lockin lane 10 — adversarial check)

Verified this PR against current origin/main (70db671). Premise CONFIRMED: the 4000-char pre-cap is still in speak_text (hermes_cli/voice.py prepare_spoken_text(text, max_chars=4000) + legacy text[:4000]) and in cli.py:_voice_speak_response; play_audio_file still uses proc.wait(timeout=300).

What this PR gets right (head 17be93cb86):

  • Removing the hard 4000-char pre-cap and deferring to the provider layer is exactly the right direction.
  • prepare_voice_tts_text centralizing markdown prep + optional voice.max_tts_chars is a clean design.
  • Scaling the ffplay wait from probed duration (+30s slack, 1h fallback) is the right fix for the 300s kill.
  • Solid regression tests + a repro script.

Residual gaps (why a class-completion PR was needed):

  1. Provider-level truncation remains. Even after removing the speak_text cap, text_to_speech_tool still truncates over-cap text to the per-provider request limit (tools/tts_tool.py _resolve_max_text_length slice) — a 20000-char xAI reply still loses its tail at 15000. The class mandate is no arbitrary truncation of user-facing speech, which requires splitting (fix(tts): split long speech by provider and platform limits #17973), not just cap removal.
  2. Streaming per-sentence truncation remains. stream_tts_to_speaker slices a single over-cap sentence (cleaned[:stream_max_len]).
  3. Status: mergeable_state=blocked (no CI checks reported on the branch), rebaseable=false — not mergeable as-is.

Composition: I shipped lane 10's class-completion PR #78234 which builds on this PR's approach (credit: "salvaged from #53589, authored by @Nomadcxx") and adds provider-level splitting + concatenation, streaming sentence splitting, the websockets v15 header fix, and regression tests. If #53589 merges first, #78234's overlapping hunks are trivially reconcilable; if not, #78234 carries the class to completion with attribution.

@Nomadcxx

Copy link
Copy Markdown
Author

@andrexibiza I merged current main into this branch and resolved the remaining conflicts. #53589 is mergeable again at 0d06a85fa.

Since your check, #81766 added provider-level splitting and removed the 4,000-character caller caps. Current main still slices over-cap sentences in the streaming path, so that part of #78234 remains relevant.

This PR keeps the optional voice.max_tts_chars behavior and replaces the fixed 300-second system-player wait without replacing the newer long-form or full-duplex paths. Verification passed 157 focused voice/TTS tests, with two macOS-only skips, and all three repro checks.

@andrexibiza

Copy link
Copy Markdown
Contributor

@andrexibiza I merged current main into this branch and resolved the remaining conflicts. #53589 is mergeable again at 0d06a85fa.

Thanks — I verified the current state, and this resolves the concern I raised.

I’m updating my position accordingly: #53589 should stay the canonical PR for the work it owns, and I’ll treat the remaining streaming-sentence issue as a separate reconciliation problem rather than something that supersedes this branch.

I also appreciate you taking the time to rebase, resolve the conflicts, and come back with a precise current-state update. This is the kind of handoff I want the interlock process to support.

@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference, author can ignore or act on any point.

fix(voice): remove 4000-char speak cap and scale ffplay wait to file duration

Removing the arbitrary 4000-char pre-truncation (deferring to the provider cap, with an optional voice.max_tts_chars override) plus duration-scaled ffplay waits is the right fix for #50081, with good test coverage on both sides. Observations:

  1. tools/voice_mode.py _play_audio_file_impl — the wait timeout is now duration + 30s with a 3600s fallback when the duration can't be probed. The old flat 300s cap was the bug for long TTS, but removing the ceiling entirely means a wedged ffplay (codec hang, zombie) now blocks playback (and the _playback_lock) for up to an hour before TimeoutExpired fires. Consider an absolute ceiling for the unknown-duration case (e.g. cap the fallback at a large-but-bounded value, or use the provider's max-audio estimate) so a hung process can't monopolize the voice layer.
  2. _audio_file_duration_seconds — spawns an ffprobe subprocess (up to 10s timeout) before every playback, even for short clips where the old 300s wait was never a problem. Latency is ~tens of ms for local files, so it's acceptable, but if playback latency ever matters for interactive voice, probing lazily (only when the file is long enough to threaten the old cap) would avoid the probe on the common short path.
  3. hermes_cli/voice.py prepare_voice_tts_text — the voice.max_tts_chars cap truncates at tts_text[:cap], which can cut mid-word/mid-sentence (same behavior as the old 4000 cap — unchanged, just configurable). A word-boundary-aware truncation (e.g. cut at the last whitespace ≤ cap) would produce cleaner TTS; optional polish.
  4. voice_max_tts_chars — int(raw) accepts numeric strings ("5000"), rejects bools/zero/negative — good fail-closed behavior. Note "5000.5" would raise ValueError → caught → returns None (defer), consistent.
  5. scripts/repro_voice_tts_playback.py — the repro uses inspect.getsource string-greps ("text[:4000]" in source). It's a manual repro script, not part of the suite, so acceptable; just be aware it will false-flag if a future refactor renames prepare_voice_tts_text (the script is a dev aid, not a contract).
  6. The streaming path (_speak_text_streaming) now receives the capped text — good consistency. Tests cover non-streaming; the streaming path shares prepare_voice_tts_text so coverage is adequate.

@Nomadcxx

Copy link
Copy Markdown
Author

Checked each item against the current branch. The unknown-duration path already times out after 3,600 seconds. _playback_lock only protects reads and writes to _active_playback; _play_audio_file_impl releases it before proc.wait(), so stop_playback() can terminate a stalled player.

I am keeping the probe and exact slice. The probe supplies the duration needed to avoid cutting off long files, and voice.max_tts_chars is an explicit character cap. The inspect-based script remains a manual repro rather than a test contract. I do not see a code change needed here.

@alt-glitch alt-glitch removed the sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows label Aug 15, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P3 Low — cosmetic, nice to have sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades tool/tts Text-to-speech and transcription type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

voice-mode TTS truncates long replies: 4000-char pre-cap in speak_text and 300s ffplay wait

5 participants