From bde487c91137436aced5819e2aea2d354a1815a6 Mon Sep 17 00:00:00 2001 From: Wesley Simplicio Date: Sat, 9 May 2026 08:55:00 -0300 Subject: [PATCH 1/2] fix(voice): honor PULSE_SERVER/PIPEWIRE_REMOTE inside Docker (#21203) detect_audio_environment() unconditionally added a hard warning when running inside a container, blocking /voice on even when the host audio socket was correctly forwarded (PulseAudio or PipeWire) and sounddevice could enumerate devices. Mirror the existing WSL/PulseAudio handling: if PULSE_SERVER or PIPEWIRE_REMOTE is set, downgrade to a notice and let the audio backend decide. When neither is set, keep the block but extend the message with the exact -v / -e flags users need. Closes #21203 --- tests/tools/test_voice_mode.py | 54 ++++++++++++++++++++++++++++++++++ tools/voice_mode.py | 16 ++++++++-- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/tests/tools/test_voice_mode.py b/tests/tools/test_voice_mode.py index 1d35c48625f..0da0a06040d 100644 --- a/tests/tools/test_voice_mode.py +++ b/tests/tools/test_voice_mode.py @@ -216,6 +216,60 @@ class TestDetectAudioEnvironment: assert any("Termux:API Android app is not installed" in w for w in result["warnings"]) + def test_docker_with_pulse_server_allows_voice(self, monkeypatch): + """Docker with PULSE_SERVER set should NOT block voice mode (#21203).""" + monkeypatch.delenv("SSH_CLIENT", raising=False) + monkeypatch.delenv("SSH_TTY", raising=False) + monkeypatch.delenv("SSH_CONNECTION", raising=False) + monkeypatch.setenv("PULSE_SERVER", "unix:/run/user/1000/pulse/native") + monkeypatch.delenv("PIPEWIRE_REMOTE", raising=False) + monkeypatch.setattr("hermes_constants.is_container", lambda: True) + monkeypatch.setattr("tools.voice_mode._import_audio", + lambda: (MagicMock(), MagicMock())) + + from tools.voice_mode import detect_audio_environment + result = detect_audio_environment() + + assert result["available"] is True + assert result["warnings"] == [] + assert any("Docker" in n for n in result.get("notices", [])) + + def test_docker_with_pipewire_remote_allows_voice(self, monkeypatch): + """Docker with PIPEWIRE_REMOTE set should NOT block voice mode (#21203).""" + monkeypatch.delenv("SSH_CLIENT", raising=False) + monkeypatch.delenv("SSH_TTY", raising=False) + monkeypatch.delenv("SSH_CONNECTION", raising=False) + monkeypatch.delenv("PULSE_SERVER", raising=False) + monkeypatch.setenv("PIPEWIRE_REMOTE", "/run/user/1000/pipewire-0") + monkeypatch.setattr("hermes_constants.is_container", lambda: True) + monkeypatch.setattr("tools.voice_mode._import_audio", + lambda: (MagicMock(), MagicMock())) + + from tools.voice_mode import detect_audio_environment + result = detect_audio_environment() + + assert result["available"] is True + assert result["warnings"] == [] + assert any("Docker" in n for n in result.get("notices", [])) + + def test_docker_without_audio_forwarding_blocks_voice(self, monkeypatch): + """Docker without PULSE_SERVER/PIPEWIRE_REMOTE keeps blocking voice mode.""" + monkeypatch.delenv("SSH_CLIENT", raising=False) + monkeypatch.delenv("SSH_TTY", raising=False) + monkeypatch.delenv("SSH_CONNECTION", raising=False) + monkeypatch.delenv("PULSE_SERVER", raising=False) + monkeypatch.delenv("PIPEWIRE_REMOTE", raising=False) + monkeypatch.setattr("hermes_constants.is_container", lambda: True) + monkeypatch.setattr("tools.voice_mode._import_audio", + lambda: (MagicMock(), MagicMock())) + + from tools.voice_mode import detect_audio_environment + result = detect_audio_environment() + + assert result["available"] is False + assert any("Docker" in w for w in result["warnings"]) + assert any("PULSE_SERVER" in w or "PIPEWIRE_REMOTE" in w for w in result["warnings"]) + def test_termux_api_microphone_allows_voice_without_sounddevice(self, monkeypatch): monkeypatch.setenv("TERMUX_VERSION", "0.118.3") monkeypatch.setenv("PREFIX", "/data/data/com.termux/files/usr") diff --git a/tools/voice_mode.py b/tools/voice_mode.py index 6166ade2a3f..7c226afbaf7 100644 --- a/tools/voice_mode.py +++ b/tools/voice_mode.py @@ -102,10 +102,22 @@ def detect_audio_environment() -> dict: if any(os.environ.get(v) for v in ('SSH_CLIENT', 'SSH_TTY', 'SSH_CONNECTION')): warnings.append("Running over SSH -- no audio devices available") - # Docker/Podman container detection + # Docker/Podman container detection — honor host audio forwarding. + # When the user mounts a PulseAudio/PipeWire socket into the container + # and points PULSE_SERVER / PIPEWIRE_REMOTE at it, audio works fine + # (issue #21203). Only block when no forwarding is configured. from hermes_constants import is_container if is_container(): - warnings.append("Running inside Docker container -- no audio devices") + if os.environ.get('PULSE_SERVER') or os.environ.get('PIPEWIRE_REMOTE'): + notices.append("Running inside Docker container with host audio forwarding") + else: + warnings.append( + "Running inside Docker container -- no audio devices.\n" + " Forward host audio with one of:\n" + " PulseAudio: -v /run/user/1000/pulse/native:/run/user/1000/pulse/native \\\n" + " -e PULSE_SERVER=unix:/run/user/1000/pulse/native\n" + " PipeWire: -e PIPEWIRE_REMOTE=/run/user/1000/pipewire-0" + ) # WSL detection — PulseAudio bridge makes audio work in WSL. # Only block if PULSE_SERVER is not configured. From 30dd5547ada8f316b6a558d4aa4c5e025f5b9ee6 Mon Sep 17 00:00:00 2001 From: Wesley Simplicio Date: Sat, 9 May 2026 15:21:12 -0300 Subject: [PATCH 2/2] fix(voice_mode): generalize container phrasing and use $XDG_RUNTIME_DIR --- tests/tools/test_voice_mode.py | 6 +++--- tools/voice_mode.py | 13 +++++++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/tools/test_voice_mode.py b/tests/tools/test_voice_mode.py index 0da0a06040d..7dd9a757752 100644 --- a/tests/tools/test_voice_mode.py +++ b/tests/tools/test_voice_mode.py @@ -232,7 +232,7 @@ class TestDetectAudioEnvironment: assert result["available"] is True assert result["warnings"] == [] - assert any("Docker" in n for n in result.get("notices", [])) + assert any("container" in n.lower() for n in result.get("notices", [])) def test_docker_with_pipewire_remote_allows_voice(self, monkeypatch): """Docker with PIPEWIRE_REMOTE set should NOT block voice mode (#21203).""" @@ -250,7 +250,7 @@ class TestDetectAudioEnvironment: assert result["available"] is True assert result["warnings"] == [] - assert any("Docker" in n for n in result.get("notices", [])) + assert any("container" in n.lower() for n in result.get("notices", [])) def test_docker_without_audio_forwarding_blocks_voice(self, monkeypatch): """Docker without PULSE_SERVER/PIPEWIRE_REMOTE keeps blocking voice mode.""" @@ -267,7 +267,7 @@ class TestDetectAudioEnvironment: result = detect_audio_environment() assert result["available"] is False - assert any("Docker" in w for w in result["warnings"]) + assert any("container" in w.lower() for w in result["warnings"]) assert any("PULSE_SERVER" in w or "PIPEWIRE_REMOTE" in w for w in result["warnings"]) def test_termux_api_microphone_allows_voice_without_sounddevice(self, monkeypatch): diff --git a/tools/voice_mode.py b/tools/voice_mode.py index 7c226afbaf7..1c019cc0400 100644 --- a/tools/voice_mode.py +++ b/tools/voice_mode.py @@ -109,14 +109,15 @@ def detect_audio_environment() -> dict: from hermes_constants import is_container if is_container(): if os.environ.get('PULSE_SERVER') or os.environ.get('PIPEWIRE_REMOTE'): - notices.append("Running inside Docker container with host audio forwarding") + notices.append("Running inside container (Docker/Podman/LXC) with host audio forwarding") else: warnings.append( - "Running inside Docker container -- no audio devices.\n" - " Forward host audio with one of:\n" - " PulseAudio: -v /run/user/1000/pulse/native:/run/user/1000/pulse/native \\\n" - " -e PULSE_SERVER=unix:/run/user/1000/pulse/native\n" - " PipeWire: -e PIPEWIRE_REMOTE=/run/user/1000/pipewire-0" + "Running inside container (Docker/Podman/LXC) -- no audio devices.\n" + " Forward host audio with one of (substitute $XDG_RUNTIME_DIR for your runtime dir,\n" + " typically /run/user/$UID):\n" + " PulseAudio: -v $XDG_RUNTIME_DIR/pulse/native:$XDG_RUNTIME_DIR/pulse/native \\\n" + " -e PULSE_SERVER=unix:$XDG_RUNTIME_DIR/pulse/native\n" + " PipeWire: -e PIPEWIRE_REMOTE=$XDG_RUNTIME_DIR/pipewire-0" ) # WSL detection — PulseAudio bridge makes audio work in WSL.