VOICEVOX(Docker)とNode.js(Express)を用いた音声ファイルの生成

# 概要
[VOICEVOX(Docker)](https://hub.docker.com/r/voicevox/voicevox_engine)とNode.jsの[Express](https://expressjs.com/)を使って、Express側にリクエストが来たら声を合成してもらう仕組みを構築します。
かなり大雑把ですが、イメージとしては下記の図の通りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017964/ecc9d25a-65ad-4da6-b9b7-c386552d80ee.png)

現時点では一体何をしたいのか分からない構成ですが、これらをフロントエンドと連携させたり、ChatGPTとExpressを連携させてChatGPTが生成したテキストを音声合成するなどの応用を想定しています。

# 前提
Dockerがインストールされていること

# Node.jsで使用するフレームワーク・ライブラリ
- Express
- [Axios](https://www.npmjs.com/package/axios) → VOICEVOXにリクエストを送るのに使用します。
- [fs](https://www.npmjs.com/package/fs) → ファイル操作に使用します。

# docker-compose.ymlの準備(VOICEVOX側)
docker-composeですぐにVOICEVOXが立ち上がるようdocker-compose.ymlを作成します。
Githubの[Readme](https://github.com/VOICEVOX/voicevox_engine)に記載されている以下のコマンドをもとにdokcer-compose.ymlへ落としこんでいきます。今回はCPU版を使用しようかと思います。

- 元コード
```
docker run --rm -it -p '127.0.0.1:50021:50021' voicevox/voicevox_engine:cpu-latest
```

- コマンドからdokcer-compose.ymlへ置き換えたもの
```docker:docker-compose.yml
version: "3.8"
services:
  voicevox:
    image: "voicevox/voicevox_engine:cpu-latest"
    container_name: voicevox
    ports:
      - "50021:50021"
    stdin_open: true 
    tty: true
    restart: always
```
voicevoxを立ち上げる時は以下のコマンドで実行します。
```
docker-compose up -d
```

docker-composeの概要や説明については割愛いたします。
下記の方の記事が分かりやすかったので、ご参考ください。
https://qiita.com/gon0821/items/77369def082745d19c38

# Node.js側の準備
以下のようなファイル構成で作成を進めます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017964/9d217abd-5d9d-47f9-aec5-9fd2e1fb6455.png)

Node.jsのプロジェクトフォルダを作成します。
```
npm init
```
必要なフレームワーク・ライブラリのインストールします。
```
npm i express fs axios
```
### コード
```javascript:app.js
const express = require("express");
const app = express();
const voicevoxApi = require("./voicevoxAPI");

const port = 8000;

//リクエストボディにあるJsonコンテンツを解析するのに必要
app.use(express.json());

app.post("/synthesize", async (req, res) => {
  try {
    const voiceText = req.body.text;
    const speakerID = req.body.speaker;
    await voicevoxApi(voiceText, speakerID)
    res.status(200).send("success");
  } catch (error) {
    res.status(500).send("error");
  }
})

app.listen(port, () => {
  console.log(`Listening at http://localhost:${port}`);
})
```
```javascript:voicevoxAPI.js
const fs = require("fs");
const axios = require("axios");
const voicevoxURL = "http://localhost:50021"
const saveAudioFilePath = "./audio.wav"

const generateAudioFile = async (voiceText, speakerID) => {
  try {
    const queryJson = await requestQueryJsonData(voiceText, speakerID);
    const audioData = await requestAudioData(queryJson, speakerID);
    fs.writeFileSync(saveAudioFilePath, audioData);
    console.log('音声ファイルが生成されました');
  } catch (error) {
    console.log('音声ファイルが生成に失敗しました', error);
    
  }
}

const requestQueryJsonData = async (voiceText, speakerID) => {
  try {
    const params = new URLSearchParams({
      text: voiceText,
      speaker: speakerID
    });

    const response = await axios({
      method: "post",
      url: `${voicevoxURL}/audio_query?${params.toString()}`
    });

    return JSON.stringify(response.data);
  } catch (error) {
    console.log(error);
  }
}

const requestAudioData = async (queryJson, speakerID) => {
  try {
    const response = await axios({
      method: 'post',
      url: `${voicevoxURL}/synthesis`,
      params: {
        speaker: speakerID
      },
      headers: {
        'Content-Type': 'application/json'
      },
      data: queryJson,
      responseType: 'arraybuffer'
    });

    return response.data;
  } catch (error) {
    console.log(error);
  }
}

module.exports = generateAudioFile;
```

### 補足
VOICEVOXの[APIドキュメント](https://voicevox.github.io/voicevox_engine/api/#tag/%E9%9F%B3%E5%A3%B0%E5%90%88%E6%88%90)を参考に、音声合成に必要なクエリを作ってもらい、そのクエリをもとに音声合成のリクエストを送っている流れとなります。
そのため、VOICEVOXのサーバには音声合成までに２回リクエストを送っていることになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017964/ffa9a1a0-0482-4343-8aa3-2a715c818dee.png)

# 実際に使ってみる
VOICEVOX(Docker)とNode.js(Express)をそれぞれ立ち上げます。
準備したdokcer-compose.ymlがあるフォルダまで移動し、下記コマンドを実行します。
```
docker-compose up -d
```

準備したnode.jsのフォルダに移動し、下記コマンドを実行します。
```
node .\app.js
```

立ち上げたExpressに対してリクエストを送るわけですが、検証としてVScodeにインストールできる[Thunder Client for VS Code](https://www.thunderclient.com/)を使っていきます。

New Requestから下記のようにリクエストを作ります。
Headersは特にいじっていません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017964/d0e699df-532d-4d2a-aeff-23d1f465050e.png)

Sendを押してしばらくすると、app.jsと同じ階層に音声ファイルが生成されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4017964/0cc4b234-1d7c-4a7b-ab42-231eec68eca2.png)

# まとめ
VOICEVOX(Docker)とNode.jsのExpressを使って音声ファイルを生成することができました。
ただ、DockerとExpressをそれぞれ立ち上げるのは面倒なので、Express側をコンテナにしてVOICEVOXのdocker-composeにまとめてしまうのもありかなと感じました。
音声ファイルについてはファイル名が固定のため、生成時に上書きされてしまう点があります。そのため、音声ファイルの管理方法についても検討する必要がありそうです。

# 参考
https://qiita.com/ahogemarumetano/items/554e95faf8749fac8631

https://github.com/VOICEVOX/voicevox_engine

https://developer.mozilla.org/ja/docs/Web/API/URLSearchParams

https://qiita.com/hirochan/items/e63d74cb70f0b97889fc