Commit Graph

192 Commits

Author SHA1 Message Date
user
f72f55148b 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>
2026-02-22 12:09:30 +01:00
user
e9d17e8b00 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>
2026-02-22 11:41:00 +01:00
user
3afeace6e7 feat: container management tools in tools/
Shell scripts for build, start, stop, restart, nuke, logs, status.
Shared helpers in _common.sh (colours, compose detection, project root).
Updated CHEATSHEET.md with new tool references.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 11:40:53 +01:00
user
b88a459142 feat: music library management, smooth fades, clickable URLs
- Audio-only downloads (-x), resume (-c), skip existing (--no-overwrites)
- Title-based filenames (e.g. never-gonna-give-you-up.opus)
- Separate cache (data/music/cache/) from kept tracks (data/music/)
- Kept track IDs: !keep assigns #id, !play #id, !kept shows IDs
- Linear fade-in (5s) and fade-out (3s) with volume-proportional step
- Fix ramp click: threshold-based convergence instead of float equality
- Clean up cache files for skipped/stopped tracks
- Auto-linkify URLs in Mumble text chat (clickable <a> tags)
- FlaskPaste links use /raw endpoint for direct content access
- Metadata fetch uses --no-playlist for reliable results

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 08:11:29 +01:00
user
ad1de1653e fix: gitleaks clone depth and opuslib discovery on musl
Some checks failed
CI / gitleaks (push) Failing after 3s
CI / lint (push) Successful in 27s
CI / test (3.11) (push) Successful in 2m13s
CI / test (3.13) (push) Successful in 2m18s
CI / test (3.12) (push) Successful in 2m20s
CI / build (push) Has been skipped
Remove invalid --depth=0 from gitleaks checkout (needs full history).
Add opus symlink and pymumble/opuslib patch step to test jobs so
ctypes.find_library works on Alpine/musl.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 07:11:50 +01:00
user
8f1df167b9 feat: fade-out on skip/stop/prev, song metadata on keep
Some checks failed
CI / gitleaks (push) Failing after 4s
CI / lint (push) Successful in 24s
CI / test (3.11) (push) Failing after 30s
CI / test (3.13) (push) Failing after 34s
CI / test (3.12) (push) Failing after 36s
CI / build (push) Has been skipped
- Add fade_step parameter to stream_audio for fast volume ramps
- _fade_and_cancel helper: smooth ~0.8s fade before track switch
- !skip, !stop, !seek now fade out instead of cutting instantly
- !prev command: go back to previous track (10-track history stack)
- !keep fetches title/artist/duration via yt-dlp, stores in bot.state
- !kept displays metadata (title, artist, duration, file size)
- !kept clear also removes stored metadata
- 29 new tests for fade, prev, history, keep metadata

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 06:38:25 +01:00
user
de2d1fdf15 fix: replace actions/checkout with git clone in container jobs
Some checks failed
CI / gitleaks (push) Failing after 4s
CI / lint (push) Successful in 25s
CI / test (3.11) (push) Failing after 32s
CI / test (3.13) (push) Failing after 35s
CI / test (3.12) (push) Failing after 39s
CI / build (push) Has been skipped
actions/checkout@v4 requires node, which isn't available in
alpine or gitleaks images. Use plain git clone instead for
containerized jobs; keep actions/checkout for the host build job.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 06:23:49 +01:00
user
82f5984631 ci: use 'linux' runner label
Some checks failed
CI / gitleaks (push) Failing after 5s
CI / lint (push) Failing after 9s
CI / test (3.11) (push) Has been skipped
CI / test (3.12) (push) Has been skipped
CI / test (3.13) (push) Has been skipped
CI / build (push) Has been skipped
2026-02-22 06:20:48 +01:00
user
1744e7087f ci: re-trigger pipeline
Some checks failed
CI / gitleaks (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.13) (push) Has been cancelled
CI / build (push) Has been cancelled
2026-02-22 06:18:27 +01:00
user
0c0adef90d feat: run CI jobs in podman containers, add requirements-dev.txt
Some checks failed
CI / gitleaks (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.13) (push) Has been cancelled
CI / build (push) Has been cancelled
- Use container: directive for gitleaks, lint, and test jobs
- Build job stays on host (needs podman for image build/push)
- Add requirements-dev.txt for unified dev/test dependency install

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 06:15:42 +01:00
user
3dada3fc06 fix: allowlist tests/ in gitleaks, add libopus for CI test job
Some checks failed
CI / gitleaks (push) Successful in 12s
CI / lint (push) Successful in 18s
CI / test (3.11) (push) Successful in 2m5s
CI / test (3.12) (push) Successful in 2m5s
CI / test (3.13) (push) Successful in 2m3s
CI / build (push) Failing after 12s
- gitleaks flagged fake api_key in test fixtures as a secret leak;
  allowlist tests/ directory since it contains only mock data
