Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 73 additions & 15 deletions bot.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
from generator import Generator
import os
import discord
from discord.ext import commands
from typing import Any, Optional
import logging
from pathlib import Path
from eleven_labs_api import ElevenLabsAPI

DEBUG = os.getenv("DEBUG")
AUDIO_DIR = os.getenv("AUDIO_DIR", "/audio")
FFMPEG_EXEC = os.getenv("FFMPEG_EXEC", "ffmpeg")
import asyncio
from user_settings import BOT_TOKEN, ELEVEN_LABS_TOKEN, AUDIO_DIR, FFMPEG_EXEC, AUTO_VOICE_LEAVE_DELAY, DEBUG

RAW_PREFIX = "!batch"
RAW_PREFIX_SHORT = "!b"
SPACE_PREFIX = RAW_PREFIX + " "
BOT_TOKEN = os.getenv("BOT_TOKEN")
ELEVEN_LABS_TOKEN = os.getenv("ELEVEN_LABS_TOKEN")
BOT_DESCRIPTION = ("A bot to generate alternate names for Benedict Cumberbatch.\n"
"\n"
f"Usage: {RAW_PREFIX_SHORT} [command] [arguments]\n"
Expand All @@ -25,6 +20,16 @@
intents = discord.Intents.default()
intents.message_content = True
intents.messages = True
intents.voice_states = True

name_api = Generator()
eleven_labs_api = ElevenLabsAPI(ELEVEN_LABS_TOKEN)

g_last_name = "Benedict Cumberbatch"
g_last_phone = "benedict cumberbatch"
g_autospeak = False

voice_leave_tasks: dict[int, asyncio.Task] = {}


class CustomHelp(commands.DefaultHelpCommand):
Expand All @@ -50,26 +55,53 @@ def add_indented_commands(self, _commands, *, heading, max_size=None):
)
)

name_api = Generator()
eleven_labs_api = ElevenLabsAPI(ELEVEN_LABS_TOKEN)

g_last_name = "Benedict Cumberbatch"
g_last_phone = "benedict cumberbatch"
g_autospeak = False


def vc_for(guild: discord.Guild) -> discord.VoiceClient | None:
return discord.utils.get(bot.voice_clients, guild=guild)


def num_humans_in_voice(channel: discord.VoiceChannel | None) -> int:
if not channel:
return 0
return sum(1 for m in channel.members if not m.bot)


async def schedule_voice_leave(guild: discord.Guild) -> None:
if task := voice_leave_tasks.pop(guild.id, None):
task.cancel()


async def _worker():
try:
await asyncio.sleep(AUTO_VOICE_LEAVE_DELAY)
vc = vc_for(guild)
if not vc or not vc.is_connected():
return

if num_humans_in_voice(vc.channel) == 0:
await vc.disconnect(force=True)
except asyncio.CancelledError:
pass

print(f"All users left voice channel for guild '{guild.name}', leaving in {AUTO_VOICE_LEAVE_DELAY} seconds")
voice_leave_tasks[guild.id] = asyncio.create_task(_worker())


def cancel_voice_leave(guild: discord.Guild, reason: Optional[str] = None) -> None:
if task := voice_leave_tasks.pop(guild.id, None):
task.cancel()
if reason:
print(reason)


async def _speak(ctx: commands.Context) -> Optional[Any]:
global g_autospeak
vc = vc_for(ctx.guild)
if not vc or not vc.is_connected():
return await ctx.reply("I am not connected to a voice channel.", mention_author=False)
if not vc.is_playing():
_count = eleven_labs_api.get_remaining_character_count()
if _count < 20:
if _count < 20 or DEBUG:
name_api.vocalize(g_last_phone)
audio_source = Path(AUDIO_DIR) / "output.wav"
else:
Expand Down Expand Up @@ -127,6 +159,30 @@ async def on_command_error(ctx: commands.Context, error: commands.CommandError):
return await ctx.reply(f"An error occurred: {str(error)}", mention_author=False)


@bot.event
async def on_voice_state_update(member: discord.Member, before: discord.VoiceState, after: discord.VoiceState):
guild = member.guild
vc = vc_for(guild)
if not vc or not vc.is_connected():
return

bot_channel = vc.channel
# If the event didn't touch the bot's channel, ignore
touched = {before.channel, after.channel}
if bot_channel not in touched:
return

# If someone joined the bot's channel, cancel any pending leave
if after.channel is bot_channel and not member.bot:
cancel_voice_leave(guild, "User joined active voice channel, cancelling auto-leave")
return

# If someone left/moved away from the bot's channel, check if it's empty of humans
if before.channel is bot_channel:
if num_humans_in_voice(bot_channel) == 0:
await schedule_voice_leave(guild)


@bot.command(name="gen",
help="Generate a new name. (Hint: You can also just type '!b')")
async def gen(ctx):
Expand All @@ -146,6 +202,7 @@ async def join(ctx: commands.Context):
await vc.move_to(channel)
else:
await channel.connect()
cancel_voice_leave(ctx.guild)
return await ctx.reply(f"Joined {channel.name}.", mention_author=False)


Expand All @@ -158,6 +215,7 @@ async def leave(ctx: commands.Context):
return await ctx.reply("I am not connected to a voice channel.", mention_author=False)
await vc.disconnect()
g_autospeak = False
cancel_voice_leave(ctx.guild)
return await ctx.reply("Disconnected.", mention_author=False)


Expand Down
2 changes: 2 additions & 0 deletions dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ WORKDIR ${APP_ROOT}
COPY requirements.txt ${APP_ROOT}/
RUN pip install --no-cache-dir -r requirements.txt

RUN python -m piper.download_voices --debug --download-dir ${PIPER_VOICES_DIR} ${PIPER_VOICE}

# Copy your bot source code
COPY . ${APP_ROOT}/

Expand Down
7 changes: 2 additions & 5 deletions generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@
from typing import Union
from piper import PiperVoice
import wave
import os
from user_settings import PIPER_VOICE, PIPER_VOICES_DIR, AUDIO_DIR

PATH_TO_JSON = Path(__file__).parent / "phonemized_words.json"
VOICE_NAME = os.getenv("PIPER_VOICE", "en_GB-alan-medium")
VOICES_DIR = os.getenv("PIPER_VOICES_DIR", "/voices")
VOICE_FILE = Path(VOICES_DIR) / f"{VOICE_NAME}.onnx"
VOICE_FILE = Path(PIPER_VOICES_DIR) / f"{PIPER_VOICE}.onnx"
VOICE = PiperVoice.load(VOICE_FILE)
AUDIO_DIR = os.getenv("AUDIO_DIR", "/audio")


class Generator:
Expand Down
3 changes: 0 additions & 3 deletions main.py

This file was deleted.

19 changes: 19 additions & 0 deletions user_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import os

# Tokens
BOT_TOKEN = os.getenv("BOT_TOKEN")
ELEVEN_LABS_TOKEN = os.getenv("ELEVEN_LABS_TOKEN")

# Voice synthesis
AUDIO_DIR = os.getenv("AUDIO_DIR", "/audio")
FFMPEG_EXEC = os.getenv("FFMPEG_EXEC", "ffmpeg")

# Piper
PIPER_VOICE = os.getenv("PIPER_VOICE", "en_GB-alan-medium")
PIPER_VOICES_DIR = os.getenv("PIPER_VOICES_DIR", "/voices")

# Discord
AUTO_VOICE_LEAVE_DELAY = os.getenv("AUTO_VOICE_LEAVE_DELAY", 20)

# Dev
DEBUG = os.getenv("DEBUG")