Amazon Polly による「文書の読み上げ」を Livebook から実行する

## はじめに

Amazon Polly は AWS 上で使える AI による音声合成 = 文書の読み上げサービスです

ブラウザからお手軽に試す場合、以下の記事を参考にしてください

https://qiita.com/RyoWakabayashi/items/2aad1952f63b4e76b652

本記事では Elixir の Livebook から Polly を使い、文章の読み上げを実装します

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

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

## Livebook とは

Livebook は Elixir　のコードをブラウザから実行し、結果を表示してくれるツールです

https://livebook.dev/

Python における Jupyter のようなもので、 Elixir 入門や Elixir を使ったデータ分析、データの視覚化などに適しています

はじめ方は以下の記事を参考にしてください

https://qiita.com/RyoWakabayashi/items/dc82ad3ed866d58a8acf

## セットアップ

Livebook で新しいノートブックを開いたら、先頭のセットアップセルに以下のコードを入力し、実行してください

```elixir
Mix.install([
  {:aws, "~> 0.13"},
  {:hackney, "~> 1.20"},
  {:kino, "~> 0.11"}
])
```

AWS サービスの操作用に AWS Elixir というモジュールをインストールしています

https://github.com/aws-beam/aws-elixir/tree/master

## クライアントの準備

AWS の認証情報を入力するためのテキストボックスを準備します

```elixir
access_key_id_input = Kino.Input.password("ACCESS_KEY_ID")
secret_access_key_input = Kino.Input.password("SECRET_ACCESS_KEY")
region_input = Kino.Input.text("REGION")

[
  access_key_id_input,
  secret_access_key_input,
  region_input
]
|> Kino.Layout.grid(columns: 3)
```

セルを実行して表示されたテキストボックスにそれぞれ入力してください

![スクリーンショット 2023-12-10 15.13.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ab5ed517-8074-1189-048f-d47fa4f8e42f.png)

AWS の API と通信するためのクライアントを用意します
この際、先ほどの認証情報を受け渡します

```elixir
client =
  AWS.Client.create(
    Kino.Input.read(access_key_id_input),
    Kino.Input.read(secret_access_key_input),
    Kino.Input.read(region_input)
  )
```

## 音声合成

Polly には standard と neural の2種類のエンジン（音声合成を処理するプログラム）があります

まずは standard を試してみます

`describe_voices` で standard エンジンの日本語（ `js-JP` ）で使える声の一覧を取得しましょう

```elixir
voice_list =
  client
  |> AWS.Polly.describe_voices("standard", nil, "ja-JP")
  |> elem(1)
  |> Map.get("Voices")
  |> Enum.map(fn voice ->
    Map.take(voice, ["Id", "Gender", "Name", "LanguageName"])
  end)

Kino.DataTable.new(voice_list)
```

実行結果
![スクリーンショット 2023-12-09 22.41.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/9ad02c64-1f8d-3fcf-f605-814f9e8bc51f.png)

男性の Takumi と女性の Mizuki が使えるようです

今回は Takumi を使ってみましょう

次に読み上げる文章を入力するため、テキストエリアを作ります

```elixir
text_input = Kino.Input.textarea("TEXT")
```

表示されたテキストエリアに読み上げさせたい文章を入力します

![スクリーンショット 2023-12-09 22.55.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/cba713a6-f5c5-30c4-bf8f-8bd0973d0033.png)

本記事では「オオカミと七ひきの子ヤギ」の冒頭を読ませてみます

https://www.aozora.gr.jp/cards/001091/files/59521_71684.html

`synthesize_speech` にエンジン、言語、声の ID 、出力フォーマット、読ませたい文章を指定します

通常の AWS の API は実行結果が JSON で返ってきますが、 `synthesize_speech` の場合、音声のバイナリデータが返ってきます

そのため、 JSON として結果を読み込まないよう、オプション（第3引数）に `receive_body_as_binary?: true` を指定しています 

```elixir
result =
  client
  |> AWS.Polly.synthesize_speech(
    %{
      "Engine" => "standard",
      "LanguageCode" => "ja-JP",
      "VoiceId" => "Takumi",
      "OutputFormat" => "mp3",
      "Text" => Kino.Input.read(text_input)
    },
    receive_body_as_binary?: true
  )
  |> elem(1)
  |> Map.get("Body")
```

実行結果
```elixir
<<73, 68, 51, 4, 0, 0, 0, 0, 0, 35, 84, 83, 83, 69, 0, 0, 0, 15, 0, 0, 3, 76, 97, 118, 102, 53, 56,
  46, 55, 54, 46, 49, 48, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 243, 96, 196, 0, ...>>
```

このバイナリデータを Base64 形式に変換することで、 Web 上で再生できるようになります

```elixir
result
|> Base.encode64()
|> then(fn base64 ->
  Kino.HTML.new("""
  <audio controls src="data:audio/mp3;base64,#{base64}">
  </audio>
  """)
end)
```

実行結果
![スクリーンショット 2023-12-09 23.01.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/dd9b91e9-2ef5-6f4b-d25c-3d8f784a1d55.png)

表示されたコントロールの再生ボタンをクリックすると、以下の動画のように音声が再生されます

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

多少ぎこちなくはありますが、きちんと読み上げています

neural エンジンも試してみましょう

neural エンジンの日本語で使用可能な声の一覧を取得します

```elixir
voice_list =
  client
  |> AWS.Polly.describe_voices("neural", nil, "ja-JP")
  |> elem(1)
  |> Map.get("Voices")
  |> Enum.map(fn voice ->
    Map.take(voice, ["Id", "Gender", "Name", "LanguageName"])
  end)

Kino.DataTable.new(voice_list)
```

![スクリーンショット 2023-12-09 23.03.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/2df25c82-4735-09f1-c552-82e536458352.png)

Takumi は neural エンジンでも付き合えるようなので、引き続き Takumi に喋ってもらいます

```elixir
client
|> AWS.Polly.synthesize_speech(
  %{
    "Engine" => "neural",
    "LanguageCode" => "ja-JP",
    "VoiceId" => "Takumi",
    "OutputFormat" => "mp3",
    "Text" => Kino.Input.read(text_input)
  },
  receive_body_as_binary?: true
)
|> elem(1)
|> Map.get("Body")
|> Base.encode64()
|> then(fn base64 ->
  Kino.HTML.new("""
  <audio controls src="data:audio/mp3;base64,#{base64}">
  </audio>
  """)
end)
```

実行結果
<iframe width="560" height="315" src="https://www.youtube.com/embed/Q9h-B25ipjk?si=B3pEwa56remql5e0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

さっきより滑らかになりました

## まとめ

Livebook から Amazon Polly による音声合成が実行できました

Livebook は音声や画像などのデータも簡単に扱えるのが便利ですね

Transcribe と Polly を組み合わせれば、音声での自動応答システムも簡単に作れそうです