Amazon Polly
問題文を読み上げ

Takanori Suzuki

BPStyle #158 / 2024 Mar 7

背景

  • 某案件で学習教材の電子化をやっている

  • 合理的配慮 の一環としてテキスト読み上げを実現したい

  • 全盲の人向けではなく、聴覚優位 の人向け

合理的配慮

合理的配慮とは、障害者から何らかの助けを求める意思の表明があった場合、過度な負担になり過ぎない範囲で、社会的障壁を取り除くために必要な便宜のことである。

聴覚優位

子どもたちの情報の取り入れ方を下記の3タイプに分類し、“知覚の優位性”という考え方が世界に広がっていったことに始まります。

視覚優先型・聴覚優先型・運動感覚/触覚優先型

やりたいこと

  • 問題文等を読み上げられるようにする

    • 聴覚優位の生徒が理解しやすくする

  • 完全に読み上げられなくてもある程度しょうがない

ちなみに全盲の場合

ゴール

  • Amazon Pollyでの音声合成を知る

  • Pythonでの実装方法を知る

  • 読み上げのカスタマイズ方法を知る

Amaozon Polly

Amaozon Polly

Amazon Pollyのデモ

PythonからAmazon Pollyを実行

(env) $ pip install boto3
(env) $ export AWS_ACCESS_KEY_ID=AKI...
(env) $ export AWS_SECRET_ACCESS_KEY=ZoWb...
(env) $ export AWS_DEFAULT_REGION=ap-northeast-1
  • Boto3をインストール

  • APIを使うための環境変数を設定

PythonからAmazon Pollyを実行

from contextlib import closing
from pathlib import Path
import boto3

polly = boto3.client('polly')  # クライアント作成
s = "zoom如きにやられるとはニューカマーのツラ汚しよ…"  # by masashinji

result = polly.synthesize_speech(
    Text=s, OutputFormat="mp3", VoiceId="Mizuki")  # テキストから音声を合成

# 音声を読み込んでファイルに保存
with closing(result["AudioStream"]) as stream:
    Path("masashinji.mp3").write_bytes(stream.read())

PythonからAmazon Pollyを実行

s = "zoom如きにやられるとはニューカマーのツラ汚しよ…"  # by masashinji

result = polly.synthesize_speech(
    Text=s, OutputFormat="mp3", VoiceId="Mizuki")  # テキストから音声を合成

Amazon Pollyカスタマイズ

言語の変更

result = polly.synthesize_speech(
    Text="時すでに🍣…", OutputFormat="mp3", VoiceId="Takumi")

読みの指定

  • 🍣を「お寿司」と読ませたい

  • <phoneme> タグでフリガナを指定

  • TextType="ssml" 引数を追加

s = '<speak>時すでに<phoneme type="ruby" ph="おすし">🍣</phoneme>…</speak>'

result = polly.synthesize_speech(Text=s,
    TextType="ssml", OutputFormat="mp3", VoiceId="Takumi")

SSMLタグ

SSMLタグ

s1 = "mypy の目をたしょうごまかしてしまった..."

# 全体をx-slow、mypyに読み設定、「目を」のあとにbreak
s2 = ('<speak><prosody rate="x-slow">'
      '<phoneme type="ruby" ph="マイパイ">mypy</phoneme>の目を'
      '<break strength="x-strong"/>たしょうごまかしてしまった...'
      '</prosody></speak>')

Lexicon で読みをカスタマイズ

Lexicon

  • 発音レキシコン: ファイルで発音をカスタマイズ

  • <phoneme> は個別、レキシコンは共通ルール

  • 複数ファイルを用意して使い分けも可能

  • レキシコンの管理 - Amazon Polly

Lexicon

  • レキシコンの例

sushi-lexicon.xml
<?xml version="1.0" encoding="UTF-8"?>
<lexicon version="1.0"
  xmlns="http://www.w3.org/2005/01/pronunciation-lexicon"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xsi:schemaLocation="http://www.w3.org/2005/01/pronunciation-lexicon 
    http://www.w3.org/TR/2007/CR-pronunciation-lexicon-20071212/pls.xsd"
  alphabet="ipa" xml:lang="ja">
  <lexeme>
    <grapheme>🍣</grapheme><alias>おすし</alias>
  </lexeme>
  <lexeme>
    <grapheme>ozk</grapheme><alias>よろしく</alias>
  </lexeme>
</lexicon>

Lexicon

ozkですし🍣...

PythonからLexiconを使用

  • 「yukieは実質天皇()」にLexiconを適用する

