A Discord soundboard bot with a web interface. Browse and search sounds on a web page, then play them in Discord voice channels or copy links to paste in chat.
- Web Interface: Browse, search, and preview sounds from a responsive web UI
- Group previews: Click a group to play a shuffled member; click it again while loading or playing to stop. Stopping does not advance the shuffle or record another play. A group's highlight and progress belong to that exact preview request and clear when another sound or group replaces it, even when both select the same member.
- Group clips: Authenticated group previews, including shared group links, automatically open the selected member's clip and retarget the existing muted viewer to its audio clock. Audio-only members leave the viewer unchanged; Hide video and Close suppress automatic reopening until Show video is selected.
- Discord Integration: Play sounds in voice channels via slash commands
- Sound Management: Add sounds from URLs (supports any site yt-dlp can download from)
- Python 3.14+
- uv (Python package manager)
- FFmpeg (for audio processing)
- Node.js 24+ (for building web assets)
- A Discord bot token
# Install Python dependencies
uv sync
# On Linux/macOS, include uvloop for better async performance
uv sync --extra unix
# Install Node.js dependencies and build web assets
npm ci
npm run buildCreate a .env file in the project root:
# Required: Discord bot token
token=your_discord_bot_token_here
# Optional: Comma-separated guild IDs for local command registration
test_guild_ids=123456789,987654321
# Optional: Override default paths
# state_file=config/state.json
# sounds_folder=mount/sounds
# static_folder=web/dist
# templates_folder=web/templatemkdir -p mount/soundsuv run python -m soundbotThe web interface will be available at http://localhost:8080.
- Copy
.env.exampleto.envand configure your settings - Run:
docker-compose up --build| Variable | Description |
|---|---|
token |
Discord bot token (required) |
test_guild_ids |
Comma-separated guild IDs for command registration |
SOUNDBOT_PORT_8080 |
Host port to map to container port 8080 |
DOCKERCONFDIR |
Base path for config/data volumes |
YTDLP_POT_PROVIDER_URL |
Internal bgutil POT provider URL (default http://192.168.0.10:4416) |
YTDLP_COOKIES_FILE |
Optional cookies file used for one retry after an anonymous yt-dlp failure; in Docker, bind the client-local file read-only (the example path is /run/secrets/yt-dlp-cookies.txt) |
The container pairs bgutil-ytdlp-pot-provider==2.0.2 with the shared HTTP
provider 2.0.2, and builds with stable yt-dlp 2026.10.21 and Node 24.22.1.
Keep youtube:player_client=mweb and
youtubepot-bgutilhttp:base_url=http://192.168.0.10:4416 unchanged.
The existing uv-owned runtime yt-dlp update path remains in place.
Coordinate the provider and every client plugin before deployment through the
canonical provider-coupled homelab-infra playbook. Check plugin discovery,
then exercise SoundBot's actual downloader arguments with a small public
simulation/download in isolated temporary storage, without starting Discord or
writing sound/state data. Provider /ping alone is not a download proof.
Do not provision or inspect cookies for this check, and do not automatically
roll back to the provider's vulnerable 1.3.2 release.
The development lock uses certifi 2026.10.19 for the
emSign CA incident.
Docker's existing uv pip installation does not consume that lock, so this is
not proof of the runtime CA selection. Check the candidate's native Python
default and yt-dlp-selected trust stores for absence of emSign ECC Root CA - G3,
without adding an SSL override or changing the global trust configuration.
The targeted frontend lock update does not resolve all Gulp 4/CLI 2 build-chain
advisories. This npm dependency tree runs in the build stage; the runtime image
copies generated assets, not node_modules. The lock update alone does not
establish that the application or its build chain is fully patched.
├── src/soundbot/ # Python backend
│ ├── core/ # Settings and state management
│ ├── discord/ # Discord bot client and commands
│ ├── models/ # Data models
│ ├── services/ # Business logic
│ └── web/ # FastAPI web server and routes
├── web/ # Frontend assets
│ ├── scripts/ # TypeScript source
│ ├── styles/ # CSS
│ ├── static/ # Static assets
│ └── template/ # Jinja2 templates
├── config/ # Runtime data (sounds, state)
├── Dockerfile
├── docker-compose.yml
└── pyproject.toml
# Install all dependencies
uv sync
npm ci
# Build web assets
npm run build
# Run the bot (serves web UI at http://localhost:8080)
uv run python -m soundbotNote: A file lock prevents multiple instances from running simultaneously. If using Docker, stop it first with
docker compose stop.
Video cuts decode the original at the saved start/end timestamps before encoding the intermediate MKV at the existing browser width limit (1280px), with even dimensions for H.264. This avoids a needless full-resolution intermediate encode for 4K sources. Accurate cuts use two decoder and encoder threads to bound retained frame memory, including tall portrait sources, without changing the established width/aspect contract. Do not use stream-copy seeking for cuts: it retains preceding-keyframe frames and shifts that preroll into the clip. Browser MP4s consume the accurately cut MKV (or apply the same cut directly to the original when no intermediate exists). The muted web viewer follows the selected sound's actual audio-playback clock, not the button-click time.
For an existing affected library, repair only demonstrated bad derivatives
from retained originals and saved timestamps. Preserve exact MKV/MP4 preimages
for operator-assisted rollback, generate and inspect candidates before atomic
replacement, and do not rewrite sound metadata or normalized OGGs just to fix
video. Deployments on the Apps fleet use the canonical homelab-infra
SoundBot digest pin and provider-coupled playbook, not this checkout's local
Compose example.
Run the isolated backend suite with uv run pytest. Its fixtures redirect
state and sound paths before imports; real FFmpeg timeline regressions require
ffmpeg and ffprobe on PATH.
The native CLI uses the same sound services as Discord and the web UI, under
the existing exclusive state lock. It is an offline administrative tool:
stop the bot before running commands against its state, including info and
list. It uses the trusted local/container account and existing configuration,
not a Discord OAuth cookie. Never bypass the state lock or run a second state
owner against the live library. --help does not load settings or take the lock.
From a configured development checkout:
uv run soundbot-cli --help
# Equivalent module entry point:
uv run python -m soundbot.cli --helpFor production, use the target's canonical operator procedure and accepted
image, existing environment, user, binds and network. The container interpreter
is /app/.venv/bin/python; a stopped container cannot accept docker exec.
Run only one offline command at a time in the coordinated bot-only window,
and restart the bot only after that process exits and releases the lock.
Do not start Discord or a second web server just to run the CLI.
Management commands print JSON and exit nonzero on failure. Names and aliases
resolve exactly; partial matches never choose a target. add and import-file
add full-length sounds without overwriting existing entries:
soundbot-cli add friday20221216 'https://www.youtube.com/watch?v=VIDEO_ID'
# Or prepare/download once elsewhere, then COPY that full source into retained
# SoundBot storage. The input remains untouched and provenance is preserved.
soundbot-cli import-file friday20221216 /input/report.mp4 \
--source-url 'https://www.youtube.com/watch?v=VIDEO_ID' \
--source-title 'Weather Report 12/16/22'
# Inspect the saved full-length sound before cutting its retained original.
# Trim does not download again. Timestamps accept HH:MM:SS, MM:SS or seconds.
soundbot-cli info friday20221216
soundbot-cli trim friday20221216 8.9 17.7
soundbot-cli list
soundbot-cli group create lynchfridays
soundbot-cli group add lynchfridays friday20221216
soundbot-cli group list lynchfridays
soundbot-cli group list
soundbot-cli group remove lynchfridays friday20221216
soundbot-cli group delete lynchfridays
# Deletion permanently removes the retained original and derivatives.
# Remove memberships first, retain recovery copies, and name only owned entries.
soundbot-cli delete friday20221216 --yes
# Existing maintenance tools:
soundbot-cli clip VIDEO NAME START END [--volume N] [--overwrite]
soundbot-cli regenerate-audio [--sound NAME] [--dry-run]
soundbot-cli check-sounds [--remove]
soundbot-cli backfill-durations [--state-file PATH] [--sounds-folder PATH] [--write]Unlike import-file, the legacy clip command does not copy its source:
it retains the absolute external path. Do not use it for full-length-first
staging, and do not remove its external source while the sound still uses it.
Inspect list/info and group membership before resuming a partial import;
a failed duplicate add is not permission to overwrite or download again.
Successful processing alone is not a playback check: verify actual served
audio/video, clip boundaries and group membership after restarting the bot.
clip requires the source to be reachable from wherever the bot runs. For
the Docker container, that means bind-mounting media shares (e.g.
/mnt/tv:/mnt/tv:ro) into the service.
Run these in separate terminals:
# Terminal 1: Watch and rebuild CSS/TypeScript on changes
npm run dev
# Terminal 2: Run the Python backend
uv run python -m soundbot- Windows: Uses standard
asyncioevent loop - Linux/macOS: Uses
uvloopfor improved async performance (install withuv sync --extra unix)
| Command | Description |
|---|---|
/add <name> <url> |
Add a new sound from URL (YouTube, etc.) with optional start/end times |
/delete <name> |
Delete a sound |
/rename <old> <new> |
Rename a sound |
/trim <name> |
Set new start/end timestamps |
/adjust <name> |
Adjust timestamps relatively (add/remove seconds) |
/volume <name> <level> |
Set volume (0.1-5.0, 1.0 = normal) |
/info <name> |
Get sound details |
/list [search] |
List all sounds or search |
New and renamed sound names may contain up to 500 Unicode code points. Existing persisted names remain loadable, and the on-disk storage slug is a separate short identifier.
| Command | Description |
|---|---|
!soundname |
Play a sound (supports partial matching) |
!stop |
Stop current playback |
!leave |
Leave voice channel |
!sounds [search] |
Quick list of sounds |
The bot automatically joins the best voice channel:
- The channel you're currently in
- A channel with users who recently used the soundbot
- The most populated channel
MIT