Skip to content

fix(voice): add WSL2 PowerShell audio fallback for TTS playback - #17608

Closed
ygd58 wants to merge 2 commits into
NousResearch:mainfrom
ygd58:fix/wsl2-tts-audio-routing
Closed

fix(voice): add WSL2 PowerShell audio fallback for TTS playback#17608
ygd58 wants to merge 2 commits into
NousResearch:mainfrom
ygd58:fix/wsl2-tts-audio-routing

Conversation

@ygd58

@ygd58 ygd58 commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Problem

WSL2 does not expose Linux audio devices by default, causing TTS playback to fail silently — files are generated but no sound plays through Windows speakers.

Fix

When running in WSL2 and powershell.exe is available, add a PowerShell SoundPlayer fallback:

  1. Detect Windows %TEMP% dir dynamically via cmd.exe + wslpath (no hardcoded username)
  2. Convert MP3 → WAV via ffmpeg to a Windows-accessible temp path
  3. Play via PowerShell SoundPlayer.PlaySync()
  4. Clean up temp WAV after playback

Falls back to ffplay/aplay if PowerShell is unavailable. No new dependencies required.

Fixes #17573

WSL2 does not expose Linux audio devices (ALSA/PulseAudio) by default,
causing play_audio_file() to fail silently — files are generated but
no sound plays through Windows speakers (issue NousResearch#17573).

When running in WSL2 (detected via /proc/version) and powershell.exe
is available, add a PowerShell SoundPlayer fallback as the first
player option:
1. Detect Windows %TEMP% dir dynamically via cmd.exe + wslpath
   (avoids hardcoding username in path)
2. Convert MP3 -> WAV via ffmpeg to a Windows-accessible temp path
3. Play via PowerShell SoundPlayer.PlaySync()
4. Clean up temp WAV after playback

Falls back to ffplay/aplay if PowerShell path is unavailable or
conversion fails. No new dependencies required beyond ffmpeg (already
installed by Hermes installer).

Fixes NousResearch#17573
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have tool/tts Text-to-speech and transcription labels Apr 29, 2026
WSL2 does not expose Linux audio devices by default, causing
play_audio_file() to fail silently (issue NousResearch#17573).

When running in WSL2 and powershell.exe is available, add a
PowerShell SoundPlayer fallback:
1. Detect Windows %TEMP% dir dynamically via cmd.exe + wslpath
2. Convert audio to WAV via ffmpeg to a Windows-accessible path
3. Play via PowerShell SoundPlayer.PlaySync()
4. Clean up temp WAV after playback

Falls back to ffplay/aplay if PowerShell path setup fails.
No new dependencies required beyond ffmpeg (already installed).

Fixes NousResearch#17573
@ygd58

ygd58 commented Apr 29, 2026

Copy link
Copy Markdown
Contributor Author

The test failure (ImportError: normalize_whatsapp_identifier) is unrelated to this PR — pre-existing failure on main caused by a missing export in gateway/whatsapp_identity.py. This PR only modifies tools/voice_mode.py.

@ygd58

ygd58 commented Apr 29, 2026

Copy link
Copy Markdown
Contributor Author

Regarding the Supply Chain Audit flag: the audit is likely triggered by the cmd.exe, powershell.exe, and wslpath subprocess calls added for WSL2 audio routing.

These are safe system calls:

  • cmd.exe /c echo %TEMP% — reads Windows temp directory path only
  • wslpath — converts between WSL and Windows paths (built-in WSL utility)
  • powershell.exe -NoProfile -Command SoundPlayer — plays a local WAV file

All calls are gated behind _is_wsl and shutil.which("powershell.exe") — they only run in WSL2 environments where these binaries exist. No network calls, no data exfiltration, no eval. Happy to refactor if there is a preferred pattern for WSL subprocess calls in this codebase.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for turning the user workaround into an in-process fallback. The current main still only tries ffplay/aplay in tools/voice_mode.py:1085-1118, so the underlying gap remains.

Problems

  • The reported /voice on then /voice tts path remains blocked before playback: WSL without PULSE_SERVER is rejected in tools/voice_mode.py:193-206, cli.py:11430-11435 returns, and /voice tts requires enabled voice mode at cli.py:11513-11517.
  • The new shell player at tools/voice_mode.py:928 cannot fall through after an ffmpeg/PowerShell failure. play_audio_file() returns True after proc.wait() without checking its exit status (tools/voice_mode.py:1102-1105).
  • The fixed hermes-tts.wav path at tools/voice_mode.py:911 collides across Hermes processes and is only removed on full pipeline success.

Suggested changes

  • Make the intended TTS-only WSL flow reachable through the environment/CLI gate, or explicitly scope the fallback to a reachable path.
  • Check player exit status, use a unique temp WAV, and clean it in an unconditional cleanup path.
  • Add mocked WSL success, failure-fallback, collision, and cleanup tests in tests/tools/test_voice_mode.py.

Automated hermes-sweeper review.

Comment thread tools/voice_mode.py
stderr=subprocess.DEVNULL, timeout=3,
).decode(errors="replace").strip()
if _win_tmp_wsl:
_wsl_wav = os.path.join(_win_tmp_wsl, "hermes-tts.wav")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This fixed %TEMP% filename is shared by every Hermes process for the same Windows user. Concurrent TTS playback can overwrite or remove another process's WAV; generate a unique temporary filename and arrange unconditional cleanup.

Comment thread tools/voice_mode.py
+ " && "
+ shlex.join(["rm", "-f", _wsl_wav])
)
players.insert(0, ["sh", "-c", _ps_cmd])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A nonzero ffmpeg or PowerShell pipeline exit will not reach ffplay/aplay: play_audio_file() returns True after proc.wait() without checking its status. Treat a nonzero player exit as failure and continue the player loop.

