初めに
今回はVoiceVoxとPycordを使って読み上げボットを作っていきます。
前回の記事を読んでいる前提ですので、環境構築などは省略させていただきます。
誤字脱字、内容が一部不正確な可能性があります。ご了承ください。
今回作成するプログラムはFFmpegをインストールしていないと動作しません。
FFmpegのインストール方法とパスの通し方は省略させていただきます。
VoiceVox
まずはVoiceVoxを以下のリンクからインストールしてください。
VoiceVoxの実態はHTTPサーバーを起動するソフトなので、リクエストを送ることで音声合成したデータを取得することができます。
まずは確認してみましょう。
VoiceVoxを起動してブラウザで次のURLに移動してください。
ここではVoiceVoxのドキュメントを確認することができます。
/audio query
を確認してみましょう。
見てみるとParameters
という欄があると思います。
ここではリクエストを送る際のパラメータを確認することができます。
赤字で* required
と書かれているところは必須のパラメータになっていて、これを設定し忘れると正常にレスポンスが返ってきません。
次は実際にプログラムを書いて確認していきます。
リクエストを送ってみる
環境構築
まずはコマンドプロンプトを起動して以下のコードを実行しましょう。
$ pip install requests
これはPythonでHTTP通信を可能にする外部ライブライをインストールしています。
コード
リクエストを送って結果を確認するコードです。
実行して<Response [200]>
と表示されるとリクエストを送ることに成功しています。
import requests
def post_audio_query(text: str, speaker: int):
URL = "http://127.0.0.1:50021/audio_query"
Parameters = {
"text": text,
"speaker": speaker
}
response = requests.post(URL, params=Parameters)
return response
print(post_audio_query("おはよう", 1))
解説
上から説明していきます。
import requests
ここでは先ほどインストールしたライブラリをインポートしています。
PythonでHTTP通信をするために使われるライブラリなのでリクエストを送るためには必須となります。
def post_audio_query(text: str, speaker: int):
ここでは関数名と引数を指定しています。
関数名は何でもいいですがわかりやすいのが良いかと思われます。
引数は先ほどdocsで確認した必須パラメータを指定できるようにしています。
URL = "http://127.0.0.1:50021/audio_query"
Parameters = {
"text": text,
"speaker": speaker
}
ここは変数にURLとパラメータを設定しています。
URLはaudio_query
にリクエストを送りたいなら
http://127.0.0.1:50021/audio_query
音声合成のリクエストを送りたいなら
http://127.0.0.1:50021/synthesis
のように変化します。
ただ、URLを変更すると必須パラメータが変わりますのでdocsからしっかりと確認してください。
Parameters
のところは辞書と呼ばれる形で、{} のなかにキーと値をセットで指定します。
今回は必須パラメータであるtext
とspeaker
に引数を指定しているのが分かると思います。
response = requests.post(URL, params=Parameters)
return response
ここでは実際にリクエストを送ってレスポンスを取得しています。
リクエストを送った結果をresponse
に代入して、結果をreturn
で返しています。
print(post_audio_query("おはよう", 1))
ここでは関数を実行して帰ってきた結果をprint
でターミナルに表示しています。
もし<Response [200]>
以外が表示されると、どこかしら間違えているので確認してください。
Botをボイスチャットに接続する。
環境構築
前回の記事では音声サポートなしのpycordをインストールしましたが、今回はボイスチャットを使うということで音声サポート有にアップデートします。
$ pip install -U py-cord[voice]
Botの設定
デベロッパーポータルのBotの権限の設定をしていきます。
今回開発するのは読み上げボットなので、テキストチャンネルにアクセスして文字列を取得する必要があります。
前回の設定のままだとメッセージを取得することができないので、トークンを取得したページの下のほうにあるPrivileged Gateway Intentsから以下の画像のように機能を有効にしてください。
設定を保存したら完了です。
コード
ボットをボイスチャンネルに接続していきます。
コマンドは/join
にします。
import discord
Token = 'トークン'
intents = discord.Intents.default()
intents.voice_states = True
bot = discord.Bot(intents=intents)
voice_client = None
@bot.event
async def on_ready():
print("起動しました。")
@bot.slash_command()
async def join(ctx):
global voice_client
user = ctx.author
if not user.voice:
await ctx.respond("ボイスチャンネルに接続していません。")
return
await ctx.respond("ボイスチャンネルに接続しました。")
voice_channel = user.voice.channel
voice_client = await voice_channel.connect()
@bot.slash_command()
async def left(ctx):
global voice_client
if not voice_client:
await ctx.respond("ボイスチャンネルに接続していません。")
return
await ctx.respond("切断しました。")
await voice_client.disconnect()
bot.run(Token)
解説
インテント
前回説明した部分は省略していきます。
intents = discord.Intents.default()
intents.voice_states = True
bot = discord.Bot(intents=intents)
今回はインテントというものを指定しています。
インテントはDiscordからイベントを受け取るかどうか指定するもので、文章を取得する場合は
intents.message_content = True
といった感じになります。
今回指定しているvoice_states
というのは、サーバーのボイスチャットの状態を取得するためのものです。
グローバル変数
voice_client = None
コマンドの指定などは前回と同じですが、今回はグローバル変数というものを扱っています。
グローバル変数とはプログラム全体で使うことできる変数のことで、プログラムのどこからでもアクセスすることが可能です。
また、関数内で扱うときには以下のように指定しなければなりません。
global voice_client
なぜかというと、関数内ではグローバル変数とローカル変数は区別がつかないからです。
そのため、グローバル変数を指定する文を書かないと関数内で新しくローカル変数が作成されてしまいます。
voice_client = await voice_channel.connect()
ここで記述している、connect()
は実行時にVOICECLIENT
という型を返す特性があります。
その帰ってきた型を、グローバル変数に代入しているというわけです。
実行
実際に実行してみましょう。
もし正常に動作しているならば/join
と書いたときにボットばボイスチャンネルに接続してきます。
正常に実行できているならば画像のように動作します。
/left
と入力することで、ボットをボイスチャンネルから切断することができるので、そちらも同じように確認しておいてください。
読み上げボットを作る。
これまで、読み上げボットを作るために必要なプログラムの説明をしてきたので実際に1つにしていきます。
コード
import discord
import requests
import tempfile
Token = 'トークン'
intents = discord.Intents.default()
intents.voice_states = True
intents.message_content = True
bot = discord.Bot(intents=intents)
voice_client = None
text_channel = None
def post_audio_query(text: str, speaker: int):
URL = "http://127.0.0.1:50021/audio_query"
Parameters = {
"text": text,
"speaker": speaker
}
response = requests.post(URL, params=Parameters)
return response.json()
def post_synthesis(json: dict, speaker: int):
URL = "http://127.0.0.1:50021/synthesis"
Parameters = {
"speaker": speaker
}
response = requests.post(URL,json=json, params=Parameters )
return response.content
def save_tempfile(text: str, speaker: int):
json = post_audio_query(text, speaker)
data = post_synthesis(json, speaker)
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as wf:
wf.write(data)
wf.close()
path = wf.name
return path
@bot.event
async def on_ready():
print("起動しました。")
@bot.event
async def on_message(message):
if message.author.bot == True:
return
global text_channel
global voice_client
channel = message.channel
if not text_channel == channel:
return
path = save_tempfile(message.content, 1)
voice_client.play(discord.FFmpegPCMAudio(path))
@bot.slash_command()
async def join(ctx):
global voice_client
global text_channel
user = ctx.author
if not user.voice:
await ctx.respond("ボイスチャンネルに接続していません。")
return
await ctx.respond("ボイスチャンネルに接続しました。")
text_channel = ctx.channel
voice_channel = user.voice.channel
voice_client = await voice_channel.connect()
path = save_tempfile("接続しました", 1)
voice_client.play(discord.FFmpegPCMAudio(path))
@bot.slash_command()
async def left(ctx):
global voice_client
global text_channel
if not voice_client:
await ctx.respond("ボイスチャンネルに接続していません。")
return
await ctx.respond("切断しました。")
await voice_client.disconnect()
voice_client = None
text_channel = None
bot.run(Token)
解説
import tempfile
今回はtempfileというライブラリを使用しています。これは、ファイルを一時保存するためのライブラリでVoiceVoxを通して作成したwavファイルを一時保存し、discordで再生しているというわけです。
intents.message_content = True
インテントは先ほど紹介した通りです。ただ、今回はメッセージの内容を取得してもらうのでmessage_content
をTrueにしています。
def post_audio_query(text: str, speaker: int):
URL = "http://127.0.0.1:50021/audio_query"
Parameters = {
"text": text,
"speaker": speaker
}
response = requests.post(URL, params=Parameters)
return response.json()
def post_synthesis(json: dict, speaker: int):
URL = "http://127.0.0.1:50021/synthesis"
Parameters = {
"speaker": speaker
}
response = requests.post(URL,json=json, params=Parameters )
return response.content
def save_tempfile(text: str, speaker: int):
json = post_audio_query(text, speaker)
data = post_synthesis(json, speaker)
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as wf:
wf.write(data)
wf.close()
path = wf.name
return path
ここでVoiceVoxから音声データを取得しています。save_tempfile
関数は上2つの関数を実行して、その結果をtempfileに保存してそのファイルのパスを返しています。
@bot.event
async def on_message(message):
on_message
はメッセージが送信されたときに呼ばれるイベントです。
if message.author.bot == True:
return
この文章は発言者がボットかどうかを判別し、もしボットであった場合は処理を終了しています。on_message
イベントはボット自身が文章を更新したときも呼ばれるので、この処理がなければ無限ループが発生してしまいます。
channel = message.channel
if not text_channel == channel:
return
ここではメッセージチャンネルがコマンドが実行されたチャンネルと同じかどうかを判別しています。
text_channelの初期値はNoneなので、/join
を実行する前や/left
を実行した後はこれ以上処理が進むことがありません。
path = save_tempfile(message.content, 1)
voice_client.play(discord.FFmpegPCMAudio(path))
ここで音声ファイルのパスを取得してdiscordで再生しています。
何度も言いますがFFmpegをインストールしていないの動作しないので気を付けてください。
終わりに
お疲れさまでした。
後半は解説が駆け足になってしまったかもしれません。
メッセージが取得できないなどのことがあればインテント関連だと思うので、読み上げボットを作成するときだけに限らずインテントの指定には気を付けてください。