From 2c9fa9a14cfc3adcc0cabe50e006daa0a3456e6f Mon Sep 17 00:00:00 2001 From: HRiggs Date: Wed, 12 Nov 2025 23:11:11 -0500 Subject: [PATCH] Intial Commit --- .gitignore | 25 + README.md | 82 +++ Test | 1 - bot.log | 834 +++++++++++++++++++++++++++++ bot.py | 1304 ++++++++++++++++++++++++++++++++++++++++++++++ goodboy.ogg | Bin 0 -> 5686 bytes requirements.txt | 11 + stt.py | 35 ++ 8 files changed, 2291 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 README.md delete mode 100644 Test create mode 100644 bot.log create mode 100644 bot.py create mode 100644 goodboy.ogg create mode 100644 requirements.txt create mode 100644 stt.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e6e9d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Python +__pycache__/ +*.pyc +.venv/ +venv/ + +# Env and local config +.env + +# Audio artifacts +audio/ +tts_output.wav + +# OS / IDE +.DS_Store +Thumbs.db +.idea/ +.vscode/ + +# yt-dlp cache +yt-dlp.conf +.cache/ + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..5f0e345 --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +## BasharBotV2 (Local audio Discord bot) + +A local-first Discord bot you can talk to. It: +- **Joins** your voice channel when you type `hey bashar join` +- **Plays YouTube music** when you say/type `hey bashar play ` +- **Speaks** “Playing ” with local TTS (pyttsx3) +- Built for Windows; runs entirely **locally** (no external APIs) + +### 1) Prerequisites (Windows) +- Python 3.10–3.12 +- FFmpeg in PATH: + - PowerShell (Admin): + ```powershell + choco install ffmpeg -y + ``` +- A Discord bot token created at the Developer Portal + +### 2) Setup +```powershell +cd "D:\7. Git\BasharBotV2" +python -m venv .venv +.venv\Scripts\Activate.ps1 +pip install -U pip +pip install -r requirements.txt +``` + +Create a `.env` file in the project root: +``` +DISCORD_TOKEN=your-bot-token-here +WHISPER_MODEL_SIZE=small +``` + +Note: The bot uses local TTS via `pyttsx3` (Windows SAPI). +Local STT (Whisper via `faster-whisper`) scaffolding is included and will be wired to live voice in a follow-up edit. + +### 3) Run +```powershell +.venv\Scripts\Activate.ps1 +python bot.py +``` + +### 4) Use +In any text channel in your server: +- `hey bashar join` — the bot joins your current voice channel. +- `hey bashar play never gonna give you up` — bot speaks “Playing …” and streams the top YouTube result. +- `hey bashar skip` — skips the current track but keeps the queue. +- `hey bashar stop` — stops playback and clears the queue. +- `hey bashar leave` — disconnects from voice. + +Speech (local STT via faster-whisper): +- Send a voice message or audio file attachment that starts with your wake word in speech: + - Example voice: “hey bashar play never gonna give you up” + - The bot transcribes locally and executes the command. +- Spoken `skip`/`stop` commands work the same way. +- The wake word is tolerant (“hey bashar” / “hey bishar” / “hey vishar”, etc.) so mispronunciations still trigger the bot. + +Live hotword listening (wake word): +- Enabled by default. After `hey bashar join`, the bot listens continuously for “hey bashar …”, even while music is playing, and reacts almost immediately (no more 5‑second clips). +- Say “hey bashar leave” to stop listening and disconnect. +- Voice receive needs Opus on your machine. On Windows install Opus via `pip install opuslib` (shown below) or drop an `opus.dll` beside `python.exe`. If you still see “Opus library not found”, set `OPUS_LIB=C:\Path\To\opus.dll` in `.env`. +- Verifying Opus (Windows PowerShell): + ```powershell + .venv\Scripts\Activate.ps1 + pip install opuslib + ``` +- To disable the listener, set `HOTWORD_ENABLED=false` in `.env`. +- Optional voice reactions: + - Set `GOODBOY_USER_ID` in `.env` (default: `94578724413902848`). When that user speaks a complete English sentence, the bot queues `goodboy.ogg` locally. +- Logging control via `.env`: + - `TRANSCRIPT_LOG_ENABLED=true/false` + - `TRANSCRIPT_LOG_PATH=custom/transcript/path.log` + +You can queue more tracks by repeating `hey bashar play ...`. The bot handles reconnectable streams. + +### 5) Notes +- All processing is local. YouTube audio is streamed via `yt-dlp` + `ffmpeg`. +- Library: now using `discord.py` 2.x (voice enabled with `PyNaCl`). +- If you get an FFmpeg error, confirm it’s in PATH: + `ffmpeg -version` + + + diff --git a/Test b/Test deleted file mode 100644 index 65f5bb0..0000000 --- a/Test +++ /dev/null @@ -1 +0,0 @@ -ss \ No newline at end of file diff --git a/bot.log b/bot.log new file mode 100644 index 0000000..8f3cfe4 --- /dev/null +++ b/bot.log @@ -0,0 +1,834 @@ +22:54:36 [DEBUG] asyncio: Using proactor: IocpProactor +22:54:36 [INFO] discord.client: logging in using static token +22:54:37 [INFO] discord.gateway: Shard ID None has sent the IDENTIFY payload. +22:54:38 [INFO] discord.gateway: Shard ID None has connected to Gateway: ["gateway-prd-arm-us-east1-d-n1d3",{"micros":1669050,"calls":["id_created",{"micros":395,"calls":[]},"session_lookup_time",{"micros":846,"calls":[]},"session_lookup_finished",{"micros":11,"calls":[]},"discord-sessions-prd-2-27",{"micros":1667557,"calls":["start_session",{"micros":1652667,"calls":["discord-api-rpc-6655fff5f-6d5d7",{"micros":1496243,"calls":["get_user",{"micros":32963},"get_guilds",{"micros":4238},"send_scheduled_deletion_message",{"micros":11},"guild_join_requests",{"micros":2},"authorized_ip_coro",{"micros":12},"pending_payments",{"micros":62429},"apex_experiments",{"micros":58529},"user_activities",{"micros":7},"played_application_ids",{"micros":4},"linked_users",{"micros":3}]}]},"starting_guild_connect",{"micros":36,"calls":[]},"presence_started",{"micros":231,"calls":[]},"guilds_started",{"micros":52,"calls":[]},"lobbies_started",{"micros":1,"calls":[]},"guilds_connect",{"micros":1,"calls":[]},"presence_connect",{"micros":14549,"calls":[]},"connect_finished",{"micros":14555,"calls":[]},"build_ready",{"micros":13,"calls":[]},"clean_ready",{"micros":0,"calls":[]},"optimize_ready",{"micros":0,"calls":[]},"split_ready",{"micros":1,"calls":[]}]}]}] (Session ID: a4edf3c8dcbd62e09a2a9a04bc3c5bbe). +22:54:40 [INFO] basharbot: Logged in as Bashar Al-Assad Version 2#4174 (id=1014194017346531419) +22:54:40 [DEBUG] basharbot: ffmpeg found: C:\ProgramData\chocolatey\bin\ffmpeg.EXE +22:54:40 [WARNING] basharbot: Opus library not found; install opuslib/opus-tools if voice receive misbehaves. +22:54:40 [INFO] basharbot: Startup checks OK +22:54:40 [INFO] basharbot: Hotword listening: ENABLED (sinks available and HOTWORD_ENABLED=True) +22:54:41 [DEBUG] basharbot: Wake word detected. Raw='hey bashar join' | action='join' | args='' +22:54:41 [DEBUG] basharbot: Connect requested by riggs0 (Riggs0) in guild 763932345400950865 +22:54:41 [INFO] basharbot: Connecting to voice channel: penisary contact with her volvo (guild 763932345400950865) +22:54:41 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 1/4) +22:54:41 [INFO] discord.voice_client: Connecting to voice... +22:54:41 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +22:54:41 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:54:41 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-nrt12-942f95a0.discord.media +22:54:42 [ERROR] discord.voice_client: Failed to connect to voice... Retrying... +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 404, in connect + self.ws = await self.connect_websocket() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 375, in connect_websocket + await ws.poll_event() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\gateway.py", line 955, in poll_event + raise ConnectionClosed(self.ws, shard_id=None, code=self._close_code) +discord.errors.ConnectionClosed: Shard ID None WebSocket closed with 4006 +22:54:43 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:54:43 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 2) +22:54:43 [INFO] basharbot: Voice state update (guild 763932345400950865): penisary contact with her volvo -> None (self_mute=False deaf=False) +22:54:43 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:54:43 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-nrt12-942f95a0.discord.media +22:54:44 [ERROR] discord.voice_client: Failed to connect to voice... Retrying... +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 404, in connect + self.ws = await self.connect_websocket() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 375, in connect_websocket + await ws.poll_event() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\gateway.py", line 955, in poll_event + raise ConnectionClosed(self.ws, shard_id=None, code=self._close_code) +discord.errors.ConnectionClosed: Shard ID None WebSocket closed with 4006 +22:54:47 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:54:47 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 3) +22:54:47 [INFO] basharbot: Voice state update (guild 763932345400950865): penisary contact with her volvo -> None (self_mute=False deaf=False) +22:54:47 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:54:47 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-nrt12-942f95a0.discord.media +22:54:48 [ERROR] discord.voice_client: Failed to connect to voice... Retrying... +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 404, in connect + self.ws = await self.connect_websocket() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 375, in connect_websocket + await ws.poll_event() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\gateway.py", line 955, in poll_event + raise ConnectionClosed(self.ws, shard_id=None, code=self._close_code) +discord.errors.ConnectionClosed: Shard ID None WebSocket closed with 4006 +22:54:53 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:54:53 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 4) +22:54:53 [INFO] basharbot: Voice state update (guild 763932345400950865): penisary contact with her volvo -> None (self_mute=False deaf=False) +22:54:53 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:54:54 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-nrt12-942f95a0.discord.media +22:54:54 [ERROR] discord.voice_client: Failed to connect to voice... Retrying... +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 404, in connect + self.ws = await self.connect_websocket() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 375, in connect_websocket + await ws.poll_event() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\gateway.py", line 955, in poll_event + raise ConnectionClosed(self.ws, shard_id=None, code=self._close_code) +discord.errors.ConnectionClosed: Shard ID None WebSocket closed with 4006 +22:55:01 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:55:01 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 5) +22:55:01 [INFO] basharbot: Voice state update (guild 763932345400950865): penisary contact with her volvo -> None (self_mute=False deaf=False) +22:55:02 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:55:02 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-nrt12-942f95a0.discord.media +22:55:03 [ERROR] discord.voice_client: Failed to connect to voice... Retrying... +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 404, in connect + self.ws = await self.connect_websocket() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 375, in connect_websocket + await ws.poll_event() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\gateway.py", line 955, in poll_event + raise ConnectionClosed(self.ws, shard_id=None, code=self._close_code) +discord.errors.ConnectionClosed: Shard ID None WebSocket closed with 4006 +22:55:12 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:55:12 [INFO] basharbot: Voice connect succeeded on attempt 1 (penisary contact with her volvo) +22:55:12 [ERROR] basharbot: Voice connect returned but not connected (guild 763932345400950865) +22:55:12 [INFO] basharbot: Voice state update (guild 763932345400950865): penisary contact with her volvo -> None (self_mute=False deaf=False) +22:55:12 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:55:12 [INFO] basharbot: Joined voice channel for guild 763932345400950865 +22:55:12 [DEBUG] basharbot: Cannot start listener without an active voice client (guild 763932345400950865) +22:55:12 [ERROR] discord.state: Exception occurred during Voice Protocol voice server update handler +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\state.py", line 151, in logging_coroutine + await coroutine + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 340, in on_voice_server_update + await self.ws.close(4000) +AttributeError: '_MissingSentinel' object has no attribute 'close' +22:55:22 [INFO] discord.client: Received signal to terminate bot and event loop. +22:55:22 [INFO] discord.client: Cleaning up tasks. +22:55:22 [INFO] discord.client: Cleaning up after 1 tasks. +22:55:22 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:55:22 [INFO] discord.client: All tasks finished cancelling. +22:55:22 [INFO] discord.client: Closing the event loop. +22:57:22 [DEBUG] asyncio: Using proactor: IocpProactor +22:57:22 [INFO] discord.client: logging in using static token +22:57:23 [INFO] discord.gateway: Shard ID None has sent the IDENTIFY payload. +22:57:23 [INFO] discord.gateway: Shard ID None has connected to Gateway: ["gateway-prd-arm-us-east1-b-pg2d",{"micros":170426,"calls":["id_created",{"micros":448,"calls":[]},"session_lookup_time",{"micros":1230,"calls":[]},"session_lookup_finished",{"micros":9,"calls":[]},"discord-sessions-prd-2-1",{"micros":167983,"calls":["start_session",{"micros":140405,"calls":["discord-api-rpc-6655fff5f-rrdvf",{"micros":45668,"calls":["get_user",{"micros":8044},"get_guilds",{"micros":2872},"send_scheduled_deletion_message",{"micros":15},"guild_join_requests",{"micros":381},"authorized_ip_coro",{"micros":12},"pending_payments",{"micros":56212},"apex_experiments",{"micros":20615},"user_activities",{"micros":5},"played_application_ids",{"micros":3},"linked_users",{"micros":2}]}]},"starting_guild_connect",{"micros":31,"calls":[]},"presence_started",{"micros":3023,"calls":[]},"guilds_started",{"micros":66,"calls":[]},"lobbies_started",{"micros":1,"calls":[]},"guilds_connect",{"micros":2,"calls":[]},"presence_connect",{"micros":24433,"calls":[]},"connect_finished",{"micros":24442,"calls":[]},"build_ready",{"micros":13,"calls":[]},"clean_ready",{"micros":1,"calls":[]},"optimize_ready",{"micros":0,"calls":[]},"split_ready",{"micros":0,"calls":[]}]}]}] (Session ID: e93a554855fde7b36468115efa09bfdd). +22:57:25 [INFO] basharbot: Logged in as Bashar Al-Assad Version 2#4174 (id=1014194017346531419) +22:57:25 [DEBUG] basharbot: ffmpeg found: C:\ProgramData\chocolatey\bin\ffmpeg.EXE +22:57:25 [WARNING] basharbot: Opus library not found; install opuslib/opus-tools if voice receive misbehaves. +22:57:25 [INFO] basharbot: Startup checks OK +22:57:25 [INFO] basharbot: Hotword listening: ENABLED (sinks available and HOTWORD_ENABLED=True) +22:57:27 [DEBUG] basharbot: Wake word detected. Raw='hey bashar join' | action='join' | args='' +22:57:27 [DEBUG] basharbot: Connect requested by riggs0 (Riggs0) in guild 763932345400950865 +22:57:27 [INFO] basharbot: Connecting to voice channel: penisary contact with her volvo (guild 763932345400950865) +22:57:28 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 1/2) +22:57:28 [INFO] discord.voice_client: Connecting to voice... +22:57:28 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +22:57:28 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:57:28 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-nrt12-942f95a0.discord.media +22:57:29 [WARNING] basharbot: Voice connect failed with ConnectionClosed(code=4006, reason=) on attempt 1/2 +22:57:29 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:57:29 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +22:57:29 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +22:57:29 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> None (self_mute=False deaf=False) +22:57:29 [INFO] basharbot: Waiting 3.0s before retry 2 (guild 763932345400950865) +22:57:32 [DEBUG] basharbot: Detected lingering voice state without client for guild 763932345400950865; clearing before reconnect +22:57:32 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 2/2) +22:57:32 [INFO] discord.voice_client: Connecting to voice... +22:57:32 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +22:57:32 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:57:33 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-nrt12-942f95a0.discord.media +22:57:33 [WARNING] basharbot: Voice connect failed with ConnectionClosed(code=4006, reason=) on attempt 2/2 +22:57:33 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:57:33 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +22:57:33 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +22:57:33 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> None (self_mute=False deaf=False) +22:57:34 [ERROR] basharbot: Voice connect retries exhausted (guild 763932345400950865): Shard ID None WebSocket closed with 4006 +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\bot.py", line 913, in connect_to_author_channel + vc = await connect_voice_with_retry(channel) + File "D:\7. Git\BasharBotV2\bot.py", line 468, in connect_voice_with_retry + raise last_exc + File "D:\7. Git\BasharBotV2\bot.py", line 414, in connect_voice_with_retry + vc = await channel.connect(timeout=25.0, reconnect=False) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\abc.py", line 1994, in connect + await voice.connect(timeout=timeout, reconnect=reconnect) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 404, in connect + self.ws = await self.connect_websocket() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 375, in connect_websocket + await ws.poll_event() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\gateway.py", line 955, in poll_event + raise ConnectionClosed(self.ws, shard_id=None, code=self._close_code) +discord.errors.ConnectionClosed: Shard ID None WebSocket closed with 4006 +22:57:45 [DEBUG] basharbot: Wake word detected. Raw='hey bashar join' | action='join' | args='' +22:57:45 [DEBUG] basharbot: Connect requested by riggs0 (Riggs0) in guild 763932345400950865 +22:57:45 [DEBUG] basharbot: Detected lingering voice state without client for guild 763932345400950865; clearing before reconnect +22:57:45 [INFO] basharbot: Connecting to voice channel: penisary contact with her volvo (guild 763932345400950865) +22:57:46 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 1/2) +22:57:46 [INFO] discord.voice_client: Connecting to voice... +22:57:46 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +22:57:46 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:57:46 [DEBUG] basharbot: Wake word detected. Raw='hey bashar join' | action='join' | args='' +22:57:46 [DEBUG] basharbot: Connect requested by riggs0 (Riggs0) in guild 763932345400950865 +22:57:46 [DEBUG] basharbot: Detected stale voice client handle for guild 763932345400950865; cleaning up before reuse +22:57:46 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:57:46 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +22:57:46 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +22:57:46 [INFO] basharbot: Connecting to voice channel: penisary contact with her volvo (guild 763932345400950865) +22:57:46 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> None (self_mute=False deaf=False) +22:57:46 [DEBUG] basharbot: Wake word detected. Raw='hey bashar join' | action='join' | args='' +22:57:46 [DEBUG] basharbot: Connect requested by riggs0 (Riggs0) in guild 763932345400950865 +22:57:46 [DEBUG] basharbot: Detected lingering voice state without client for guild 763932345400950865; clearing before reconnect +22:57:46 [INFO] basharbot: Connecting to voice channel: penisary contact with her volvo (guild 763932345400950865) +22:57:46 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 1/2) +22:57:46 [INFO] discord.voice_client: Connecting to voice... +22:57:46 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +22:57:46 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:57:47 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-nrt12-942f95a0.discord.media +22:57:47 [DEBUG] basharbot: Wake word detected. Raw='hey bashar join' | action='join' | args='' +22:57:47 [DEBUG] basharbot: Connect requested by riggs0 (Riggs0) in guild 763932345400950865 +22:57:47 [DEBUG] basharbot: Detected stale voice client handle for guild 763932345400950865; cleaning up before reuse +22:57:47 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:57:47 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +22:57:47 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +22:57:47 [INFO] basharbot: Connecting to voice channel: penisary contact with her volvo (guild 763932345400950865) +22:57:47 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> None (self_mute=False deaf=False) +22:57:47 [DEBUG] basharbot: Detected lingering voice state without client for guild 763932345400950865; clearing before reconnect +22:57:47 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 1/2) +22:57:47 [INFO] discord.voice_client: Connecting to voice... +22:57:47 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +22:57:47 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:57:47 [DEBUG] basharbot: Wake word detected. Raw='hey bashar join' | action='join' | args='' +22:57:47 [DEBUG] basharbot: Connect requested by riggs0 (Riggs0) in guild 763932345400950865 +22:57:47 [DEBUG] basharbot: Detected stale voice client handle for guild 763932345400950865; cleaning up before reuse +22:57:47 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:57:47 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +22:57:47 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +22:57:47 [INFO] basharbot: Connecting to voice channel: penisary contact with her volvo (guild 763932345400950865) +22:57:47 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> None (self_mute=False deaf=False) +22:57:47 [DEBUG] basharbot: Detected lingering voice state without client for guild 763932345400950865; clearing before reconnect +22:57:47 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 1/2) +22:57:47 [INFO] discord.voice_client: Connecting to voice... +22:57:47 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +22:57:47 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:57:47 [DEBUG] basharbot: Wake word detected. Raw='hey bashar join' | action='join' | args='' +22:57:47 [DEBUG] basharbot: Connect requested by riggs0 (Riggs0) in guild 763932345400950865 +22:57:47 [DEBUG] basharbot: Detected stale voice client handle for guild 763932345400950865; cleaning up before reuse +22:57:47 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:57:47 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +22:57:47 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +22:57:47 [INFO] basharbot: Connecting to voice channel: penisary contact with her volvo (guild 763932345400950865) +22:57:47 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> None (self_mute=False deaf=False) +22:57:47 [WARNING] basharbot: Voice connect failed with ConnectionClosed(code=4006, reason=) on attempt 1/2 +22:57:47 [DEBUG] basharbot: Cleared lingering voice state for guild 763932345400950865 during cleanup +22:57:48 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 1/2) +22:57:48 [INFO] discord.voice_client: Connecting to voice... +22:57:48 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +22:57:48 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:57:48 [DEBUG] basharbot: Wake word detected. Raw='hey bashar join' | action='join' | args='' +22:57:48 [DEBUG] basharbot: Connect requested by riggs0 (Riggs0) in guild 763932345400950865 +22:57:48 [DEBUG] basharbot: Detected stale voice client handle for guild 763932345400950865; cleaning up before reuse +22:57:48 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:57:48 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +22:57:48 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +22:57:48 [INFO] basharbot: Connecting to voice channel: penisary contact with her volvo (guild 763932345400950865) +22:57:48 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> None (self_mute=False deaf=False) +22:57:48 [DEBUG] basharbot: Detected lingering voice state without client for guild 763932345400950865; clearing before reconnect +22:57:48 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 1/2) +22:57:48 [INFO] discord.voice_client: Connecting to voice... +22:57:48 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +22:57:48 [DEBUG] basharbot: Wake word detected. Raw='hey bashar join' | action='join' | args='' +22:57:48 [DEBUG] basharbot: Connect requested by riggs0 (Riggs0) in guild 763932345400950865 +22:57:48 [DEBUG] basharbot: Detected stale voice client handle for guild 763932345400950865; cleaning up before reuse +22:57:48 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:57:48 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +22:57:48 [INFO] basharbot: Connecting to voice channel: penisary contact with her volvo (guild 763932345400950865) +22:57:48 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:57:48 [INFO] basharbot: Waiting 3.0s before retry 2 (guild 763932345400950865) +22:57:48 [INFO] basharbot: Voice state update (guild 763932345400950865): penisary contact with her volvo -> None (self_mute=False deaf=False) +22:57:48 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 1/2) +22:57:48 [INFO] discord.voice_client: Connecting to voice... +22:57:48 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +22:57:48 [DEBUG] basharbot: Wake word detected. Raw='hey bashar join' | action='join' | args='' +22:57:48 [DEBUG] basharbot: Connect requested by riggs0 (Riggs0) in guild 763932345400950865 +22:57:48 [DEBUG] basharbot: Detected stale voice client handle for guild 763932345400950865; cleaning up before reuse +22:57:48 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:57:48 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +22:57:48 [INFO] basharbot: Connecting to voice channel: penisary contact with her volvo (guild 763932345400950865) +22:57:48 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:57:48 [INFO] basharbot: Voice state update (guild 763932345400950865): penisary contact with her volvo -> None (self_mute=False deaf=False) +22:57:48 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 1/2) +22:57:48 [INFO] discord.voice_client: Connecting to voice... +22:57:48 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +22:57:48 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:57:49 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-nrt12-942f95a0.discord.media +22:57:49 [DEBUG] basharbot: Detected stale voice client handle for guild 763932345400950865; cleaning up before reuse +22:57:49 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:57:49 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +22:57:49 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +22:57:49 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 1/2) +22:57:49 [INFO] discord.voice_client: Connecting to voice... +22:57:49 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +22:57:49 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:57:49 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:57:49 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-nrt12-942f95a0.discord.media +22:57:50 [WARNING] basharbot: Voice connect failed with ConnectionClosed(code=4006, reason=) on attempt 1/2 +22:57:50 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:57:50 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +22:57:50 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +22:57:50 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> None (self_mute=False deaf=False) +22:57:50 [WARNING] basharbot: Voice connect failed with ConnectionClosed(code=4006, reason=) on attempt 1/2 +22:57:50 [DEBUG] basharbot: Cleared lingering voice state for guild 763932345400950865 during cleanup +22:57:50 [INFO] basharbot: Waiting 3.0s before retry 2 (guild 763932345400950865) +22:57:50 [INFO] basharbot: Waiting 3.0s before retry 2 (guild 763932345400950865) +22:57:51 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 2/2) +22:57:51 [INFO] discord.voice_client: Connecting to voice... +22:57:51 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +22:57:51 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:57:51 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-nrt12-942f95a0.discord.media +22:57:52 [WARNING] basharbot: Voice connect failed with ConnectionClosed(code=4006, reason=) on attempt 2/2 +22:57:52 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:57:52 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +22:57:52 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +22:57:52 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> None (self_mute=False deaf=False) +22:57:52 [ERROR] basharbot: Voice connect retries exhausted (guild 763932345400950865): Shard ID None WebSocket closed with 4006 +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\bot.py", line 913, in connect_to_author_channel + vc = await connect_voice_with_retry(channel) + File "D:\7. Git\BasharBotV2\bot.py", line 468, in connect_voice_with_retry + raise last_exc + File "D:\7. Git\BasharBotV2\bot.py", line 414, in connect_voice_with_retry + vc = await channel.connect(timeout=25.0, reconnect=False) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\abc.py", line 1994, in connect + await voice.connect(timeout=timeout, reconnect=reconnect) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 404, in connect + self.ws = await self.connect_websocket() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 375, in connect_websocket + await ws.poll_event() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\gateway.py", line 955, in poll_event + raise ConnectionClosed(self.ws, shard_id=None, code=self._close_code) +discord.errors.ConnectionClosed: Shard ID None WebSocket closed with 4006 +22:57:53 [DEBUG] basharbot: Detected lingering voice state without client for guild 763932345400950865; clearing before reconnect +22:57:53 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 2/2) +22:57:53 [INFO] discord.voice_client: Connecting to voice... +22:57:53 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +22:57:53 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:57:53 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-nrt12-942f95a0.discord.media +22:57:53 [DEBUG] basharbot: Detected stale voice client handle for guild 763932345400950865; cleaning up before reuse +22:57:53 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:57:53 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +22:57:53 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +22:57:53 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 2/2) +22:57:53 [INFO] discord.voice_client: Connecting to voice... +22:57:53 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +22:57:53 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> None (self_mute=False deaf=False) +22:57:53 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:57:54 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-nrt12-942f95a0.discord.media +22:57:54 [WARNING] basharbot: Voice connect failed with ConnectionClosed(code=4006, reason=) on attempt 2/2 +22:57:54 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:57:54 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +22:57:54 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +22:57:54 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> None (self_mute=False deaf=False) +22:57:55 [ERROR] basharbot: Voice connect retries exhausted (guild 763932345400950865): Shard ID None WebSocket closed with 4006 +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\bot.py", line 913, in connect_to_author_channel + vc = await connect_voice_with_retry(channel) + File "D:\7. Git\BasharBotV2\bot.py", line 468, in connect_voice_with_retry + raise last_exc + File "D:\7. Git\BasharBotV2\bot.py", line 414, in connect_voice_with_retry + vc = await channel.connect(timeout=25.0, reconnect=False) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\abc.py", line 1994, in connect + await voice.connect(timeout=timeout, reconnect=reconnect) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 404, in connect + self.ws = await self.connect_websocket() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 375, in connect_websocket + await ws.poll_event() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\gateway.py", line 955, in poll_event + raise ConnectionClosed(self.ws, shard_id=None, code=self._close_code) +discord.errors.ConnectionClosed: Shard ID None WebSocket closed with 4006 +22:57:55 [WARNING] basharbot: Voice connect failed with ConnectionClosed(code=4006, reason=) on attempt 2/2 +22:57:55 [DEBUG] basharbot: Cleared lingering voice state for guild 763932345400950865 during cleanup +22:57:55 [ERROR] basharbot: Voice connect retries exhausted (guild 763932345400950865): Shard ID None WebSocket closed with 4006 +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\bot.py", line 913, in connect_to_author_channel + vc = await connect_voice_with_retry(channel) + File "D:\7. Git\BasharBotV2\bot.py", line 468, in connect_voice_with_retry + raise last_exc + File "D:\7. Git\BasharBotV2\bot.py", line 414, in connect_voice_with_retry + vc = await channel.connect(timeout=25.0, reconnect=False) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\abc.py", line 1994, in connect + await voice.connect(timeout=timeout, reconnect=reconnect) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 404, in connect + self.ws = await self.connect_websocket() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 375, in connect_websocket + await ws.poll_event() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\gateway.py", line 955, in poll_event + raise ConnectionClosed(self.ws, shard_id=None, code=self._close_code) +discord.errors.ConnectionClosed: Shard ID None WebSocket closed with 4006 +22:58:11 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:58:11 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:58:11 [ERROR] basharbot: Voice connect raised on attempt 1/2 +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\bot.py", line 414, in connect_voice_with_retry + vc = await channel.connect(timeout=25.0, reconnect=False) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\abc.py", line 1994, in connect + await voice.connect(timeout=timeout, reconnect=reconnect) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 396, in connect + await utils.sane_wait_for(futures, timeout=timeout) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\utils.py", line 721, in sane_wait_for + raise asyncio.TimeoutError() +asyncio.exceptions.TimeoutError +22:58:11 [INFO] basharbot: Waiting 3.0s before retry 2 (guild 763932345400950865) +22:58:12 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:58:12 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:58:12 [ERROR] basharbot: Voice connect raised on attempt 1/2 +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\bot.py", line 414, in connect_voice_with_retry + vc = await channel.connect(timeout=25.0, reconnect=False) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\abc.py", line 1994, in connect + await voice.connect(timeout=timeout, reconnect=reconnect) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 396, in connect + await utils.sane_wait_for(futures, timeout=timeout) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\utils.py", line 721, in sane_wait_for + raise asyncio.TimeoutError() +asyncio.exceptions.TimeoutError +22:58:12 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:58:12 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:58:12 [ERROR] basharbot: Voice connect raised on attempt 1/2 +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\bot.py", line 414, in connect_voice_with_retry + vc = await channel.connect(timeout=25.0, reconnect=False) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\abc.py", line 1994, in connect + await voice.connect(timeout=timeout, reconnect=reconnect) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 396, in connect + await utils.sane_wait_for(futures, timeout=timeout) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\utils.py", line 721, in sane_wait_for + raise asyncio.TimeoutError() +asyncio.exceptions.TimeoutError +22:58:12 [INFO] basharbot: Waiting 3.0s before retry 2 (guild 763932345400950865) +22:58:13 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:58:13 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:58:13 [ERROR] basharbot: Voice connect raised on attempt 1/2 +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\bot.py", line 414, in connect_voice_with_retry + vc = await channel.connect(timeout=25.0, reconnect=False) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\abc.py", line 1994, in connect + await voice.connect(timeout=timeout, reconnect=reconnect) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 396, in connect + await utils.sane_wait_for(futures, timeout=timeout) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\utils.py", line 721, in sane_wait_for + raise asyncio.TimeoutError() +asyncio.exceptions.TimeoutError +22:58:13 [INFO] basharbot: Waiting 3.0s before retry 2 (guild 763932345400950865) +22:58:13 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:58:13 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:58:13 [ERROR] basharbot: Voice connect raised on attempt 1/2 +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\bot.py", line 414, in connect_voice_with_retry + vc = await channel.connect(timeout=25.0, reconnect=False) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\abc.py", line 1994, in connect + await voice.connect(timeout=timeout, reconnect=reconnect) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 396, in connect + await utils.sane_wait_for(futures, timeout=timeout) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\utils.py", line 721, in sane_wait_for + raise asyncio.TimeoutError() +asyncio.exceptions.TimeoutError +22:58:13 [INFO] basharbot: Waiting 3.0s before retry 2 (guild 763932345400950865) +22:58:13 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:58:13 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:58:13 [ERROR] basharbot: Voice connect raised on attempt 1/2 +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\bot.py", line 414, in connect_voice_with_retry + vc = await channel.connect(timeout=25.0, reconnect=False) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\abc.py", line 1994, in connect + await voice.connect(timeout=timeout, reconnect=reconnect) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 396, in connect + await utils.sane_wait_for(futures, timeout=timeout) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\utils.py", line 721, in sane_wait_for + raise asyncio.TimeoutError() +asyncio.exceptions.TimeoutError +22:58:13 [INFO] basharbot: Waiting 3.0s before retry 2 (guild 763932345400950865) +22:58:14 [INFO] basharbot: Waiting 3.0s before retry 2 (guild 763932345400950865) +22:58:14 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 2/2) +22:58:14 [INFO] discord.voice_client: Connecting to voice... +22:58:14 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +22:58:14 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:58:15 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-nrt12-942f95a0.discord.media +22:58:15 [DEBUG] basharbot: Detected stale voice client handle for guild 763932345400950865; cleaning up before reuse +22:58:15 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:58:15 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +22:58:15 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +22:58:15 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 2/2) +22:58:15 [INFO] discord.voice_client: Connecting to voice... +22:58:15 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +22:58:15 [WARNING] basharbot: Voice connect failed with ConnectionClosed(code=4006, reason=) on attempt 2/2 +22:58:15 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:58:15 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +22:58:15 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:58:15 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:58:15 [INFO] basharbot: Voice state update (guild 763932345400950865): penisary contact with her volvo -> None (self_mute=False deaf=False) +22:58:16 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 2/2) +22:58:16 [INFO] discord.voice_client: Connecting to voice... +22:58:16 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +22:58:16 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:58:16 [ERROR] basharbot: Voice connect retries exhausted (guild 763932345400950865): Shard ID None WebSocket closed with 4006 +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\bot.py", line 913, in connect_to_author_channel + vc = await connect_voice_with_retry(channel) + File "D:\7. Git\BasharBotV2\bot.py", line 468, in connect_voice_with_retry + raise last_exc + File "D:\7. Git\BasharBotV2\bot.py", line 414, in connect_voice_with_retry + vc = await channel.connect(timeout=25.0, reconnect=False) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\abc.py", line 1994, in connect + await voice.connect(timeout=timeout, reconnect=reconnect) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 404, in connect + self.ws = await self.connect_websocket() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 375, in connect_websocket + await ws.poll_event() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\gateway.py", line 955, in poll_event + raise ConnectionClosed(self.ws, shard_id=None, code=self._close_code) +discord.errors.ConnectionClosed: Shard ID None WebSocket closed with 4006 +22:58:16 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-nrt12-942f95a0.discord.media +22:58:16 [DEBUG] basharbot: Detected stale voice client handle for guild 763932345400950865; cleaning up before reuse +22:58:16 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:58:16 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +22:58:16 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +22:58:16 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 2/2) +22:58:16 [INFO] discord.voice_client: Connecting to voice... +22:58:16 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +22:58:16 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:58:16 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:58:16 [DEBUG] basharbot: Detected stale voice client handle for guild 763932345400950865; cleaning up before reuse +22:58:16 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:58:16 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +22:58:16 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +22:58:16 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 2/2) +22:58:16 [INFO] discord.voice_client: Connecting to voice... +22:58:16 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +22:58:16 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:58:16 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +22:58:16 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-nrt12-942f95a0.discord.media +22:58:17 [DEBUG] basharbot: Detected stale voice client handle for guild 763932345400950865; cleaning up before reuse +22:58:17 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:58:17 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +22:58:17 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +22:58:17 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 2/2) +22:58:17 [INFO] discord.voice_client: Connecting to voice... +22:58:17 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +22:58:17 [WARNING] basharbot: Voice connect failed with ConnectionClosed(code=4006, reason=) on attempt 2/2 +22:58:17 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +22:58:17 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +22:58:17 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> None (self_mute=False deaf=False) +22:58:17 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> None (self_mute=False deaf=False) +22:58:17 [INFO] basharbot: Voice state update (guild 763932345400950865): penisary contact with her volvo -> None (self_mute=False deaf=False) +22:58:17 [ERROR] basharbot: Voice connect retries exhausted (guild 763932345400950865): Shard ID None WebSocket closed with 4006 +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\bot.py", line 913, in connect_to_author_channel + vc = await connect_voice_with_retry(channel) + File "D:\7. Git\BasharBotV2\bot.py", line 468, in connect_voice_with_retry + raise last_exc + File "D:\7. Git\BasharBotV2\bot.py", line 414, in connect_voice_with_retry + vc = await channel.connect(timeout=25.0, reconnect=False) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\abc.py", line 1994, in connect + await voice.connect(timeout=timeout, reconnect=reconnect) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 404, in connect + self.ws = await self.connect_websocket() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 375, in connect_websocket + await ws.poll_event() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\gateway.py", line 955, in poll_event + raise ConnectionClosed(self.ws, shard_id=None, code=self._close_code) +discord.errors.ConnectionClosed: Shard ID None WebSocket closed with 4006 +22:58:17 [WARNING] basharbot: Voice connect failed with ConnectionClosed(code=4006, reason=) on attempt 2/2 +22:58:18 [ERROR] basharbot: Voice connect retries exhausted (guild 763932345400950865): Shard ID None WebSocket closed with 4006 +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\bot.py", line 913, in connect_to_author_channel + vc = await connect_voice_with_retry(channel) + File "D:\7. Git\BasharBotV2\bot.py", line 468, in connect_voice_with_retry + raise last_exc + File "D:\7. Git\BasharBotV2\bot.py", line 414, in connect_voice_with_retry + vc = await channel.connect(timeout=25.0, reconnect=False) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\abc.py", line 1994, in connect + await voice.connect(timeout=timeout, reconnect=reconnect) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 404, in connect + self.ws = await self.connect_websocket() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 375, in connect_websocket + await ws.poll_event() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\gateway.py", line 955, in poll_event + raise ConnectionClosed(self.ws, shard_id=None, code=self._close_code) +discord.errors.ConnectionClosed: Shard ID None WebSocket closed with 4006 +22:58:29 [WARNING] discord.gateway: Shard ID None has stopped responding to the gateway. Closing and restarting. +22:58:33 [WARNING] discord.gateway: Shard ID None has stopped responding to the gateway. Closing and restarting. +22:58:34 [INFO] discord.client: Received signal to terminate bot and event loop. +22:58:34 [INFO] discord.client: Cleaning up tasks. +22:58:34 [INFO] discord.client: Cleaning up after 17 tasks. +22:58:34 [INFO] discord.client: All tasks finished cancelling. +22:58:34 [INFO] discord.client: Closing the event loop. +23:02:02 [DEBUG] asyncio: Using proactor: IocpProactor +23:02:02 [INFO] discord.client: logging in using static token +23:02:03 [INFO] discord.gateway: Shard ID None has sent the IDENTIFY payload. +23:02:03 [INFO] discord.gateway: Shard ID None has connected to Gateway: ["gateway-prd-arm-us-east1-c-fm76",{"micros":259222,"calls":["id_created",{"micros":499,"calls":[]},"session_lookup_time",{"micros":6930,"calls":[]},"session_lookup_finished",{"micros":10,"calls":[]},"discord-sessions-prd-2-83",{"micros":251294,"calls":["start_session",{"micros":240690,"calls":["discord-api-rpc-7bb4cd58cb-992xx",{"micros":94850,"calls":["get_user",{"micros":36846},"get_guilds",{"micros":4073},"send_scheduled_deletion_message",{"micros":24},"guild_join_requests",{"micros":989},"authorized_ip_coro",{"micros":16},"pending_payments",{"micros":55765},"apex_experiments",{"micros":77320},"user_activities",{"micros":6},"played_application_ids",{"micros":3},"linked_users",{"micros":3}]}]},"starting_guild_connect",{"micros":30,"calls":[]},"presence_started",{"micros":347,"calls":[]},"guilds_started",{"micros":57,"calls":[]},"lobbies_started",{"micros":1,"calls":[]},"guilds_connect",{"micros":2,"calls":[]},"presence_connect",{"micros":10134,"calls":[]},"connect_finished",{"micros":10154,"calls":[]},"build_ready",{"micros":12,"calls":[]},"clean_ready",{"micros":1,"calls":[]},"optimize_ready",{"micros":0,"calls":[]},"split_ready",{"micros":1,"calls":[]}]}]}] (Session ID: d470797968f07f6fbe8932acee60e614). +23:02:05 [INFO] basharbot: Logged in as Bashar Al-Assad Version 2#4174 (id=1014194017346531419) +23:02:05 [DEBUG] basharbot: ffmpeg found: C:\ProgramData\chocolatey\bin\ffmpeg.EXE +23:02:05 [WARNING] basharbot: Opus library not found; install opuslib/opus-tools if voice receive misbehaves. +23:02:05 [INFO] basharbot: Startup checks OK +23:02:05 [INFO] basharbot: Hotword listening: ENABLED (sinks available and HOTWORD_ENABLED=True) +23:02:09 [DEBUG] basharbot: Wake word detected. Raw='hey bashar join' | action='join' | args='' +23:02:09 [DEBUG] basharbot: Connect requested by riggs0 (Riggs0) in guild 763932345400950865 +23:02:09 [INFO] basharbot: Connecting to voice channel: penisary contact with her volvo (guild 763932345400950865) +23:02:10 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 1/2) +23:02:10 [INFO] discord.voice_client: Connecting to voice... +23:02:10 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +23:02:10 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +23:02:10 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-nrt12-942f95a0.discord.media +23:02:11 [WARNING] basharbot: Voice connect failed with ConnectionClosed(code=4006, reason=) on attempt 1/2 +23:02:11 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +23:02:11 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +23:02:11 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +23:02:11 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> None (self_mute=False deaf=False) +23:02:11 [INFO] basharbot: Waiting 3.0s before retry 2 (guild 763932345400950865) +23:02:14 [DEBUG] basharbot: Detected lingering voice state without client for guild 763932345400950865; clearing before reconnect +23:02:14 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 2/2) +23:02:14 [INFO] discord.voice_client: Connecting to voice... +23:02:14 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +23:02:15 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +23:02:15 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-nrt12-942f95a0.discord.media +23:02:16 [WARNING] basharbot: Voice connect failed with ConnectionClosed(code=4006, reason=) on attempt 2/2 +23:02:16 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +23:02:16 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +23:02:16 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +23:02:16 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> None (self_mute=False deaf=False) +23:02:16 [ERROR] basharbot: Voice connect retries exhausted (guild 763932345400950865): Shard ID None WebSocket closed with 4006 +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\bot.py", line 913, in connect_to_author_channel + vc = await connect_voice_with_retry(channel) + File "D:\7. Git\BasharBotV2\bot.py", line 468, in connect_voice_with_retry + raise last_exc + File "D:\7. Git\BasharBotV2\bot.py", line 414, in connect_voice_with_retry + vc = await channel.connect(timeout=25.0, reconnect=False) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\abc.py", line 1994, in connect + await voice.connect(timeout=timeout, reconnect=reconnect) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 404, in connect + self.ws = await self.connect_websocket() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 375, in connect_websocket + await ws.poll_event() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\gateway.py", line 955, in poll_event + raise ConnectionClosed(self.ws, shard_id=None, code=self._close_code) +discord.errors.ConnectionClosed: Shard ID None WebSocket closed with 4006 +23:03:11 [WARNING] discord.gateway: Shard ID None has stopped responding to the gateway. Closing and restarting. +23:03:15 [WARNING] discord.gateway: Shard ID None has stopped responding to the gateway. Closing and restarting. +23:04:10 [INFO] discord.client: Received signal to terminate bot and event loop. +23:04:10 [INFO] discord.client: Cleaning up tasks. +23:04:10 [INFO] discord.client: Cleaning up after 1 tasks. +23:04:10 [INFO] discord.client: All tasks finished cancelling. +23:04:10 [INFO] discord.client: Closing the event loop. +23:04:17 [DEBUG] asyncio: Using proactor: IocpProactor +23:04:17 [INFO] discord.client: logging in using static token +23:04:18 [INFO] discord.gateway: Shard ID None has sent the IDENTIFY payload. +23:04:18 [INFO] discord.gateway: Shard ID None has connected to Gateway: ["gateway-prd-arm-us-east1-d-nhmv",{"micros":163493,"calls":["id_created",{"micros":471,"calls":[]},"session_lookup_time",{"micros":1464,"calls":[]},"session_lookup_finished",{"micros":12,"calls":[]},"discord-sessions-prd-2-48",{"micros":161191,"calls":["start_session",{"micros":126626,"calls":["discord-api-rpc-7bb4cd58cb-zm2h7",{"micros":54974,"calls":["get_user",{"micros":5380},"get_guilds",{"micros":3405},"send_scheduled_deletion_message",{"micros":66},"guild_join_requests",{"micros":4},"authorized_ip_coro",{"micros":12},"pending_payments",{"micros":56692},"apex_experiments",{"micros":4683},"user_activities",{"micros":7},"played_application_ids",{"micros":4},"linked_users",{"micros":3}]}]},"starting_guild_connect",{"micros":31,"calls":[]},"presence_started",{"micros":7196,"calls":[]},"guilds_started",{"micros":52,"calls":[]},"lobbies_started",{"micros":1,"calls":[]},"guilds_connect",{"micros":1,"calls":[]},"presence_connect",{"micros":27261,"calls":[]},"connect_finished",{"micros":27269,"calls":[]},"build_ready",{"micros":13,"calls":[]},"clean_ready",{"micros":1,"calls":[]},"optimize_ready",{"micros":0,"calls":[]},"split_ready",{"micros":1,"calls":[]}]}]}] (Session ID: cc32b08f7fe9da7d3ca9fc376d47999c). +23:04:20 [INFO] basharbot: Logged in as Bashar Al-Assad Version 2#4174 (id=1014194017346531419) +23:04:20 [DEBUG] basharbot: ffmpeg found: C:\ProgramData\chocolatey\bin\ffmpeg.EXE +23:04:20 [WARNING] basharbot: Opus library not found; install opuslib/opus-tools if voice receive misbehaves. +23:04:20 [INFO] basharbot: Startup checks OK +23:04:20 [INFO] basharbot: Hotword listening: ENABLED (sinks available and HOTWORD_ENABLED=True) +23:04:24 [DEBUG] basharbot: Wake word detected. Raw='hey bashar join' | action='join' | args='' +23:04:24 [DEBUG] basharbot: Connect requested by riggs0 (Riggs0) in guild 763932345400950865 +23:04:24 [INFO] basharbot: Connecting to voice channel: penisary contact with her volvo (guild 763932345400950865) +23:04:24 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 1/2) +23:04:24 [INFO] discord.voice_client: Connecting to voice... +23:04:24 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +23:04:24 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +23:04:25 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-nrt12-942f95a0.discord.media +23:04:25 [WARNING] basharbot: Voice connect failed with ConnectionClosed(code=4006, reason=) on attempt 1/2 +23:04:25 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +23:04:25 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +23:04:25 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +23:04:25 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> None (self_mute=False deaf=False) +23:04:26 [INFO] basharbot: Waiting 3.0s before retry 2 (guild 763932345400950865) +23:04:29 [DEBUG] basharbot: Detected lingering voice state without client for guild 763932345400950865; clearing before reconnect +23:04:29 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 2/2) +23:04:29 [INFO] discord.voice_client: Connecting to voice... +23:04:29 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +23:04:29 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +23:04:29 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-nrt12-942f95a0.discord.media +23:04:30 [WARNING] basharbot: Voice connect failed with ConnectionClosed(code=4006, reason=) on attempt 2/2 +23:04:30 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +23:04:30 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +23:04:30 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +23:04:30 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> None (self_mute=False deaf=False) +23:04:30 [ERROR] basharbot: Voice connect retries exhausted (guild 763932345400950865): Shard ID None WebSocket closed with 4006 +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\bot.py", line 913, in connect_to_author_channel + vc = await connect_voice_with_retry(channel) + File "D:\7. Git\BasharBotV2\bot.py", line 468, in connect_voice_with_retry + raise last_exc + File "D:\7. Git\BasharBotV2\bot.py", line 414, in connect_voice_with_retry + vc = await channel.connect(timeout=25.0, reconnect=False) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\abc.py", line 1994, in connect + await voice.connect(timeout=timeout, reconnect=reconnect) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 404, in connect + self.ws = await self.connect_websocket() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 375, in connect_websocket + await ws.poll_event() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\gateway.py", line 955, in poll_event + raise ConnectionClosed(self.ws, shard_id=None, code=self._close_code) +discord.errors.ConnectionClosed: Shard ID None WebSocket closed with 4006 +23:05:25 [WARNING] discord.gateway: Shard ID None has stopped responding to the gateway. Closing and restarting. +23:05:30 [WARNING] discord.gateway: Shard ID None has stopped responding to the gateway. Closing and restarting. +23:06:07 [INFO] discord.client: Received signal to terminate bot and event loop. +23:06:07 [INFO] discord.client: Cleaning up tasks. +23:06:07 [INFO] discord.client: Cleaning up after 1 tasks. +23:06:07 [INFO] discord.client: All tasks finished cancelling. +23:06:07 [INFO] discord.client: Closing the event loop. +23:06:11 [DEBUG] asyncio: Using proactor: IocpProactor +23:06:11 [INFO] discord.client: logging in using static token +23:06:11 [INFO] discord.gateway: Shard ID None has sent the IDENTIFY payload. +23:06:11 [INFO] discord.gateway: Shard ID None has connected to Gateway: ["gateway-prd-arm-us-east1-d-kd59",{"micros":148219,"calls":["id_created",{"micros":671,"calls":[]},"session_lookup_time",{"micros":228,"calls":[]},"session_lookup_finished",{"micros":7,"calls":[]},"discord-sessions-prd-2-123",{"micros":146923,"calls":["start_session",{"micros":125803,"calls":["discord-api-rpc-7bb4cd58cb-k7rvk",{"micros":41197,"calls":["get_user",{"micros":11194},"get_guilds",{"micros":1897},"send_scheduled_deletion_message",{"micros":12},"guild_join_requests",{"micros":422},"authorized_ip_coro",{"micros":10},"pending_payments",{"micros":56242},"apex_experiments",{"micros":4961},"user_activities",{"micros":7},"played_application_ids",{"micros":3},"linked_users",{"micros":4}]}]},"starting_guild_connect",{"micros":33,"calls":[]},"presence_started",{"micros":1308,"calls":[]},"guilds_started",{"micros":53,"calls":[]},"lobbies_started",{"micros":2,"calls":[]},"guilds_connect",{"micros":1,"calls":[]},"presence_connect",{"micros":19689,"calls":[]},"connect_finished",{"micros":19708,"calls":[]},"build_ready",{"micros":15,"calls":[]},"clean_ready",{"micros":0,"calls":[]},"optimize_ready",{"micros":0,"calls":[]},"split_ready",{"micros":0,"calls":[]}]}]}] (Session ID: c2002617afb42be2f14e30a597c93f7d). +23:06:13 [INFO] basharbot: Logged in as Bashar Al-Assad Version 2#4174 (id=1014194017346531419) +23:06:13 [DEBUG] basharbot: ffmpeg found: C:\ProgramData\chocolatey\bin\ffmpeg.EXE +23:06:13 [WARNING] basharbot: Opus library not found; install opuslib/opus-tools if voice receive misbehaves. +23:06:13 [INFO] basharbot: Startup checks OK +23:06:13 [INFO] basharbot: Hotword listening: ENABLED (sinks available and HOTWORD_ENABLED=True) +23:06:16 [DEBUG] basharbot: Wake word detected. Raw='hey bashar join' | action='join' | args='' +23:06:16 [DEBUG] basharbot: Connect requested by riggs0 (Riggs0) in guild 763932345400950865 +23:06:16 [INFO] basharbot: Connecting to voice channel: penisary contact with her volvo (guild 763932345400950865) +23:06:16 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 1/2) +23:06:16 [INFO] discord.voice_client: Connecting to voice... +23:06:16 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +23:06:16 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +23:06:17 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-bom12-7cf82593.discord.media +23:06:18 [WARNING] basharbot: Voice connect failed with ConnectionClosed(code=4006, reason=) on attempt 1/2 +23:06:18 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +23:06:18 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +23:06:18 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +23:06:18 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> None (self_mute=False deaf=False) +23:06:19 [INFO] basharbot: Waiting 3.0s before retry 2 (guild 763932345400950865) +23:06:22 [DEBUG] basharbot: Detected lingering voice state without client for guild 763932345400950865; clearing before reconnect +23:06:22 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 2/2) +23:06:22 [INFO] discord.voice_client: Connecting to voice... +23:06:22 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +23:06:22 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +23:06:22 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-bom12-7cf82593.discord.media +23:06:23 [WARNING] basharbot: Voice connect failed with ConnectionClosed(code=4006, reason=) on attempt 2/2 +23:06:23 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +23:06:23 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +23:06:23 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +23:06:23 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> None (self_mute=False deaf=False) +23:06:23 [ERROR] basharbot: Voice connect retries exhausted (guild 763932345400950865): Shard ID None WebSocket closed with 4006 +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\bot.py", line 913, in connect_to_author_channel + vc = await connect_voice_with_retry(channel) + File "D:\7. Git\BasharBotV2\bot.py", line 468, in connect_voice_with_retry + raise last_exc + File "D:\7. Git\BasharBotV2\bot.py", line 414, in connect_voice_with_retry + vc = await channel.connect(timeout=25.0, reconnect=False) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\abc.py", line 1994, in connect + await voice.connect(timeout=timeout, reconnect=reconnect) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 404, in connect + self.ws = await self.connect_websocket() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 375, in connect_websocket + await ws.poll_event() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\gateway.py", line 955, in poll_event + raise ConnectionClosed(self.ws, shard_id=None, code=self._close_code) +discord.errors.ConnectionClosed: Shard ID None WebSocket closed with 4006 +23:06:33 [INFO] discord.client: Received signal to terminate bot and event loop. +23:06:33 [INFO] discord.client: Cleaning up tasks. +23:06:33 [INFO] discord.client: Cleaning up after 1 tasks. +23:06:33 [INFO] discord.client: All tasks finished cancelling. +23:06:33 [INFO] discord.client: Closing the event loop. +23:07:43 [DEBUG] asyncio: Using proactor: IocpProactor +23:07:43 [INFO] discord.client: logging in using static token +23:07:43 [INFO] discord.gateway: Shard ID None has sent the IDENTIFY payload. +23:07:43 [INFO] discord.gateway: Shard ID None has connected to Gateway: ["gateway-prd-arm-us-east1-b-c88w",{"micros":140067,"calls":["id_created",{"micros":652,"calls":[]},"session_lookup_time",{"micros":307,"calls":[]},"session_lookup_finished",{"micros":10,"calls":[]},"discord-sessions-prd-2-136",{"micros":138132,"calls":["start_session",{"micros":134164,"calls":["discord-api-rpc-7bb4cd58cb-vp2zs",{"micros":49810,"calls":["get_user",{"micros":7051},"get_guilds",{"micros":4627},"send_scheduled_deletion_message",{"micros":25},"guild_join_requests",{"micros":13},"authorized_ip_coro",{"micros":119},"pending_payments",{"micros":71819},"apex_experiments",{"micros":3493},"user_activities",{"micros":5},"played_application_ids",{"micros":3},"linked_users",{"micros":3}]}]},"starting_guild_connect",{"micros":32,"calls":[]},"presence_started",{"micros":267,"calls":[]},"guilds_started",{"micros":56,"calls":[]},"lobbies_started",{"micros":10,"calls":[]},"guilds_connect",{"micros":3,"calls":[]},"presence_connect",{"micros":3579,"calls":[]},"connect_finished",{"micros":3588,"calls":[]},"build_ready",{"micros":13,"calls":[]},"clean_ready",{"micros":0,"calls":[]},"optimize_ready",{"micros":1,"calls":[]},"split_ready",{"micros":0,"calls":[]}]}]}] (Session ID: 7db50e74307181ab1e9626ac8f6d151a). +23:07:45 [INFO] basharbot: Logged in as Bashar Al-Assad Version 2#4174 (id=1014194017346531419) +23:07:45 [DEBUG] basharbot: ffmpeg found: C:\ProgramData\chocolatey\bin\ffmpeg.EXE +23:07:45 [WARNING] basharbot: Opus library not found; install opuslib/opus-tools if voice receive misbehaves. +23:07:45 [INFO] basharbot: Startup checks OK +23:07:45 [INFO] basharbot: Hotword listening: ENABLED (sinks available and HOTWORD_ENABLED=True) +23:07:49 [DEBUG] basharbot: Wake word detected. Raw='hey bashar join' | action='join' | args='' +23:07:49 [DEBUG] basharbot: Connect requested by riggs0 (Riggs0) in guild 763932345400950865 +23:07:49 [INFO] basharbot: Connecting to voice channel: penisary contact with her volvo (guild 763932345400950865) +23:07:50 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 1/2) +23:07:50 [INFO] discord.voice_client: Connecting to voice... +23:07:50 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +23:07:50 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +23:07:50 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-bom12-7cf82593.discord.media +23:07:51 [WARNING] basharbot: Voice connect failed with ConnectionClosed(code=4006, reason=) on attempt 1/2 +23:07:51 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +23:07:51 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +23:07:51 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +23:07:51 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> None (self_mute=False deaf=False) +23:07:52 [INFO] basharbot: Waiting 3.0s before retry 2 (guild 763932345400950865) +23:07:55 [DEBUG] basharbot: Detected lingering voice state without client for guild 763932345400950865; clearing before reconnect +23:07:55 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 2/2) +23:07:55 [INFO] discord.voice_client: Connecting to voice... +23:07:55 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +23:07:55 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +23:07:55 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-bom12-7cf82593.discord.media +23:07:56 [WARNING] basharbot: Voice connect failed with ConnectionClosed(code=4006, reason=) on attempt 2/2 +23:07:56 [INFO] discord.voice_client: The voice handshake is being terminated for Channel ID 832692051811369022 (Guild ID 763932345400950865) +23:07:56 [DEBUG] basharbot: Forced voice disconnect to clear stale session (guild 763932345400950865) +23:07:56 [DEBUG] basharbot: Cleared lingering voice state post-disconnect for guild 763932345400950865 +23:07:56 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> None (self_mute=False deaf=False) +23:07:57 [ERROR] basharbot: Voice connect retries exhausted (guild 763932345400950865): Shard ID None WebSocket closed with 4006 +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\bot.py", line 913, in connect_to_author_channel + vc = await connect_voice_with_retry(channel) + File "D:\7. Git\BasharBotV2\bot.py", line 468, in connect_voice_with_retry + raise last_exc + File "D:\7. Git\BasharBotV2\bot.py", line 414, in connect_voice_with_retry + vc = await channel.connect(timeout=25.0, reconnect=False) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\abc.py", line 1994, in connect + await voice.connect(timeout=timeout, reconnect=reconnect) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 404, in connect + self.ws = await self.connect_websocket() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 375, in connect_websocket + await ws.poll_event() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\gateway.py", line 955, in poll_event + raise ConnectionClosed(self.ws, shard_id=None, code=self._close_code) +discord.errors.ConnectionClosed: Shard ID None WebSocket closed with 4006 +23:08:05 [INFO] discord.client: Received signal to terminate bot and event loop. +23:08:05 [INFO] discord.client: Cleaning up tasks. +23:08:05 [INFO] discord.client: Cleaning up after 1 tasks. +23:08:05 [INFO] discord.client: All tasks finished cancelling. +23:08:05 [INFO] discord.client: Closing the event loop. +23:09:31 [DEBUG] asyncio: Using proactor: IocpProactor +23:09:31 [INFO] discord.client: logging in using static token +23:09:31 [INFO] discord.gateway: Shard ID None has sent the IDENTIFY payload. +23:09:32 [INFO] discord.gateway: Shard ID None has connected to Gateway: ["gateway-prd-arm-us-east1-d-x9h4",{"micros":244067,"calls":["id_created",{"micros":418,"calls":[]},"session_lookup_time",{"micros":2209,"calls":[]},"session_lookup_finished",{"micros":11,"calls":[]},"discord-sessions-prd-2-3",{"micros":241168,"calls":["start_session",{"micros":221022,"calls":["discord-api-rpc-7bb4cd58cb-h9pqf",{"micros":136741,"calls":["get_user",{"micros":31935},"get_guilds",{"micros":2834},"send_scheduled_deletion_message",{"micros":13},"guild_join_requests",{"micros":2756},"authorized_ip_coro",{"micros":14},"pending_payments",{"micros":49634},"apex_experiments",{"micros":3574},"user_activities",{"micros":5},"played_application_ids",{"micros":3},"linked_users",{"micros":2}]}]},"starting_guild_connect",{"micros":28,"calls":[]},"presence_started",{"micros":222,"calls":[]},"guilds_started",{"micros":58,"calls":[]},"lobbies_started",{"micros":1,"calls":[]},"guilds_connect",{"micros":1,"calls":[]},"presence_connect",{"micros":19805,"calls":[]},"connect_finished",{"micros":19824,"calls":[]},"build_ready",{"micros":12,"calls":[]},"clean_ready",{"micros":0,"calls":[]},"optimize_ready",{"micros":0,"calls":[]},"split_ready",{"micros":0,"calls":[]}]}]}] (Session ID: 94104bff8f38973ea0719562a1e529b5). +23:09:34 [INFO] basharbot: Logged in as Bashar Al-Assad Version 2#4174 (id=1014194017346531419) +23:09:34 [DEBUG] basharbot: ffmpeg found: C:\ProgramData\chocolatey\bin\ffmpeg.EXE +23:09:34 [WARNING] basharbot: Opus library not found; install opuslib/opus-tools if voice receive misbehaves. +23:09:34 [INFO] basharbot: Startup checks OK +23:09:34 [INFO] basharbot: Hotword listening: ENABLED (sinks available and HOTWORD_ENABLED=True) +23:09:41 [DEBUG] basharbot: Wake word detected. Raw='hey bashar join' | action='join' | args='' +23:09:41 [DEBUG] basharbot: Connect requested by riggs0 (Riggs0) in guild 763932345400950865 +23:09:41 [INFO] basharbot: Connecting to voice channel: penisary contact with her volvo (guild 763932345400950865) +23:09:42 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 1/2) +23:09:42 [INFO] discord.voice_client: Connecting to voice... +23:09:42 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +23:09:42 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +23:09:42 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-bom12-7cf82593.discord.media +23:09:43 [WARNING] basharbot: Voice connect failed with ConnectionClosed(code=4006, reason=) on attempt 1/2 +23:09:43 [DEBUG] basharbot: Cleared lingering voice state for guild 763932345400950865 during cleanup +23:09:43 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> None (self_mute=False deaf=False) +23:09:44 [INFO] basharbot: Waiting 3.0s before retry 2 (guild 763932345400950865) +23:09:47 [DEBUG] basharbot: Detected lingering voice state without client for guild 763932345400950865; clearing before reconnect +23:09:47 [INFO] basharbot: Attempting voice connect to penisary contact with her volvo (attempt 2/2) +23:09:47 [INFO] discord.voice_client: Connecting to voice... +23:09:47 [INFO] discord.voice_client: Starting voice handshake... (connection attempt 1) +23:09:47 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> penisary contact with her volvo (self_mute=False deaf=False) +23:09:47 [INFO] discord.voice_client: Voice handshake complete. Endpoint found c-bom12-7cf82593.discord.media +23:09:48 [WARNING] basharbot: Voice connect failed with ConnectionClosed(code=4006, reason=) on attempt 2/2 +23:09:48 [DEBUG] basharbot: Cleared lingering voice state for guild 763932345400950865 during cleanup +23:09:48 [INFO] basharbot: Voice state update (guild 763932345400950865): None -> None (self_mute=False deaf=False) +23:09:49 [ERROR] basharbot: Voice connect retries exhausted (guild 763932345400950865): Shard ID None WebSocket closed with 4006 +Traceback (most recent call last): + File "D:\7. Git\BasharBotV2\bot.py", line 939, in connect_to_author_channel + vc = await connect_voice_with_retry(channel) + File "D:\7. Git\BasharBotV2\bot.py", line 494, in connect_voice_with_retry + raise last_exc + File "D:\7. Git\BasharBotV2\bot.py", line 433, in connect_voice_with_retry + await voice_client.connect(timeout=25.0, reconnect=False) + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 404, in connect + self.ws = await self.connect_websocket() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\voice_client.py", line 375, in connect_websocket + await ws.poll_event() + File "D:\7. Git\BasharBotV2\.venv\lib\site-packages\discord\gateway.py", line 955, in poll_event + raise ConnectionClosed(self.ws, shard_id=None, code=self._close_code) +discord.errors.ConnectionClosed: Shard ID None WebSocket closed with 4006 +23:09:51 [INFO] discord.client: Received signal to terminate bot and event loop. +23:09:51 [INFO] discord.client: Cleaning up tasks. +23:09:51 [INFO] discord.client: Cleaning up after 1 tasks. +23:09:51 [INFO] discord.client: All tasks finished cancelling. +23:09:51 [INFO] discord.client: Closing the event loop. diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..90a779e --- /dev/null +++ b/bot.py @@ -0,0 +1,1304 @@ +import asyncio +import concurrent.futures +import io +import logging +import logging.handlers +import os +import re +import tempfile +import time +from collections import defaultdict, deque +from dataclasses import dataclass, field +from datetime import datetime +from typing import Callable, Deque, Optional, Tuple + +import discord +import numpy as np +import pyttsx3 +import soundfile as sf +from concurrent.futures import ThreadPoolExecutor +from discord import Intents +from discord.errors import ClientException, ConnectionClosed +from dotenv import load_dotenv +from yt_dlp import YoutubeDL + +from stt import transcribe_file + +try: + from discord import sinks # Available in discord.py >=2.0 and py-cord + HAS_SINKS = True +except Exception: + HAS_SINKS = False + +load_dotenv() + + +# ---------- Logging ---------- + +_log_level = os.getenv("LOG_LEVEL", "INFO").upper() +HOTWORD_ENABLED = os.getenv("HOTWORD_ENABLED", "true").lower() in {"1", "true", "yes", "on"} +logging.basicConfig(level=getattr(logging, _log_level, logging.INFO)) +_formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s", "%H:%M:%S") + +# Root logger handlers +_root = logging.getLogger() +_root.setLevel(getattr(logging, _log_level, logging.INFO)) + +_have_console = any(isinstance(h, logging.StreamHandler) for h in _root.handlers) +if not _have_console: + _ch = logging.StreamHandler() + _ch.setLevel(getattr(logging, _log_level, logging.INFO)) + _ch.setFormatter(_formatter) + _root.addHandler(_ch) + +_have_file = any(isinstance(h, logging.handlers.RotatingFileHandler) for h in _root.handlers) +if not _have_file: + try: + _fh = logging.handlers.RotatingFileHandler("bot.log", maxBytes=2 * 1024 * 1024, backupCount=3, encoding="utf-8") + _fh.setLevel(getattr(logging, _log_level, logging.DEBUG)) + _fh.setFormatter(_formatter) + _root.addHandler(_fh) + except Exception: + # Continue without file logging if filesystem not writable + pass + +# Tweak library log levels +logging.getLogger("discord").setLevel(logging.INFO) +logging.getLogger("aiohttp").setLevel(logging.INFO) +logger = logging.getLogger("basharbot") + + +# ---------- Global configuration ---------- + +FFMPEG_OPTIONS = { + "before_options": "-reconnect 1 -reconnect_streamed 1 -reconnect_delay_max 5", + "options": "-vn" +} + +YTDLP_OPTIONS = { + "format": "bestaudio/best", + "noplaylist": True, + "default_search": "ytsearch1", + "quiet": True, + "source_address": "0.0.0.0", +} + +BOT_WAKE_WORD = "hey bashar" +WAKE_WORD_TOKENS = BOT_WAKE_WORD.split() +WAKE_FIRST_WORDS = {"hey", "hi", "hay", "heyya", "heyyo", "yo", "ok", "okay", "oi"} +WAKE_SECOND_WORDS = { + "bashar", + "basher", + "beshar", + "busher", + "buchar", + "bishar", + "bishor", + "bichar", + "bisharp", + "bishars", + "bisharp", + "bayshar", + "bashara", + "bashir", + "basheer", + "bishara", + "basar", + "bahsar", + "vishar", + "vashar", + "wishar", + "bishaar", + "pashar", +} +COMMAND_ALIASES = { + "next": "skip", +} + +PCM_SAMPLE_RATE = 48000 +PCM_CHANNELS = 2 +PCM_SAMPLE_WIDTH = 2 # bytes per sample +PCM_BYTES_PER_SECOND = PCM_SAMPLE_RATE * PCM_CHANNELS * PCM_SAMPLE_WIDTH +TRANSCRIPT_LOG_ENABLED = os.getenv("TRANSCRIPT_LOG_ENABLED", "true").lower() in {"1", "true", "yes", "on"} +TRANSCRIPT_LOG_PATH = os.getenv("TRANSCRIPT_LOG_PATH", "transcript.log") +GOODBOY_USER_ID = int(os.getenv("GOODBOY_USER_ID", "94578724413902848")) +GOODBOY_AUDIO_PATH = os.path.join(os.getcwd(), "goodboy.ogg") + + +def _display_name(user: object) -> str: + name = getattr(user, "display_name", None) or getattr(user, "name", None) + if not name: + name = str(user) + return name + + +def append_transcript_entry(name: str, text: str) -> None: + if not TRANSCRIPT_LOG_ENABLED: + return + try: + timestamp = datetime.now().strftime("%m/%d/%Y %H:%M") + line = f"{timestamp} {name} - {text}".rstrip() + "\n" + with open(TRANSCRIPT_LOG_PATH, "a", encoding="utf-8") as log_file: + log_file.write(line) + except Exception as exc: + logger.warning("Failed to append to transcript log: %s", exc) + + +_ENGLISH_SENTENCE_RE = re.compile(r"^[a-zA-Z0-9 ,.'\"?!-]+$") + + +def is_probably_english_sentence(text: str) -> bool: + if not text: + return False + if len(text.split()) < 3: + return False + if not text.endswith((".", "!", "?")): + return False + return bool(_ENGLISH_SENTENCE_RE.match(text)) + + +async def announce_listening_roster(channel, voice_channel: Optional[discord.VoiceChannel]): + if channel is None or voice_channel is None: + return + others = [m for m in voice_channel.members if m != voice_channel.guild.me] + if not others: + return + names = ", ".join(_display_name(m) for m in others) + try: + await channel.send(f"Listening for: {names}") + except Exception: + logger.debug("Unable to send listening roster message") + + +def normalize_for_command(text: str) -> str: + cleaned = re.sub(r"[^a-z0-9\s]", " ", text.lower()) + tokens = cleaned.split() + if tokens: + if tokens[0] in WAKE_FIRST_WORDS: + tokens[0] = "hey" + if len(tokens) > 1 and tokens[1] in WAKE_SECOND_WORDS: + tokens[1] = "bashar" + return " ".join(tokens) + + +def parse_wake_command(text: str) -> Optional[Tuple[str, str]]: + """ + Return (action, args) if the text starts with the wake word, after stripping punctuation. + Args may be empty. Returns None if wake word not detected. + """ + normalized = normalize_for_command(text) + if not normalized: + return None + tokens = normalized.split() + if len(tokens) < len(WAKE_WORD_TOKENS): + return None + if tokens[0] != "hey" or tokens[1] != "bashar": + return None + remainder = tokens[len(WAKE_WORD_TOKENS):] + if not remainder: + return ("", "") + action = remainder[0] + action = COMMAND_ALIASES.get(action, action) + args = " ".join(remainder[1:]) if len(remainder) > 1 else "" + return action, args + + +async def send_recent_logs(channel: discord.abc.Messageable, max_lines: int = 300) -> None: + def _tail_lines(path: str, max_lines: int = 300) -> str: + try: + with open(path, "r", encoding="utf-8", errors="replace") as f: + lines = f.readlines() + return "".join(lines[-max_lines:]) + except Exception as e: + return f"Failed to read log: {e}" + + tail = _tail_lines("bot.log", max_lines) + if len(tail) > 1800: + with io.BytesIO(tail.encode("utf-8", errors="replace")) as bio: + bio.seek(0) + await channel.send(content="Here are the last 300 lines of bot.log", file=discord.File(bio, filename="bot-tail.txt")) + else: + await channel.send(f"```{tail}```") + + +# ---------- Utilities ---------- + + +def ensure_opus_loaded() -> bool: + """Attempt to load the Opus library for voice receive/playback.""" + if discord.opus.is_loaded(): + logger.debug("Opus already loaded.") + return True + + candidates = [] + env_candidate = os.getenv("OPUS_LIB") + if env_candidate: + candidates.append(env_candidate) + candidates.extend([ + "opus.dll", + "libopus-0.dll", + "libopus.so.0", + "libopus.so", + "libopus.dylib", + ]) + + for name in candidates: + try: + discord.opus.load_opus(name) + logger.info("Loaded opus library: %s", name) + return True + except OSError: + continue + + logger.warning("Opus library not found; install opuslib/opus-tools if voice receive misbehaves.") + return False + +def ensure_ffmpeg_available() -> None: + """Raise a clear error if ffmpeg is not in PATH.""" + from shutil import which + path = which("ffmpeg") + if path is None: + logger.error("ffmpeg not found in PATH") + raise RuntimeError( + "ffmpeg not found in PATH. Install it and restart.\n" + "Windows (PowerShell): choco install ffmpeg -y" + ) + logger.debug("ffmpeg found: %s", path) + + +def make_tts_engine() -> pyttsx3.Engine: + engine = pyttsx3.init() + # Slightly slower and clearer speech + try: + rate = engine.getProperty("rate") + engine.setProperty("rate", max(120, int(rate * 0.9))) + except Exception: + pass + logger.debug("Initialized TTS engine (pyttsx3)") + return engine + + +_tts_engine_singleton: Optional[pyttsx3.Engine] = None +_tts_executor: Optional[ThreadPoolExecutor] = None + + +def get_tts_engine() -> pyttsx3.Engine: + global _tts_engine_singleton + if _tts_engine_singleton is None: + _tts_engine_singleton = make_tts_engine() + return _tts_engine_singleton + + +def get_tts_executor() -> ThreadPoolExecutor: + global _tts_executor + if _tts_executor is None: + _tts_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="tts") + return _tts_executor + + +async def synthesize_tts_to_wav(text: str, wav_path: str) -> str: + """Generate TTS to a WAV file using pyttsx3 in a background thread.""" + loop = asyncio.get_running_loop() + engine = get_tts_engine() + + def _save(): + logger.debug("Synthesizing TTS to %s: %s", wav_path, (text if len(text) < 120 else text[:117] + "...")) + engine.save_to_file(text, wav_path) + engine.runAndWait() + + await loop.run_in_executor(get_tts_executor(), _save) + logger.debug("TTS synthesis complete: %s", wav_path) + return wav_path + + +def yt_search_and_resolve(query: str) -> Tuple[str, str]: + """ + Search YouTube for the best match and return (stream_url, title). + Stream URL is suitable for ffmpeg input. + """ + logger.info("Resolving YouTube query: %s", query) + with YoutubeDL(YTDLP_OPTIONS) as ydl: + info = ydl.extract_info(query, download=False) + if "entries" in info: + info = info["entries"][0] + stream_url = info["url"] + title = info.get("title", "Unknown") + logger.info("Resolved track: title=%s", title) + return stream_url, title + + +# ---------- Audio playback management ---------- + + +async def _force_cleanup_voice_client(guild: Optional[discord.Guild]) -> None: + if guild is None: + return + voice_client: Optional[discord.VoiceClient] = getattr(guild, "voice_client", None) + if voice_client is None: + # Ensure we don't retain stale session metadata if Discord believes we're still connected. + try: + bot_id = getattr(getattr(guild, "me", None), "id", None) or getattr(guild._state, "self_id", None) + if bot_id is not None: + removed = guild._voice_states.pop(bot_id, None) + if removed is not None: + logger.debug("Cleared lingering voice state for guild %s during cleanup", guild.id) + guild._state._voice_clients.pop(guild.id, None) + except Exception as state_err: + logger.debug("Failed to clear lingering voice state for guild %s: %s", getattr(guild, "id", "?"), state_err) + return + try: + await voice_client.disconnect(force=True) + logger.debug("Forced voice disconnect to clear stale session (guild %s)", guild.id) + except Exception as disconnect_err: + logger.debug("Force disconnect raised for guild %s: %s", guild.id, disconnect_err) + try: + voice_client.cleanup() + logger.debug("Voice cleanup fallback succeeded (guild %s)", guild.id) + except Exception as cleanup_err: + logger.debug("Voice cleanup fallback failed (guild %s): %s", guild.id, cleanup_err) + try: + bot_id = getattr(getattr(guild, "me", None), "id", None) or getattr(guild._state, "self_id", None) + if bot_id is not None: + removed = guild._voice_states.pop(bot_id, None) + if removed is not None: + logger.debug("Cleared lingering voice state post-disconnect for guild %s", guild.id) + except Exception as state_err: + logger.debug("Failed to clear voice state post-disconnect for guild %s: %s", guild.id, state_err) + try: + guild._state._voice_clients.pop(guild.id, None) + except Exception as remove_err: + logger.debug("Failed to clear cached voice client for guild %s: %s", guild.id, remove_err) + + +async def _get_active_voice_client(guild: Optional[discord.Guild]) -> Optional[discord.VoiceClient]: + if guild is None: + return None + voice_client: Optional[discord.VoiceClient] = getattr(guild, "voice_client", None) + try: + bot_id = getattr(getattr(guild, "me", None), "id", None) or getattr(guild._state, "self_id", None) + if bot_id is not None and voice_client is None and guild._voice_states.get(bot_id): + logger.debug("Detected lingering voice state without client for guild %s; clearing before reconnect", guild.id) + guild._voice_states.pop(bot_id, None) + guild._state._voice_clients.pop(guild.id, None) + except Exception as state_err: + logger.debug("Failed to inspect lingering voice state for guild %s: %s", getattr(guild, "id", "?"), state_err) + if voice_client and not voice_client.is_connected(): + logger.debug("Detected stale voice client handle for guild %s; cleaning up before reuse", guild.id) + await _force_cleanup_voice_client(guild) + return getattr(guild, "voice_client", None) + return voice_client + + +async def connect_voice_with_retry(channel: discord.abc.Connectable, *, retries: int = 2, delay: float = 3.0) -> discord.VoiceClient: + """ + Robust voice connect helper that mitigates transient 4006 invalid session errors + by aggressively clearing stale session_id to force fresh handshakes. + """ + last_exc: Optional[Exception] = None + guild: Optional[discord.Guild] = getattr(channel, "guild", None) + if guild is None: + raise RuntimeError("Voice channel without guild cannot establish a connection.") + + # Always start with a clean slate to avoid 4006 stale-session loops + await _force_cleanup_voice_client(guild) + await asyncio.sleep(0.5) + + for attempt in range(1, retries + 1): + existing_vc: Optional[discord.VoiceClient] = await _get_active_voice_client(guild) + if existing_vc and existing_vc.channel == channel and existing_vc.is_connected(): + logger.info("Re-using existing voice connection to %s (attempt %d/%d)", channel, attempt, retries) + return existing_vc + try: + logger.info("Attempting voice connect to %s (attempt %d/%d)", channel, attempt, retries) + + # WORKAROUND for persistent 4006: Manually create VoiceClient and patch it to prevent session resume + from discord import VoiceClient as VoiceClientClass + state = guild._state + key_id = guild.id + + # Remove any existing voice client to force fresh connection + state._remove_voice_client(key_id) + + # Create new voice client + voice_client = VoiceClientClass(state._get_client(), channel) + # Force clear any stale session_id that might be cached + if hasattr(voice_client, 'session_id'): + voice_client.session_id = None + + # Register it + state._add_voice_client(key_id, voice_client) + + # Now connect with no reconnect to avoid resume attempts + try: + await voice_client.connect(timeout=25.0, reconnect=False) + except Exception: + # If connection fails, clean up the registered client + state._remove_voice_client(key_id) + raise + + vc = voice_client + + # Ensure we are self-undeafened so we can receive audio + try: + await channel.guild.change_voice_state(channel=channel, self_mute=False, self_deaf=False) + except Exception as change_err: + logger.debug("Unable to explicitly set voice state: %s", change_err) + if not vc.is_connected(): + logger.warning("Voice connect returned but client not connected on attempt %d; cleaning up (guild %s)", attempt, guild.id) + await _force_cleanup_voice_client(guild) + raise ConnectionClosed(None, shard_id=None, code=4006) + logger.info("Voice connect succeeded on attempt %d (%s)", attempt, channel) + return vc + except ConnectionClosed as e: + last_exc = e + logger.warning( + "Voice connect failed with ConnectionClosed(code=%s, reason=%s) on attempt %d/%d", + getattr(e, "code", None), + getattr(e, "reason", None), + attempt, + retries, + ) + await _force_cleanup_voice_client(guild) + except ClientException as e: + last_exc = e + logger.warning( + "Voice connect failed with ClientException('%s') on attempt %d/%d", + e, + attempt, + retries, + ) + if "Already connected to a voice channel" in str(e): + existing_vc = await _get_active_voice_client(guild) + if existing_vc and existing_vc.channel == channel and existing_vc.is_connected(): + logger.info("Detected active voice session despite ClientException; re-using existing client on guild %s", guild.id) + return existing_vc + await _force_cleanup_voice_client(guild) + except Exception as e: + last_exc = e + logger.exception("Voice connect raised %s on attempt %d/%d", e, attempt, retries) + await _force_cleanup_voice_client(guild) + + # Reset voice state and wait before retrying + try: + await guild.change_voice_state(channel=None, self_mute=False, self_deaf=False) + await asyncio.sleep(0.5) + except Exception as reset_err: + logger.debug("Failed to reset guild voice state: %s", reset_err) + + if attempt < retries: + wait_time = delay * (1.5 ** (attempt - 1)) + logger.info("Waiting %.1fs before retry %d (guild %s)", wait_time, attempt + 1, guild.id) + await asyncio.sleep(wait_time) + + assert last_exc is not None + raise last_exc + +@dataclass +class QueueItem: + title: str + source_factory: Callable[[], discord.AudioSource] + announce: Optional[str] = None + + +class HotwordStreamSink(sinks.Sink): + def __init__( + self, + state: "GuildAudioState", + text_channel: discord.abc.Messageable, + loop: asyncio.AbstractEventLoop, + min_chunk_seconds: float = 1.0, + window_seconds: float = 4.5, + inactivity_seconds: float = 1.0, + ): + super().__init__() + self.state = state + self.text_channel = text_channel + self.loop = loop + self.closed = False + self.buffers: defaultdict[int, bytearray] = defaultdict(bytearray) + self.last_activity: defaultdict[int, float] = defaultdict(lambda: 0.0) + self.processing_users: set[int] = set() + self.pending_tasks: dict[int, concurrent.futures.Future] = {} + self.min_chunk_bytes = int(max(PCM_BYTES_PER_SECOND * min_chunk_seconds, PCM_BYTES_PER_SECOND * 0.5)) + self.window_bytes = int(PCM_BYTES_PER_SECOND * window_seconds) + self.inactivity_seconds = inactivity_seconds + + def close(self): + self.closed = True + for fut in list(self.pending_tasks.values()): + try: + fut.cancel() + except Exception: + pass + self.pending_tasks.clear() + self.buffers.clear() + self.processing_users.clear() + + def update_text_channel(self, channel: discord.abc.Messageable): + self.text_channel = channel + + def cleanup(self): + self.closed = True + for fut in list(self.pending_tasks.values()): + try: + fut.cancel() + except Exception: + pass + self.pending_tasks.clear() + return super().cleanup() + + @sinks.Filters.container + def write(self, data, user): + if self.closed or user is None: + return + try: + user_id = int(user) + except Exception: + return + + buffer = self.buffers[user_id] + buffer.extend(data) + if len(buffer) > self.window_bytes: + del buffer[: len(buffer) - int(self.window_bytes)] + + now = time.perf_counter() + self.last_activity[user_id] = now + + if len(buffer) < self.min_chunk_bytes: + return + existing = self.pending_tasks.get(user_id) + if existing and not existing.done(): + existing.cancel() + self.pending_tasks.pop(user_id, None) + + async def delayed_dispatch(uid: int, expected_time: float): + try: + await asyncio.sleep(self.inactivity_seconds) + if self.closed: + return + last = self.last_activity.get(uid, 0.0) + if abs(last - expected_time) > 1e-6: + return + buffer = self.buffers.get(uid) + if not buffer or len(buffer) < self.min_chunk_bytes: + return + if uid in self.processing_users: + return + self.processing_users.add(uid) + chunk = bytes(buffer) + buffer.clear() + try: + await self.state.handle_hotword_buffer(uid, chunk, self.text_channel) + finally: + self.processing_users.discard(uid) + except asyncio.CancelledError: + return + finally: + self.pending_tasks.pop(uid, None) + + future = asyncio.run_coroutine_threadsafe(delayed_dispatch(user_id, now), self.loop) + + def _done_callback(fut, uid=user_id): + if fut.cancelled(): + return + try: + fut.result() + except asyncio.CancelledError: + return + except Exception as exc: + logger.exception("Hotword delayed dispatch failed for user %s: %s", uid, exc) + finally: + self.pending_tasks.pop(uid, None) + + future.add_done_callback(_done_callback) + self.pending_tasks[user_id] = future + + +@dataclass +class GuildAudioState: + guild_id: int + voice_client: Optional[discord.VoiceClient] = None + queue: Deque[QueueItem] = field(default_factory=deque) + current: Optional[QueueItem] = None + player_task: Optional[asyncio.Task] = None + queue_event: asyncio.Event = field(default_factory=asyncio.Event) + listen_enabled: bool = False + hotword_sink: Optional["HotwordStreamSink"] = None + last_transcripts: dict[int, Tuple[str, float]] = field(default_factory=dict) + + async def ensure_player(self): + if self.player_task is None or self.player_task.done(): + logger.debug("Starting player loop for guild %s", self.guild_id) + self.player_task = asyncio.create_task(self._player_loop()) + + def enqueue(self, item: QueueItem): + self.queue.append(item) + logger.info("Enqueued: %s (qsize=%d) on guild %s", item.title, len(self.queue), self.guild_id) + self.queue_event.set() + + def skip_current(self): + if self.voice_client and self.voice_client.is_playing(): + logger.info("Skipping current track (guild %s)", self.guild_id) + self.voice_client.stop() + + def stop_all(self): + logger.info("Stopping playback and clearing queue (guild %s)", self.guild_id) + self.queue.clear() + if self.voice_client and self.voice_client.is_playing(): + self.voice_client.stop() + self.current = None + self.queue_event.set() + + async def enqueue_local_clip(self, text_channel: discord.abc.Messageable, file_path: str): + if not os.path.exists(file_path): + await text_channel.send("Audio clip not found on server.") + return + if not self.voice_client or not self.voice_client.is_connected(): + await text_channel.send("I'm not in a voice channel right now.") + return + def source_factory() -> discord.AudioSource: + return discord.FFmpegPCMAudio(file_path, **FFMPEG_OPTIONS) + item = QueueItem(title=os.path.basename(file_path), source_factory=source_factory, announce=None) + self.enqueue(item) + await self.ensure_player() + + async def _play_tts(self, text: str): + if not self.voice_client or not self.voice_client.is_connected(): + logger.debug("Skipping TTS; voice client not connected (guild %s)", self.guild_id) + return + with tempfile.TemporaryDirectory() as tmpdir: + tts_path = os.path.join(tmpdir, "tts.wav") + await synthesize_tts_to_wav(text, tts_path) + source = discord.FFmpegPCMAudio(tts_path, **FFMPEG_OPTIONS) + fut = asyncio.get_running_loop().create_future() + + def after_playback(_): + logger.debug("Finished TTS playback (guild %s)", self.guild_id) + if not fut.done(): + fut.set_result(True) + + try: + self.voice_client.play(source, after=after_playback) + except Exception as e: + logger.exception("Error starting TTS playback (guild %s): %s", self.guild_id, e) + return + await fut + await asyncio.sleep(0.1) + + async def _player_loop(self): + logger.debug("Player loop running (guild %s)", self.guild_id) + while True: + await self.queue_event.wait() + self.queue_event.clear() + + if not self.voice_client or not self.voice_client.is_connected(): + logger.debug("No connected voice client; waiting (guild %s)", self.guild_id) + await asyncio.sleep(0.2) + continue + + if self.voice_client.is_playing(): + # Will trigger when current track finishes (after callback sets event) + logger.debug("Voice already playing; yielding (guild %s)", self.guild_id) + await asyncio.sleep(0.2) + continue + + if not self.queue: + # Nothing to play, wait + logger.debug("Queue empty; waiting (guild %s)", self.guild_id) + continue + + self.current = self.queue.popleft() + logger.info("Starting next track: %s (remaining qsize=%d) guild %s", self.current.title, len(self.queue), self.guild_id) + + # Announce if needed + if self.current.announce: + try: + await self._play_tts(self.current.announce) + except Exception as e: + # Announce failures shouldn't break playback + logger.warning("TTS announce failed (guild %s): %s", self.guild_id, e) + + # Start playing audio + source = self.current.source_factory() + play_done = asyncio.get_running_loop().create_future() + + def _after_play(err: Optional[Exception]): + # FFmpeg end or error – wake the loop + if err: + logger.error("Playback finished with error (guild %s): %s", self.guild_id, err) + else: + logger.debug("Playback finished normally (guild %s)", self.guild_id) + if not play_done.done(): + play_done.set_result(True) + # Nudge the loop to consider the next item + self.queue_event.set() + + try: + self.voice_client.play(source, after=_after_play) + logger.info("FFmpeg playback started (guild %s) on channel %s", self.guild_id, getattr(self.voice_client.channel, "name", "?")) + except Exception as e: + logger.exception("Failed to start playback (guild %s): %s", self.guild_id, e) + self.current = None + # Wake loop to attempt next or wait + self.queue_event.set() + continue + await play_done + self.current = None + + async def start_listening(self, text_channel: discord.abc.Messageable): + if not HOTWORD_ENABLED: + logger.debug("Hotword listening disabled by environment (guild %s)", self.guild_id) + return + if not HAS_SINKS: + logger.warning("Hotword listening requested but sinks are unavailable on this stack.") + try: + await text_channel.send("Live hotword listening is unavailable on this install. Send a voice message instead.") + except Exception: + pass + return + if not self.voice_client or not self.voice_client.is_connected(): + logger.debug("Cannot start listener without an active voice client (guild %s)", self.guild_id) + return + self.listen_enabled = True + self.last_transcripts.clear() + if self.hotword_sink and not self.hotword_sink.closed: + self.hotword_sink.update_text_channel(text_channel) + logger.debug("Hotword listener already running (guild %s)", self.guild_id) + return + + # If another recording is running, stop it first + if getattr(self.voice_client, "recording", False): + try: + self.voice_client.stop_recording() + except Exception: + pass + + loop = asyncio.get_running_loop() + sink = HotwordStreamSink(self, text_channel, loop) + self.hotword_sink = sink + logger.info("Starting continuous hotword listener (guild %s)", self.guild_id) + + async def _finished_callback(sink_obj, *_): + await self._on_sink_finished(sink_obj) + + self.voice_client.start_recording(sink, _finished_callback) + + channel = getattr(self.voice_client, "channel", None) + if channel: + others = [m for m in channel.members if m.id != client.user.id] + if others: + names = ", ".join(_display_name(m) for m in others) + try: + await text_channel.send(f"Listening for: {names}") + except Exception: + logger.debug("Unable to send listening roster message (guild %s)", self.guild_id) + + async def _on_sink_finished(self, sink_obj: "HotwordStreamSink"): + sink_obj.close() + if self.hotword_sink is sink_obj: + self.hotword_sink = None + self.listen_enabled = False + logger.debug("Hotword sink finished (guild %s)", self.guild_id) + + async def stop_listening(self): + if self.listen_enabled: + logger.info("Stopping hotword listener (guild %s)", self.guild_id) + self.listen_enabled = False + sink = self.hotword_sink + if sink: + sink.close() + if self.voice_client and getattr(self.voice_client, "recording", False): + try: + self.voice_client.stop_recording() + except Exception: + pass + self.hotword_sink = None + + async def handle_hotword_buffer(self, user_id: int, pcm_bytes: bytes, text_channel: discord.abc.Messageable): + if not pcm_bytes: + return + if not self.voice_client or not self.voice_client.is_connected(): + return + + guild = client.get_guild(self.guild_id) + if guild is None: + return + member = guild.get_member(int(user_id)) + if member is None: + return + + samples = np.frombuffer(pcm_bytes, dtype=np.int16) + if samples.size == 0: + return + if PCM_CHANNELS > 1: + usable = (samples.size // PCM_CHANNELS) * PCM_CHANNELS + if usable == 0: + return + samples = samples[:usable].reshape(-1, PCM_CHANNELS) + + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp: + tmp_path = tmp.name + try: + sf.write(tmp_path, samples, PCM_SAMPLE_RATE, subtype="PCM_16") + loop = asyncio.get_running_loop() + text, score = await loop.run_in_executor(None, transcribe_file, tmp_path) + except Exception as e: + logger.warning("Transcription failed for speaker %s: %s", user_id, e) + return + finally: + try: + os.remove(tmp_path) + except OSError: + pass + + await self.handle_hotword_transcript(user_id, text, score, member, text_channel) + + async def handle_hotword_transcript(self, user_id: int, text: str, score: float, member: discord.Member, text_channel: discord.abc.Messageable): + text_norm = (text or "").strip() + logger.info( + "Hotword transcript (stream) guild=%s speaker=%s text='%s' score=%.3f", + self.guild_id, + getattr(member, "display_name", member), + text_norm, + score, + ) + if not text_norm: + return + + normalized = normalize_for_command(text_norm) + if not normalized: + return + + now = time.perf_counter() + last = self.last_transcripts.get(user_id) + if last and last[0] == normalized and (now - last[1]) < 2.0: + logger.debug("Skipping duplicate transcript for speaker %s (guild %s)", member, self.guild_id) + return + append_transcript_entry(_display_name(member), text_norm) + self.last_transcripts[user_id] = (normalized, now) + if member.id == GOODBOY_USER_ID and is_probably_english_sentence(text_norm): + await self.enqueue_local_clip(text_channel, GOODBOY_AUDIO_PATH) + + await route_transcribed_command_from_member(member.guild, member, text_channel, text_norm) + + +# ---------- Bot setup ---------- + +intents = Intents.default() +intents.message_content = True +intents.voice_states = True +client = discord.Client(intents=intents) + +audio_states: dict[int, GuildAudioState] = {} + + +def get_state_for_guild(guild_id: int) -> GuildAudioState: + state = audio_states.get(guild_id) + if state is None: + state = GuildAudioState(guild_id=guild_id) + audio_states[guild_id] = state + return state + + +async def connect_to_author_channel(message: discord.Message) -> Optional[discord.VoiceClient]: + if not isinstance(message.author, discord.Member): + return None + logger.debug("Connect requested by %s in guild %s", message.author, getattr(message.guild, "id", "?")) + voice_state = message.author.voice + if not voice_state or not voice_state.channel: + logger.info("Author not in a voice channel; cannot join (guild %s)", getattr(message.guild, "id", "?")) + await message.channel.send("Join a voice channel first, then say 'hey bashar join'.") + return None + channel = voice_state.channel + vc = await _get_active_voice_client(message.guild) + if vc and vc.channel == channel and vc.is_connected(): + logger.debug("Already connected to requested channel: %s (guild %s)", channel, getattr(message.guild, "id", "?")) + return vc + if vc: + try: + logger.info("Moving voice client to channel: %s (guild %s)", channel, getattr(message.guild, "id", "?")) + await vc.move_to(channel) + await announce_listening_roster(message.channel, channel) + return vc + except Exception as e: + logger.warning("Move failed; reconnecting fresh (guild %s): %s", getattr(message.guild, "id", "?"), e) + try: + await vc.disconnect(force=True) + except Exception: + pass + try: + vc = await connect_voice_with_retry(channel) + await announce_listening_roster(message.channel, channel) + except Exception as e: + logger.exception("Voice connect retries exhausted (guild %s): %s", getattr(message.guild, "id", "?"), e) + await message.channel.send("I couldn't join the voice channel (error 4006). Try again in a few seconds.") + return None + else: + logger.info("Connecting to voice channel: %s (guild %s)", channel, getattr(message.guild, "id", "?")) + try: + vc = await connect_voice_with_retry(channel) + await announce_listening_roster(message.channel, channel) + except Exception as e: + logger.exception("Voice connect retries exhausted (guild %s): %s", getattr(message.guild, "id", "?"), e) + await message.channel.send("I couldn't join the voice channel (error 4006). Try again in a few seconds.") + return None + + if vc and vc.is_connected(): + logger.info("Connected to voice: %s (guild %s)", vc.channel, getattr(message.guild, "id", "?")) + else: + logger.error("Voice connect returned but not connected (guild %s)", getattr(message.guild, "id", "?")) + return vc + + +def make_ffmpeg_source(url: str) -> discord.AudioSource: + logger.debug("Creating FFmpeg source for url: %s", url[:64] + ("..." if len(url) > 64 else "")) + return discord.FFmpegPCMAudio(url, **FFMPEG_OPTIONS) + + +async def handle_play_query(message: discord.Message, query: str): + assert message.guild is not None + logger.info("Play request: '%s' (guild %s)", query, message.guild.id) + state = get_state_for_guild(message.guild.id) + vc = await connect_to_author_channel(message) + if vc is None: + logger.info("Aborting play; no voice client (guild %s)", message.guild.id) + return + state.voice_client = vc + + # Resolve YouTube stream and title + try: + stream_url, title = await asyncio.get_running_loop().run_in_executor( + None, yt_search_and_resolve, query + ) + except Exception as e: + logger.exception("YouTube resolve failed for '%s' (guild %s): %s", query, message.guild.id, e) + await message.channel.send(f"Couldn't find or load audio for: {query}") + return + + # Enqueue TTS announcement + track + item = QueueItem( + title=title, + source_factory=lambda: make_ffmpeg_source(stream_url), + announce=f"Playing {title}" + ) + state.enqueue(item) + await state.ensure_player() + await message.channel.send(f"Queued: {title}") + logger.info("Queued and scheduled: %s (guild %s)", title, message.guild.id) + +async def handle_play_for_member(guild: discord.Guild, member: discord.Member, text_channel: discord.abc.Messageable, query: str): + logger.info("Play request (voice) by %s: '%s' (guild %s)", getattr(member, "display_name", member.id), query, guild.id) + state = get_state_for_guild(guild.id) + # Ensure voice connected to member channel + if not member.voice or not member.voice.channel: + await text_channel.send("Join a voice channel first, then say 'hey bashar play <song>'.") + return + channel = member.voice.channel + vc = await _get_active_voice_client(guild) + try: + if vc and vc.channel != channel and vc.is_connected(): + try: + await vc.move_to(channel) + except Exception: + try: + await vc.disconnect(force=True) + except Exception: + pass + vc = await connect_voice_with_retry(channel) + elif not vc: + vc = await connect_voice_with_retry(channel) + except Exception as e: + logger.exception("Voice connect retries exhausted (guild %s): %s", guild.id, e) + await text_channel.send("I couldn't join the voice channel (error 4006). Try again in a few seconds.") + return + state.voice_client = vc + + # Resolve and enqueue + try: + stream_url, title = await asyncio.get_running_loop().run_in_executor(None, yt_search_and_resolve, query) + except Exception as e: + logger.exception("YouTube resolve failed for '%s' (guild %s): %s", query, guild.id, e) + await text_channel.send(f"Couldn't find or load audio for: {query}") + return + item = QueueItem( + title=title, + source_factory=lambda: make_ffmpeg_source(stream_url), + announce=f"Playing {title}" + ) + state.enqueue(item) + await state.ensure_player() + await text_channel.send(f"Queued: {title}") + logger.info("Queued and scheduled (voice): %s (guild %s)", title, guild.id) + + +@client.event +async def on_ready(): + logger.info("Logged in as %s (id=%s)", client.user, client.user.id) + ensure_ffmpeg_available() + ensure_opus_loaded() + logger.info("Startup checks OK") + if HOTWORD_ENABLED and HAS_SINKS: + logger.info("Hotword listening: ENABLED (sinks available and HOTWORD_ENABLED=True)") + elif HOTWORD_ENABLED and not HAS_SINKS: + logger.info("Hotword listening: DISABLED (HOTWORD_ENABLED=True but sinks unavailable)") + else: + logger.info("Hotword listening: DISABLED (HOTWORD_ENABLED unset/false)") + + +@client.event +async def on_message(message: discord.Message): + # Ignore our own messages + if message.author.id == client.user.id: + return + + # "hey bashar logs" -> send last lines of bot.log + try: + content_raw = message.content or "" + if content_raw.lower().strip() == f"{BOT_WAKE_WORD} logs": + def _tail_lines(path: str, max_lines: int = 300) -> str: + try: + with open(path, "r", encoding="utf-8", errors="replace") as f: + lines = f.readlines() + return "".join(lines[-max_lines:]) + except Exception as e: + return f"Failed to read log: {e}" + tail = _tail_lines("bot.log", 300) + if len(tail) > 1800: + with io.BytesIO(tail.encode("utf-8", errors="replace")) as bio: + bio.seek(0) + await message.channel.send(content="Here are the last 300 lines of bot.log", file=discord.File(bio, filename="bot-tail.txt")) + else: + await message.channel.send(f"```{tail}```") + return + except Exception as e: + logger.exception("Failed handling 'logs' command: %s", e) + # fall through + + # Handle audio/voice-message attachments for STT + if message.attachments: + for att in message.attachments: + ct = (att.content_type or "").lower() + filename = (att.filename or "").lower() + is_audio = ct.startswith("audio/") or any(filename.endswith(ext) for ext in (".wav", ".mp3", ".m4a", ".ogg", ".oga", ".opus", ".webm")) + if not is_audio: + continue + logger.info("Downloading audio attachment for STT: %s (%s)", att.filename, att.content_type) + with tempfile.TemporaryDirectory() as tmpdir: + tmp_path = os.path.join(tmpdir, att.filename or "audio_input") + try: + await att.save(tmp_path) + logger.debug("Saved attachment to %s", tmp_path) + except Exception as e: + logger.exception("Failed to save attachment: %s", e) + continue + + loop = asyncio.get_running_loop() + try: + text, score = await loop.run_in_executor(None, transcribe_file, tmp_path) + except Exception as e: + logger.exception("Transcription failed: %s", e) + continue + + text_norm = (text or "").strip() + if not text_norm: + await message.channel.send("I couldn't hear anything useful in that audio.") + continue + + logger.info("Transcribed: %s (score=%.3f)", text_norm, score) + append_transcript_entry(_display_name(message.author), text_norm) + await route_transcribed_command(message, text_norm) + # Only process first audio attachment per message + return + + content = message.content.strip() + parsed = parse_wake_command(content) + if not parsed: + return + action, args = parsed + logger.debug("Wake word detected. Raw='%s' | action='%s' | args='%s'", content, action, args) + append_transcript_entry(_display_name(message.author), content) + + if action == "logs": + try: + await send_recent_logs(message.channel) + except Exception as e: + logger.exception("Failed handling 'logs' command: %s", e) + await message.channel.send("Couldn't fetch logs.") + return + + # "hey bashar join" + if action == "join": + vc = await connect_to_author_channel(message) + if vc: + state = get_state_for_guild(message.guild.id) + state.voice_client = vc + await message.channel.send("Joined your voice channel. Say 'hey bashar play <song>' here.") + logger.info("Joined voice channel for guild %s", message.guild.id) + # Auto-start hotword listener + await state.start_listening(message.channel) + return + + # "hey bashar leave" + if action == "leave": + state = get_state_for_guild(message.guild.id) + await state.stop_listening() + if state.voice_client and state.voice_client.is_connected(): + await message.channel.send("Leaving voice channel.") + logger.info("Disconnecting from voice (guild %s)", message.guild.id) + await state.voice_client.disconnect(force=True) + return + + # "hey bashar play <query>" + if action == "play": + if not args: + await message.channel.send("Say 'hey bashar play <search terms>'.") + return + await handle_play_query(message, args) + return + + if action == "skip": + state = get_state_for_guild(message.guild.id) + state.skip_current() + await message.channel.send("Skipped the current track.") + return + + if action == "stop": + state = get_state_for_guild(message.guild.id) + state.stop_all() + await message.channel.send("Stopped playback and cleared the queue.") + return + + # Unknown + await message.channel.send("Commands: 'hey bashar join', 'hey bashar play <song>', 'hey bashar skip', 'hey bashar stop', 'hey bashar leave'.") + logger.debug("Sent help for unknown command") + + +async def route_transcribed_command(message: discord.Message, text: str): + """Route transcribed text to existing handlers if it starts with the wake word.""" + parsed = parse_wake_command(text) + if not parsed: + logger.debug("Ignoring transcript without wake word: %s", text) + return + action, args = parsed + if action == "join": + vc = await connect_to_author_channel(message) + if vc: + state = get_state_for_guild(message.guild.id) + state.voice_client = vc + await message.channel.send("Joined your voice channel. Say 'hey bashar play <song>' here.") + return + if action == "leave": + state = get_state_for_guild(message.guild.id) + if state.voice_client and state.voice_client.is_connected(): + await message.channel.send("Leaving voice channel.") + await state.voice_client.disconnect(force=True) + return + if action == "play": + if not args: + await message.channel.send("Say 'hey bashar play <search terms>'.") + return + await handle_play_query(message, args) + return + if action == "skip": + state = get_state_for_guild(message.guild.id) + state.skip_current() + await message.channel.send("Skipped the current track.") + return + if action == "stop": + state = get_state_for_guild(message.guild.id) + state.stop_all() + await message.channel.send("Stopped playback and cleared the queue.") + return + await message.channel.send("Commands: 'hey bashar join', 'hey bashar play <song>', 'hey bashar skip', 'hey bashar stop', 'hey bashar leave'.") + +async def route_transcribed_command_from_member(guild: discord.Guild, member: discord.Member, text_channel: discord.abc.Messageable, text: str): + logger.debug( + "Routing voice transcript (guild %s, speaker=%s): %s", + guild.id, + getattr(member, "display_name", member), + text, + ) + parsed = parse_wake_command(text) + if not parsed: + return + action, args = parsed + if action == "join": + # Connect to the member's voice channel + if not member.voice or not member.voice.channel: + await text_channel.send("Join a voice channel first, then say 'hey bashar join'.") + return + state = get_state_for_guild(guild.id) + vc = await _get_active_voice_client(guild) + try: + if vc and vc.channel != member.voice.channel and vc.is_connected(): + try: + await vc.move_to(member.voice.channel) + except Exception: + try: + await vc.disconnect(force=True) + except Exception: + pass + vc = await connect_voice_with_retry(member.voice.channel) + elif not vc: + vc = await connect_voice_with_retry(member.voice.channel) + except Exception as e: + logger.exception("Voice connect retries exhausted (guild %s): %s", guild.id, e) + await text_channel.send("I couldn't join the voice channel (error 4006). Try again in a few seconds.") + return + state.voice_client = vc + await text_channel.send("Joined your voice channel. Say 'hey bashar play <song>' here.") + # Start listening if not already + await state.start_listening(text_channel) + return + if action == "leave": + state = get_state_for_guild(guild.id) + await state.stop_listening() + if state.voice_client and state.voice_client.is_connected(): + await text_channel.send("Leaving voice channel.") + await state.voice_client.disconnect(force=True) + return + if action == "play": + if not args: + await text_channel.send("Say 'hey bashar play <search terms>'.") + return + await handle_play_for_member(guild, member, text_channel, args) + return + if action == "skip": + state = get_state_for_guild(guild.id) + state.skip_current() + await text_channel.send("Skipped the current track.") + return + if action == "stop": + state = get_state_for_guild(guild.id) + state.stop_all() + await text_channel.send("Stopped playback and cleared the queue.") + return + await text_channel.send("Commands: 'hey bashar join', 'hey bashar play <song>', 'hey bashar skip', 'hey bashar stop', 'hey bashar leave'.") + +@client.event +async def on_voice_state_update(member: discord.Member, before: discord.VoiceState, after: discord.VoiceState): + # Log only the bot's own state changes to avoid noise + if client.user and member.id == client.user.id: + logger.info( + "Voice state update (guild %s): %s -> %s (self_mute=%s deaf=%s)", + member.guild.id, + getattr(before.channel, "name", None), + getattr(after.channel, "name", None), + after.self_mute, + after.self_deaf, + ) + + +@client.event +async def on_error(event_method: str, /, *args, **kwargs): + logger.exception("Unhandled error in event '%s'", event_method) + + +if __name__ == "__main__": + token = os.getenv("DISCORD_TOKEN") + if not token: + raise SystemExit("Missing DISCORD_TOKEN in environment. Create a .env file or set the variable.") + client.run(token) + + + diff --git a/goodboy.ogg b/goodboy.ogg new file mode 100644 index 0000000000000000000000000000000000000000..265a6b221d64afefbae2e1f4e59b7c60b453ff17 GIT binary patch literal 5686 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 literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7822006 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +discord.py>=2.4.0 +PyNaCl==1.5.0 +yt-dlp==2025.8.11 +pyttsx3==2.90 +faster-whisper==1.0.3 +soundfile==0.12.1 +numpy==1.26.4 +python-dotenv==1.0.1 + + +requests>=2.31.0 diff --git a/stt.py b/stt.py new file mode 100644 index 0000000..4aee806 --- /dev/null +++ b/stt.py @@ -0,0 +1,35 @@ +import os +from typing import Optional, Tuple, List + +from faster_whisper import WhisperModel + +_model_singleton: Optional[WhisperModel] = None + + +def get_model() -> WhisperModel: + global _model_singleton + if _model_singleton is None: + model_size = os.getenv("WHISPER_MODEL_SIZE", "small") + # CPU-friendly defaults; set compute_type="int8" for lower RAM + _model_singleton = WhisperModel(model_size, device="cpu", compute_type="int8") + return _model_singleton + + +def transcribe_file(audio_path: str) -> Tuple[str, float]: + """ + Transcribe an audio file to text locally using faster-whisper. + Returns (text, avg_logprob). + """ + model = get_model() + segments, info = model.transcribe(audio_path, beam_size=1, vad_filter=True) + + text_parts: List[str] = [] + for seg in segments: + text_parts.append(seg.text.strip()) + + text = " ".join(part for part in text_parts if part) + avg_logprob = getattr(info, "avg_logprob", 0.0) if info is not None else 0.0 + return text.strip(), float(avg_logprob) + + +