voicevox speed test

voicevox_core python binding の速度テスト。バージョンは多分0.14.4。
普通にインストールした（voicevox_engine経由？）ものがちょっと遅かったので、出だしを含めて高速化することが目的です。最終的な目標は1サーバで、50台くらいのロボットなどが並行して話せるようにしたい。
テストに使ったPCは、Windows11、CPU: Core i9-10900F 2.8GHz、GPU: GeForce RTX 3060 (メモリ12GB）で、WSL のUbuntu20 で実行しています。
スクリプトは末尾に。文章は青空文庫の「檸檬」の一部です。http://www.kepe.net/ruby/ でルビ無し版を表示してコピーペーストしました。

## テスト結果（抜粋）
- CPU
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2882052/7f1a05fb-544a-e505-f241-f187e1b56188.png)
- GPU
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2882052/98e97eb3-4bb4-6a35-f0c3-9998bedd6cf7.png)

「檸檬」の文章を句読点などで分割して、それぞれの句を順に音声合成する、というのを10回繰り返しています。同じフレーズを10回つづけてではなく、一通り順に合成するのを10回繰り返す。
表中、len は文字数、wav_dur は生成された音声データの長さ、dur はエンコード時間（いずれもms）。薄赤はM+2σより大きい、青はM-2σ未満。平均とったりとか色付けはExcel でやってます。

- CPU版の負荷は33%くらい、GPUは80%以上3D を使っていて、メモリは3G くらい使っている
- CPU, GPU 共に音声再生時間より早くエンコードできている
- GPU速い
- GPUはそれぞれのフレーズの初回で時間がかかっている
- GPUは最初のフレーズの初回が特に時間がかかっている
- CPUで赤いのが固まっているのは、多分、実行中にメール読んだりしていたせい

load_model などの初期化はループの外側で呼んでいますが、GPU版ではどうも一発目や、あたらしい音素？が出てくると遅くなる（なんかロードしている？）のではないかと思われます。CPU版は安定している。
なので、実行前にダミーの文章を一度合成させてみるとどうなるか、を調べました。

## テスト結果 (w/warmup)
- CPU (w/warmup)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2882052/26561e35-43e8-ea32-2132-88da6ad1f40b.png)
- GPU (w/warmup)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2882052/405fb055-6745-4bfb-b62f-1175aa2c667f.png)

GPUで最初のフレーズの初回、で実行時間が長かったのが解消されました。ただ、各フレーズの初回で遅いのは変わらず。理由はソースを読めばわかるかな？IPAコーパスとか最初に合成させておけばいいだろうか？

あと、合成するフレーズが短いとオーバヘッドが大きい。これはCPUもGPUも。
- CPU (w/warmup)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2882052/117d581c-94ab-bb63-dd9d-b21b53d1af41.png)
- GPU (w/warmup)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2882052/394b093c-9fe1-befe-b955-6d7701b4eb91.png)

合成対象の文章を分割するときに気を付けないといけない。

## TODO
- MQTTベースのサーバ作る
- プライオリティつきキュー
- 並列合成を試す
- ソース読んでみる

## スクリプト

```py
import voicevox_core
from voicevox_core import VoicevoxCore
from pathlib import Path
import datetime
import random
import time
import re
import wave
import io
import sys

# output in SJIS for Excel
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='shift-jis')

sent = """
えたいの知れない不吉な塊が私の心を始終圧えつけていた。焦躁と言おうか、嫌悪と言おうか――酒を飲んだあとに宿酔があるように、酒を毎日飲んでいると宿酔に相当した時期がやって来る。それが来たのだ。これはちょっといけなかった。結果した肺尖カタルや神経衰弱がいけないのではない。また背を焼くような借金などがいけないのではない。いけないのはその不吉な塊だ。以前私を喜ばせたどんな美しい音楽も、どんな美しい詩の一節も辛抱がならなくなった。蓄音器を聴かせてもらいにわざわざ出かけて行っても、最初の二三小節で不意に立ち上がってしまいたくなる。何かが私を居堪らずさせるのだ。それで始終私は街から街を浮浪し続けていた。
何故だかその頃私は見すぼらしくて美しいものに強くひきつけられたのを覚えている。風景にしても壊れかかった街だとか、その街にしてもよそよそしい表通りよりもどこか親しみのある、汚い洗濯物が干してあったりがらくたが転がしてあったりむさくるしい部屋が覗いていたりする裏通りが好きであった。雨や風が蝕んでやがて土に帰ってしまう、と言ったような趣きのある街で、土塀が崩れていたり家並が傾きかかっていたり――勢いのいいのは植物だけで、時とするとびっくりさせるような向日葵があったりカンナが咲いていたりする。
時どき私はそんな路を歩きながら、ふと、そこが京都ではなくて京都から何百里も離れた仙台とか長崎とか――そのような市へ今自分が来ているのだ――という錯覚を起こそうと努める。私は、できることなら京都から逃げ出して誰一人知らないような市へ行ってしまいたかった。第一に安静。がらんとした旅館の一室。清浄な蒲団。匂いのいい蚊帳と糊のよくきいた浴衣。そこで一月ほど何も思わず横になりたい。希わくはここがいつの間にかその市になっているのだったら。――錯覚がようやく成功しはじめると私はそれからそれへ想像の絵具を塗りつけてゆく。なんのことはない、私の錯覚と壊れかかった街との二重写しである。そして私はその中に現実の私自身を見失うのを楽しんだ。
私はまたあの花火というやつが好きになった。花火そのものは第二段として、あの安っぽい絵具で赤や紫や黄や青や、さまざまの縞模様を持った花火の束、中山寺の星下り、花合戦、枯れすすき。それから鼠花火というのは一つずつ輪になっていて箱に詰めてある。そんなものが変に私の心を唆った。
それからまた、びいどろという色硝子で鯛や花を打ち出してあるおはじきが好きになったし、南京玉が好きになった。またそれを嘗めてみるのが私にとってなんともいえない享楽だったのだ。あのびいどろの味ほど幽かな涼しい味があるものか。私は幼い時よくそれを口に入れては父母に叱られたものだが、その幼時のあまい記憶が大きくなって落ち魄れた私に蘇えってくる故だろうか、まったくあの味には幽かな爽やかななんとなく詩美と言ったような味覚が漂って来る。
"""

list_sent = re.split('\n|。|―|、', sent)
list_sent = [x for x in list_sent if x]

speaker_id = 1
core = VoicevoxCore(
    acceleration_mode='CPU',
    open_jtalk_dict_dir=Path("open_jtalk_dic_utf_8-1.11")
)
print(voicevox_core.SUPPORTED_DEVICES, file=sys.stderr)
print('GPU mode:', core.is_gpu_mode, file=sys.stderr)
core.load_model(speaker_id)

# generate dummy wav for warmup
#core.tts('ダミーの文章です', speaker_id)

repeat_count = 10
durs = []
wav_durs = []
for rep in range(repeat_count):
    for i, line in enumerate(list_sent):
        if rep == 0:
            durs.append([])

        print(rep, i, line, file=sys.stderr)
        ts = time.perf_counter()
        wave_bytes = core.tts(line, speaker_id)
        dur = (time.perf_counter() - ts) * 1000
        durs[i].append(dur)

        # get wave duration
        if rep == 0:
            aud = wave.open(io.BytesIO(wave_bytes))
            wav_durs.append( (aud.getnframes() / aud.getframerate()) * 1000 )

out = []
out.append('text')
out.append('len')
out.append('wav_dur')
out.extend(['dur'] * repeat_count)
print(','.join(out))

for i, line in enumerate(list_sent):
    out = []
    out.append(line)
    out.append(str(len(line)))
    out.append('%.2f' % wav_durs[i])
    out.extend(['%.2f' % x for x in durs[i]])
    print(','.join(out))

```