diff --git a/gateway/platforms/discord.py b/gateway/platforms/discord.py index aebae49b4a..203f6535ed 100644 --- a/gateway/platforms/discord.py +++ b/gateway/platforms/discord.py @@ -695,13 +695,14 @@ class DiscordAdapter(BasePlatformAdapter): ) -> SendResult: """Play auto-TTS audio. - When the bot is in a voice channel for this chat's guild, skip the - file attachment — the gateway runner plays audio in the VC instead. + When the bot is in a voice channel for this chat's guild, play + directly in the VC instead of sending as a file attachment. """ for gid, text_ch_id in self._voice_text_channels.items(): if str(text_ch_id) == str(chat_id) and self.is_in_voice_channel(gid): - logger.debug("[%s] Skipping play_tts for %s — VC playback handled by runner", self.name, chat_id) - return SendResult(success=True) + logger.info("[%s] Playing TTS in voice channel (guild=%d)", self.name, gid) + success = await self.play_in_voice_channel(gid, audio_path) + return SendResult(success=success) return await self.send_voice(chat_id=chat_id, audio_path=audio_path, **kwargs) async def send_voice( diff --git a/gateway/run.py b/gateway/run.py index 716e981f22..62d1aaad1d 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -2575,18 +2575,9 @@ class GatewayRunner: if has_agent_tts: return False - # Dedup: base adapter auto-TTS already handles voice input. - # Exception: Discord voice channel — play_tts override is a no-op, - # so the runner must handle VC playback. - skip_double = is_voice_input - if skip_double: - adapter = self.adapters.get(event.source.platform) - guild_id = self._get_guild_id(event) - if (guild_id and adapter - and hasattr(adapter, "is_in_voice_channel") - and adapter.is_in_voice_channel(guild_id)): - skip_double = False - if skip_double: + # Dedup: base adapter auto-TTS already handles voice input + # (play_tts plays in VC when connected, so runner can skip). + if is_voice_input: return False return True diff --git a/tests/gateway/test_voice_command.py b/tests/gateway/test_voice_command.py index 545f2b28fb..1969694d40 100644 --- a/tests/gateway/test_voice_command.py +++ b/tests/gateway/test_voice_command.py @@ -292,14 +292,14 @@ class TestAutoVoiceReply: # -- Discord VC exception: runner must handle -------------------------- - def test_discord_vc_voice_input_runner_fires(self, runner): - """Discord VC + voice input: base play_tts skips (VC override), - so runner must handle via play_in_voice_channel.""" - assert self._call(runner, "all", MessageType.VOICE, in_voice_channel=True) is True + def test_discord_vc_voice_input_base_handles(self, runner): + """Discord VC + voice input: base adapter play_tts plays in VC, + so runner skips to avoid double playback.""" + assert self._call(runner, "all", MessageType.VOICE, in_voice_channel=True) is False - def test_discord_vc_voice_only_runner_fires(self, runner): - """Discord VC + voice_only + voice: runner must handle.""" - assert self._call(runner, "voice_only", MessageType.VOICE, in_voice_channel=True) is True + def test_discord_vc_voice_only_base_handles(self, runner): + """Discord VC + voice_only + voice: base adapter handles.""" + assert self._call(runner, "voice_only", MessageType.VOICE, in_voice_channel=True) is False # -- Edge cases -------------------------------------------------------- @@ -422,17 +422,23 @@ class TestDiscordPlayTtsSkip: return adapter @pytest.mark.asyncio - async def test_play_tts_skipped_when_in_vc(self): + async def test_play_tts_plays_in_vc_when_connected(self): adapter = self._make_discord_adapter() # Simulate bot in voice channel for guild 111, text channel 123 mock_vc = MagicMock() mock_vc.is_connected.return_value = True + mock_vc.is_playing.return_value = False adapter._voice_clients[111] = mock_vc adapter._voice_text_channels[111] = 123 + # Mock play_in_voice_channel to avoid actual ffmpeg call + async def fake_play(gid, path): + return True + adapter.play_in_voice_channel = fake_play + result = await adapter.play_tts(chat_id="123", audio_path="/tmp/test.ogg") + # play_tts now plays in VC instead of being a no-op assert result.success is True - # send_voice should NOT have been called (no client, would fail) @pytest.mark.asyncio async def test_play_tts_not_skipped_when_not_in_vc(self):