- Install libopus0 in test runner for pymumble/opuslib import chain

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 06:09:28 +01:00
user
6e40daa8a9 fix: resolve CI gitleaks download and missing pymumble dep
Some checks failed
CI / gitleaks (push) Failing after 12s
CI / lint (push) Successful in 17s
CI / test (3.11) (push) Failing after 25s
CI / test (3.12) (push) Failing after 28s
CI / test (3.13) (push) Failing after 27s
CI / build (push) Has been skipped
- Dynamically resolve latest gitleaks version from GitHub releases
  instead of hardcoded tarball URL that 404'd
- Add pymumble to test job install (needed by derp.mumble import
  chain, not in pyproject.toml base deps)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 06:06:47 +01:00
user
ba1af461de fix: use gitleaks CLI instead of licensed action, fix lint errors
Some checks failed
CI / gitleaks (push) Failing after 10s
CI / lint (push) Successful in 18s
CI / test (3.11) (push) Failing after 19s
CI / test (3.12) (push) Failing after 20s
CI / test (3.13) (push) Failing after 20s
CI / build (push) Has been skipped
- Replace gitleaks-action (requires paid license) with direct CLI
  invocation -- same engine, no license needed
- Fix ruff I001 import sorting in voice.py and test_llm.py
- Remove unused imports: _chat_request (test_llm), Path (test_music)
- Remove unused assignment: original_spawn (test_voice)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 05:58:10 +01:00
user
004656a64f feat: add Harbor image build+push to CI pipeline
Some checks failed
CI / gitleaks (push) Failing after 10s
CI / lint (push) Failing after 17s
CI / test (3.11) (push) Has been skipped
CI / test (3.12) (push) Has been skipped
CI / test (3.13) (push) Has been skipped
CI / build (push) Has been skipped
Build and push to harbor.mymx.me/library/derp after gitleaks
and test jobs pass. Only runs on push to master. Tags with
short SHA and latest.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 05:55:34 +01:00
user
192ea717a7 feat: split CI into gitleaks, lint, and test jobs
Some checks failed
CI / gitleaks (push) Failing after 15s
CI / lint (push) Failing after 17s
CI / test (3.11) (push) Has been skipped
CI / test (3.12) (push) Has been skipped
CI / test (3.13) (push) Has been skipped
- Add gitleaks secret scanning (full history)
- Separate lint (ruff, Python 3.13 only) from test matrix
- Test job gates on lint; gitleaks runs in parallel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 05:51:53 +01:00
user
7a4aa65882 fix: align cmd_stop else branch with _play_loop finally cleanup
The else branch (no active task) only cleared current, task, and
duck_vol. Now resets all play state fields to match _play_loop's
finally block: done_event, duck_task, progress, and cur_seek.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 05:49:53 +01:00
user
2cd1d5efb1 fix: race condition in skip/seek/stop losing track state
task.cancel() triggers _play_loop's finally block asynchronously.
When cmd_skip or cmd_seek called _ensure_loop before the finally
block ran, the old task's cleanup would overwrite the new task's
state -- causing !np to report "Nothing playing" while audio
was still streaming.

Now await the cancelled task before restarting the loop, ensuring
the finally block completes first.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 05:45:00 +01:00
user
95981275b5 feat: add OpenRouter LLM chat plugin (!ask, !chat)
Single-shot (!ask) and conversational (!chat) LLM commands backed by
OpenRouter's API. Per-user history (20 msg cap), 5s cooldown, reasoning
model fallback, and model switching via subcommands.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 05:39:11 +01:00
user
66116d2caf docs: update Piper TTS endpoint and document available voices
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 05:01:57 +01:00
user
eded764f6a fix: update Piper TTS endpoint and request format
Piper is on 192.168.129.9:5100, not :5000. It expects POST with
JSON body {"text": "..."}, not GET with query params. Also update
Whisper default to 192.168.129.9.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 04:56:24 +01:00
user
9783365b1e feat: add extra Mumble bot instances and TTS greeting
Support [[mumble.extra]] config for additional Mumble identities that
inherit connection settings from the main [mumble] section. Extra bots
get their own state DB and do not run the voice trigger by default.

Add TTS greeting on first connect via mumble.greet config option.
Merlin joins as a second identity with his own client certificate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 04:34:10 +01:00
user
165938a801 fix: mumble disconnect loop from stale socket and dead parent thread
Two issues causing ~2min reconnect cycles:

1. pymumble captures threading.current_thread() as parent_thread at
   init. Since we construct it in a run_in_executor thread, the parent
   dies after _connect_sync returns, triggering pymumble's loop exit.
   Fix: point parent_thread at threading.main_thread().

