feat: playlist shuffle, lazy resolution, TTS ducking, kept repair
Some checks failed
CI / gitleaks (push) Failing after 3s
CI / lint (push) Successful in 22s
CI / test (3.11) (push) Failing after 2m47s
CI / test (3.13) (push) Failing after 2m52s
CI / test (3.12) (push) Failing after 2m54s
CI / build (push) Has been skipped

Music:
- #random URL fragment shuffles playlist tracks before enqueuing
- Lazy playlist resolution: first 10 tracks resolve immediately,
  remaining are fetched in a background task
- !kept repair re-downloads kept tracks with missing local files
- !kept shows [MISSING] marker for tracks without local files
- TTS ducking: music ducks when merlin speaks via voice peer,
  smooth restore after TTS finishes

Performance (from profiling):
- Connection pool: preload_content=True for SOCKS connection reuse
- Pool tuning: 30 pools / 8 connections (up from 20/4)
- _PooledResponse wrapper for stdlib-compatible read interface
- Iterative _extract_videos (replace 51K-deep recursion with stack)
- proxy=False for local SearXNG

Voice + multi-bot:
- Per-bot voice config lookup ([<username>.voice] in TOML)
- Mute detection: skip duck silence when all users muted
- Autoplay shuffle deck (no repeats until full cycle)
- Seek clamp to track duration (prevent seek-past-end stall)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
user
2026-02-22 16:21:47 +01:00
parent 6d6b957557
commit 6083de13f9
17 changed files with 1706 additions and 118 deletions

View File

@@ -38,6 +38,18 @@ _MAX_SAY_LEN = 500 # max characters for !say
_WHISPER_URL = "http://192.168.129.9:8080/inference"
_PIPER_URL = "http://192.168.129.9:5100/"
def _find_voice_peer(bot):
"""Find the voice-capable peer (the bot with 'voice' in only_plugins)."""
bots = getattr(bot.registry, "_bots", {})
for name, b in bots.items():
if name == bot._username:
continue
if getattr(b, "_only_plugins", None) and "voice" in b._only_plugins:
return b
return None
# -- Per-bot state -----------------------------------------------------------
@@ -172,8 +184,10 @@ async def _flush_monitor(bot):
remainder = text[len(trigger):].strip()
if remainder:
log.info("voice: trigger from %s: %s", name, remainder)
bot._spawn(
_tts_play(bot, remainder), name="voice-tts",
# Route TTS through voice-capable peer if available
speaker = _find_voice_peer(bot) or bot
speaker._spawn(
_tts_play(speaker, remainder), name="voice-tts",
)
continue
@@ -242,10 +256,13 @@ async def _tts_play(bot, text: str):
if wav_path is None:
return
try:
# Signal music plugin to duck while TTS is playing
bot.registry._tts_active = True
done = asyncio.Event()
await bot.stream_audio(str(wav_path), volume=1.0, on_done=done)
await done.wait()
finally:
bot.registry._tts_active = False
Path(wav_path).unlink(missing_ok=True)