VOICEVOX ENGINE（API）のスループット検証

こんにちは。最近、ChatGPT×VRM 3Dモデル×VOICEVOXで性格・見た目・声をカスタマイズできるAIアシスタントアプリを開発・運営しているうえぞうと申します。

今回、このアプリの3大要素の一つである声を担うVOICEVOXのAPIサーバーの運用が結構大変だったので、その知見を還元することで少しでもコミュニティーに貢献したいと思いこの記事を書きました。

諸々の工夫についてはまた別途記事化するとして、本記事では企画段階で知っておくべきスループットの特徴についてシェアしたいと思います。

# 検証内容

まずは手元のPC（MacBook Pro 2020 / Core i7 論理8コア / 32GB RAM）で多重度の検証、その後一般的なAWSのサーバーでその処理時間を計測してみます。みなさんがVOICEVOXを利用したサービスを展開される際、一つの目安にしていただけるかと思います。

# 環境準備

はじめに、VOICEVOXのMac版をインストールします。以下からインストーラーをダウンロードしてインストーラーでインストールするのみです。とても簡単ですね👍
https://voicevox.hiroshiba.jp

続いて、VOICEVOX ENGINE（REST API）を起動します。インストール先のディレクトリに移動して、以下のコマンドを実行します。拍子抜けするほど簡単ですね👍

```bash
$ ./run
Warning: cpu_num_threads is set to 0. ( The library leaves the decision to the synthesis runtime )
INFO:     Started server process [49315]
INFO:     Waiting for application startup.
reading /Users/****/Library/Application Support/voicevox-engine/tmpyxvs819f ... 62
emitting double-array: 100% |###########################################| 

done!
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:50021 (Press CTRL+C to quit)
```

上記のように表示されたら、http://127.0.0.1:50021/docs にアクセスして、APIドキュメントが表示されたら起動成功です。検証に移っていきましょう🐕

# 多重度の検証

まず、クライアントプログラムとして10多重で音声合成リクエストを投げるクライアントプログラムを作ります。また、比較のため10多重ではなく逐次的に10回のリクエストも投げられるようにします。Pythonだとこんな感じでしょうか。オマケで音声合成用クエリー生成（`/audio_query`）も投げられるようにしています。他のエンドポイントも試したい方は参考にしてください。

```python:client.py
import asyncio
import requests
from time import time

count = 10

def audio_query(i, params):
    print(f"[{i}] start")
    start_time = time()
    requests.post("http://localhost:50021/audio_query", params=params)
    print(f"[{i}] {time() - start_time}")

def synthesis(i, params, query_json):
    print(f"[{i}] start")
    start_time = time()
    requests.post("http://localhost:50021/synthesis", params=params, json=query_json)
    print(f"[{i}] {time() - start_time}")

async def main_concurrent(func, **args):
    async def do_async(loop, i):
        return await loop.run_in_executor(None, func, i, *args.values())

    start_time = time()
    loop = asyncio.get_event_loop()
    await asyncio.gather(*[do_async(loop, i) for i in range(count)])
    print(f"total: {time() - start_time}")

def main(func, **args):
    start_time = time()
    for i in range(count):
        func(i, **args)
    print(f"total: {time() - start_time}")

if __name__ == "__main__":
    query_json = {}

    asyncio.run(main_concurrent(synthesis, params={"speaker": 46}, query_json=query_json))
```

ここで、一番下の方で音声合成用のクエリー`query_json`が空っぽになっていますので、先ほどの http://127.0.0.1:50021 にアクセスしてJSONを作ります。漢字まじりなので厳密ではないですが、手始めに10文字「何について話しますか」のクエリーを生成します。
![スクリーンショット 2023-04-14 20.04.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/53296/f66f72b4-185e-7ef7-c3e1-50737175afe6.png)
`Exexute`ボタンを押して生成されたJSONを`query_json`にセットしたら、実行してみましょう。

```bash
$ python client.py
[0] start
[1] start
[2] start
[3] start
[4] start
[6] start
[7] start
[8] start
[9] start
[5] start
[2] 0.7748432159423828
[1] 1.4667820930480957
[9] 2.189162015914917
[5] 2.876537799835205
[3] 3.6025688648223877
[7] 4.302885055541992
[4] 5.047386884689331
[0] 5.801153182983398
[6] 6.506537914276123
[8] 7.239416837692261
total: 7.246844053268433
```

