Discord bot that records voice channel audio, transcribes it with faster-whisper, and optionally generates a short recap with Ollama. Saves WAV files and transcript text to disk; posts only a recap (if enabled) and links to saved files in the channel.
- Voice recording β Joins a voice channel and records participants (WAV). Max length and βwarning before stopβ are configurable.
- Transcription β Speech-to-text via faster-whisper (model/device/compute_type in
.env). Runs in a thread so the bot stays responsive. - Saved files β WAV and transcript
.txtare written to a recordings directory (configurable). The bot does not post transcript text in the channel, only a recap (if Ollama is on) and safe references to saved files (paths relative to the recordings dir, or basenames only via env). - Ollama recap β Optional short summary (200β300 chars) after each recording: what was discussed, decisions, whoβs responsible. In the same language as the dialogue. Prompt is in
prompts/recap.txt. - Auto-stop β When the last human leaves the voice channel, recording stops and processing runs. Mute/deafen in the same channel is ignored (bot does not leave).
- Config β All settings via
.env(prefix, temp/recordings dirs, Whisper, Ollama, recap prompt path). OptionalWATSON_FORCE_IPV4if voice fails on broken IPv6; optionalWATSON_LOCKDOWN_VOICE_COMMANDSand path privacy flags β see.env.example.
- Python 3.10β3.13 (
py-cord2.8.x RC wheels do not declare support for 3.14 yet; use 3.12 for local dev if your system Python is newer). - Discord bot token β create an application and bot; enable Message Content Intent and Server Members Intent
- macOS (Homebrew): Opus from
/opt/homebrew/lib/libopus.dylib(or setOPUS_LIB_PATHin.env)
For recap: Ollama installed and running (e.g. ollama run llama3.2). Set OLLAMA_RECAP_MODEL in .env to enable.
- Open the invite link at the top of this README.
- Select the server (you need Manage Server or Administrator).
- Click Authorize.
Then use !join, !record, !stop, !leave, !check in a text channel (see Usage).
-
Clone and venv
cd Watson python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate
-
Install dependencies
pip install -r requirements.txt
Main deps: py-cord 2.8.0rc2 (2.8.x prerelease on PyPI with ongoing DAVE/voice fixes β pin to stable
py-cord==2.8.0when it appears),faster-whisper,ollama,python-dotenv,aiohttp. -
Configure
cp .env.example .env
Set at least:
DISCORD_TOKEN=your_bot_token_here
Optional (see
.env.example):BOT_COMMAND_PREFIX(default!)WATSON_TEMP_DIR,WATSON_RECORDINGS_DIR(default./temp,./recordings)RECORDING_MAX_MINUTES,WARNING_BEFORE_STOP_MINUTESWHISPER_MODEL,WHISPER_DEVICE,WHISPER_COMPUTE_TYPE,TRANSCRIPT_LANGUAGE,TRANSCRIPT_BEAM_SIZEOLLAMA_HOST,OLLAMA_RECAP_MODEL,RECAP_PROMPT_FILE(recap prompt path)WATSON_LOCKDOWN_VOICE_COMMANDS,WATSON_ALLOWED_ROLE_IDS(restrict voice commands on public servers)WATSON_DISCORD_HIDE_PATHS(show only filenames in Discord instead of relative paths)WATSON_MAX_CONCURRENT_TRANSCRIPTIONS(parallel Whisper pipelines across guilds; default1)
Bot and invite: Developer Portal β Bot β enable intents β OAuth2 URL Generator (scope bot, permissions: View Channels, Connect, Speak, Send Messages, Read Message History, Attach Files).
-
Invite the bot. Required permissions: View Channels, Connect, Speak, Send Messages, Read Message History, Attach Files.
-
In a text channel:
Command Description !joinBot joins your current voice channel !recordStart recording (max length from RECORDING_MAX_MINUTES; warning in channel before auto-stop)!stopStop recording and process (transcribe + optional recap, then post recap and file links) !leaveBot leaves the voice channel !checkConnection status and bot permissions (embed) -
After processing, the bot posts Done, then the recap (if
OLLAMA_RECAP_MODELis set), then references to saved files (relative to your recordings directory by default). No full transcript text is sent in the channel.
For public servers, enable WATSON_LOCKDOWN_VOICE_COMMANDS so only moderators (Manage Server / Administrator / configured roles) can use !join, !record, !stop, and !leave. !check stays available for diagnostics.
Saved transcript file format: first line = header (date, time, guild name, channel name); blank line; recap (if any); blank line; transcript body.
- Ollama runs as a separate service; the bot connects to it via
OLLAMA_HOST. - Recordings directory in the container is mounted from the host so files persist.
docker compose up -dThen pull a model for recap (if you use it):
docker compose exec ollama ollama pull llama3.2In .env set OLLAMA_RECAP_MODEL=llama3.2 (and optionally override OLLAMA_HOST; compose sets OLLAMA_HOST=http://ollama:11434).
On a remote host, mount your host folders in docker-compose.yml, e.g.:
volumes:
- /data/watson/recordings:/app/recordings
- /data/watson/temp:/app/temp- Bot left when I muted β Fixed: the bot only leaves when someone actually leaves the channel; mute/deafen in the same channel is ignored.
- High memory β Use
WHISPER_DEVICE=cpuandWHISPER_COMPUTE_TYPE=int8; prefer smaller Whisper models if needed. - Voice works better over IPv4 β Some networks break Discord UDP over IPv6; set
WATSON_FORCE_IPV4=1in.env. - Voice
4017/ DAVE (E2EE) β If Discord closes the voice websocket withConnectionClosedcode4017, the gateway rejected the handshake because DAVE (mandatory Discord voice endβtoβend encryption) negotiation failed. Py-cord may not yet satisfy DAVE on your shards; Watson only surfaces the failure in chat/logs β fix requires a Py-cord release with DAVEβcompatible voice (or another supported client). See pycord#3135. - Slow transcription β Use GPU:
WHISPER_DEVICE=cuda,WHISPER_COMPUTE_TYPE=float16(and install CUDA deps). - Heavy load from multiple guilds β Default
WATSON_MAX_CONCURRENT_TRANSCRIPTIONS=1serializes Whisper work across servers; increase only if CPU/GPU allows. - No recap β Ensure Ollama is running and
OLLAMA_RECAP_MODELis set; in Docker,OLLAMA_HOST=http://ollama:11434is set by compose. - Bot doesnβt respond β Enable Message Content Intent (and Server Members Intent) in the Developer Portal.
- "Error occurred while decoding opus frame" β Usually a single bad voice packet; recording often continues. If itβs frequent, install libopus (e.g.
brew install opuson macOS,apt install libopus0on Debian) and setOPUS_LIB_PATHin.envto the library path (see.env.example).
Discord and faster-whisper are mocked; no token or model needed:
pytest tests/ -vSee tests/README.md for testing concurrent recordings (mocks and manual).
watson/
βββ __init__.py
βββ bootstrap.py # Logging, dirs, env check, bot.run
βββ config.py # Env-backed Settings
βββ discord_patches.py # Optional IPv4, Opus, VoiceClient shutdown patch
βββ env_check.py # Writable dirs + optional Ollama probe
βββ handlers.py # Commands, listeners, recording pipeline
βββ paths.py # Safe path strings for Discord messages
βββ permissions.py # Optional lockdown for voice commands
βββ recap.py # Ollama recap (blocking helper)
βββ state.py # Bot instance + transcribing guild set
βββ transcribe.py # Lazy Whisper + transcript formatting
main.py # Entry: dotenv, IPv4 patch, run_bot()
prompts/
βββ recap.txt # Prompt for recap ({{TRANSCRIPT}} placeholder)
scripts/
βββ discord_voice_smoke.py # Standalone voice gateway diagnostic
tests/
βββ conftest.py # Mocks for handlers/transcribe imports
βββ test_main.py # Helpers, config, command behaviour
βββ README.md # Concurrent recording notes
Dockerfile
docker-compose.yml
.env.example
requirements.txt
README.md
Use and modify as you like.