PipSqueekBot
A Discord music bot I run myself, with an optional mode where an AI plays DJ and keeps a mood.
I built this for my friends. We spend a lot of time in Discord voice channels, and most music bots are vending machines: you type a command, a song comes out. I wanted one that behaves more like the friend who has the aux in a small group. It notices who is in the call, queues a handful of songs by an artist instead of one, and says something dry between tracks. I call it Pip, short for PipSqueek. It is a program that joins a Discord voice channel the way a member would and plays music into it.
It has two modes. In the first you type short commands like pip play and pip skip, and it does exactly that. In the second I hand the whole text channel over to a language model, the same kind of program that sits behind the well-known chatbots. People just talk to it. "Put on something chill" works. It can do most of what the commands do, like queueing, skipping, volume and playlists, and it looks at who is listening and what they liked before it picks anything.
Most of the work went into making it a character. I wrote a short description of who it is: small, enormous taste, dry, writes in lowercase, a bit dramatic about small things. Getting skipped is a personal attack. It keeps a mood. If people skip its picks it gets salty, if they heart them it gets smug, and the mood fades over a couple of hours, the way a bad moment does for a person. It also saves inside jokes from the server and brings them back later when they fit. It can also run a guess-the-song game, post a weekly recap, and roast your music taste from what you actually played. When it did that to me, it decided I have terrible taste in music. That is wrong, by the way.
It works, and people have actually used it. It has run in a couple of servers, about ten friends in each, and it went down pretty well. People liked its attitude and its features, though different people reacted to it in different ways. I run it on my own computer. The AI part took far more effort than the music part. In the first ten days I rebuilt how the model talks to the rest of the bot three times, and early versions got stuck in loops or went silent after the last song ended. It also posted notes to itself, like "(silent check-in, queue is healthy)", where the whole channel could read them. I fixed that with a rule in its instructions and a filter in the code.
There is a version where you talk to it out loud in the voice channel. It is written, but I haven't switched it on. I never wrote automated tests either; I found problems by using it.
What's next
Finishing the voice version, and pushing the newer code to GitHub. The copy there is older and doesn't have the mood, the games or the personality yet. I would also like to put it in front of more people than the couple of servers it has been in so far.
What I wanted
I built it for my friends, to use in our own voice channels. A music bot is usually a command parser attached to a queue. I wanted one with a bit of judgement. It should queue four or five tracks for an artist instead of one, notice who is in the voice channel and what they have hearted before, and say one dry thing between songs. It should also know when to say nothing, which turned out to be the hard part.
It has two modes. Mk1 is ordinary prefix commands (pip play, pip skip) and is always available. Mk2 hands one text channel to a language model that can call the same playback functions as tools. I started the bot at the end of April 2026 and added Mk2 a few days later. Since then it has run in a couple of servers with about ten friends each. People liked the attitude and the features, and different people reacted to it differently.
How playback works
Playback is discord.py with yt-dlp and FFmpeg. A request is resolved to a video, downloaded and converted to opus (quality 192) into a cache folder, then played through FFmpeg into the voice client. While one track plays, the next three in the queue download in the background (LOOKAHEAD_DEPTH = 3), so the next track is normally ready by the time the current one ends. The cache holds up to 500 songs or 2048 MB and removes whichever file was accessed longest ago. A replayed song is instant.
What are yt-dlp and FFmpeg?
yt-dlp is a command-line tool that finds a video on YouTube and downloads its audio. FFmpeg is the standard tool for converting audio and video between formats and for streaming them. The bot uses yt-dlp to fetch and FFmpeg to convert the audio and feed it to Discord in the format its voice connection expects.
The player sends PCM audio through a volume wrapper instead of passing opus straight through, which is what lets volume change live while a track is playing. I switched to it from raw opus to get rid of audio jitter. The now-playing message edits itself every 10 seconds with a progress bar, and has pause and skip buttons that only work for whoever queued the track, and for me. A heart reaction on it saves the song to that person's taste profile, which Mk2 reads later. Around that there are playlists with shareable 8-character IDs, Spotify playlist import (matched to YouTube songs, with a report of what didn't match), and a radio mode where Pip introduces tracks out loud with a text-to-speech voice (edge-tts).
The Mk2 agent
In Mk2, every message in the agent channel that isn't a pip command starts a turn, and so does every @mention of the bot. Messages that start with "pip " still run as normal commands. A turn is a loop. The model gets the conversation, replies with tool calls, the bot runs them and appends the results, and this repeats until the model stops calling tools, up to 50 rounds per turn. Only one turn runs per server at a time; anything that arrives in the meantime waits in a queue. The conversation is trimmed to the last 240 entries and saved to SQLite after each turn, so it survives a restart. pip forget wipes it, and lore, memories and mood stay.
What is function calling?
Instead of only writing text, the model can answer with a structured request such as "call play_song with the query 'jungle fred again'". The program runs that function, hands the result back as the next message, and the model carries on from there. It is how a language model gets to do things in the world rather than just talk about them. Here there are 41 such functions.
There are 41 tools. The prompt says that a request for an artist should become four or five play_song calls in one turn. When the model does that, the bot starts all the lookups at the same time and queues the tracks in order, so a whole set doesn't wait on one search after another.
| Group | Tools | Examples |
|---|---|---|
| Playback and queue | 15 | play_song, skip_song, seek, set_volume, shuffle_queue, move_in_queue |
| Looking things up | 3 | get_queue, get_now_playing, search_songs |
| Playlists | 4 | create_playlist, play_playlist, add_to_playlist |
| History and taste | 4 | get_user_history, get_liked_songs, get_recommendations |
| Memory | 4 | store_memory, retrieve_memory, list_memories, delete_memory |
| Waking itself up | 3 | schedule_wakeup, cancel_wakeup, sleep |
| Talking in Discord | 3 | send_message, add_reaction, poll |
| Character and games | 3 | add_lore, shift_mood, start_guess_game |
| Other | 2 | web_search, leave_note |
Before each model call the bot rebuilds a status block and appends it to the system prompt: who is in which voice channel and what they have hearted, what is playing, the next five tracks, the last ten plays, the saved memories, Pip's mood, five random lore entries and the last few things it said, so it doesn't repeat itself. The tools are the hands. The prompts are the actual configuration.
A turn can also start without a message. A bit before a track ends (5% of its length before the end, at least 5 seconds away, and only for tracks longer than 15 seconds) the player wakes the agent. If a track ends and the queue is empty, it wakes the agent 2 seconds later. On a wakeup with nobody in voice the prompt says to leave, with a low queue to call get_recommendations and queue from that, and with a healthy queue to do nothing and say nothing. The model can also schedule its own wakeups, between 5 seconds and an hour.
The part of the prompt I care about most is the split between chat mode and DJ mode. Someone just talking gets a short reply, a reaction or silence, and the queue is never touched. Someone asking for music gets action. When in doubt it is chat mode. Here is the real text for a few of those rules.
"yo wassup" → send_message, something short in your voice "lol" → add_reaction or nothing at all. "i'm bored" → ambiguous. Offer, don't auto-play. "play [artist]" → play_song x 4-5 (varied tracks across eras, all in ONE turn), then send_message calling out the set When in doubt, it's chat mode.
Giving it a character
The voice lives in one file that every line Pip says goes through: agent replies, DJ comments, radio intros, game banter, the recap. It says what Pip is like and how it writes: lowercase, short, dry, almost no exclamation marks, and a list of banned words (banger, slaps, "let's go"). A few of the example lines in it: "queued. you're welcome in advance", "skipped my pick. noted. forever", "baby shark. bold. queued." The file also says what Pip does when someone is rude (an unbothered comeback, never anything cruel about a person) and what it does when someone is actually down (drops the bit a little).
Mood is a single number between -1 and 1. A skip of one of Pip's own picks moves it by -0.15, a heart on one of its picks moves it by +0.15, and the model can nudge it by 0.2 either way when someone is properly nice or properly rude. A skip counts when a track ends more than 10 seconds before its duration. The number decays toward 0 with a half-life of 2 hours, and it is turned into one of five labels that go into the prompt as "your mood right now". The last few reasons it moved are included too, with a note not to keep harping on them.
What does a half-life of 2 hours mean here?
Every 2 hours, half of whatever mood is left disappears. After 2 hours a score of -0.4 has faded to -0.2, after 4 hours to -0.1, and so on. It never snaps back to neutral, it just drifts there, which is closer to how a person gets over something than a timer that resets.
| After | Share left | Mood |
|---|---|---|
| 0 h | 100% | -0.150 |
| 2 h | 50% | -0.075 |
| 4 h | 25% | -0.038 |
| 6 h | 12.5% | -0.019 |
Lore is a list of memorable moments per server. Pip can save one itself with add_lore (the prompt says only genuinely memorable things, not every message), or I can add one by hand with pip lore add. Five random entries go into the prompt on every turn, so old jokes come back at random instead of on a schedule.
What does the temperature setting do?
When a language model writes, it picks each next word from a ranked list of likely choices. Temperature controls how adventurous that pick is: low means it almost always takes the top choice, high means it sometimes goes for a less likely one. I run the agent at 1.0 because this model family is tuned for it, and lowering it can make the bot get stuck in loops.
The rest
Guess the song (pip guess) has Pip pick tracks from the server's taste and play a clip. You get 30 seconds a round, a hint of the first letter of each word at 15 seconds, 2 points for the title and 1 for the artist, with a 3 to 10 round limit (5 by default). Guesses are cleaned up first (brackets and "feat." removed) and accepted if they match closely enough, so a small typo still counts. The queue is paused and restored around the game, and there is an all-time leaderboard. pip wrapped is a recap of the week, month or all time: top songs, top requesters, most skipped, most hearted, and a "worst request" award, with Pip writing the commentary from those numbers. It posts itself on Sunday evenings if you leave it on. pip roast roasts someone's taste from what they have actually played. My favourite result so far is that it thinks I have terrible taste in music. It is wrong.
What didn't work
I have no measurements for this one. I never scored how good the DJ is, and there are no automated tests, so most problems showed up by using it, mostly with friends in the call.
The agent loop took the most rework. In the first ten days I rewrote it three times. The first version already used function calling. Then I had the model reply in JSON that I parsed myself, then a streaming ReAct-style loop, and then I went back to plain function calling, which is what is there now. On the way I fixed deadlocks, a loop where it kept searching, and stale answers about the queue. A skip with an empty queue once left the bot silent indefinitely, which is why the player now wakes the agent 2 seconds after the queue runs out. At one point the model also wrote notes to itself, like "(silent check-in, queue is healthy)", and they were posted to the channel. So the prompt now says everyone can see them, and a filter drops anything shaped like that.
The model has changed too. I started on Google's Gemini, moved to DeepSeek through OpenRouter for a while, and the default now is a Gemini Flash model again. It goes to Google's API directly when a Gemini key is set, and through OpenRouter otherwise or when a Gemini call fails. To compare models I have a small script that runs the same scripted Discord scenarios (like "play Baby Shark") through two of them. I read the replies and decide by eye. It doesn't score anything.
Unfinished
The voice version (speech-to-text, then the same agent) is written but not loaded, so for now you type to it. The public GitHub repo is an earlier version: the personality, mood, lore, games and recap code described above is only on my machine. I want to push it, write some tests, and try it in more servers than the couple it has been in.
- Stack
- Python, discord.py 2.x, yt-dlp, FFmpeg, SQLite (aiosqlite), edge-tts, a Gemini or OpenRouter model
- Status
- Working
- Code
- github.com/Forkei/PipSqueekBot (an earlier version; the newer code is not pushed yet)