実行すると、一斉に10多重でリクエストが投げられ、その後、処理結果を受け取るまでの秒数が表示されました。処理結果が後半に行くほど、処理時間がほぼ等間隔で長くなっています。
もうお気づきですね？少なくとも標準の設定では、VOICEVOX ENGINEでは多重度1、つまりシリアルで音声合成リクエストが処理されます。

比較のため、リクエスト自体を多重度1（逐次リクエスト）にしてリトライしてみましょう。まずはクライアントプログラムをちょっと直して`main`を呼ぶようにします。

```python:client.py
    # asyncio.run(main_concurrent(synthesis, params={"speaker": 46}, query_json=query_json))
    main(synthesis, params={"speaker": 46, "text": text20}, query_json=query_json)
```

そして実行します。

```bash
$ client.py
[0] start
[0] 0.7305850982666016
[1] start
[1] 0.7499232292175293
[2] start
[2] 0.813500165939331
[3] start
[3] 0.7538890838623047
[4] start
[4] 0.706693172454834
[5] start
[5] 0.7327671051025391
[6] start
[6] 0.7460808753967285
[7] start
[7] 0.7143492698669434
[8] start
[8] 0.717134952545166
[9] start
[9] 0.7223377227783203
total: 7.38783073425293
```

トータル7秒程度であり、10多重リクエストのときとほぼ同じ結果が得られました。このことからも並列処理は行われないと推定できるでしょう。

なお、100文字くらいのクエリーに変更したところ10件で66秒程度（1件6.6秒程度）になりました。10倍時間がかかるわけではないですが、概ね比例すると考えた方が良さそうです。

つまり、「どうしたの？」などの短いテキスト読み上げ要求であっても、その直前に100文字のリクエストを受けていると、100文字の生成分待たされてからの発声になります。これはユーザー体験を大きく損ねる原因となるでしょう。そこで、
- ウェイクワードへの反応ワードなど頻出ボイスや時報ボイスなど一時に集中する音声についてはキャッシュしておく（クライアント側・サーバー側双方）
- 読み上げ文字数の上限を設定する
- 読み上げリクエストが長文にならないようにクライアント側で区切る
- DoS攻撃に備える（長文リクエストの連発を検知、警告、遮断、必要に応じてその後の躊躇のない法的措置等）
- サーバーを並列化する

などの対策をすると良いでしょう。私は中継サーバーを立ててキャッシュと文字数制限、その他の制御を行っています。

# 処理速度の検証

本番ではそこそこのスペックのサーバーで運用すると思います。今回はAWS EC2の`c6i.4xlarge`（16コア/32GBメモリ）で検証してみます。

まずは10文字。

```bash
$ client.py
[6] 0.5498449802398682
[4] 0.8265678882598877
[3] 1.2927660942077637
[7] 1.5597119331359863
[9] 2.0225908756256104
[0] 2.489521026611328
[8] 3.0169360637664795
[5] 3.2147819995880127
[1] 3.6837329864501953
[2] 4.050853967666626
total: 4.0535688400268555
```

平均0.4秒くらいになりました。

続いて100文字。

```bash
[1] 3.826274871826172
[3] 6.5676429271698
[4] 9.734511852264404
[0] 11.963320970535278
[5] 15.105709791183472
[7] 17.109949827194214
[9] 20.285220861434937
[6] 23.44343590736389
[8] 25.462272882461548
[2] 27.431261777877808
total: 27.45194697380066
```

こちらは倍以上のスピードになりました。


# GPUについて

GPUを使用（Windows）することでびっくりするほど高速になりますが、私の環境では（メインメモリーよりも搭載量が少ない）VRAMがすぐ枯渇してしまい、本番運用は厳しいと思いました。この辺の詳しい方、逆に教えてください。

# CALについて

ここで間違ったことを書いちゃうといけないので詳細は書きませんが、基本的にWindows PCをサーバーとして複数デバイスにサービスを提供することはライセンス違反になるはずです。PCを音声サーバーとして利用したい場合は気をつけてください。

# ✨おしゃべりAI✨

この記事がVOICEVOX活用の一助になれば幸いですが、もし良かったらVOICEVOXを使わせていただいているこちらのアプリも使ってみてくださいね！

https://twitter.com/uezochan/status/1643575375451267078

あと、私が知らないだけで多重度を上げたりスループットを改善する方法をご存知の方は、ぜひコメント欄などで教えていただけると幸いです。

では、Enjoy VOICEVOX🙌