2. pymumble's init_connection() drops the control_socket reference
   without closing it. The lingering TCP connection makes Murmur kick
   the new session with "connected from another device". Fix: patch
   init_connection to close the old socket before reconnecting.
2026-02-22 04:24:23 +01:00
user
221cb1f06b fix: voice trigger not receiving audio from pymumble
pymumble passes a User object, not a dict. The isinstance(user, dict)
check returned False, setting name to None and silently discarding
every voice packet. Use try/except for dict-like access instead.
2026-02-22 04:02:23 +01:00
user
c4908f2a63 docs: document seek command and volume persistence 2026-02-22 03:31:39 +01:00
user
c493583a71 feat: add !seek command and persist volume across restarts
Seek to absolute or relative positions mid-track via !seek. Supports
M:SS and plain seconds with +/- prefixes. Volume is now saved to
bot.state and restored on connect.
2026-02-22 03:31:35 +01:00
user
7c099d8cf0 docs: document voice trigger configuration 2026-02-22 03:24:07 +01:00
user
e127f72660 feat: add always-on voice trigger mode with TTS echo
When [voice] trigger is set in config, the bot continuously listens and
transcribes voice. Speech starting with the trigger word is stripped and
echoed back via TTS. Non-triggered speech is silently discarded unless
!listen is also active.
2026-02-22 03:24:03 +01:00
user
7b9359c152 docs: document voice plugin commands
Add Voice STT/TTS section covering !listen, !say, and optional
[voice] config block with whisper_url, piper_url, silence_gap.
2026-02-22 03:08:10 +01:00
user
9fbf45f67d feat: add voice plugin with STT and TTS
Whisper STT: buffers incoming voice PCM per user, transcribes on
silence gap via local whisper.cpp endpoint, posts results as actions.

Piper TTS: !say fetches WAV from local Piper endpoint and plays via
stream_audio(). 37 tests cover buffering, flush logic, transcription,
WAV encoding, commands, and lifecycle.
2026-02-22 03:08:02 +01:00
user
039f060b50 feat: add sound listener hook to MumbleBot
Allow plugins to register callbacks for incoming voice PCM via
bot._sound_listeners. Empty list by default = zero overhead.
2026-02-22 03:07:55 +01:00
user
df20c154ca feat: download audio before playback, add !keep and !kept commands
Audio is now downloaded to data/music/ before playback begins,
eliminating CDN hiccups mid-stream. Falls back to streaming on
download failure. Files are deleted after playback unless marked
with !keep. stream_audio detects local files and uses a direct
ffmpeg pipeline (no yt-dlp).
2026-02-22 02:52:51 +01:00
user
ab924444de fix: survive mumble disconnects without restarting audio stream
Guard stream_audio with _is_audio_ready() so that PCM frames are
dropped (not crashed on) when pymumble recreates SoundOutput with
encoder=None during reconnect. The ffmpeg pipeline stays alive,
position tracking remains accurate, and audio feeding resumes once
the codec is negotiated. Listeners hear brief silence instead of
a 30+ second restart with URL re-resolution.

Also adds chat messages to _auto_resume so users see what the bot
intends ("Resuming 'X' at M:SS in a moment" / "...aborted").
2026-02-22 02:41:44 +01:00
user
ec55c2aef1 feat: auto-resume music on reconnect, sorcerer tier, cert auth
Auto-resume: save playback position on stream errors and cancellation,
restore automatically after reconnect or container restart once the
channel is silent. Plugin lifecycle hook (on_connected) ensures the
reconnect watcher starts without waiting for user commands.

Sorcerer tier: new permission level between oper and admin. Configured
via [mumble] sorcerers list in derp.toml.

Mumble cert auth: pass certfile/keyfile to pymumble for client
certificate authentication.

