Amazon Polly
問題文を読み上げ
数式も読み上げ

Takanori Suzuki

PyCon mini Shizuoka logo

PyCon mini Shizuoka 2024 continue / 2025 Feb 8

アジェンダ 📜

  • 背景とゴール

  • Amazon Pollyの基本

  • 読み上げをカスタマイズ

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

  • 数式読み上げ

背景 🏞️

  • 学習教材の 電子化 プロジェクト

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

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

合理的配慮

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

聴覚優位

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

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

やりたいこと

  • 問題文などを 読み上げ られる

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

    • 数式 も読み上げられる

  • 完全な読み上げじゃなくてもよい

ちなみに全盲の場合

ゴール 🥅

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

  • Pythonでの 実装方法 を知る

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

  • 数式を読み上げ る方法を知る

Photos 📷 Tweets 🐦 👍

#pyconshizu / @takanory

slides.takanory.net 💻

slides.takanory.net

Who am I? / お前 誰よ 👤

takanory profile kuro-chan and kuri-chan

PyCon JP Association 🐍

日本国内のPythonユーザのために、Pythonの普及及び開発支援を行うために、継続的にカンファレンス(PyCon)を開くことを目的とした 非営利組織

www.pycon.jp

pycon jp logo

PyCon JP 2025

静岡駅から広島国際会議場 🚅

静岡駅から広島国際会議場

BeProud Inc. 🏢

  • BeProud: Pythonシステム開発、コンサル

  • connpass: IT勉強会支援プラットフォーム

  • PyQ: Python独学プラットフォーム

  • TRACERY: システム開発ドキュメントサービス

BeProud logos

Amazon Pollyの基本 🗣️

Amaozon Polly

Amazon Pollyの画面

Amazon Pollyの画面

Amazon Pollyの画面

  • Amazon Pollyの画面からmp3をダウンロード

  • 「ボールはともだち」

  • 「The ball is my friend」

PythonからAmazon Pollyを実行

  • Boto3をインストール

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

