├── .gitignore ├── private_sample.py ├── Item.py ├── requirements.txt ├── README.md ├── text2wav.py ├── LICENSE ├── main.py ├── ui.py └── bot.py /.gitignore: -------------------------------------------------------------------------------- 1 | private.py 2 | *.wav 3 | __pycache__/ -------------------------------------------------------------------------------- /private_sample.py: -------------------------------------------------------------------------------- 1 | token = "your_token" 2 | voice_channel_id = 1234 3 | -------------------------------------------------------------------------------- /Item.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | import discord 3 | 4 | @dataclass 5 | class Item: 6 | text_channel: discord.TextChannel = None 7 | voice_channel: discord.VoiceChannel = None 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp>=3.7.4 2 | async-timeout==3.0.1 3 | attrs==21.2.0 4 | cffi==1.14.5 5 | chardet==4.0.0 6 | idna==3.2 7 | multidict==5.1.0 8 | pycparser==2.20 9 | PyNaCl==1.4.0 10 | pyvcroid2==0.21 11 | six==1.16.0 12 | typing-extensions==3.10.0.0 13 | yarl==1.6.3 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 導入方法 2 | - discordのbotを作成しtokenを取得後、利用したいサーバーに招待する 3 | - private_sample.pyを参考に`private.py`作成し、tokenとvoice_channel_idを定義する 4 | - tokenは[ここ](https://discord.com/developers/applications)からMy Applications -> Botで確認できる 5 | - voice_channel_idはdiscordのボイスチャンネルを右クリック -> IDをコピー 6 | - 必要なライブラリをインストール 7 | - pip install git+https://github.com/Nkyoku/pyvcroid2.git 8 | - pip install git+https://github.com/Rapptz/discord.py.git 9 | - 2.0.0a3267+gd30fea5bで動作確認済み 10 | - pip install -r requirements.txt 11 | - `python main.py` 12 | -------------------------------------------------------------------------------- /text2wav.py: -------------------------------------------------------------------------------- 1 | import pyvcroid2 2 | 3 | def main(): 4 | vc = pyvcroid2.VcRoid2() 5 | lang_list = vc.listLanguages() 6 | if "standard" in lang_list: 7 | vc.loadLanguage("standard") 8 | else: 9 | raise Exception("No language library") 10 | 11 | voice_list = vc.listVoices() 12 | if 0 < len(voice_list): 13 | vc.loadVoice(voice_list[0]) 14 | else: 15 | raise Exception("No voice library") 16 | vc.param.volume = 1.0 17 | vc.param.speed = 1.0 18 | vc.param.pitch = 1.0 19 | vc.param.emphasis = 1.0 20 | vc.param.pauseMiddle = 80 21 | vc.param.pauseLong = 100 22 | vc.param.pauseSentence = 200 23 | vc.param.masterVolume = 1.0 24 | 25 | text2wav(vc, "おはよう") 26 | 27 | def text2wav(vc, text): 28 | filename = "temp.wav" 29 | speech, tts_events = vc.textToSpeech(text) 30 | 31 | with open(filename, mode="wb") as f: 32 | f.write(speech) 33 | return filename 34 | 35 | 36 | if __name__ == "__main__": 37 | main() 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 nohararc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import pyvcroid2 2 | from discord.ext import commands 3 | 4 | import private 5 | from bot import VoiceroidTTSBot 6 | 7 | 8 | def main(): 9 | # voiceroid2 client 10 | vc = pyvcroid2.VcRoid2() 11 | lang_list = vc.listLanguages() 12 | if "standard" in lang_list: 13 | vc.loadLanguage("standard") 14 | else: 15 | raise Exception("No language library") 16 | 17 | voice_list = vc.listVoices() 18 | if 0 < len(voice_list): 19 | vc.loadVoice(voice_list[0]) 20 | else: 21 | raise Exception("No voice library") 22 | vc.param.volume = 1.0 23 | vc.param.speed = 1.0 24 | vc.param.pitch = 1.0 25 | vc.param.emphasis = 1.0 26 | vc.param.pauseMiddle = 80 27 | vc.param.pauseLong = 100 28 | vc.param.pauseSentence = 200 29 | vc.param.masterVolume = 1.0 30 | 31 | token = private.token 32 | bot = commands.Bot(command_prefix=commands.when_mentioned_or(".")) 33 | 34 | @bot.event 35 | async def on_ready(): 36 | print("ready") 37 | 38 | bot.add_cog(VoiceroidTTSBot(bot, vc)) 39 | bot.run(token) 40 | 41 | 42 | if __name__ == "__main__": 43 | main() 44 | -------------------------------------------------------------------------------- /ui.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from typing import List 3 | 4 | from Item import Item 5 | 6 | 7 | class TextChannelButton(discord.ui.Button["SelectTextChannel"]): 8 | def __init__(self, label: str, text_channel: discord.TextChannel): 9 | super().__init__(style=discord.ButtonStyle.secondary, label=None) 10 | self.text_channel = text_channel 11 | self.label = label 12 | 13 | async def callback(self, interaction: discord.Interaction): 14 | content = f"{self.label}を読み上げます" 15 | for child in self.view.children: 16 | child.disabled = True 17 | self.view.stop() 18 | 19 | Item.text_channel = self.text_channel 20 | 21 | await interaction.response.edit_message(content=content, view=self.view) 22 | 23 | 24 | class SelectTextChannel(discord.ui.View): 25 | def __init__(self, text_channels: List[discord.TextChannel]): 26 | super().__init__() 27 | for tc in text_channels: 28 | self.add_item(TextChannelButton(label=tc.name, text_channel=tc)) 29 | 30 | 31 | class VoiceChannelButton(discord.ui.Button["SelectVoiceChannel"]): 32 | def __init__(self, label: str, voice_channel: discord.VoiceChannel): 33 | super().__init__(style=discord.ButtonStyle.secondary, label=None) 34 | self.voice_channel = voice_channel 35 | self.label = label 36 | 37 | async def callback(self, interaction: discord.Interaction): 38 | content = f"{self.label}で読み上げます" 39 | for child in self.view.children: 40 | child.disabled = True 41 | self.view.stop() 42 | 43 | Item.voice_channel = self.voice_channel 44 | 45 | await interaction.response.edit_message(content=content, view=self.view) 46 | 47 | 48 | class SelectVoiceChannel(discord.ui.View): 49 | def __init__(self, voice_channels: List[discord.VoiceChannel]): 50 | super().__init__() 51 | for vc in voice_channels: 52 | self.add_item(VoiceChannelButton(label=vc.name, voice_channel=vc)) -------------------------------------------------------------------------------- /bot.py: -------------------------------------------------------------------------------- 1 | from ui import SelectTextChannel, SelectVoiceChannel 2 | import discord 3 | import asyncio 4 | 5 | from discord.ext import commands 6 | from text2wav import text2wav 7 | from Item import Item 8 | import pyvcroid2 9 | 10 | class VoiceroidTTSBot(commands.Cog): 11 | def __init__(self, bot, vc): 12 | self.bot: commands.Bot = bot 13 | self.vcroid: pyvcroid2.VcRoid2 = vc 14 | self.voice_channel: discord.VoiceChannel = None 15 | self.voice_client: discord.VoiceClient = None 16 | 17 | @commands.Cog.listener() 18 | async def on_message(self, message: discord.Message): 19 | if message.author.bot: # bot自身のメッセージは何もしない 20 | return 21 | if Item.text_channel is None: # 初期設定 22 | # text channel選択画面出す 23 | text_channels = message.guild.text_channels 24 | await message.channel.send( 25 | "読み上げるテキストチャンネルを選んでください", 26 | view=SelectTextChannel(text_channels=text_channels[:5]) 27 | ) 28 | 29 | # voice channel選択画面出す 30 | voice_channels = message.guild.voice_channels 31 | await message.channel.send( 32 | "ボイスチャンネルを選んでください", 33 | view=SelectVoiceChannel(voice_channels=voice_channels[:5]) 34 | ) 35 | else: 36 | # ここに読み上げの処理を書く 37 | if not self.voice_client: 38 | self.voice_client = await Item.voice_channel.connect() 39 | if message.channel == Item.text_channel: 40 | # 喋っている途中は待つ 41 | while self.voice_client.is_playing(): 42 | await asyncio.sleep(0.1) 43 | print(message.content) 44 | source = discord.FFmpegPCMAudio(text2wav(self.vcroid, message.content)) 45 | self.voice_client.play(source) 46 | 47 | @commands.command() 48 | async def d(self, ctx: commands.Context): 49 | print(f"debug: {Item.text_channel, Item.voice_channel}") 50 | --------------------------------------------------------------------------------