fix: ignore bot audio in sound callback, self-mute support

- _on_sound_received skips audio from our own bots entirely,
  preventing self-ducking and STT transcribing bot TTS/music
- New self_mute config: mute on connect, unmute before stream_audio,
  re-mute 3s after audio finishes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
user
2026-02-22 12:09:30 +01:00
parent e9d17e8b00
commit f72f55148b

View File

@@ -165,6 +165,8 @@ class MumbleBot:
self._connect_count: int = 0
self._sound_listeners: list = []
self._receive_sound: bool = mu_cfg.get("receive_sound", True)
self._self_mute: bool = mu_cfg.get("self_mute", False)
self._mute_task: asyncio.Task | None = None
self._only_plugins: set[str] | None = (
set(mu_cfg["only_plugins"]) if "only_plugins" in mu_cfg else None
)
@@ -225,6 +227,11 @@ class MumbleBot:
session = getattr(self._mumble.users, "myself_session", "?")
log.info("mumble: %s as %s on %s:%d (session=%s)",
kind, self._username, self._host, self._port, session)
if self._self_mute:
try:
self._mumble.users.myself.mute()
except Exception:
log.exception("mumble: failed to self-mute on connect")
if self._loop:
asyncio.run_coroutine_threadsafe(
self._notify_plugins_connected(), self._loop,
@@ -277,16 +284,18 @@ class MumbleBot:
def _on_sound_received(self, user, sound_chunk) -> None:
"""Callback from pymumble thread: voice audio received.
Updates the timestamp used by the music plugin's duck monitor.
When this callback is registered, pymumble passes decoded PCM
directly and does not queue it -- no memory buildup.
Ignores audio from our own bots entirely -- prevents self-ducking
and avoids STT transcribing bot TTS/music.
"""
name = user["name"] if isinstance(user, dict) else None
bots = getattr(self.registry, "_bots", {})
if name and name in bots:
return
prev = self._last_voice_ts
self._last_voice_ts = time.monotonic()
self.registry._voice_ts = self._last_voice_ts
if prev == 0.0:
name = user["name"] if isinstance(user, dict) else "?"
log.info("mumble: first voice packet from %s", name)
log.info("mumble: first voice packet from %s", name or "?")
for fn in self._sound_listeners:
try:
fn(user, sound_chunk)
@@ -604,6 +613,16 @@ class MumbleBot:
if self._mumble is None:
return
# Unmute before streaming if self_mute is enabled
if self._self_mute:
if self._mute_task and not self._mute_task.done():
self._mute_task.cancel()
self._mute_task = None
try:
self._mumble.users.myself.unmute()
except Exception:
pass
_get_vol = volume if callable(volume) else lambda: volume
log.info("stream_audio: starting pipeline for %s (vol=%.0f%%, seek=%.1fs)",
url, _get_vol() * 100, seek)
@@ -822,6 +841,19 @@ class MumbleBot:
stderr_out.decode(errors="replace")[:500])
if on_done is not None:
on_done.set()
# Re-mute after audio finishes
if self._self_mute:
self._mute_task = self._spawn(
self._delayed_mute(3.0), name="self-mute",
)
async def _delayed_mute(self, delay: float) -> None:
"""Re-mute after a delay (lets the audio buffer drain fully)."""
await asyncio.sleep(delay)
try:
self._mumble.users.myself.mute()
except Exception:
pass
async def shorten_url(self, url: str) -> str:
"""Shorten a URL via FlaskPaste. Returns original on failure."""