Amazon Polly を使う

スライドを音声入りビデオにして送れ、と言われたので試してみたかった音声合成を試してみた。まあ、どう考えても私の英語よりは聞きやすいだろう。

## Amazon Polly
Amazon Polly は、AWSの音声サービス。コンソールから使うこともできるが、AWSなので、APIでも使うことができる。関係ないけどAWSのサービスって、AWS XXX という場合と Amazon XXX という場合があるような気がするけど、どうやって使い分けてるんだろう。PollyにはAWSではなくAmazonがついている。

https://aws.amazon.com/polly/

## 使い方
1. AWS アカウントを用意する
2. 権限としてPollyFullAccess のみを付与した Polly専用IAMを作り、ACCESS KEYと SECRET KEYを取得する。
  ルートアカウントで、ACCESS KEYを作ることももちろんできるが、まあ絶対やめたほうがいい。言うまでもないが、漏洩した場合のダメージを最小化するため。
3. `~/.aws/credentials` にこれらの値を書く。
    ```
    [default]
    aws_access_key_id = XXXXX
    aws_secret_access_key = XXXXX
    ```

4. `~/.aws/config` に下のようにリージョンを指定する。
   ```
    [default]
    region=us-east-1
    ```
5. python3 と boto3 をインストールする。
    ```
    > pip install boto3
    ```
## コード
使い方は思いの外簡単で、 ミニマルにはこんな感じ。
``` python
import boto3
client = boto3.client('polly')

response = client.synthesize_speech(
    OutputFormat='mp3',
    SampleRate='8000',
    Text='こんにちは、アマゾン ポリー です',
    TextType='text',
    VoiceId='Mizuki',
)

b = response['AudioStream'].read()
with open('test.mp3', "wb") as f:
    f.write(b)
```

VoiceIdとして`Mizuki`を選んでいるが、Idを指定することで、暗黙裡に言語と男性/女性を指定したことになっている。VoiceIdのリストは下のサイトにある。

https://docs.aws.amazon.com/polly/latest/dg/voicelist.html

## TextとSSML
テキストで直接指定すると、話す速度を変更したり、文の一部を強調したり、ちょっと間を置くなどのこまかい制御ができない。これを可能にするにはSSMLというXML形式を使う必要がある。SSMLは標準らしくて、Googleなどの音声合成でも同じものが使えるようだ。方言が色々ありそうだけど。

https://docs.aws.amazon.com/ja_jp/polly/latest/dg/supportedtags.html

SSMLを使う際には、`TextType`に`ssml`を指定し、`Text`に`<speak>`タグで囲まれた文書を使えばいい。

``` python
response = client.synthesize_speech(
    OutputFormat='mp3',
    SampleRate='8000',
    Text='<speak>こんにちは、アマゾン ポリー です</speak>',
    TextType='ssml',
    VoiceId='Mizuki',
)
```

SSMLを使うと発話速度を調節できる。
AWS Pollyの合成音声はやや速めだと思うので、これが重要になる。
次のように書くと速度が90%になる。
``` xml
<speak>
    <prosody rate="90%"> 
        こんにちは、アマゾン ポリー です
    </prosody>
</speak>
```
## スクリプト
スライドの音声を合成するのが目的なので、1つのファイルでまとめて管理したい。ということで、簡単なフォーマットを考えてパーサを書いた。スクリプトは下に示す感じで、`--- スライド名` で区切って文章を書く。スライド名はファイル名に使われる。また、1枚のスライドの中で、複数の音声を使うことも多いので、`[click]`で区切ると前後を別の音声ファイルとして扱うようにした。また、3つ以上連続した改行があった場合、空白をあけるようにしてある。


```
--- test1
1枚め

--- test2
2枚め

--- test3
3枚め
[click]
3枚めの2
```

このようなスクリプトに対して、

``` python
generate_mp3("script2.txt", targets=None, voice_id="Mizuki", speech_rate="60%")
```

のようにすると、`test1-1.mp3, test2-1.mp3, test3-1.mp3, test3-2.mp3`の4つのファイルができる。voice-id と発話速度を制御できる。


コードは以下のような感じ。
``` python
import boto3

DEFAULT_VOICE_ID="Matthew"

client = boto3.client('polly')

def generate_ssml(text, speech_rate):
    text = text.replace("\n\n\n\n", '<break time="2s"/>')
    text = text.replace("\n\n\n", '<break time="1.5s"/>')
    return '<speak><prosody rate="{}">{}</prosody></speak>'.format(speech_rate, text)

def speak_to_mp3(client, text, filename, voice_id=DEFAULT_VOICE_ID, speech_rate="100%"):
    response = client.synthesize_speech(
        OutputFormat='mp3',
        SampleRate='8000',
        Text=generate_ssml(text, speech_rate),
        TextType='ssml',
        VoiceId=voice_id
    )
    b = response['AudioStream'].read()
    with open(filename, "wb") as f:
        f.write(b)

def parse_script(filename, targets=None):
    ''' read the file. use --- as page separator, [click] as file separator.
    targets = "page1, page2" '''
    if targets:
        targets = [x.strip() for x in targets.split(',')]
    
    print(targets)
    d = {}  # key: filname, value: text
    section_counter = 1
    page = None
    content = ""
    
    def push_content():
        if (targets == None) or (page in targets):
            nonlocal content
            fn = "{}-{}.mp3".format(page, section_counter)
            d[fn] = content
        content = ""

    with open(filename, "r") as f:
        for l in f.readlines():
            if l.startswith("---"):
                if page:
                    push_content()
                section_counter = 1
                page = l.split()[-1]
                continue
            if l.startswith("[click]"):
                push_content()
                section_counter += 1
                continue
            content += l
    push_content()
    return d
        
def generate_mp3(script_file, targets=None, voice_id=None, speech_rate="90%"):
    d = parse_script(script_file, targets=targets)
    for filen, text in d.items():
        speak_to_mp3(client, text, filen, voice_id=voice_id, speech_rate=speech_rate)

```


## 所感
Polly の生成する音声の質は十分に高い。
生成された音声をPowerPoint に張り込んで、export でmp4ファイルを作成して出来上がり。ただ、アニメーションのタイミングをあわせたりなど、そんなに楽ではない。スライドをビデオにする方法として、全体として楽だったかどうかはかなり疑問かな。。

どうせやるならMarkdownやLatexでスライドを書いて、静止画像と音声データから、直接ビデオを生成するとかやったほうがいいかもしれない。そのうちやってみよう。