From 79449f810523de961a9c7124fefe73aebdd85422 Mon Sep 17 00:00:00 2001 From: Mycroft Kang Date: Sun, 11 Feb 2024 20:45:03 +0900 Subject: [PATCH] Add voice command --- changelogs/unreleased/add-voice-command.yml | 4 + locales/ko_KR/LC_MESSAGES/mkbot.po | 2 +- locales/mkbot.pot | 9 +- poetry.lock | 21 ++- pyproject.toml | 1 + src/bot/core/controllers/discord/tts.py | 143 -------------------- src/bot/core/controllers/discord/voice.py | 64 +++++++++ src/bot/discord_ext.py | 2 +- 8 files changed, 93 insertions(+), 153 deletions(-) create mode 100644 changelogs/unreleased/add-voice-command.yml delete mode 100644 src/bot/core/controllers/discord/tts.py create mode 100644 src/bot/core/controllers/discord/voice.py diff --git a/changelogs/unreleased/add-voice-command.yml b/changelogs/unreleased/add-voice-command.yml new file mode 100644 index 00000000..53a26b4c --- /dev/null +++ b/changelogs/unreleased/add-voice-command.yml @@ -0,0 +1,4 @@ +--- +title: Add voice command +pull_request: +type: features diff --git a/locales/ko_KR/LC_MESSAGES/mkbot.po b/locales/ko_KR/LC_MESSAGES/mkbot.po index 5edbce2b..bde93d21 100644 --- a/locales/ko_KR/LC_MESSAGES/mkbot.po +++ b/locales/ko_KR/LC_MESSAGES/mkbot.po @@ -834,7 +834,7 @@ msgid "Your enemy is not ready yet, please wait until they are" msgstr "적이 아직 준비되지 않았습니다, 잠시 기다려주세요" msgid "[MK Bot]({url}) said on behalf of {author}" -msgstr "[MK Bot]({url}) {author}을(를) 대신하여 말했습니다" +msgstr "[MK Bot]({url})이 {author}을(를) 대신하여 말했습니다." msgid "chose... %s!" msgstr "선택... %s!" diff --git a/locales/mkbot.pot b/locales/mkbot.pot index 814eb963..440e3d2b 100644 --- a/locales/mkbot.pot +++ b/locales/mkbot.pot @@ -245,9 +245,6 @@ msgstr "" msgid "Invalid language" msgstr "" -msgid "Invalid parameter. For more information, type `{commandPrefix}help tts`." -msgstr "" - msgid "Invalid time format. The required format is `:`." msgstr "" @@ -695,12 +692,10 @@ msgstr "" #, docstring msgid "" "TTS voice available\n" -"{commandPrefix}tts [option] \"Content\" : Says the content in \"content\". You do not have to use Quotation marks even if there are spaces included in content.\n" +"{commandPrefix}tts \"Content\" : Says the content in \"content\". You do not have to use Quotation marks even if there are spaces included in content.\n" "\n" "*Example*\n" -"{commandPrefix}tts \"Content\"\n" -"{commandPrefix}tts -m \"Content\"\n" -"{commandPrefix}tts -w \"Content\": -m speaks in male voice and -w speaks in female voice. Default voice is male." +"{commandPrefix}tts \"Content\"" msgstr "" #, docstring diff --git a/poetry.lock b/poetry.lock index d3ce2f70..e45b9cc1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -964,6 +964,25 @@ files = [ docs = ["Sphinx", "furo"] test = ["objgraph", "psutil"] +[[package]] +name = "gtts" +version = "2.5.1" +description = "gTTS (Google Text-to-Speech), a Python library and CLI tool to interface with Google Translate text-to-speech API" +optional = false +python-versions = ">=3.7" +files = [ + {file = "gTTS-2.5.1-py3-none-any.whl", hash = "sha256:273ec8a5077b25e60ca5a266ed254b54d1f14032b0af3ba00092d14966148664"}, + {file = "gTTS-2.5.1.tar.gz", hash = "sha256:02d0a9874f945dee9cd5092991c60bc88d4b7767b8cd81144b6fb49dc3de6897"}, +] + +[package.dependencies] +click = ">=7.1,<8.2" +requests = ">=2.27,<3" + +[package.extras] +docs = ["sphinx", "sphinx-autobuild", "sphinx-click", "sphinx-mdinclude", "sphinx-rtd-theme"] +tests = ["pytest (>=7.1.3,<8.1.0)", "pytest-cov", "testfixtures"] + [[package]] name = "identify" version = "2.5.33" @@ -2745,4 +2764,4 @@ release = ["mulgyeol-telemetry"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.11" -content-hash = "f94d738e3eb0e0ef150ea0d2c0d63dff1729c7a0d5b4083d78b5344e8e1f0b7b" +content-hash = "71d64b64191249f4c1c734740c20382142bb5421168cbb748c05c28adf7a09b7" diff --git a/pyproject.toml b/pyproject.toml index 35000497..5f380b13 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ requests = "2.31.0" sqlalchemy = "1.4.46" yt-dlp = "2023.12.30" mulgyeol-telemetry = {git = "https://github.com/MycroftKang/application-insights-python.git", rev = "c03c4d415b15648134717bb402d895d9b7ebea57", optional = true} +gtts = "^2.5.1" [tool.poetry.group.dev.dependencies] black = "22.3.0" diff --git a/src/bot/core/controllers/discord/tts.py b/src/bot/core/controllers/discord/tts.py deleted file mode 100644 index 8c1fb856..00000000 --- a/src/bot/core/controllers/discord/tts.py +++ /dev/null @@ -1,143 +0,0 @@ -import io -import shlex -import subprocess - -import aiohttp -import discord -from discord.ext import commands -from discord.opus import Encoder - -from mgylabs.i18n import __ -from mgylabs.utils import logger -from mgylabs.utils.config import CONFIG - -from .utils.exceptions import UsageError -from .utils.MGCert import Level, MGCertificate -from .utils.MsgFormat import MsgFormatter -from .utils.voice import validate_voice_client - -log = logger.get_logger(__name__) - - -class FFmpegPCMAudio(discord.AudioSource): - def __init__( - self, - source, - *, - executable="ffmpeg", - pipe=False, - stderr=None, - before_options=None, - options=None, - ): - stdin = None if not pipe else source - args = [executable] - if isinstance(before_options, str): - args.extend(shlex.split(before_options)) - args.append("-i") - args.append("-" if pipe else source) - args.extend(("-f", "s16le", "-ar", "48000", "-ac", "2", "-loglevel", "warning")) - if isinstance(options, str): - args.extend(shlex.split(options)) - args.append("pipe:1") - self._process = None - try: - self._process = subprocess.Popen( - args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=stderr - ) - self._stdout = io.BytesIO(self._process.communicate(input=stdin)[0]) - except FileNotFoundError: - raise commands.CommandError(executable + " was not found.") from None - except subprocess.SubprocessError as exc: - raise commands.CommandError( - "Popen failed: {0.__class__.__name__}: {0}".format(exc) - ) from exc - - def read(self): - ret = self._stdout.read(Encoder.FRAME_SIZE) - if len(ret) != Encoder.FRAME_SIZE: - return b"" - return ret - - def cleanup(self): - proc = self._process - if proc is None: - return - proc.kill() - if proc.poll() is None: - proc.communicate() - - self._process = None - - -@commands.command() -@MGCertificate.verify(level=Level.TRUSTED_USERS) -async def tts(ctx: commands.Context, *args): - """ - TTS voice available - {commandPrefix}tts [option] "Content" : Says the content in "content". You do not have to use Quotation marks even if there are spaces included in content. - - *Example* - {commandPrefix}tts "Content" - {commandPrefix}tts -m "Content" - {commandPrefix}tts -w "Content": -m speaks in male voice and -w speaks in female voice. Default voice is male. - """ - - if not await validate_voice_client(ctx): - raise UsageError( - __( - "You are not in any voice channel. Please join a voice channel to use TTS." - ) - ) - - headers = { - "Content-Type": "application/xml", - "Authorization": "KakaoAK " + CONFIG.kakaoToken, - } - - if args[0][0] == "-": - voice = args[0] - string = " ".join(args[1:]) - if voice.upper() == "-M": - vs = "MAN_DIALOG_BRIGHT" - elif voice.upper() == "-W": - vs = "WOMAN_DIALOG_BRIGHT" - else: - raise UsageError( - __( - "Invalid parameter. For more information, type `{commandPrefix}help tts`." - ).format(commandPrefix=CONFIG.commandPrefix) - ) - else: - string = " ".join(args) - vs = "MAN_DIALOG_BRIGHT" - - data = '{}'.format(vs, string).encode( - "utf-8" - ) - - async with aiohttp.ClientSession(headers=headers, raise_for_status=True) as session: - async with session.post( - "https://kakaoi-newtone-openapi.kakao.com/v1/synthesize", data=data - ) as r: - mp3 = io.BytesIO(await r.read()) - - ctx.voice_client.play(FFmpegPCMAudio(mp3.read(), pipe=True)) - - await ctx.message.delete() - embed = MsgFormatter.get( - ctx, - string, - __("[MK Bot]({url}) said on behalf of {author}").format( - url="https://github.com/mgylabs/mkbot", author=ctx.author.mention - ), - ) - - embed.set_author( - name=ctx.message.author.name, icon_url=ctx.message.author.avatar.url - ) - await ctx.send(embed=embed) - - -async def setup(bot: commands.Bot): - bot.add_command(tts) diff --git a/src/bot/core/controllers/discord/voice.py b/src/bot/core/controllers/discord/voice.py new file mode 100644 index 00000000..83e7dbf1 --- /dev/null +++ b/src/bot/core/controllers/discord/voice.py @@ -0,0 +1,64 @@ +import io + +import discord +from discord.ext import commands +from gtts import gTTS + +from mgylabs.i18n import __ +from mgylabs.utils import logger + +from .utils.exceptions import UsageError +from .utils.feature import Feature +from .utils.MGCert import Level, MGCertificate +from .utils.MsgFormat import MsgFormatter +from .utils.voice import validate_voice_client + +log = logger.get_logger(__name__) + + +@commands.hybrid_command() +@MGCertificate.verify(level=Level.TRUSTED_USERS) +@Feature.Experiment() +async def voice(ctx: commands.Context, *, content: str): + """ + TTS voice available + {commandPrefix}tts "Content" : Says the content in "content". You do not have to use Quotation marks even if there are spaces included in content. + + *Example* + {commandPrefix}tts "Content" + """ + + if not await validate_voice_client(ctx): + raise UsageError( + __( + "You are not in any voice channel. Please join a voice channel to use TTS." + ) + ) + + mp3 = io.BytesIO() + gtts = gTTS(content, lang="ko") + gtts.write_to_fp(mp3) + mp3.seek(0) + + if not ctx.interaction: + await ctx.message.delete() + + embed = MsgFormatter.get( + ctx, + content, + __("[MK Bot]({url}) said on behalf of {author}").format( + url="https://github.com/mgylabs/mkbot", author=ctx.author.mention + ), + show_req_user=False, + ) + + embed.set_author( + name=ctx.message.author.display_name, icon_url=ctx.message.author.avatar.url + ) + await ctx.send(embed=embed) + + ctx.voice_client.play(discord.FFmpegPCMAudio(mp3, pipe=True)) + + +async def setup(bot: commands.Bot): + bot.add_command(voice) diff --git a/src/bot/discord_ext.py b/src/bot/discord_ext.py index 7a2f438b..172649eb 100644 --- a/src/bot/discord_ext.py +++ b/src/bot/discord_ext.py @@ -8,7 +8,7 @@ "core.controllers.discord.leave", "core.controllers.discord.logout", "core.controllers.discord.ping", - # "core.controllers.discord.tts", + "core.controllers.discord.voice", "core.controllers.discord.poll", "core.controllers.discord.roulette", "core.controllers.discord.dice",