fix(voice): add WSL2 PowerShell audio fallback for TTS playback - #17608
fix(voice): add WSL2 PowerShell audio fallback for TTS playback#17608ygd58 wants to merge 2 commits into
Conversation
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
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
|
The test failure ( |
|
Regarding the Supply Chain Audit flag: the audit is likely triggered by the These are safe system calls:
All calls are gated behind |
teknium1
left a comment
There was a problem hiding this comment.
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 onthen/voice ttspath remains blocked before playback: WSL withoutPULSE_SERVERis rejected intools/voice_mode.py:193-206,cli.py:11430-11435returns, and/voice ttsrequires enabled voice mode atcli.py:11513-11517. - The new shell player at
tools/voice_mode.py:928cannot fall through after anffmpeg/PowerShell failure.play_audio_file()returnsTrueafterproc.wait()without checking its exit status (tools/voice_mode.py:1102-1105). - The fixed
hermes-tts.wavpath attools/voice_mode.py:911collides 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.
| stderr=subprocess.DEVNULL, timeout=3, | ||
| ).decode(errors="replace").strip() | ||
| if _win_tmp_wsl: | ||
| _wsl_wav = os.path.join(_win_tmp_wsl, "hermes-tts.wav") |
There was a problem hiding this comment.
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.
| + " && " | ||
| + shlex.join(["rm", "-f", _wsl_wav]) | ||
| ) | ||
| players.insert(0, ["sh", "-c", _ps_cmd]) |
There was a problem hiding this comment.
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.
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.
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.
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.exeis available, add a PowerShell SoundPlayer fallback:%TEMP%dir dynamically viacmd.exe+wslpath(no hardcoded username)PowerShell SoundPlayer.PlaySync()Falls back to
ffplay/aplayif PowerShell is unavailable. No new dependencies required.Fixes #17573