VOICEVOX(Core)をAWSLambdaで動かす

# はじめに

[VOICEVOX](https://voicevox.hiroshiba.jp/)という「無料で使える中品質なテキスト読み上げ・歌声合成ソフトウェア」があります。これで「中品質かー」と思うくらいよくできていて、ちょっと自作アプリに組み込んだり、Web 上で喋らせたり、コマンドラインから起動できたら楽しそうです。
配布するのもライセンス的にかなり緩いので問題ないのですが、環境(CPU/OS)やファイルサイズ(合計 1GB くらいある)的に苦労するところがあります。ちょっと遊びに使いたいだけなんだけどなー、というときにはやはり Lambda みたいなところで WebAPI として使えると便利です。

そこでこの VOICEVOX(Core) を Lambda で動くようにした
https://github.com/mokemokechicken/lambda-vvcore
というのを作ったので共有します。

# 作ったもの

## 何ができるのか

Lambda としてデプロイすると、HTTP リクエストを受け取って VOICEVOX で音声を生成し、その Wav データを base64 エンコードして返す API ができます。
「テキスト」「speakerId」を指定すると、そのテキストを指定した話者の声で読み上げた音声データを返します。

### 使い方例 1: ブラウザの JavaScript から

```javascript
synthesizeAndPlayVoice("今日は台風ですね", 0);
```

<details><summary>synthesizeAndPlayVoiceの中身</summary>

```javascript
const LAMBDA_ENDPOINT = "https://YOUR-LAMBDA-URL.lambda-url.ap-northeast-1.on.aws/";
const LAMBDA_APIKEY = "YOUR-LAMBDA-APIKEY";

async function synthesizeAndPlayVoice(text, speakerId) {
  const requestBody = JSON.stringify({
    text: text,
    speaker_id: speakerId,
  });

  try {
    // Lambda関数を呼び出し
    const response = await fetch(LAMBDA_ENDPOINT, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${LAMBDA_APIKEY}`,
      },
      body: requestBody,
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const data = await response.json();

    // Base64エンコードされたWAVデータをデコード
    const wavData = atob(data.wav);

    // WAVデータをUint8Arrayに変換
    const wavArray = new Uint8Array(wavData.length);
    for (let i = 0; i < wavData.length; i++) {
      wavArray[i] = wavData.charCodeAt(i);
    }

    // AudioContextを作成
    const audioContext = new (window.AudioContext || window.webkitAudioContext)();

    // WAVデータをデコード
    const audioBuffer = await audioContext.decodeAudioData(wavArray.buffer);

    // 音源を作成
    const source = audioContext.createBufferSource();
    source.buffer = audioBuffer;

    // 出力に接続
    source.connect(audioContext.destination);

    // 再生開始
    source.start();

    console.log("音声の再生を開始しました。");
  } catch (error) {
    console.error("エラーが発生しました:", error);
  }
}
```

</details>

### 使い方例 2: コマンドラインから

こういう感じで使えます。

```bash
export LAMBDA_ENDPOINT="https://YOUR-LAMBDA-URL.lambda-url.ap-northeast-1.on.aws/";
export LAMBDA_APIKEY="YOUR-LAMBDA-APIKEY";

TEXT="ボイスボックスへ、ようこそ！"
SPEAKER=0
JSON='{"text":"'$TEXT'","speaker_id":'$SPEAKER'}'
curl -s -XPOST "$LAMBDA_ENDPOINT" -H "Content-Type: application/json" -d "$JSON" -H "Authorization: Bearer $LAMBDA_APIKEY" | jq -r '.wav' | base64 -d > voice.wav
# play voice.wav
```

## デプロイ方法

https://github.com/mokemokechicken/lambda-vvcore に書いてある通りですが、ざっくり以下の手順です。

0. AWSCDKv2 が使える環境を用意します。AWS のアカウントや認証情報は設定済みであることを前提とします。
1. このリポジトリを clone して、`lambda-vvcore` ディレクトリに移動します。
2. `lambda-vvcore` ディレクトリで `npm install` します。
3. 環境変数を設定して、`npx cdk deploy` します。
4. デプロイが完了したら、出力された URL と API キーを使って API を呼び出します。

## 処理速度 や 料金 について

### 処理速度

- 話者Model の Load: 約 3 秒
  - Lambda は再利用されるので、Speaker 毎の最初のリクエストのみ
    - でも、Lambda はリクエストがない場合数分で終了するので、再度リクエストが来たときには再度 Load が走る
- 音声合成: 「ボイスボックスをエーダブリューエス ラムダで動かす」 で 約 2.2 秒

つまり およそ 初回5.2秒、2回目なら2.2秒 という感じ。

### 料金

この Lambda の場合、メモリサイズを 10240MB(最大) に設定しています。これが 1 秒動くと約 0.020 円(1 ドル 150 円換算)かかります。
※ 東京リージョン、Arm、10240MB の 1 ミリ秒あたりの料金: `0.0000001333USD` (2024 年 8 月現在) [※](https://aws.amazon.com/jp/lambda/pricing/)
※ リクエスト料金は相当低いので無視します。

AWS Lambda は メモリサイズを上げると処理速度が上がるらしいです（ついでに料金も上がります）。
メモリサイズを落とすと、顕著に処理速度が遅くなります。
あまり細かく計測はしていませんが、同じくらいの価格になるような感じがあるので、処理速度が速くなる 10240MB に設定しています(が好みで変えてください)。

実際のメモリ消費量は 1 モデルだと 300MB ~ 600MB くらいです。

## その他

### 中身の構成

- Rust で [vvcore](https://docs.rs/vvcore/latest/vvcore/) というライブラリを経由して VOICEVOX(Core) を呼び出しています
- Docker コンテナ内に VOICEVOX(Core) の 共有ライブラリや モデルデータ等を入れて、それを Lambda で動かしています
- VOICEVOX CORE は 0.15.4 を使っています。0.14系だと「初期化にとても時間がかかる(`load_all_models=false` にしても全ModelをLoadしている?）」

### メモリサイズとスレッド数

VVCore の方で Lambda の CPU 数を動的に取得できないようなので(まあそうか)、[固定値で thread 数を設定](https://github.com/mokemokechicken/lambda-vvcore/blob/25829ba582ae0a4673c49c37c6e605397531f975/lambda-vvcore/src/voicevox_wrapper.rs#L23)しています。
[10240MB では vCPU が 6 らしい](https://qiita.com/komikoni/items/101ccd3cb98fdc4f4389)ので、6 スレッドにしています。
（よく見るとこの話では 8896MB から 6 スレッドらしいので、8896MB にしてもいいかもしれないな...）

# さいごに

新しいおもちゃができて楽しいです。開発者の方々、ありがとうございます。
結局 1 リクエストで 0.05 ~ 0.1 円くらいかかるので、まあちょっとした用途に使う感じかなと思います。

英語などの読み上げは得意ではないので、gpt-4o-mini とかで 平仮名に変換してから読み上げるオプションを追加するとか、そういうのも面白いかもしれませんね。