tenno-lexicon.xml
<?xml version="1.0" encoding="UTF-8"?>
<lexicon version="1.0"
  xmlns="http://www.w3.org/2005/01/pronunciation-lexicon"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xsi:schemaLocation="http://www.w3.org/2005/01/pronunciation-lexicon 
    http://www.w3.org/TR/2007/CR-pronunciation-lexicon-20071212/pls.xsd"
  alphabet="ipa" xml:lang="ja">
  <lexeme>
    <grapheme>天皇</grapheme><alias>えんぺらー</alias>
  </lexeme>
  <lexeme>
    <grapheme>()</grapheme><alias>かっこ</alias>
  </lexeme>
  <lexeme>
    <grapheme>①</grapheme><alias>まるいち</alias>
  </lexeme>
  <lexeme>
    <grapheme>②</grapheme><alias>まるに</alias>
  </lexeme>
  <lexeme>
    <grapheme>③</grapheme><alias>まるさん</alias>
  </lexeme>
</lexicon>

PythonからLexiconを使用

# Lexiconを名前をつけて登録
LEXICON = "tennolexicon"
data = Path("tenno-lexicon.xml").read_text(encoding="utf-8")
polly.put_lexicon(Name=LEXICON, Content=data)

s = "yukieは実質天皇()"
# Lexiconを使用しない
result = polly.synthesize_speech(Text=s,
    OutputFormat="mp3", VoiceId="Mizuki")

# Lexiconを使用
result = polly.synthesize_speech(Text=s,
    OutputFormat="mp3", VoiceId="Mizuki",
    LexiconNames=[LEXICON])

問題文読み上げでやったこと

Lexiconを作成

  • ①、②:まるいち、まるに

  • ()〔〕:括弧

  • 〜:から

  • →:やじるし

  • +:プラス

  • ・:、 (句点と同じ空白が入る)

スペースを <break> タグに

  • 選択式の文章「〜〜を選べ ① ほげ ② ふが」

  • スペース部分を 一時停止タグ に置換

    • <break strength="x-strong"/>

フリガナを <phoneme> タグに

  • 問題文はHTML形式

  • フリガナは <ruby> タグを使用

<ruby>天皇<rt>えんぺらー</rt></ruby>
↓
<phoneme type="ruby" ph="えんぺらー">天皇</phoneme>

英語と日本語を分割

  • Mizuki 等日本語音声で英語を読ませると発音がやばい

  • 「May the Force be with you.」

  • force-ja.mp3

  • force-en.mp3

英語と日本語を分割

  • 英語と日本語に 分割 し音声読み上げ

  • 1つのmp3にまとめる

  • →いい感じの音声になりそう

英語と日本語を分割

  • 指定した言語で読み上げる関数

def text2speech(polly, text, f, lang):
    """指定した言語で読み上げ"""
    voice = {"en": "Salli", "ja": "Mizuki"}[lang]
    result = polly.synthesize_speech(Text=text,
        OutputFormat="mp3", VoiceId=voice,
        LexiconNames=[LEXICON])
    with closing(result["AudioStream"]) as stream:
        f.write(stream.read())

英語と日本語を分割

  • 正規表現で日英を分割して読み上げ

  • yoda.mp3

text = "()に当てはまる単語を選べ。 "
text += "Do, or do not. () is no try. ① They ② There ③ Then"

with open("yoda.mp3", "wb") as f:
    while m := re.search(r"(^|[\s\b])(\w[\w\s/\.,:;'\"!?]*)([\s\b]|$)", text, re.A):
        en_text = m.group(2)
        ja_text, text = text.split(en_text, maxsplit=1)
        text2speech(polly, ja_text, f, "ja")
        text2speech(polly, en_text, f, "en")
    if text.strip():
        text2speech(polly, ja_text, f, "ja")

その他Amazon Polly情報

2種類の音声

  • 標準音声とニューラル音声がある

  • ニューラル音声の方がよいいい感じ

  • 対応している声が異なる

  • ニューラル音声の方がお高い

  • Amazon Polly の音声 - Amazon Polly

2種類の音声

  • ピックアップした問題文を2種類の音声で生成

  • 聞き比べるHTMLを生成してブラウザで確認

同期処理の 文字数制限

  • synthesize_speech() での音声合成は文字数制限がある

  • 長文は start_speech_synthesis_task() で非同期処理

  • 結果はS3に保存される

  • get_speech_synthesis_task() でタスクの状態が取れる

数式読み上げ

  • 問題文ではmathjaxで数式を描画

  • svgになっているため読み上げできない

  • ライブラリを最新にあげるとMathMLも出力されるっぽい

    • 数式も読み上げられるようになるかも!?

まとめ

  • Amazon Pollyで音声合成は簡単にできる

  • 多言語に対応

  • 細かい調整も可能

  • なにかに使えるかも?

  • sample code

Thank You 🙏

slides.takanory.net

@takanory takanory takanory takanory

takanory profile kuro-chan and kuri-chan