slackのメッセージをVOICEVOXのAPIを使って読み上げてみるテスト

2023.01.12

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

VOICEVOXという音声合成のソフトウェアの存在を教えてもらったんですが、無料で使えて、なおかつAPIが用意されていて外部からも実行できそうなので簡単なアプリを作って試してみようと思います。

環境

タイトルにもある通り、今回はslackで投稿されたメッセージを読み上げるテストを行います。

  • ローカル環境をセットアップして、Slackワークスペースからのメッセージをリッスンして応答するSlackアプリを作成
  • VOICEVOXをローカル環境にインストール

が必要になってきます。 

slackアプリ

※ 詳細な内容については下記に示すチュートリアルに書かれているのでここではかなり省いています

slackではボットを作成して、Slack ワークスペースで発生するイベントをリッスンすることができます。

アプリの作成に関してですが、

上記ページの内容をそのまま使いました。

コード例)

slackでhelloというメッセージが含まれていたら反応するボット

const { App } = require('@slack/bolt');

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  socketMode: true,
  appToken: process.env.SLACK_APP_TOKEN,
  port: process.env.PORT || 3000
});

// Listens to incoming messages that contain "hello"
app.message('hello', async ({ message, say }) => {
  // say() sends a message to the channel where the event was triggered
  await say(`Hey there <@${message.user}>!`);
});

(async () => {
  // Start your app
  await app.start();

  console.log('⚡️ Bolt app is running!');
})();

messageは、チャンネルにメッセージが送信されたときに発火するAPIです。

https://api.slack.com/events/message

VOICEVOX

VOICEVOXは公式サイトからダウンロードすることができます。

インストールして起動するとGUIが立ち上がりますが、この段階でHTTPサーバも立ち上がります。

http://localhost:50021/docs にアクセスすると、openapiのドキュメントが参照可能です。

VOICEVOXで音声を流す流れとしては、

  1. 読み上げるメッセージを作成
  2. APIで音声合成用のクエリ作成 (/audio_query)
  3. APIで音声合成をする (/synthesis)
  4. 再生

となっています。

※ アクセントを変えたり、音を高くしたり等もできますが、今回はやりませんでした。

APIの実行や音声の再生に使うライブラリは以下にしてみました。

npm install node-fetch
npm install web-audio-api
npm install speaker
const fetch = require('node-fetch');
const audioCtx = require('web-audio-api').AudioContext
const context = new audioCtx()
const Speaker = require('speaker')

1. 読み上げるメッセージを作成

slackで送信されたメッセージを使います。

作成したアプリのコードを修正します。

app.message で待ち受けている箇所を以下のようにしてみました

app.message(async ({ message }) => {
  // 読み上げたいメッセージをslackから取得
  const msg = message.text
});

2, APIで音声合成用のクエリ作成

1で作成したメッセージを使って、音声合成用のクエリ作成を作成します。

app.message(async ({ message }) => {
  // 読み上げたいメッセージをslackから取得
  const msg = message.text

  // クエリ作成
  const audio_query_response = await fetch(
        process.env.VOICEVOX_ENDPOINT + "/audio_query?text=" + msg + "&speaker=" + process.env.VOICEVOX_SPEAKER,
        {
            method: 'post',
            headers: {'Content-Type': 'application/json'}
        }
    ) 
  const audio_query_json = await audio_query_response.json()
});

textパラメーターには読み上げたいメッサージ、 speakerには音声合成を行うキャラクターの音声IDを指定します。

※ speakerの情報に関してですが、/speakersエンドポイントにアクセスすると取得することができます。

3. APIで音声合成をする

2で返ってきたクエリを使って音声合成を行います.

app.message(async ({ message }) => {
  // 読み上げたいメッセージをslackから取得
  const msg = message.text

  // クエリ作成
  const audio_query_response = await fetch(
        process.env.VOICEVOX_ENDPOINT + "/audio_query?text=" + msg + "&speaker=" + process.env.VOICEVOX_SPEAKER,
        {
            method: 'post',
            headers: {'Content-Type': 'application/json'}
        }
    ) 
  const audio_query_json = await audio_query_response.json()

  // 音声合成
  const synthesis_response = await fetch(
        process.env.VOICEVOX_ENDPOINT + "/synthesis?speaker=" + process.env.VOICEVOX_SPEAKER,
        {
            method: 'post',
            body: JSON.stringify(audio_query_json),
            responseType: 'arraybuffer',
            headers: {"accept": "audio/wav", 'Content-Type': 'application/json'},
        }
    ) 
  const synthesis_response_buf = await synthesis_response.arrayBuffer() 
});

/synthesis エンドポイントは、オーディオファイル(audio/wav)を返却します。

この返り値を使ってwavファイルとしてローカルに保存することもできます

const buf = Buffer.from(synthesis_response_buf)
fs.writeFileSync('voice.wav', buf)

今回は、Bufferから再生するためwavファイルにするのではなくarrayBufferでバッファーを取得しています。

4. 再生

音声合成をして返ってきたバッファーを再生してみます。

web-audio-api は生成したサウンドを再生しません となっています。

公式のレポジトリに書かれている通り、AudioContextを作成したら、その出力ストリームを設定します。

app.message(async ({ message }) => {
  // 読み上げたいメッセージをslackから取得
  const msg = message.text

  // クエリ作成
  const audio_query_response = await fetch(
        process.env.VOICEVOX_ENDPOINT + "/audio_query?text=" + msg + "&speaker=" + process.env.VOICEVOX_SPEAKER,
        {
            method: 'post',
            headers: {'Content-Type': 'application/json'}
        }
    ) 
  const audio_query_json = await audio_query_response.json()

  // 音声合成
  const synthesis_response = await fetch(
        process.env.VOICEVOX_ENDPOINT + "/synthesis?speaker=" + process.env.VOICEVOX_SPEAKER,
        {
            method: 'post',
            body: JSON.stringify(audio_query_json),
            responseType: 'arraybuffer',
            headers: {"accept": "audio/wav", 'Content-Type': 'application/json'},
        }
    ) 
  const synthesis_response_buf = await synthesis_response.arrayBuffer() 

  // 再生
  context.outStream = new Speaker({
        channels: context.format.numberOfChannels,
        bitDepth: context.format.bitDepth,
        sampleRate: context.sampleRate
  })

  context.decodeAudioData(synthesis_response_buf, function(audioBuffer) {
        var bufferNode = context.createBufferSource()
        bufferNode.connect(context.destination)
        bufferNode.buffer = audioBuffer
        //bufferNode.loop = true
        bufferNode.start(0)
  })
});

AudioContextのcreateBufferSourceというメソッドを使ってオーディオバッファを再生できるようにしました

実行テストしてみる

作成したslackアプリを実行し、slackのイベントを受信できる状態にしてテストしてみます

node {path_to_js_file}

ソケットモードで立ち上がりました。

この状態でボットゆーざーを招待したチャンネルでメッセージを送信すると、設定したスピーカーが喋ってくれるはずです。

※ console.logでメッセージの受信を出力した時のキャプチャ

実際に喋らせた時の様子を録画してみました。

ずんだもんがしゃべってくれてます!!

slackとweb-audio-apiのサンプルコードを丸ごと使っただけでだいぶ駆け足になってしまいました。 投稿されてから再生するまでのレイテンシー、大量にメッセージが送信された時の挙動などは調べ切れてはいませんが、 slackのチャンネルに投稿されたメッセージを読み上げさせることができました。

社内のイベントのスレで使ってみたら面白いかも?