mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(discord): drain pending voice input before disconnect
This commit is contained in:
parent
a5b32a721a
commit
388f612435
2 changed files with 70 additions and 0 deletions
|
|
@ -744,6 +744,26 @@ class VoiceReceiver:
|
|||
|
||||
return completed
|
||||
|
||||
def flush_pending(self) -> list:
|
||||
"""Return buffered utterances that have not yet reached silence."""
|
||||
completed = []
|
||||
|
||||
with self._lock:
|
||||
ssrc_user_map = dict(self._ssrc_to_user)
|
||||
for ssrc, buf in list(self._buffers.items()):
|
||||
# 48kHz, 16-bit, stereo = 192000 bytes/sec
|
||||
buf_duration = len(buf) / (self.SAMPLE_RATE * self.CHANNELS * 2)
|
||||
if buf_duration >= self.MIN_SPEECH_DURATION:
|
||||
user_id = ssrc_user_map.get(ssrc, 0)
|
||||
if not user_id:
|
||||
user_id = self._infer_user_for_ssrc(ssrc)
|
||||
if user_id:
|
||||
completed.append((user_id, bytes(buf)))
|
||||
self._buffers.pop(ssrc, None)
|
||||
self._last_packet_time.pop(ssrc, None)
|
||||
|
||||
return completed
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# PCM -> WAV conversion (for Whisper STT)
|
||||
# ------------------------------------------------------------------
|
||||
|
|
@ -3940,12 +3960,19 @@ class DiscordAdapter(BasePlatformAdapter):
|
|||
async with self._voice_locks.setdefault(guild_id, asyncio.Lock()):
|
||||
# Stop voice receiver first
|
||||
receiver = self._voice_receivers.pop(guild_id, None)
|
||||
pending_inputs = []
|
||||
if receiver:
|
||||
pending_inputs = receiver.flush_pending()
|
||||
receiver.stop()
|
||||
listen_task = self._voice_listen_tasks.pop(guild_id, None)
|
||||
if listen_task:
|
||||
listen_task.cancel()
|
||||
|
||||
guild = self._client.get_guild(guild_id) if self._client is not None else None
|
||||
for user_id, pcm_data in pending_inputs:
|
||||
if self._is_allowed_user(str(user_id), guild=guild, is_dm=False):
|
||||
await self._process_voice_input(guild_id, user_id, pcm_data)
|
||||
|
||||
# Tear down the mixer (stops the continuous outgoing stream).
|
||||
if getattr(self, "_voice_mixers", None) is not None:
|
||||
self._voice_mixers.pop(guild_id, None)
|
||||
|
|
|
|||
|
|
@ -717,6 +717,18 @@ class TestVoiceReceiver:
|
|||
completed = receiver.check_silence()
|
||||
assert len(completed) == 0
|
||||
|
||||
def test_flush_pending_returns_recent_utterance_before_silence(self):
|
||||
"""Disconnect drains a valid utterance even before silence is detected."""
|
||||
receiver = self._make_receiver()
|
||||
receiver.map_ssrc(100, 42)
|
||||
pcm_data = bytearray(b"\x00" * 96000)
|
||||
receiver._buffers[100] = pcm_data
|
||||
receiver._last_packet_time[100] = time.monotonic()
|
||||
|
||||
assert receiver.flush_pending() == [(42, bytes(pcm_data))]
|
||||
assert 100 not in receiver._buffers
|
||||
assert 100 not in receiver._last_packet_time
|
||||
|
||||
def test_check_silence_unknown_user_discarded(self):
|
||||
receiver = self._make_receiver()
|
||||
# No SSRC mapping — user_id will be 0
|
||||
|
|
@ -1159,6 +1171,37 @@ class TestDiscordVoiceChannelMethods:
|
|||
assert 111 not in adapter._voice_sources
|
||||
assert 111 not in adapter._voice_receivers
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_leave_voice_channel_processes_pending_audio_before_disconnect(self):
|
||||
"""Recent speech is transcribed before the voice connection is torn down."""
|
||||
adapter = self._make_adapter()
|
||||
events = []
|
||||
mock_vc = MagicMock()
|
||||
mock_vc.is_connected.return_value = True
|
||||
|
||||
async def disconnect():
|
||||
events.append("disconnect")
|
||||
|
||||
mock_vc.disconnect = disconnect
|
||||
adapter._voice_clients[111] = mock_vc
|
||||
|
||||
mock_receiver = MagicMock()
|
||||
mock_receiver.flush_pending.side_effect = lambda: events.append("flush") or [(42, b"pcm")]
|
||||
mock_receiver.stop.side_effect = lambda: events.append("stop")
|
||||
adapter._voice_receivers[111] = mock_receiver
|
||||
adapter._voice_listen_tasks[111] = MagicMock()
|
||||
adapter._is_allowed_user = MagicMock(return_value=True)
|
||||
|
||||
async def process(guild_id, user_id, pcm_data):
|
||||
events.append("process")
|
||||
|
||||
adapter._process_voice_input = process
|
||||
|
||||
await adapter.leave_voice_channel(111)
|
||||
|
||||
assert events == ["flush", "stop", "process", "disconnect"]
|
||||
adapter._is_allowed_user.assert_called_once_with("42", guild=adapter._client.get_guild(111), is_dm=False)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_leave_voice_channel_no_connection(self):
|
||||
"""Leave when not connected — no crash."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue