作業メモ:VoiceVoxのインストールからpythonで音声合成するまで

# 概要
音声対話システムを作るためのステップとして、voicevoxをインストールし、pythonで音声合成しましたので、作業内容をメモします。
すでに多くのノウハウが世に公開されていると思いますが、環境依存のところもあると思いますので、自分の環境ではこうやった、というのを情報として残します。

# 環境
- VoiceVoxサーバ
  - ubuntu 24.04、RTX4060Ti
  - docker, docker composeはインストール済みとします。
  - dockerからnvidiaのgpuを使う設定も済とします。
- 音声合成用クライアント
  - macOS M1
  - python 3.11.2
# VoiceVoxのインストール
GUIは不要なので[voicevox engine](https://github.com/VOICEVOX/voicevox_engine/)のみ使います。
dockerを用いた利用方法がを公式にありますので参考にします。
再利用性を考えて、docker-compose.yamlを作成します。

```
mkdir -p ~/app/voicevox
cd ~/app/voicevox
nano docker-compose.yml
```

```yaml:docker-compose.yml
version: "3.8"

services:
  voicevox_engine:
    image: voicevox/voicevox_engine:nvidia-ubuntu20.04-latest
    ports:
      - "50021:50021"
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
    environment:
      NVIDIA_VISIBLE_DEVICES: "all"
      NVIDIA_DRIVER_CAPABILITIES: "compute,utility"
    runtime: nvidia
```

作成したら保存し、docker composeで起動します。

```
docker compose up -d
```

voicevox engineが起動したら、クライアントPCから音声合成できるか試してみます。
クライアントPCで以下のファイルを作成します。

```sh:synthesis.sh
echo -n "こんにちは、音声合成の世界へようこそ" >text.txt

curl -s \
    -X POST \
    "{UBUNTUのIPアドレス}:50021/audio_query?speaker=1"\
    --get --data-urlencode text@text.txt \
    > query.json

curl -s \
    -H "Content-Type: application/json" \
    -X POST \
    -d @query.json \
    "{UBUNTUのIPアドレス}:50021/synthesis?speaker=1" \
    > audio.wav
```

作成したら実行します。

```sh
sh synthesis.sh
```

成功したらaudio.wavというファイルが同じフォルダに作成されているはずですので、再生して、synthesis.shで指定した文章が再生されるか確認してください。

```sh
afplay audio.wav
```

# systemdへの登録（オプション）
必須ではありませんが、サーバPCの起動時にvoicevox engineを自動起動できるように、systemdにコマンドを登録します。

まずすでにvoicevox engineを起動済みであればstopします。

```sh
cd ~/app/voicevox/
docker compose down
```

サーバPC（ubuntu）で以下のファイルを作成します。

```sh
sudo nano /etc/systemd/system/voicevox.service
```

```voicevox.service
[Unit]
Description=VoiceVox Engine via Docker Compose
After=network.target docker.service
Requires=docker.service

[Service]
WorkingDirectory=/home/USER/app/voicevox
ExecStart=/usr/bin/docker compose up
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=0
Restart=always

[Install]
WantedBy=multi-user.target
```

`USER`あるいはWorkingDirecotryの値は環境に合わせて書き換えてください。
ExecStartのコマンドは`docker compose up`にしており、バックグラウンド実行用の引数である`-d`は除いています。systemd自体がバックグラウンドで実行されるためです。

ファイル作成後、サービスを有効化します。これによりPC起動時にvoicevox engineのサービスも起動されるようになります。

```sh
sudo systemctl enable voicevox
```

サービスの起動してstatusを確認します。

```sh
sudo systemctl start voicevox
systemctl status voicevox
```

`active (running)`みたいな文字列が出力されていればOKです。

終了したい場合は以下のコマンドを使います。

```sh
sudo systemctl stop voicevox
```

PC起動時に自動実行したくない場合は以下で無効化できます。

```sh
sudo systemctl disable voicevox
```


# pythonクライアントの作成
pythonのシステムと連携させたいので、pythonから音声合成できるようにします。
pipでinstall可能な[voicevox-client](https://github.com/voicevox-client/python)というライブラリがあります。asyncが使われているので非同期処理に抵抗がなければこれを使っても良いと思います。
今回はasyncを使いたくないので、自前で実装します。

```python:voicevox.py
import wave
import requests
import json
import simpleaudio
import io

class VoiceVoxClient:
    def __init__(self, host="localhost", port=50021):
        self.host = host
        self.port = port

    def get_content(self, text, speaker=0):
        params = (
            ('text', text),
            ('speaker', speaker),
        )
        response1 = requests.post(
            f'http://{self.host}:{self.port}/audio_query',
            params=params
        )
        headers = {'Content-Type': 'application/json',}
        response2 = requests.post(
            f'http://{self.host}:{self.port}/synthesis',
            headers=headers,
            params=params,
            data=json.dumps(response1.json())
        )
        return response2.content

    # VOICEVOXで音声合成    
    def generate_wav(self, text, speaker=0, filepath=None) -> tuple[io.BytesIO, float]:
        content = self.get_content(text, speaker)
        
        if filepath:
            with open(filepath, "wb") as f:
                f.write(content)
                
        wav_io = io.BytesIO(content)
        with wave.open(wav_io, 'rb') as wf:
            frame_rate = wf.getframerate()
            duration = wf.getnframes() / float(frame_rate)
        
        return content, duration
    
    
    def play_voice(self, text, speaker=8, *, wait=False) -> tuple[simpleaudio.PlayObject, float]:
        content = self.get_content(text, speaker)
        
        # WAVデータをメモリ上で読み込み
        wav_io = io.BytesIO(content)
        
        with wave.open(wav_io, 'rb') as wf:
            audio_data = wf.readframes(wf.getnframes())
            play_obj = simpleaudio.play_buffer(audio_data, wf.getnchannels(), wf.getsampwidth(), wf.getframerate())
            
            # 再生が終了するまで待機
            if wait:
                play_obj.wait_done()
            
            duration = wf.getnframes() / float(wf.getframerate())
            return play_obj, duration    

if __name__ == "__main__":
    VOICEVOX_IP = "サーバのIP"
    voicevox_PORT = "サーバのポート"
    voicevox_client = VoiceVoxClient(VOICEVOX_IP, VOICEVOX_PORT)
    voicevox_client.play_voice("こんにちはー、今日は来てくれてありがとうね", 8)

```

上記プログラムを実行して、音声が再生されたら成功です。

処理としては、get_contentで音声合成結果のバイトを受けとり、generate_wavかplay_voiceに渡しています。
generate_wavとplay_voiceは用途に応じて使い分けます。
generate_wavはfilepathが指定されていればwavに保存します。また、wavを別スレッドで再生するときなどに待機できるように再生時間（duration）を音声のバイト列と合わせて返します。play_voiceは実行PCのデフォルトスピーカで直接再生します。