feat: voice profiles, rubberband FX, per-bot plugin filtering

- Add rubberband package to container for pitch-shifting FX
- Split FX chain: rubberband CLI for pitch, ffmpeg for filters
- Configurable voice profile (voice, fx, piper params) in [voice]
- Extra bots inherit voice config (minus trigger) for own TTS
- Greeting is voice-only, spoken directly by the greeting bot
- Per-bot only_plugins/except_plugins filtering on Mumble
- Alias plugin, core plugin tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
user
2026-02-22 11:41:00 +01:00
parent 3afeace6e7
commit e9d17e8b00
13 changed files with 1398 additions and 111 deletions

View File

@@ -174,6 +174,34 @@ async def cmd_admins(bot, message):
await bot.reply(message, " | ".join(parts))
@command("deaf", help="Toggle voice listener deaf on Mumble")
async def cmd_deaf(bot, message):
"""Toggle the voice listener's deaf state on Mumble.
Targets the bot with ``receive_sound = true`` (merlin) so that
deafening stops ducking without affecting the music bot's playback.
"""
# Find the listener bot (receive_sound=true) among registered peers
listener = None
bots = getattr(bot.registry, "_bots", {})
for peer in bots.values():
if getattr(peer, "_receive_sound", False):
listener = peer
break
mumble = getattr(listener or bot, "_mumble", None)
if mumble is None:
return
myself = mumble.users.myself
name = getattr(listener, "nick", "bot")
if myself.get("self_deaf", False):
myself.undeafen()
myself.unmute()
await bot.reply(message, f"{name}: undeafened")
else:
myself.deafen()
await bot.reply(message, f"{name}: deafened")
@command("state", help="Inspect plugin state: !state <list|get|del|clear> ...", admin=True)
async def cmd_state(bot, message):
"""Manage the plugin state store.