feat: playlist save/load, queue durations, whisper bias, greet fix
- Move TTS greeting from mumble._play_greet to voice.on_connected (fires once on first connect, gated on _is_audio_ready) - Add initial_prompt multipart field to Whisper STT for trigger word bias (auto-generated from trigger config, overridable) - Enhanced !queue: elapsed/total on now-playing, per-track durations, footer with track count and total time - New !playlist command: save/load/list/del named playlists via bot.state persistence (playlist:<name> keys) - Fix duck floor test (1% -> 2% to match default change) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -47,9 +47,12 @@ _PIPER_URL = "http://192.168.129.9:5100/"
|
||||
def _ps(bot):
|
||||
"""Per-bot plugin runtime state."""
|
||||
cfg = getattr(bot, "config", {}).get("voice", {})
|
||||
trigger = cfg.get("trigger", "")
|
||||
# Bias Whisper toward the trigger word unless explicitly configured
|
||||
default_prompt = f"{trigger.capitalize()}, " if trigger else ""
|
||||
return bot._pstate.setdefault("voice", {
|
||||
"listen": False,
|
||||
"trigger": cfg.get("trigger", ""),
|
||||
"trigger": trigger,
|
||||
"buffers": {}, # {username: bytearray}
|
||||
"last_ts": {}, # {username: float monotonic}
|
||||
"flush_task": None,
|
||||
@@ -62,6 +65,7 @@ def _ps(bot):
|
||||
"noise_scale": cfg.get("noise_scale", 0.667),
|
||||
"noise_w": cfg.get("noise_w", 0.8),
|
||||
"fx": cfg.get("fx", ""),
|
||||
"initial_prompt": cfg.get("initial_prompt", default_prompt),
|
||||
"_listener_registered": False,
|
||||
})
|
||||
|
||||
@@ -170,8 +174,17 @@ def _transcribe(ps, pcm: bytes) -> str:
|
||||
).encode() + wav_data + (
|
||||
f"\r\n--{boundary}\r\n"
|
||||
f'Content-Disposition: form-data; name="response_format"\r\n\r\n'
|
||||
f"json\r\n--{boundary}--\r\n"
|
||||
f"json"
|
||||
).encode()
|
||||
# Bias Whisper toward the trigger word when configured
|
||||
prompt = ps.get("initial_prompt", "")
|
||||
if prompt:
|
||||
body += (
|
||||
f"\r\n--{boundary}\r\n"
|
||||
f'Content-Disposition: form-data; name="initial_prompt"\r\n\r\n'
|
||||
f"{prompt}"
|
||||
).encode()
|
||||
body += f"\r\n--{boundary}--\r\n".encode()
|
||||
req = urllib.request.Request(ps["whisper_url"], data=body, method="POST")
|
||||
req.add_header("Content-Type", f"multipart/form-data; boundary={boundary}")
|
||||
resp = _urlopen(req, timeout=30, proxy=False)
|
||||
@@ -613,10 +626,22 @@ async def cmd_audition(bot, message):
|
||||
|
||||
|
||||
async def on_connected(bot) -> None:
|
||||
"""Re-register listener after reconnect."""
|
||||
"""Re-register listener after reconnect; play TTS greeting on first connect."""
|
||||
if not _is_mumble(bot):
|
||||
return
|
||||
ps = _ps(bot)
|
||||
if ps["listen"] or ps["trigger"]:
|
||||
_ensure_listener(bot)
|
||||
_ensure_flush_task(bot)
|
||||
|
||||
# Greet via TTS on first connection only
|
||||
greet = getattr(bot, "config", {}).get("mumble", {}).get("greet")
|
||||
if greet and not ps.get("_greeted"):
|
||||
ps["_greeted"] = True
|
||||
ready = getattr(bot, "_is_audio_ready", None)
|
||||
if ready:
|
||||
for _ in range(20):
|
||||
if ready():
|
||||
break
|
||||
await asyncio.sleep(0.5)
|
||||
bot._spawn(_tts_play(bot, greet), name="voice-greet")
|
||||
|
||||
Reference in New Issue
Block a user