$ python3.12 -m venv env
$ . env/bin/activate
(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

PythonからAmazon Pollyを実行

from contextlib import closing
from pathlib import Path
import boto3

polly = boto3.client('polly')  # クライアント作成
s = "ボールはともだち こわくないよ"

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

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

PythonからAmazon Pollyを実行

  • mp3ファイルができた!! clap

s = "ボールはともだち こわくないよ"

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

Amazon Pollyの基本まとめ 🗣️

  • AWSの画面 から使える

  • Pythonでは Boto3 経由で使える

読み上げをカスタマイズ 🔧

言語の変更

  • VoiceId引数で 言語と音声 を指定

  • 参考: Amazon Polly の音声

    • 日本語: Mizuki, Takumi, Kazuha, Tomoko

    • 英語: Ivy, Salli, Joey, Justinなど

result = polly.synthesize_speech(
    Text="S・G・G・K 若林源三",
    OutputFormat="mp3", VoiceId="Takumi")

読みの指定

  • 「S・G・G・K」をちゃんと読ませたい

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

  • 全体を <speak> タグで囲む

  • TextType="ssml" 引数を追加

読みの指定

# speakタグとphonemeタグ
s = ('<speak><phoneme type="ruby" ph="すーぱーぐれーと">S・G</phoneme>・'
     '<phoneme type="ruby" ph="ごーるきーぱー">G・K</phoneme>・'
     '若林源三。</speak>')

# 引数に TextType="ssml" を追加
result = polly.synthesize_speech(
    Text=s, TextType="ssml", OutputFormat="mp3", VoiceId="Takumi")

SSMLタグ

読み上げをカスタマイズまとめ 🔧

  • 言語音声 を変更できる

  • 読み を指定できる

  • SSMLタグ でカスタマイズできる

Lexicon で読みをカスタマイズ 🛠️

Lexicon とは

  • 発音レキシコン: 発音の定義ファイル

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

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

  • 参考: レキシコンの管理

Lexiconを使用する

  • Lexiconファイル(tsubasa-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>S・G・G・K</grapheme>
    <alias>スーパー・グレート・ゴール・キーパー</alias>
  </lexeme>
</lexicon>

Amazon Pollyの画面でLexiconを使用

  • 名前を付けてXMLファイルをアップロード

  • レキシコンを使用する

Lexiconをアップロード

PythonからLexiconを使用

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

s = "反動蹴速迅砲。"
# Lexiconを使用しない
result = polly.synthesize_speech(
    Text=s, OutputFormat="mp3", VoiceId="Takumi")
# Lexiconを使用
result = polly.synthesize_speech(
    Text=s, OutputFormat="mp3", VoiceId="Takumi",
    LexiconNames=[LEXICON])

  • Lexiconなし

  • Lexiconあり

Lexiconで読みをカスタマイズまとめ 🛠️

  • Lexiconファイル をXMLで作成

  • Lexiconファイルを 登録

  • 任意のLexiconを 適用

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

Lexicon を作成

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

  • ()〔〕:括弧

  • 〜:から

  • →:やじるし

  • +:プラス

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

スペースを <break> タグに

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

  • 選択肢のラベルを区切って読む

〜〜を選べ ① ほげ ② ふが

〜〜を選べ<break strength="x-strong"/>①<break strength="x-strong"/>ほげ<break strength="x-strong"/>②<break strength="x-strong"/>ふが

フリガナを <phoneme> タグに

  • 問題文はHTML形式

  • フリガナはHTMLの <ruby> タグ

<ruby>反動蹴速迅砲<rt>はんどうしゅうそくじんほう</rt></ruby>

<phoneme type="ruby" ph="はんどうしゅうそくじんほう">反動蹴速迅砲</phoneme>

日本語の英語の混ざった文章

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

    • カタカナ英語みたいになる

  • 英語の問題文は日本語と英語が 混ざっている

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

  • 1つのmp3にまとめる

日本語の英語の混ざった文章

  • 問題文の例(question.txt

次の日本語を英訳したときの()に当てはまる単語を選べ。
「ボールはともだち」
The ball is my () . 
① father ② friend ③ enemy ④ neighbour

日本語の英語の混ざった文章

  • ()、①を読ませるために Lexicon を登録

  <lexeme>
    <grapheme>()</grapheme><alias>カッコ</alias>
  </lexeme>
  <lexeme>
    <grapheme>①</grapheme><alias>まるいち</alias>
  </lexeme>
  <lexeme>
    <grapheme>②</grapheme><alias>まるに</alias>
  </lexeme>
# Lexiconを名前をつけて登録
LEXICON = "tsubasaLexicon"
data = Path("tsubasa-lexicon.xml").read_text(encoding="utf-8")
polly.put_lexicon(Name=LEXICON, Content=data)

日本語の英語の混ざった文章

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


def text2speech(polly, text, f, lang):
    """指定した言語で読み上げる"""
    # 言語で音声切り替え
    voice = {"en": "Matthew", "ja": "Takumi"}[lang]
    result = polly.synthesize_speech(Text=text,
        OutputFormat="mp3", VoiceId=voice,
        LexiconNames=[LEXICON])
    with closing(result["AudioStream"]) as stream:
        f.write(stream.read())

日本語の英語の混ざった文章

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

# ファイルから問題文読み込み
p = Path("question.txt")
text = p.read_text(encoding="utf-8")

with open("question.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")

日本語の英語の混ざった文章

  • 日本語と英語の混ざった音声ができた!! medetai

次の日本語を英訳したときの()に当てはまる単語を選べ。
「ボールはともだち」
The ball is my () . 
① father ② friend ③ enemy ④ neighbour

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

  • Lexiconを作成

  • スペースを <break> タグに

  • フリガナを <phoneme> タグに

  • 日英の混ざった文章対応

数式読み上げ 🧮

数式読み上げ 🧮

  • 数学や理科の問題文には数式がでてくる

  • 数式も読み上げたい

数式はどう表現している?

  • $$ または $ で数式を囲む

$$x = {-b \pm \sqrt{b^2-4ac} \over 2a}$$

次の式を解きなさい $(5x+4)(5x+1)$

$$x = {-b \pm \sqrt{b^2-4ac} \over 2a}$$

次の式を解きなさい $(5x+4)(5x+1)$

どんな数式 が書ける?

\begin{eqnarray} \triangle ABC \equiv \triangle DEF \end{eqnarray}

\begin{eqnarray} \varliminf_{ n \to \infty } A_n = \bigcup_{ n = 1 }^{ \infty } \bigcap_{ k = n }^{ \infty } A_k = \bigcup_{ n \in \mathbb{ N } } \bigcap_{ k \geqq n } A_k \end{eqnarray}

どうやって 数式を表示 している?

数式はSVGで表示されている

$$x = {-b \pm \sqrt{b^2-4ac} \over 2a}$$

<mjx-container class="MathJax CtxtMenu_Attached_0" jax="SVG" display="true" style="position: relative;" tabindex="0" ctxtmenu_counter="11">
  <svg style="vertical-align: -1.575ex;" xmlns="http://www.w3.org/2000/svg" width="20.765ex" height="5.291ex" role="img" focusable="false" viewBox="0 -1642.5 9178 2338.5" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true">
    <defs>
      <path id="MJX-1-TEX-I-1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path>
      <path id="MJX-1-TEX-N-3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path>
      <path id="MJX-1-TEX-N-2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path>
      <path id="MJX-1-TEX-I-1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z"></path><path id="MJX-1-TEX-N-B1" d="M56 320T56 333T70 353H369V502Q369 651 371 655Q376 666 388 666Q402 666 405 654T409 596V500V353H707Q722 345 722 333Q722 320 707 313H409V40H707Q722 32 722 20T707 0H70Q56 7 56 20T70 40H369V313H70Q56 320 56 333Z"></path><path id="MJX-1-TEX-N-221A" d="M95 178Q89 178 81 186T72 200T103 230T169 280T207 309Q209 311 212 311H213Q219 311 227 294T281 177Q300 134 312 108L397 -77Q398 -77 501 136T707 565T814 786Q820 800 834 800Q841 800 846 794T853 782V776L620 293L385 -193Q381 -200 366 -200Q357 -200 354 -197Q352 -195 256 15L160 225L144 214Q129 202 113 190T95 178Z"></path><path id="MJX-1-TEX-N-32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path><path id="MJX-1-TEX-N-34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z"></path><path id="MJX-1-TEX-I-1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z"></path><path id="MJX-1-TEX-I-1D450" d="M34 159Q34 268 120 355T306 442Q362 442 394 418T427 355Q427 326 408 306T360 285Q341 285 330 295T319 325T330 359T352 380T366 386H367Q367 388 361 392T340 400T306 404Q276 404 249 390Q228 381 206 359Q162 315 142 235T121 119Q121 73 147 50Q169 26 205 26H209Q321 26 394 111Q403 121 406 121Q410 121 419 112T429 98T420 83T391 55T346 25T282 0T202 -11Q127 -11 81 37T34 159Z"></path></defs><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><use data-c="1D465" xlink:href="#MJX-1-TEX-I-1D465"></use></g><g data-mml-node="mo" transform="translate(849.8,0)"><use data-c="3D" xlink:href="#MJX-1-TEX-N-3D"></use></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(1905.6,0)"><g data-mml-node="mfrac"><g data-mml-node="mrow" transform="translate(220,676)"><g data-mml-node="mo"><use data-c="2212" xlink:href="#MJX-1-TEX-N-2212"></use></g><g data-mml-node="mi" transform="translate(778,0)"><use data-c="1D44F" xlink:href="#MJX-1-TEX-I-1D44F"></use></g><g data-mml-node="mo" transform="translate(1429.2,0)"><use data-c="B1" xlink:href="#MJX-1-TEX-N-B1"></use></g><g data-mml-node="msqrt" transform="translate(2429.4,0)"><g transform="translate(853,0)"><g data-mml-node="msup"><g data-mml-node="mi"><use data-c="1D44F" xlink:href="#MJX-1-TEX-I-1D44F"></use></g><g data-mml-node="mn" transform="translate(462,289) scale(0.707)"><use data-c="32" xlink:href="#MJX-1-TEX-N-32"></use></g></g><g data-mml-node="mo" transform="translate(1087.8,0)"><use data-c="2212" xlink:href="#MJX-1-TEX-N-2212"></use></g><g data-mml-node="mn" transform="translate(2088,0)"><use data-c="34" xlink:href="#MJX-1-TEX-N-34"></use></g><g data-mml-node="mi" transform="translate(2588,0)"><use data-c="1D44E" xlink:href="#MJX-1-TEX-I-1D44E"></use></g><g data-mml-node="mi" transform="translate(3117,0)"><use data-c="1D450" xlink:href="#MJX-1-TEX-I-1D450"></use></g></g><g data-mml-node="mo" transform="translate(0,106.5)"><use data-c="221A" xlink:href="#MJX-1-TEX-N-221A"></use></g><rect width="3550" height="60" x="853" y="846.5"></rect></g></g><g data-mml-node="mrow" transform="translate(3121.7,-686)"><g data-mml-node="mn"><use data-c="32" xlink:href="#MJX-1-TEX-N-32"></use></g><g data-mml-node="mi" transform="translate(500,0)"><use data-c="1D44E" xlink:href="#MJX-1-TEX-I-1D44E"></use></g></g><rect width="7032.4" height="60" x="120" y="220"></rect></g></g></g></g>
  :
</mjx-container>

SVGは画像フォーマットなので
読めない… mesareta

Mathjaxを読み上げる 💬

MathJaxのAccessibility機能

  • Accessibility Features - Screen Reader Support

    The assistive-mml extension embeds visually hidden MathML alongside MathJax’s visual rendering while hiding the visual rendering from assistive technology (AT) such as screenreaders.

MathJaxのAccessibility機能

  • assistive-mml拡張によって視覚的なレンダリングの横に、隠されたMathMLを埋め込む

  • スクリーンリーダーなどはそのMathMLを読む

MathJaxの出力を再確認

$$x = {-b \pm \sqrt{b^2-4ac} \over 2a}$$

<mjx-container class="MathJax CtxtMenu_Attached_0" jax="SVG" display="true" style="position: relative;" tabindex="0" ctxtmenu_counter="11"><svg style="vertical-align: -1.575ex;" xmlns="http://www.w3.org/2000/svg" width="20.765ex" height="5.291ex" role="img" focusable="false" viewBox="0 -1642.5 9178 2338.5" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true"><defs><path id="MJX-1-TEX-I-1D465" d="M52 289Q59 331 106 386T222 442Q257 442 286 424T329 379Q371 442 430 442Q467 442 494 420T522 361Q522 332 508 314T481 292T458 288Q439 288 427 299T415 328Q415 374 465 391Q454 404 425 404Q412 404 406 402Q368 386 350 336Q290 115 290 78Q290 50 306 38T341 26Q378 26 414 59T463 140Q466 150 469 151T485 153H489Q504 153 504 145Q504 144 502 134Q486 77 440 33T333 -11Q263 -11 227 52Q186 -10 133 -10H127Q78 -10 57 16T35 71Q35 103 54 123T99 143Q142 143 142 101Q142 81 130 66T107 46T94 41L91 40Q91 39 97 36T113 29T132 26Q168 26 194 71Q203 87 217 139T245 247T261 313Q266 340 266 352Q266 380 251 392T217 404Q177 404 142 372T93 290Q91 281 88 280T72 278H58Q52 284 52 289Z"></path><path id="MJX-1-TEX-N-3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path><path id="MJX-1-TEX-N-2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path><path id="MJX-1-TEX-I-1D44F" d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z"></path><path id="MJX-1-TEX-N-B1" d="M56 320T56 333T70 353H369V502Q369 651 371 655Q376 666 388 666Q402 666 405 654T409 596V500V353H707Q722 345 722 333Q722 320 707 313H409V40H707Q722 32 722 20T707 0H70Q56 7 56 20T70 40H369V313H70Q56 320 56 333Z"></path><path id="MJX-1-TEX-N-221A" d="M95 178Q89 178 81 186T72 200T103 230T169 280T207 309Q209 311 212 311H213Q219 311 227 294T281 177Q300 134 312 108L397 -77Q398 -77 501 136T707 565T814 786Q820 800 834 800Q841 800 846 794T853 782V776L620 293L385 -193Q381 -200 366 -200Q357 -200 354 -197Q352 -195 256 15L160 225L144 214Q129 202 113 190T95 178Z"></path><path id="MJX-1-TEX-N-32" d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z"></path><path id="MJX-1-TEX-N-34" d="M462 0Q444 3 333 3Q217 3 199 0H190V46H221Q241 46 248 46T265 48T279 53T286 61Q287 63 287 115V165H28V211L179 442Q332 674 334 675Q336 677 355 677H373L379 671V211H471V165H379V114Q379 73 379 66T385 54Q393 47 442 46H471V0H462ZM293 211V545L74 212L183 211H293Z"></path><path id="MJX-1-TEX-I-1D44E" d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z"></path><path id="MJX-1-TEX-I-1D450" d="M34 159Q34 268 120 355T306 442Q362 442 394 418T427 355Q427 326 408 306T360 285Q341 285 330 295T319 325T330 359T352 380T366 386H367Q367 388 361 392T340 400T306 404Q276 404 249 390Q228 381 206 359Q162 315 142 235T121 119Q121 73 147 50Q169 26 205 26H209Q321 26 394 111Q403 121 406 121Q410 121 419 112T429 98T420 83T391 55T346 25T282 0T202 -11Q127 -11 81 37T34 159Z"></path></defs><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mi"><use data-c="1D465" xlink:href="#MJX-1-TEX-I-1D465"></use></g><g data-mml-node="mo" transform="translate(849.8,0)"><use data-c="3D" xlink:href="#MJX-1-TEX-N-3D"></use></g><g data-mml-node="TeXAtom" data-mjx-texclass="ORD" transform="translate(1905.6,0)"><g data-mml-node="mfrac"><g data-mml-node="mrow" transform="translate(220,676)"><g data-mml-node="mo"><use data-c="2212" xlink:href="#MJX-1-TEX-N-2212"></use></g><g data-mml-node="mi" transform="translate(778,0)"><use data-c="1D44F" xlink:href="#MJX-1-TEX-I-1D44F"></use></g><g data-mml-node="mo" transform="translate(1429.2,0)"><use data-c="B1" xlink:href="#MJX-1-TEX-N-B1"></use></g><g data-mml-node="msqrt" transform="translate(2429.4,0)"><g transform="translate(853,0)"><g data-mml-node="msup"><g data-mml-node="mi"><use data-c="1D44F" xlink:href="#MJX-1-TEX-I-1D44F"></use></g><g data-mml-node="mn" transform="translate(462,289) scale(0.707)"><use data-c="32" xlink:href="#MJX-1-TEX-N-32"></use></g></g><g data-mml-node="mo" transform="translate(1087.8,0)"><use data-c="2212" xlink:href="#MJX-1-TEX-N-2212"></use></g><g data-mml-node="mn" transform="translate(2088,0)"><use data-c="34" xlink:href="#MJX-1-TEX-N-34"></use></g><g data-mml-node="mi" transform="translate(2588,0)"><use data-c="1D44E" xlink:href="#MJX-1-TEX-I-1D44E"></use></g><g data-mml-node="mi" transform="translate(3117,0)"><use data-c="1D450" xlink:href="#MJX-1-TEX-I-1D450"></use></g></g><g data-mml-node="mo" transform="translate(0,106.5)"><use data-c="221A" xlink:href="#MJX-1-TEX-N-221A"></use></g><rect width="3550" height="60" x="853" y="846.5"></rect></g></g><g data-mml-node="mrow" transform="translate(3121.7,-686)"><g data-mml-node="mn"><use data-c="32" xlink:href="#MJX-1-TEX-N-32"></use></g><g data-mml-node="mi" transform="translate(500,0)"><use data-c="1D44E" xlink:href="#MJX-1-TEX-I-1D44E"></use></g></g><rect width="7032.4" height="60" x="120" y="220"></rect></g></g></g></g></svg>
<mjx-assistive-mml unselectable="on" display="block">
  <math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
    <mi>x</mi><mo>=</mo>
    <mrow data-mjx-texclass="ORD">
      <mfrac>
        <mrow><mo>−</mo><mi>b</mi><mo>±</mo><msqrt><msup><mi>b</mi><mn>2</mn></msup><mo>−</mo><mn>4</mn><mi>a</mi><mi>c</mi></msqrt></mrow>
        <mrow><mn>2</mn><mi>a</mi></mrow>
      </mfrac>
    </mrow>
  </math>
</mjx-assistive-mml>
</mjx-container>

なんか読めそう!!👍

MathMLだけ取り出す

<math xmlns="http://www.w3.org/1998/Math/MathML" display="block">
  <mi>x</mi><mo>=</mo>  # x =
  <mrow data-mjx-texclass="ORD">
    <mfrac>  # 分数(fraction)
      <mrow>  # 分子
        <mo>−</mo><mi>b</mi><mo>±</mo>  # -b±
        <msqrt>  # ルート(sqrt)
          <msup><mi>b</mi><mn>2</mn></msup>  # b 2乗
          <mo>−</mo><mn>4</mn><mi>a</mi><mi>c</mi>  # -4ac
        </msqrt>
      </mrow>
      <mrow><mn>2</mn><mi>a</mi></mrow>  # 分母(2a)
    </mfrac>
  </mrow>
</math>

MathMLとは

MathMLの主な要素

  • <mi>: 識別子(a, b, x, y等)

  • <mo>: 演算子(+、-等)

  • <mn>: 数字

  • <mfrac>: 分数

  • <msqrt>: ルート

  • <msup><msub>: 上付き、下付き

  • <mover>: 弧で使用

読み上げテキスト作成 ✍️

MathMLをBeautiful Soup 4で解析 🥣

(venv) $ pip install beautifulsoup4
from bs4 import BeautifulSoup

def mathml_to_text(text: str) -> str:
    """MathMLを取りだして読み上げ文字列にする"""
    soup = BeautifulSoup(text, "html.parser")
    for math in soup.find_all("math"):
        # ここで変換処理

    # 最後に全部テキストにする
    return soup.text

識別子演算子 を変換

# 識別子、演算子と読みの対応表
OPERATORS = {
    "±": "プラスマイナス",
    "+": "プラス",
    "−": "マイナス",
}
        # 識別子、演算子を変換する
        for mo_mi in math.find_all(["mo", "mi"]):
            if mo_mi.text in OPERATORS:
                mo_mi.string = OPERATORS[mo_mi.text]

分数に対応

$\dfrac{1}{2}$ を「2ぶんの1」と読ませる

# MathML
<mfrac>
  <mn>1</mn>
  <mn>2</mn>
</mfrac>
        # 分母と分子の順番を逆にする
        for mfrac in math.find_all("mfrac"):
            bunshi, bunbo = mfrac.contents
            mfrac.clear()
            mfrac.append(bunbo)
            mfrac.append("ぶんの")
            mfrac.append(bunshi)

2乗に対応

$x^2$ を「x2乗」と読ませる

# MathML
<msup>
  <mi>x</mi>
  <mn>2</mn>
</msup>
        # <msup><mi>x</mi><mn>2</mn></msup> を「x2乗」と読む
        for msup in math.find_all("msup"):
            exponent = msup.contents[1]
            exponent.string = f"{exponent.text}乗"

ルートに対応

$\sqrt{5}$ を「ルート5」と読ませる

# MathML
<msqrt>5</msqrt>
        # <msqrt> を「ルート」と読む
        for msqrt in math.find_all("msqrt"):
            msqrt.insert(0, "ルート")

数式読み上げ

二次方程式 $ax^2 + bx + c = 0$ の解は

$$x = {-b \pm \sqrt{b^2-4ac} \over 2a}$$

# 読み上げ用テキスト
二次方程式 ax2乗プラスbxプラスc=0 の解は
x=2aぶんのマイナスbプラスマイナスルートb2乗マイナス4ac

おしい… wink

  • ax → あっくす

  • 2a → にあーる

まとめ good

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

  • 多言語に対応

  • SSMLLexiconで細かい調整が可能

  • 数式MathMLの解析で読み上げられる

  • サンプルコード: code

お知らせ

Thank You pray

slides.takanory.net

takanory takanory takanory takanory

takanory profile kuro-chan and kuri-chan