Livebook で VOICEVOX の「ずんだもん」や「WhiteCUL」に喋ってもらう

## はじめに

@t-yamanashi　さんの記事を参考に、 Livebook で VOICEVOX の ずんだもん や WhiteCUL に喋ってもらいました

https://qiita.com/t-yamanashi/items/75c40103b7e240c78288

実装したノートブックはこちら

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/voicevox.livemd

## クレジット

VOICEVOX:ずんだもん

https://zunko.jp/

VOICEVOX:WhiteCUL

https://www.whitecul.com/

## VOICEVOX とは

商用・非商用問わず無料で使えるテキスト読み上げソフトウェアです

https://voicevox.hiroshiba.jp/

様々なキャラクターの音声でテキストを読み上げることができます

音声の利用については各音声の利用規約に則ります

## 実行環境

- macOS Ventura 13.4.1
- VOICEVOX 0.14.7
- Elixir 1.15.4
- Erlang 26.0.2
- Livebook 0.10.0

## 事前準備

VOICEVOX 公式から VOICEVOX をインストールし、起動しておきます

VOICEVOX を起動している状態では、 <http://localhost:50021> で VOICEVOX の API が呼び出せます

また、以下の URL で API の仕様を確認できます

<http://localhost:50021/docs>

やはり OpenAPI は偉大

これさえ参照すれば Elixir から VOICEVOX を操作し放題です

![スクリーンショット 2023-07-26 16.12.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/e3602d53-4078-4476-6df2-14025edba70e.png)

この状態で Livebook を起動し、ノートブックを作成します

## セットアップ

```elixir
Mix.install([
  {:kino, "~> 0.10"},
  {:req, "~> 0.3"}
])
```

音声出力等のために [Kino](https://github.com/livebook-dev/kino) 、 VOICEVOX API へのリクエストのために [Req](https://github.com/wojtekmach/req) をインストールしています

## 音声一覧の取得

VOICEVOX には 2023年7月現在、 26 キャラクターの音声が存在します

また、各キャラに「ノーマル」「たのしい」「かなしい」「びえーん」などのスタイルが複数存在します

音声を喋ってもらうとき、どの声にするのかを指定する必要があるため、まずは音声ID、キャラクター、スタイルの一覧を取得し、目当ての音声を探します

音声一覧を取得するためには core_version を指定する必要があります

以下のコードを実行し、 VOICEVOX の core_version を取得します

```elixir
core_version =
  "http://localhost:50021/core_versions"
  |> Req.get!()
  |> Map.get(:body)
  |> Enum.at(0)
```

実行結果は `"0.14.4"` になります

VOICEVOX 本体のバージョンは 0.14.7 ですが、コアのバージョンは 0.14.4 のようです

では、この値を使って音声の一覧を取得します

```elixir
Req.new(url: "http://localhost:50021/speakers", params: %{core_version: core_version})
|> Req.get!()
|> Map.get(:body)
|> Enum.flat_map(fn speaker ->
  speaker["styles"]
  |> Enum.map(fn style ->
    %{
      name: speaker["name"],
      id: style["id"],
      style_name: style["name"]
    }
  end)
end)
|> Enum.sort_by(& &1.id)
|> Kino.DataTable.new()
```

![スクリーンショット 2023-07-26 17.15.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/59426fab-ea77-9dad-e120-6ce60b26276b.png)

`id: 0` が「四国めたん」の「あまあま」、 `id: 1` が「ずんだもん」の「あまあま」というように一覧が取得できました

この一覧から id を選択し、音声を喋ってもらいます

## 音声合成

まず「ずんだもん」の「ノーマル」に喋ってもらいます

喋ってもらいたい内容を `text` に指定し、「ずんだもん」「ノーマル」の音声id 3 を指定します

先に音声合成用のクエリを生成します

クエリ生成によってテキストをカナに変換し、各音毎に高さやアクセントを設定しています

その後、生成したクエリを使って音声合成し、結果として音声のバイナリデータを取得します

```elixir
text = "ずんだもんなのだ。エリクサー使いのアルケミストになったのだ"
speaker_id = 3

# 音声合成用のクエリを生成する
audio_query =
  Req.new(
    url: "http://localhost:50021/audio_query",
    params: %{
      text: text,
      speaker: speaker_id
    }
  )
  |> Req.post!()
  |> Map.get(:body)

# クエリを使って音声を合成する
data =
  Req.new(
    url: "http://localhost:50021/synthesis",
    params: %{speaker: speaker_id}
  )
  |> Req.post!(json: audio_query)
  |> Map.get(:body)
```

## 音声再生

VOICEVOX から取得したバイナリのままでは Web 上で再生できないので、 Base64 エンコードします

```elixir
base64 = Base.encode64(data)
```

結果は `"UklGRiRSBABXQVZFZm10IBAAAAABA..."` というような長い文字列になります

この文字列を HTML の audio タグの src に指定することで音声を再生します

```elixir
Kino.HTML.new("""
<audio controls src="data:audio/wav;base64,#{base64}">
</audio>
""")
```

<iframe width="560" height="315" src="https://www.youtube.com/embed/JaHcWCSp4PU" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

## モジュール化

使いやすいようにモジュール化しました

```elixir
defmodule VoiceVox do
  @base_url "http://localhost:50021"

  defp get_audio_query(text, speaker_id) do
    Req.new(
      url: "#{@base_url}/audio_query",
      params: %{
        text: text,
        speaker: speaker_id
      }
    )
    |> Req.post!()
    |> Map.get(:body)
  end

  defp synthesis(audio_query, speaker_id) do
    Req.new(
      url: "#{@base_url}/synthesis",
      params: %{speaker: speaker_id}
    )
    |> Req.post!(json: audio_query)
    |> Map.get(:body)
  end

  defp view(data) do
    base64 = Base.encode64(data)

    Kino.HTML.new("""
    <audio controls src="data:audio/wav;base64,#{base64}">
    </audio>
    """)
  end

  def new(text, speaker_id) do
    text
    |> get_audio_query(speaker_id)
    |> synthesis(speaker_id)
    |> view()
  end
end
```

今度はVOICEVOXの「面白い女」で有名な「WhiteCUL」「びえーん」で喋ってもらいます

```elixir
VoiceVox.new("こんにちは。WhiteCULです。今日はライブブックから喋っています", 26)
```

<iframe width="560" height="315" src="https://www.youtube.com/embed/l0roiMQWqDw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

## まとめ

VOICEVOX は API を備えているので、 Elixir からも簡単に操作できました

また、音声データも Base64 エンコーディングすることで Livebook 上で再生できました

実は初めて YouTube に動画を投稿したので、これからちょくちょく Livebook ネタを投稿していこうと思います