2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Pycord 読み上げBot開発

Posted at

初めに

今回は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のところは辞書と呼ばれる形で、{} のなかにキーと値をセットで指定します。

今回は必須パラメータであるtextspeakerに引数を指定しているのが分かると思います。

    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から以下の画像のように機能を有効にしてください。
image.png

設定を保存したら完了です。

コード

ボットをボイスチャンネルに接続していきます。
コマンドは/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と書いたときにボットばボイスチャンネルに接続してきます。

Discord_1Kgxhs5JCs.png

Discord_kxoi5ADrUA.png
正常に実行できているならば画像のように動作します。
/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をインストールしていないの動作しないので気を付けてください。

終わりに

お疲れさまでした。
後半は解説が駆け足になってしまったかもしれません。
メッセージが取得できないなどのことがあればインテント関連だと思うので、読み上げボットを作成するときだけに限らずインテントの指定には気を付けてください。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?