Fixes: stream_audio now re-raises CancelledError and Exception so
_play_loop detects failures correctly. Subprocess cleanup uses 3s
timeout. Graceful shutdown cancels background tasks before stopping
pymumble. Safe getattr for _opers in core plugin.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 02:14:43 +01:00
user
f899241d73 feat: support relative volume adjustment (+N/-N)
!volume +10 increases by 10, !volume -5 decreases by 5.
Out-of-range results (below 0 or above 100) are rejected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 00:18:43 +01:00
user
f189cbd290 feat: add !resume to continue playback from last interruption
Tracks playback position via frame counting in stream_audio().
On stop/skip, saves URL + elapsed time to bot.state (SQLite).
!resume reloads the track and seeks to the saved position via
ffmpeg -ss. State persists across bot restarts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 00:15:39 +01:00
user
9d58a5d073 fix: slow volume ramp to 1s for smoother transitions
Reduce _max_step from 0.1 to 0.02 per frame, extending the full
0-to-1 volume ramp from ~200ms to ~1 second.
2026-02-21 23:56:04 +01:00
user
e4e1e219f0 feat: add YouTube search to !play and fix NA URL fallback
Non-URL input (e.g. !play classical music) searches YouTube for 10
results and picks one randomly. Also fixes --flat-playlist returning
"NA" as the URL for single videos by falling back to the original
input URL.
2026-02-21 23:52:01 +01:00
user
6b7d733650 feat: smooth volume ramping over 200ms in audio streaming
Some checks failed
CI / test (3.11) (push) Failing after 22s
CI / test (3.12) (push) Failing after 22s
CI / test (3.13) (push) Failing after 22s
Volume changes now ramp linearly per-sample via _scale_pcm_ramp instead
of jumping abruptly. Each frame steps _cur_vol toward target by at most
0.1, giving ~200ms for a full 0-to-1 sweep. Fast path unchanged when
volume is stable.
2026-02-21 23:32:22 +01:00
user
c5c61e63cc feat: expand YouTube playlists into individual queue tracks
_resolve_title replaced with _resolve_tracks using --flat-playlist to
enumerate playlist entries. cmd_play enqueues each track individually,
with truncation when the queue is nearly full. Single-video behavior
unchanged.
2026-02-21 23:32:16 +01:00
user
67b2dc827d fix: make !volume apply immediately during playback
stream_audio now accepts a callable for volume, re-read on each PCM
frame instead of capturing a static float at track start.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 23:20:17 +01:00
user
eae36aa1f9 docs: update Mumble docs for pymumble transport
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 23:16:56 +01:00
user
d884d2bb55 refactor: switch Mumble voice to pymumble transport
asyncio's SSL memory-BIO transport silently drops voice packets even
though text works fine. pymumble uses blocking ssl.SSLSocket.send()
which reliably delivers voice data.

- Rewrite MumbleBot to use pymumble for connection, SSL, ping, and
  voice encoding/sending
- Bridge pymumble thread callbacks to asyncio via
  run_coroutine_threadsafe for text dispatch
- Voice via sound_output.add_sound(pcm) -- pymumble handles Opus
  encoding, packetization, and timing
- Remove custom protobuf codec, voice varint, and opus ctypes wrapper
- Add container patches for pymumble ssl.wrap_socket (Python 3.13) and
  opuslib find_library (musl/Alpine)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 23:15:42 +01:00
user
d756e7c020 fix: add opus, ffmpeg, yt-dlp to container image
Music playback requires system libraries that were missing from the
Alpine-based container.
2026-02-21 21:47:49 +01:00
user
7206b27fb0 docs: add music playback documentation
USAGE.md music section under Mumble, CHEATSHEET.md music commands,
TASKS.md sprint update for v2.3.0.
2026-02-21 21:42:33 +01:00
user
47b13c3f1f feat: add Mumble music playback with Opus streaming
ctypes libopus encoder (src/derp/opus.py), voice varint/packet builder
and stream_audio method on MumbleBot (src/derp/mumble.py), music plugin
with play/stop/skip/queue/np/volume commands (plugins/music.py).
Audio pipeline: yt-dlp|ffmpeg subprocess -> PCM -> Opus -> UDPTunnel.
67 new tests (1561 total).
2026-02-21 21:42:28 +01:00
user
b074356ec6 fix: always pass server_hostname for Mumble TLS on pre-connected socket
asyncio.open_connection(sock=..., ssl=...) requires server_hostname
even when check_hostname is disabled. Pass self._host unconditionally.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 21:21:33 +01:00
user
9d4cb09069 feat: make SOCKS5 proxy configurable per adapter
Add `proxy` config option to server (IRC), teams, telegram, and mumble
sections. IRC defaults to false (preserving current direct-connect
behavior); all others default to true. The `derp.http` module now
accepts `proxy=True/False` on urlopen, create_connection,
open_connection, and build_opener -- when false, uses stdlib directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 21:19:22 +01:00
user
ca46042c41 docs: update docs for Mumble integration
Add Mumble sections to USAGE.md, CHEATSHEET.md, API.md, README.md.
Mark Mumble done in ROADMAP.md and TODO.md. Update TASKS.md sprint.
2026-02-21 21:02:46 +01:00
user
37c858f4d7 feat: add Mumble bot adapter with minimal protobuf codec
TCP/TLS connection over SOCKS5 proxy to Mumble servers for text chat.
Minimal varint/field protobuf encoder/decoder (no external dep) handles
Version, Authenticate, Ping, ServerSync, ChannelState, UserState, and
TextMessage message types. MumbleBot exposes the same duck-typed plugin
API as Bot/TeamsBot/TelegramBot. 93 new tests (1470 total).
2026-02-21 21:02:41 +01:00
user
0d92e6ed31 docs: update docs for Telegram integration 2026-02-21 20:06:29 +01:00