@teknium1 teknium1 added sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users labels Jul 12, 2026
@ygd58 ygd58 closed this Jul 13, 2026
teknium1 pushed a commit that referenced this pull request Jul 28, 2026
Ports #63768 forward onto current main per teknium1's review.

On WSL2 without a PulseAudio bridge, ffplay and aplay have no audio
device and TTS playback silently fails (issue #17608). When
powershell.exe and ffmpeg are available, convert the audio to a
uniquely-named WAV in the Windows %TEMP% directory and play it via
Media.SoundPlayer.

Per review, this fixes two gaps in the original port:

1. Exit-status masking: the cleanup subshell was
   '( ffmpeg && powershell ); rm -f wav' -- the shell's exit status is
   the LAST command's (rm -f, which is always 0), so a real
   ffmpeg/PowerShell failure could never be detected by the rc-checking
   fallback logic added to the player loop. Now captures the real
   status before cleanup and re-exits with it:
   '( ffmpeg && powershell ); rc=0; rm -f wav; exit '.

2. The no-Pulse WSL gate in detect_audio_environment() still hard-blocked
   voice mode entirely (input AND output) even when the PowerShell
   fallback made TTS output viable. Added _wsl_powershell_tts_available()
   and use it to downgrade the WSL-without-Pulse case from a hard
   'warnings' block to a non-blocking 'notices' entry when the fallback
   is available -- the same PulseAudio-bridge recording guidance is still
   surfaced (mic capture genuinely still needs it), it just no longer
   blocks /voice on for TTS-only usage. cli.py's existing
   env_check['available'] gate needed no changes since it already
   respects this flag.

Also fixed the flaky uniqueness test (the original asserted
len(filenames) >= 2, which passed trivially on zero captured
filenames) and added a real fallback-triggering regression test for
the exit-status fix.

10 new/fixed tests pass in TestWSL2PowerShellFallback and the new
TestWSLAudioEnvironmentGate; 80/80 in the full tests/tools/test_voice_mode.py file.
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
Ports NousResearch#63768 forward onto current main per teknium1's review.

On WSL2 without a PulseAudio bridge, ffplay and aplay have no audio
device and TTS playback silently fails (issue NousResearch#17608). When
powershell.exe and ffmpeg are available, convert the audio to a
uniquely-named WAV in the Windows %TEMP% directory and play it via
Media.SoundPlayer.

Per review, this fixes two gaps in the original port:

1. Exit-status masking: the cleanup subshell was
   '( ffmpeg && powershell ); rm -f wav' -- the shell's exit status is
   the LAST command's (rm -f, which is always 0), so a real
   ffmpeg/PowerShell failure could never be detected by the rc-checking
   fallback logic added to the player loop. Now captures the real
   status before cleanup and re-exits with it:
   '( ffmpeg && powershell ); rc=0; rm -f wav; exit '.

2. The no-Pulse WSL gate in detect_audio_environment() still hard-blocked
   voice mode entirely (input AND output) even when the PowerShell
   fallback made TTS output viable. Added _wsl_powershell_tts_available()
   and use it to downgrade the WSL-without-Pulse case from a hard
   'warnings' block to a non-blocking 'notices' entry when the fallback
   is available -- the same PulseAudio-bridge recording guidance is still
   surfaced (mic capture genuinely still needs it), it just no longer
   blocks /voice on for TTS-only usage. cli.py's existing
   env_check['available'] gate needed no changes since it already
   respects this flag.

Also fixed the flaky uniqueness test (the original asserted
len(filenames) >= 2, which passed trivially on zero captured
filenames) and added a real fallback-triggering regression test for
the exit-status fix.

10 new/fixed tests pass in TestWSL2PowerShellFallback and the new
TestWSLAudioEnvironmentGate; 80/80 in the full tests/tools/test_voice_mode.py file.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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 sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows 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.

[Bug]: Possible WSL2 TTS audio routing issue — playback doesn't reach Windows speakers?

3 participants