Microsoft Edge TTS サービスについての、やや多めのメモ

情報が散らかりすぎててめっさストレス。

2022-09-18追記:残念なおしらせ

python の edge_tts を久しぶりに 4.x のまま使ってまったく動作せず、アップグレードしたら「ますます動かなくて」どうしたもんかと思ったら:

だめじゃん…。てわけで、このページに書いたほぼすべての情報が廃れてしまった。

Microsoft Edge TTS サービスについての、やや多めのメモ

全体的な話

各論に入る前に総論

「Microsoft Edge TTS サービス」の満足度、について、最初に強く言っておきたい。

「7割の満足度に即座に到達するものの、残り3割を埋めるのはほとんど絶望的」、こんな感じである。

特に Google の TTS サービスや eSpeak、あるいは VOICEROID なんかと比較するとよーくわかるのだが、「何も考えずに結構な長文を何の制御もせずにそのまま読み上げさせ」た場合の読み上げの自然さ、これはひじょーーーーに優れている。しかも読み間違え率がかなり低くて、500文字程度のテキストで、数箇所間違える程度で済むことが多いので、この「素の状態で使う」満足度は非常に高いのだ。いやほんと、Google のと是非較べてみ、ttsfree.com で実際に比較出来るから。

そして、その「残り3割を埋めるのはほとんど絶望的」の詳細について書こう、というのが、今回のネタの本題、てこと。最初の7割のスタートダッシュだけならこのサービスは優れていたとしても、なので結果的により細かいコントロールが可能な VOICEROID の方がより良い読み上げになる可能性がある、てことね。

今回のネタに至る前の道程

ワタシの「Text To Speech」の動機で当初大きかったのは二つ。一つは英語の勉強、もう一つは「ffmpeg などの、ビデオ・オーディオ操作におけるテスト用メディアなど、何らかの技術検証のためのバリエーション増やし」。後者が特に大きくて、とりわけ「Youtube などにアップロードするための素材として、ライセンスなどの問題に煩わされない「単純すぎず複雑すぎない恣意的なテスト用オーディオ」」の必要性。ゆえに、「手早く作れるなら手段は問わないが出来るだけコストが小さいアプローチが良い」というノリで使えるものを探っていた、というわけだ。(無論何かの技術説明でナレーションを入れたいなぁ、てのも、少しは考えてた。ワタシがあげてきた動画って、よくて字幕だけだったけど、ナレーションで説明したほうがいいかもしれないもの、てのは、なかったわけではない。)

そんな動機でもって展開していたのがこれら

これは Windows で動くエンジンとボイスを「インストールして Powershell 経由で使う」というネタとして書いたのだけれど、追記で書いた通り、Windows 10 で「日本語ボイスが動かない」事象からどうしても抜け出すことが出来ず諦め、その代わりに見つけたのが、今回のお題の「Microsoft Edge TTS Server」だった、てこと。

最初の動機がミニマルだったため、PowerShell で「オフラインで」やってたネタも最小限しかやってきてないんだけれど、ここ最近のワタシの動機が「フリーのテキストを朗読させたい」にシフトしてきたので、TTS に芝居させることがちょっと重要になってきてるのね。そうなんだけれど、その「PowerShellで」ネタが、もはやワタシの Windows 10 機で(日本語ボイスについては)続けられなくなってしまったので、そうした「細かい制御」は、ワタシにはこの Microsoft Edge’s TTS 相手が初体験、てことになるのよね。そうして「ちゃんとした制御」をイザやろうとしてみると…。

多くの技術って「スタートダッシュ」部分の理解を補うだけで多くの人はそれだけで「始められる」ことが多いのよ、だからワタシが書くネタの多くがその「はじめの(適切な)一歩」を提供しようとする意図のものが多いのね。このサービスについてもそれで済むなら本当はそうしたいんだけれど、全然そういうわけにいかなくてな。ここ数週間ほど実際に色々実用的な使い方をしてみていたんだけれど、かなり注意点とか制限事項が多いのよ。特に「要求仕様と現実の実装状況の乖離」もしくは「undocumented」が結構多くて。これはもう諦めて「ちゃんとしたメモ」を書くしかないわ、と。

今回書くメモのもしかしたら全ては、元のオフラインでやってたヤツでも同じなのかもしれないけれども、今回のネタはあくまでも「Microsoft Edge’s TTS Server についての話」としての話に徹する。オフラインでのネタも英語ボイスについては動くので、余力があったらそれとの対応関係も追記の形では書くかもしれないけれど、それは気力があれば、の話。ひとまずは「Microsoft Edge’s TTS Server についての話」とだけしておく。

「読み上げさせた音声」の利用規約的な話

一番大事、のような話だと思うのだが、どうやっても「公式な」利用規約の記述にたどり着かずに結構長時間困り果てていたのだが、「microsoft edge tts terms of service」というごく自然な検索ワードで見つかった。うん、何してたんだワタシは…。ここね:
(この全文に対するGoogle翻訳はちゃんと読めるものになるので、英語が苦手ならそちらをお試せばよろしい。)

著作権についてはこれが公式の見解だということだと思うけれど、たとえば「18禁音声には利用出来ない」とかみたいな約束事があるのかないのかが、やっぱりわからんのよね…。なんでこういう大事なのが、すぐにアクセス出来る場所にないわけ?? (例えばここからすぐに辿り着けて欲しいんだよね…。)

公式ドキュメントが存在しないんだよ、なんでなんだ

これで辿り着いた「edge-tts」のコードを読むと、Microsoft Edge’s TTS Server がどこでホストされているのかはわかる:

edge_tts/constants.py
 1 """
 2 Constants for the edgeTTS package.
 3 """
 4 
 5 TRUSTED_CLIENT_TOKEN = "6A5AA1D4EAFF4E9FB37E23D68491D6F4"
 6 WSS_URL = (
 7     "wss://speech.platform.bing.com/consumer/speech/synthesize/"
 8     + "readaloud/edge/v1?TrustedClientToken="
 9     + TRUSTED_CLIENT_TOKEN
10 )
11 VOICE_LIST = (
12     "https://speech.platform.bing.com/consumer/speech/synthesize/"
13     + "readaloud/voices/list?trustedclienttoken="
14     + TRUSTED_CLIENT_TOKEN
15 )
edge_tts/communicate.py (chrome 拡張を移植したのかなこれは)
257 # ...
258         async with aiohttp.ClientSession(trust_env=True) as session:
259             async with session.ws_connect(
260                 f"{WSS_URL}&ConnectionId={connect_id()}",
261                 compress=15,
262                 autoclose=True,
263                 autoping=True,
264                 headers={
265                     "Pragma": "no-cache",
266                     "Cache-Control": "no-cache",
267                     "Origin": "chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold",
268                     "Accept-Encoding": "gzip, deflate, br",
269                     "Accept-Language": "en-US,en;q=0.9",
270                     "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
271                     " (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36 Edg/91.0.864.41",
272                 },
273             ) as websocket:
274                 for message in messages:
275                     # ...

これをヒントに探してみても、このサービスについてのドキュメントは見つからない。同じ Microsoft のものなのだから、根底にあるものはおそらく以下と同じものなのだろうとは思う:

けど、これは via http に仕立て上げられたサービス固有のドキュメントそのものではない。たとえば「使えるボイスの列挙方法」みたいな API のドキュメントは、少なくともワタシは見つけられてない。そういうわけで、ドキュメントがない中の試行錯誤が必要だ、ということ。近しいドキュメントと実態に乖離がないならいいんだけどさ、あるんだわ、かなり。そいつぁ弱ったなと。とりわけ SSML サポートについて、RFC が求めるものとかなり違ってる。

Microsoft Edge’s TTS Server へのアクセス方法いくつか

いくつかといっても、ワタシは今のところ二つしか使ってない。

  1. ttsfree.com
  2. edge-tts

前者が「オンラインツール」として公開されているヤツなのだが、「無登録利用の場合は500文字マデ」という、なんというか「不当な」制限を持ったツール。解せん、でしょ? 気楽に使える良さがあるにせよ、だよ。そして後者が Python 製だけれど、サービスにリクエストをぶん投げてるだけね、そういう仕組なので、「Windows でしか使えない」ことはなくて、なんなら Unix からでも android からでも使える、てことになる。オフラインなのは見かけというかユーザインターフェイスがオフラインなだけであって、中身はオフラインではない(インターネット前提てこと)。

たぶんねぇ、名前の通りの「Microsoft Edge ブラウザ」のアドオンがあるはずで、それがきっと限りなく公式、なのだと思うのね。Python 製の edge-tts も、おそらくそれをストレートに移植したもの。同じように、きっと Chrome 用のそれもある。そもそも「Microsoft Edge ブラウザ」って、コードベースは Chronium だからさ、Edge 用のアドオンが書かれたなら、それを Chrome に持ってくるのは簡単なハズ。拡張の仕組みを踏襲してれば、だけどね。ワタシ、「Microsoft Edge ブラウザ」は起動した経験すらほぼないんだよね、興味ないから。だって Chrome とほぼ同じなら、別に Chrome だけでええやんけ、て気分になっちゃうもん。

そういうわけで、ワタシがここで挙げる実例は全て Python の edge-tts を前提にしたものとする。しかも SSML 利用だけを考え、edge-tts の使い方もその用法固定とする:

 1 [me@host: ~]$ # pip でインストールしたとして、その場合、モジュールだけでなく
 2 [me@host: ~]$ # main エントリとなるスクリプトもインストールされる。Windows の
 3 [me@host: ~]$ # たとえば「c:/Program Files/Python39/Scripts」にパスが通って
 4 [me@host: ~]$ # いれば、edge-tts を直接起動出来る。-z は、入力が SSML 文書
 5 [me@host: ~]$ # であることを edge-tts.exe に教えるオプション。
 6 [me@host: ~]$ edge-tts.exe -f my_speech.xml -z \
 7 > -c 'audio-48khz-96kbitrate-mono-mp3' > my_speech.mp3
 8 [me@host: ~]$ #
 9 [me@host: ~]$ # Python の基礎だが一応以下も説明しとく:
10 [me@host: ~]$ py -3 -m edge_tts -f my_speech.xml -z \
11 > -c 'audio-48khz-96kbitrate-mono-mp3' > my_speech.mp3
12 [me@host: ~]$ # ↑「py」は Windows にしかないランチャ。ほかの環境の場合たとえば
13 [me@host: ~]$ /usr/local/bin/python3 -m edge_tts -f my_speech.xml -z \
14 > -c 'audio-48khz-96kbitrate-mono-mp3' > my_speech.mp3
15 [me@host: ~]$ #
16 [me@host: ~]$ # 入力を標準入力としたい場合は、-f の引数として(Windows であっても)
17 [me@host: ~]$ # /dev/stdin を渡す:
18 [me@host: ~]$ sed 's@foo@bar@g' my_speech.xml | \
19 > edge_tts -f '/dev/stdin' -z > my_speech.mp3
20 [me@host: ~]$ # ↑ただしこれ、MSYS ユーザは実行出来ないので注意。理由はやって
21 [me@host: ~]$ #  みればわかる。回避出来ない MSYS の乱暴な仕様のせい。

上のでわからない人向けビデオ(Windows のコマンドプロンプトから使ってる例である):

これだけなのだが、ちょっと注意すべきは、「安定性」の問題があって、これは「時々原因不明のエラーで失敗する」ということと、「記述間違えなどのちゃんとした理由がある失敗との区別が出来ない」ということだけは言っておく。かなりヒドい作りになってて、エラー報告は全くなされない。「なんか死んだ」と言われるだけ。キツいが、とりあえずこれはしょうがない、諦めておこう、今のところ。前者だった場合は数回繰り返すとうまくいく。数回繰り返してもうまくいかない場合は、SSML の記述に問題がある。

「-c として他に何を与えることが出来るか」などの細かい話が若干あるにしても、基本的にはこれ以上の説明はなくて、なので、本題の話は全て SSML 記述の話になる。

ところで、上の実行例で触れた「MSYS だと出来ないのよ」問題はこれはもうどうしようもないのだが、どうにかしたいなら edge_tts をモジュールとして使って自作スクリプトを書いてしまえばいい。幸いワタシの場合は「SSML を直接渡す」以外の使い方はしないので、いっそ元の main よりも単純化してしまって:

my_edge_tts.py (1行目の「#! py -3」はご自分の環境に合わせて適宜変えてくれ)
 1 #! py -3
 2 # -*- coding: utf-8 -*-
 3 import argparse
 4 import asyncio
 5 import io
 6 import sys
 7 
 8 from edge_tts import Communicate, SubMaker
 9 
10 
11 async def _tts(args):
12     tts = Communicate()
13     subs = SubMaker(args.overlapping)
14     if args.write_media:
15         media_file = io.open(args.write_media, "wb")  # pylint: disable=consider-using-with
16     async for i in tts.run(
17         args.text,
18         sentence_boundary=args.enable_sentence_boundary,
19         word_boundary=args.enable_word_boundary,
20         codec=args.codec,
21         customspeak=True,
22     ):
23         if i[2] is not None:
24             if not args.write_media:
25                 sys.stdout.buffer.write(i[2])
26             else:
27                 media_file.write(i[2])
28         elif i[0] is not None and i[1] is not None:
29             subs.create_sub(i[0], i[1])
30     if args.write_media:
31         media_file.close()
32     if not args.write_subtitles:
33         sys.stderr.write(subs.generate_subs())
34     else:
35         with io.open(args.write_subtitles, "w", encoding="utf-8") as file:
36             file.write(subs.generate_subs())
37 
38 
39 async def _main():
40     parser = argparse.ArgumentParser(description="Microsoft Edge TTS")
41     parser.add_argument("file", help="what TTS will say, from file")
42     parser.add_argument(
43         "-c",
44         "--codec",
45         help="codec format. Default: audio-24khz-48kbitrate-mono-mp3. "
46         "Another choice is webm-24khz-16bit-mono-opus. "
47         "For more info check https://bit.ly/2T33h6S",
48         default="audio-24khz-48kbitrate-mono-mp3",
49     )
50     parser.add_argument(
51         "-s",
52         "--enable-sentence-boundary",
53         help="enable sentence boundary",
54         action="store_true",
55     )
56     parser.add_argument(
57         "-w",
58         "--enable-word-boundary",
59         help="enable word boundary",
60         action="store_true",
61     )
62     parser.add_argument(
63         "-O",
64         "--overlapping",
65         help="overlapping subtitles in seconds",
66         default=5,
67         type=float,
68     )
69     parser.add_argument(
70         "--write-media", help="instead of stdout, send media output to provided file"
71     )
72     parser.add_argument(
73         "--write-subtitles",
74         help="instead of stderr, send subtitle output to provided file",
75     )
76     args = parser.parse_args()
77     if args.file == "-":
78         args.text = sys.stdin.read()
79     else:
80         with io.open(args.file, "r", encoding="utf-8") as file:
81             args.text = file.read()
82 
83     await _tts(args)
84 
85 
86 def main():
87     """
88     Main function.
89     """
90     asyncio.run(_main())
91 
92 
93 if __name__ == "__main__":
94     main()

オリジナルよりも指定は単純化し、「-f/-t」ではなく位置引数としてファイル名を必ず取るようにしつつ、標準入力指示は「/dev/stdin」ではなく「-」に変え、また、SSML を必ず使うことにするので「-z」オプションを指定させるのをやめた、てことね。ワタシのこれを使う場合は:

 1 [me@host: ~]$ # my_edge_tts.py をパスが通った場所に置いたとして:
 2 [me@host: ~]$ my_edge_tts.py my_speech.xml \
 3 > -c 'audio-48khz-96kbitrate-mono-mp3' > my_speech.mp3
 4 [me@host: ~]$ #
 5 [me@host: ~]$ # 入力を標準入力としたい場合は、「-」
 6 [me@host: ~]$ sed 's@foo@bar@g' my_speech.xml | \
 7 > my_edge_tts.py - > my_speech.mp3
 8 [me@host: ~]$ #
 9 [me@host: ~]$ # 入力のエンコーディングが問題となる場合は環境変数で
10 [me@host: ~]$ sed 's@foo@bar@g' my_speech.xml | \
11 > PYTHONIOENCODING=utf-8 my_edge_tts.py - > my_speech.mp3

というかゴメン、↑この例は今度は MSYS みたいな Unix もどき前提ね。マジックナンバー「#! py -3」みたいなのを解釈できるのは MSYS などの bash (のような Unix ライクなシェル)であって、Windows そのものにはない機能。

ちなみにワタシは

XML ならではの「冗長性」というのもあるし、下で説明してる「break のアホさ」みたいなのもあるから、ワタシは「テキストエディタで頑張って SSML を書く」なんてことはしてない。今のワタシのマイブームが Go 言語だもんで、Go の jet(を使った自作コマンドラインツール)で書いている。

結構記述が鬱陶しいでしょ、ので、何かそういうものに頼ることは考えたほうがいいと思うよ。ワタシのを使ってもらってもいいし、何かほかの汎用テンプレートエンジンを見繕うのでもいいから。老舗だと java の FreeMarker とかもいいだろうし、Python をあてにするなら jinja2、Mako など、Ruby だったら ERB、みたいに、探せばちょっと記述が楽になる道具立ては出来るハズ。ワタシのだと例えば:

base_common.jet
 1 {*- most outer container -*}
 2 {{- block speak(lang="ja-JP") -}}
 3 <speak version='1.0'
 4  xmlns='http://www.w3.org/2001/10/synthesis' xmlns:mstts='https://www.w3.org/2001/mstts' xml:lang='{{lang}}'>
 5 {{- yield content -}}
 6 </speak>
 7 {{- end -}}
 8 
 9 {*- voice element -*}
10 {{- block voice(lang="ja-JP", voice="KeitaNeural") -}}
11 <voice name='Microsoft Server Speech Text to Speech Voice ({{lang}}, {{voice}})'>
12 <mstts:silence type="Leading" value="0ms"/>
13 <mstts:silence type="Tailing" value="0ms"/>
14 <mstts:silence type="Sentenceboundary" value="0ms"/>
15 {{- yield content -}}
16 </voice>
17 {{- end -}}
18 
19 {*- prosody element -*}
20 {{- block prosody(pitch="", rate="", range_="", volume="90", contour="") -}}
21 <prosody
22 {{- if pitch }} pitch="{{pitch}}"{{ end -}}
23 {{- if rate }} rate="{{rate}}"{{ end -}}
24 {{- if range_ }} range="{{range_}}"{{ end -}}
25 {{- if volume }} volume="{{volume}}"{{ end -}}
26 {{- if contour }} contour="{{contour}}"{{ end -}}
27 >
28 {{- yield content -}}
29 </prosody>
30 {{- end -}}
31 
32 {*- break element -*}
33 {{- block break(time="1s") -}}
34 <break time="{{time}}"/>
35 {{- end -}}
36 
37 {{ block break_s(sec=5) }}
38 {{- if sec }}
39 {{- if sec < 5 }}
40 <break time="{{sec}}s"/>
41 {{- else }}
42 {{- range make_seq(sec / 5) }}
43 {{- nxt := ((. + 1) * 5) }}
44 {{- if nxt > sec }}{{ nxt = sec }}{{ end -}}
45 {{- t := nxt - (. * 5) -}}
46 <break time="{{t}}s"/>
47 {{ end -}}
48 {{ end -}}
49 {{ end -}}
50 {{ end }}
51 
52 
53 {*- p element -*}
54 {{- block p(pause=2.3) -}}
55 <p>
56 {{- yield content -}}
57 {{- yield break_s(sec=pause) -}}
58 </p>
59 {{- end -}}
60 
61 {*- s element -*}
62 {{- block s(pause=1.0, pitch="", rate="", volume="") -}}
63 <s>
64 {{- if pitch || rate || volume -}}
65 {{- yield prosody(pitch=pitch, rate=rate, volume=volume) content -}}{{- yield content -}}{{- end -}}
66 {{- else -}}
67 {{- yield content -}}
68 {{- end -}}
69 {{- yield break_s(sec=pause) -}}
70 </s>
71 {{- end -}}
72 
73 {{- block ph(txt, ph) -}}
74 <phoneme alphabet="sapi" ph="{{ph | raw}}">{{txt}}</phoneme>
75 {{- end -}}
doc.jet
 1 {{ import "./base_common.jet" }}
 2 {{ yield speak() content }}
 3 {{ yield voice(voice="NanamiNeural") content }}
 4 
 5 {{ yield p() content }}
 6 {{ yield s() content }}楽な姿勢を探しましょう。{{ end }}
 7 {{ yield s() content }}両<w>{{ yield ph(txt="腕", ph="ウデ'") }}</w>も
 8 やや広げて。{{ end }}
 9 {{ end }}
10 
11 {{ end }}
12 {{ end }}

base_common.jet は一度書いてしまえば何度も使う共通。目的の SSML の元となる doc.jet はこの base_common.jet に書いたマクロを活用して書く、てこと。個人的には prosody と phoneme が剥き身で書くと特にウザいと思ってる、ので、このテンプレートで随分ストレス軽減出来てる。そしてもっといえば、この「doc.jet」ですら記述量が多くて大変だ、てことで、プレインテキストを入力としてこの形式に変換するジェネレータスクリプトなんてのも実際は駆使してる。

個々のメモたち

気付いたものについての列挙、という形になる。つまり綺麗に整理して、という形にはしない。一応大事な順という「心意気」だけは持つつもりだけれど。気付いたことが増えたら、追記していくかもしれない。

なお、実例を示すのが難しくて、なので実例には期待しないで。他人の書いたテキスト相手に発覚した問題の場合、そのテキストには自動的に著作権が発生するので、無断転載は出来ないので、紹介するには小さくして別のテキストにして再現させる必要があるのだけれど、以下で説明していく通り「全文読ませた場合と部分を抜き出した場合とで結果が異なる(文脈に依存するということ)」ため、再現するパターンを生み出せないことが非常に多いの。ので「紹介した SSML を実行するとワタシが言ったとおりになる」ことは期待しないで。

二つの基本構造と、lexicon についてのメモ

「まずは何も考えずに最小のお試し」という意味だと以下となる:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xml:lang='ja-JP'>
 4 <voice
 5     name='Microsoft Server Speech Text to Speech Voice (ja-JP, NanamiNeural)'>
 6 <p>
 7 <s>テキスト。</s>
 8 </p>
 9 </voice>
10 </speak>

これが基本構造の一つ目であり、本来の SSML だけで理解出来る中でも最もプリミティブなもの。

これとともに「MS 拡張」があって、これの基本構造はこうなる:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='ja-JP'>
 5 <voice
 6     name='Microsoft Server Speech Text to Speech Voice (ja-JP, NanamiNeural)'>
 7 <!--msttsの例:
 8 <mstts:silence type="Leading" value="0ms"/>
 9 <mstts:silence type="Tailing" value="0ms"/>
10 <mstts:silence type="Sentenceboundary" value="0ms"/>
11 -->
12 <p>
13 <s>テキスト。</s>
14 </p>
15 </voice>
16 </speak>

この MS 拡張部分とそうでない部分について、ごっちゃに Improve synthesis with Speech Synthesis Markup Language (SSML) に書いてある。拡張部分なのかどうかは「mstts:」プレフィクスが付いてるかどうかで区別するしかない。

もう一つ示しておきたいものは、これは「SSML」の枠内に収まる話なので「本来は特筆すべき、というほどのものではない」のだが、「via HTTP で給仕されるサービス」の宿命とでも言うべき話。それは lexicon について。これ、ワタシは実際に試みてもいないんだけれど、おそらく「Microsoft Edge’s TTS Server」相手で使う場合は、たとえ使えたとしても実用にはならないと思うの。あとで触れる予定の「audio」などもそうなのだが、よく考えれば当り前だけど、SSML ドキュメントからみた「外」のリソースを指す場合は、それは「「Microsoft Edge’s TTS Server」からみて可視」でなければならないわけね。つまり、lexicon が指す uri は、「オレが全世界に向けて公開」するか「誰かが書いて公開してくれたもの」を使うしかないわけ。つまり、これを自分で見繕うことは、たぶん全然現実的じゃない。というわけで、たぶん「lexicon は実用にならない」とみなしてしまって支障ないかなと。これに限らず「SSML ドキュメントからみた「外」のリソースを指す」ものは、近い制約になる可能性がある、ということね。

実際に動かして確認していないので間違ってたらゴメン、なのだが、lexicon は phoneme 集辞書、だと理解している。つまり「使えれば便利だが不可欠なものではない」と思っている。実際ワタシ自身はこのブログを公開しているホストに配置するなどでこの lexicon を、使おうと思えば使える手段は持っているけれど、そういうわけなので「そこまでして使おうとしなくてもいいのでは」と思ったのね。ので、ワタシはこれはやらない。どうしても使いたい人は、是非「皆様向けお役立ち lexicon」を全世界に公開しとくれ。

利用可能なボイスの一覧

edge-tts 機能で可能なことになっているが期待しないこと。何かしらの問題があって、ちゃんと全部列挙出来ない。ので、ttsfree.com の一覧を参考にすると良い。

lang=”ja-JP” のものは、2022年3月時点で二つだけ。KeitaNeural と NanamiNeural。

ボイス違いは別物

ロケールが違えば別物となるのは納得出来ると思うけれど、ボイスを、日本語なら Nanami から Keita に変えることで「読み方が変わってしまう」なんてことが起こる…のは、実は上に貼り付けたビデオで実例は既に見せた。

「男女両用」として男声・女声を自由に切り替えたい場合は注意して欲しい。一方でうまくいった記述が他方でうまくいかない、なんてことは結構日常茶飯事。ゆえに、「ボイスだけ変更するだけでいい」という管理の仕方はたぶん諦めたほうがいい。各々 Nanami 専用・Keita 専用、として管理するほうが、きっとうまくいく、と思う。管理ファイルは増えて大変とは思うけれど。(テンプレートエンジンみたいなものを使うんであれば、それこそ条件分岐するマクロ、みたいなのを書けばいいんだけどさ。)

break の「アホ」な制約

これについてはGo 言語によるテンプレートエンジンの話題の中で既に書いた。以下は「正しい記述で、正当であり、なんら責を負うべき記述ではない」:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='ja-JP'>
 5 <voice
 6     name='Microsoft Server Speech Text to Speech Voice (ja-JP, NanamiNeural)'>
 7 <p>
 8 <s>すってー<break time="6s"/>はいてー<break time="8s"/></s>
 9 </p>
10 </voice>
11 </speak>

けれどもこれは、意図した結果にはならない。どんな大きな値を指定しても、エラーとなるでもなく「黙って5秒に切り詰められる」という非常に迷惑な振る舞いをする。Microsoft 自身の説明は「This value should be set less than 5,000 ms.」であり、「黙って捨てる」なんて書いてない。実装者のスキルレベルが低過ぎるんじゃないのか。これは「仕様通り」とは到底言えない。これをするならせめてエラーにしてくれ。と思うよ、思うけどね、ともかく今現状そうなので、もうね、「繰り返し呼び出す」しかないのよ:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='ja-JP'>
 5 <voice
 6     name='Microsoft Server Speech Text to Speech Voice (ja-JP, NanamiNeural)'>
 7 <p>
 8 <s>すってー<break time="5s"/><break time="1s"/>はいてー<break time="5s"/><break time="3s"/></s>
 9 </p>
10 </voice>
11 </speak>

volumeの制御はメチャクチャ

下に書く「読み方の指示についての色々」に含まれるべき内容だが、これだけ先に別途。

「ボリュームの指示」は3つの場所に含まれうる。一つは下で説明する prosody で、あと2つは audiomstts:backgroundaudio

audio エレメントについては、たぶん Extended Profile だから、だろう、soundLevel 指定は無視される模様:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='ja-JP'>
 5 <voice name='Microsoft Server Speech Text to Speech Voice (ja-JP, NanamiNeural)'>
 6 <p>
 7 <s>
 8 布がこすれる音。
 9 <audio
10     src='https://on-jin.com/sound/ag/s1857e1af4/se/c/sei_ge_nuno_kosu01.mp3'
11     speed="400%"
12     soundLevel="-6dB"
13 >
14 </audio>
15 <break time="400ms"/>
16 </s>
17 </p>
18 </voice>
19 </speak>

なんだかんだこれ大問題で、効果音使用の大きな妨げになってしまう。読み上げの音声とのバランスをこれで調整出来ないことが理由で「その効果音を使えない」という決断にならざるを得なかったりするから。「予め加工しておく」ことが現実的に難しいことも理解出来る? 先にちょっと言った通り「MS TTS Server から可視」のリソースしか指せないんだよ、つまり MS TTS Server のホストが HTTP GET で取得出来る「全世界から見える」ものでなければならない、ということだから、加工して配布するのを禁止しているような効果音はもうそれだけでアウト、てことね。(あと、後述のように、仮に無視されない場合でも、おそらく「dB指定」が理由で効かない、てなことにはなると思う。)

mstts:backgroundaudio については、過去のワタシが使うのを諦めた形跡があって、なんでだろうとやり直してみたら、「volume 指定以前にめちゃくちゃ」だった。例えばサンプルそのまんまの以下:

1 <speak version="1.0" xml:lang="en-US" xmlns:mstts="http://www.w3.org/2001/mstts">
2     <mstts:backgroundaudio src="https://contoso.com/sample.wav" volume="0.7" fadein="3000" fadeout="4000"/>
3     <voice name="Microsoft Server Speech Text to Speech Voice (en-US, JennyNeural)">
4         The text provided in this document will be spoken over the background audio.
5     </voice>
6 </speak>

これは、https://contoso.com/sample.wav が現存してないので「単に無視される」が、これを実在する例えばこれに差し替えると、空の結果が返ってくる。最初に上で説明した通り、何らか問題があってもその報告は一切見えないので、何が起こったのかは一切不明。とにかく mstts:backgroundaudio はワタシはこれが理由で「なんかダメっぽい」として諦めたらしい。ただこれ、仮に使えたとしても volume 指定の説明がオカシイ:

Specifies the volume of the background audio file. Accepted values: 0 to 100 inclusive. The default value is 1.

実例として「volume=”0.7″」を与えておいて「0 to 100 inclusive、デフォルトは1」って、あからさまに「float の 0.0~1.0と間違えてるんだろう」とわかる。この混乱は prosody でちゃんと起こっているので、mstts:backgroundaudio のも多分そうだろう。

結局のところボリュームの指示がちゃんと有効なのは prosody だけということになるのだが、結論としては「volume 指定は効くがメチャクチャ」である。メチャクチャ、というのは…以下はおそらく「non-negative percentage value」のつもりなのだと思う:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='ja-JP'>
 5 <voice name='Microsoft Server Speech Text to Speech Voice (ja-JP, NanamiNeural)'>
 6 <p>
 7 <s>
 8 <prosody volume="30">
 9 RFC で言うところの volume="30%" らしいよ、これでも。
10 </prosody>
11 </s>
12 <s>
13 <prosody volume="100">
14 RFC で言うところの volume="100%" らしいよ、これでも。
15 </prosody>
16 </s>
17 </p>
18 </voice>
19 </speak>

じっくりドキュメントを熟読してみるに、もしかしたらこれは SSML バージョンの差異なのかもしれない。MSTTS ドキュメントの説明はこう:

Indicates the volume level of the speaking voice. You can express the volume as:

  • An absolute value, expressed as a number in the range of 0.0 to 100.0, from quietest to loudest. An example is 75. The default is 100.0.
  • A relative value, expressed as a number preceded by “+” or “-” that specifies an amount to change the volume. Examples are +10 or -5.5.
  • A constant value:
    • silent
    • x-soft
    • soft
    • medium
    • loud
    • x-loud
    • default

この説明でスッキリ納得するならいいんだけれど、そうじゃないんだよ、それが非常に困る。以下は「単に無視される」:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='ja-JP'>
 5 <voice name='Microsoft Server Speech Text to Speech Voice (ja-JP, NanamiNeural)'>
 6 <p>
 7 <s>
 8 <prosody volume="30%">
 9 A non-negative percentage is an unsigned number immediately followed by "%". と言うとるやんけ、なんで効かんだ
10 </prosody>
11 </s>
12 <s>
13 <prosody volume="100%">
14 A non-negative percentage is an unsigned number immediately followed by "%". と言うとるやんけ、なんで効かんだ
15 </prosody>
16 </s>
17 </p>
18 </voice>
19 </speak>

対して、以下はエラーになる:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='ja-JP'>
 5 <voice name='Microsoft Server Speech Text to Speech Voice (ja-JP, NanamiNeural)'>
 6 <p>
 7 <s>
 8 <prosody volume="-12dB">
 9 エラーになる。
10 </prosody>
11 </s>
12 <s>
13 <prosody volume="-4dB">
14 エラーになる。
15 </prosody>
16 </s>
17 </p>
18 </voice>
19 </speak>

一貫性がない…。無視が一番ダメだよ、エラーにしろや。ともあれこういう迷惑な振る舞いなので、注意深く使うこと。実際に耳で聞いてみるまで効いてるかどうかわからない、というのは、ほんと非効率だからよ。

読み方の指示についての色々

一つずつ取り出して一つずつ説明するほうがわかりやすいかと言えば、そういうことにならんので困る。ちょっと一気にまとめて説明せざるを得ない。

「読み方の指示」と簡単に言ってしまっているが、これには「愛でる」を「メデル」と読むか「アイデル」と読むかの話もあるし、イントネーションや抑揚の話もあるし、あるいは「陽気に」とか「ささやくように」のような演出の話もある。無論ピッチやスピードの制御の話もある。

この制御に関係するエレメントは、100%満足出来るものは一つもないが、「使えるものは使える」ものとして w/tokenprosodyphoneme、「日本語では使えないかほとんど実用にならない」ものとして emphasissay-asmstts:express-as がある。

「日本語では使えないかほとんど実用にならない」の3つ

先に「日本語では使えないかほとんど実用にならない」の3つから。say-as は正確に言えば「効かない」とかではない。ただ、これはたぶん実際に自分でやってくと「phoneme でええやんけ」となってくるんじゃないかと思うんだよね。これ、たとえば「3/8 を八分の三と読む」みたいな制御をするための機構:

1 <say-as interpret-as="fraction">3/8</say-as> of an inch
2 
3 <!--As "three eighths of an inch."-->

なんだけれど、日本語向けにちゃんとなるかは「予め試行錯誤して確かめておく必要がある」てことだよ、それ、かなり手間がかかるの。だったら phoneme で直接読みを指定してしまったほうがいい、となりがち、て話ね。これはまぁそういうことなので、使えるんであれば使ってもいいとは思う。

対して、emphasis と mstts:express-as は、これは本当に「効かない」。前者の説明を Microsoft のドキュメントは一切しないが、以下は全く効果がなく、二つのセンテンスは全く同じになる:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='ja-JP'>
 5 <voice
 6     name='Microsoft Server Speech Text to Speech Voice (ja-JP, NanamiNeural)'>
 7 <p>
 8 <s>テキスト。</s>
 9 <s><emphasis level="strong">テキスト。</emphasis></s>
10 </p>
11 </voice>
12 </speak>
 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='en-US'>
 5 <voice
 6     name='Microsoft Server Speech Text to Speech Voice (en-US, AriaNeural)'>
 7 <p>
 8 <s>That'd be just amazing!</s>
 9 <s><emphasis level="strong">That'd be just amazing!</emphasis></s>
10 </p>
11 </voice>
12 </speak>

MSTTS が従うのは最新の SSML ではなく古いもの(1.0)なので、これはそのせいかもしれんとは思うんだけれど、未知のエレメントに出会った場合の振る舞いとして、これは正しいんかね? 無視すべし、なの? そうならそうなんだろうけど…。ともあれ、少なくとも耳で聴く限りは、emphasis は完全に無視されてると思う。一方 mstts:express-as は、英語だと効くんだよねこれが:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='en-US'>
 5 <voice
 6     name='Microsoft Server Speech Text to Speech Voice (en-US, AriaNeural)'>
 7 <p>
 8 <s>That'd be just amazing!</s>
 9 <s>
10 <mstts:express-as style="cheerful">
11 That'd be just amazing!
12 </mstts:express-as>
13 </s>
14 <s>
15 <mstts:express-as style="angry">
16 That'd be just amazing!
17 </mstts:express-as>
18 </s>
19 </p>
20 </voice>
21 </speak>

結構識別しづらい差にしかならないとはいえ、それでもちゃんと「何かしようとはしてくれる」。けれども日本語ボイスは全く何の効果もないみたい:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='ja-JP'>
 5 <voice
 6     name='Microsoft Server Speech Text to Speech Voice (ja-JP, KeitaNeural)'>
 7 <p>
 8 <s>なんかステキらしい!</s>
 9 <s>
10 <mstts:express-as style="cheerful">
11 なんかステキらしい!
12 </mstts:express-as>
13 </s>
14 <s>
15 <mstts:express-as style="angry">
16 なんかステキらしい!
17 </mstts:express-as>
18 </s>
19 </p>
20 </voice>
21 </speak>

英語で「識別しづらい差にしかならない」だったのでそれなのかと疑ってはみるんだけど、何度聴いてみても同じにしか聞こえないし、ffmpeg とかで可視化してみたとて「完全に同一の波形」と思うのよね。実装されてない、てことかなぁと思う。とても困るんだよなぁ、実用的に朗読させようとすると、こういう「演技」に関係するものって、結構やりたくなるんだけどさ、もう完全にアウトなの、出来ないの、今のところは。待ってりゃいつか出来るようになるかなぁ…? (そもそも「whisper」がない、とか、日本語でも出来るようになったとしてもまだ不十分なんだろうとは思うけれど、だとしても全くないよりは、多少でも変化を持たせられるのなら、出来たほうがいいに決まってる。)

prosody と phoneme については、まずは全体の話

SSML の仕様の話というよりはこれはシンセサイザーの実装の話なんじゃないかと想像してるんだけど、まずはこれでコントロールする「単位」に注意。ちゃんとドキュメントされてないが、おそらく「prosody や phoneme による指示」はプロセッサにおいてかなり早い段階で処理され、そして「センテンス全体として自然な読み方になるように調整する」というステップが(prosody/phoneme 指示有無によらず)きっと最後に「強く」入るんだと思う。この処理順こそが「全然言うこと聞いてくんない」の元凶と思うのだが、その「全体調整」が、おそらく「センテンス単位」「パラグラフ単位」の多段階で行われている、と思う。ここで何が言いたいかと言うと、以下は「絶対にあなたが思うのと違うことをする」てこと:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='ja-JP'>
 5 <voice
 6     name='Microsoft Server Speech Text to Speech Voice (ja-JP, KeitaNeural)'>
 7 <p>
 8 <s>
 9 <prosody pitch="+8000Hz">ここで何が</prosody>言いたいかと言うと、以下は、絶対にあなたが思うのと違うことをする、てこと。
10 </s>
11 </p>
12 </voice>
13 </speak>

「指示したこと」に対する反応はしてくれるよ、けど、期待したのとは絶対に違うはず。これがつまり「センテンス全体として自然な読み方になるように調整する」の結果、のはず。実際やってみると、「最初に高音で」だけをやってくれるのではなくて、その高音にした部分以外は、むしろ普段よりも低音になるはず。そんな指示、したつもりないでしょ? けどそうなる。おそらく「抑揚を表現するエンベロープ関数」みたいなのがあって、最後にそれに乗っけるんじゃないかと思うのね。ここは SSML では絶対にコントロール出来ない領域になってる。そんなわけで、どうしても prosody でコントロールしたいならば、少なくともセンテンスよりも小さい単位で指示するのは、たぶん諦めたほうがいいと思う。つまり、以下みたいな使い方ならまだ比較的言うことを聞いてくれる:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='ja-JP'>
 5 <voice
 6     name='Microsoft Server Speech Text to Speech Voice (ja-JP, KeitaNeural)'>
 7 <p>
 8 <s>
 9 <prosody pitch="+8000Hz">センテンス単位ならば。</prosody>
10 </s>
11 <s>
12 <prosody pitch="-2000Hz">まぁそれなりに言うことを聞いてくれる。</prosody>
13 </s>
14 </p>
15 </voice>
16 </speak>

phoneme にも似た話があるが、それは phoneme 個別の話の中で。

もう一つ言うべきことがある。それは、「全く同じセンテンスでも、前後の文脈が加味され、読み方が変化する」ということ。だから、短い文を切り出してテストしたのと、長い文章の中に埋め込んだのとで、抑揚はもとより、読みすらも変わることがある、という点。これは本当に「賢過ぎるのを恨む」というレベル、まったく予測不可能なうえにかなり制御不能。

息切れ、とか

「賢過ぎるのを恨む」は色んなシチュエーションで実感することになると思うが、その一つの実例を紹介しとく。

以下は「後半息切れする」:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='ja-JP'>
 5 <voice name='Microsoft Server Speech Text to Speech Voice (ja-JP, NanamiNeural)'>
 6 <p>
 7 <s>
 8 会いたくて会いたくて、会いたくて会いたくて、会いたくて会いたくて、会いたくて会いたくて、会いたくて会いたくて、会いたくて会いたくて、会いたくて会いたくて、会いたくて会いたくて、会いたくて会いたくて、会いたくて会いたくて、会いたくて会いたくて、会いたくて会いたくて、会いたくて会いたくて、会いたくて会いたくて、会いたくて会いたくて、会いたくて会いたくて、震える。
 9 </s>
10 </p>
11 </voice>
12 </speak>

ご覧の通り何の特別な指示もしていないが、シンセサイザーは「息切れして声がうわずる」表現を生成する(最後の方のピッチが上がってボリュームが下がる)。こういう非常にクレバーな振る舞いを随所でやってしまうのだが、この振る舞いのコントロールは出来ない。これが問題ならテキストの方を変えるしかない。おそらく長促音系で似たような経験をすると思う。これも正直言って「これが問題な場合は諦めるしかない」。本当に人間にこれを読ませれば本当に「息切れして声がうわずる」んだろうから、正しいんだよ、凄いことでしょ、だけど、そうしたくない場合には困る、という話ね。

phoneme と w/token

読み上げに付き物の「読み間違え」に立ち向かう道具は二つ。先に単純な方から。

以下はおそらく「1。」から「4。」まで同じになると思う。けれども、原文のテキストの中に含まれていた際は、w なしでは「はますます」という切り方をしてしまっていた。これを「ますます」というトークンなのだと示すのがこれ。:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='ja-JP'>
 5 <voice name='Microsoft Server Speech Text to Speech Voice (ja-JP, NanamiNeural)'>
 6 <p>
 7 <s>
 8 1。あなたはますます私の思い通りになる。
 9 </s>
10 <s>
11 2。あなたは ますます 私の思い通りになる。
12 </s>
13 <s>
14 3。あなたは<w>ますます</w>私の思い通りになる。
15 </s>
16 <s>
17 4。あなたは<token>ますます</token>私の思い通りになる。
18 </s>
19 </p>
20 </voice>
21 </speak>

このような、トークンの切り方の間違いだけなら、w/token は簡単に使えて良い。

一方、phoneme には二つの責務があって、一つは html で言うところの ruby、つまり「読み」を指示すること。もう一つが「抑揚」のコントロール。けれどもこれ、前者は完璧なのだが、後者は「完璧なこともあるしそうでないこともある」。つまり「全体として自然な読み方になるように調整する」「前後の文脈が加味され、読み方が変化する」の影響により、指示を全く受け付けてくれないことがある。経験的にはフィフティ・フィフティくらい。かなり困るんだよこれ。読み方に統一感がなくなってしまう。

phoneme は RFC で求めている仕様だが、日本語で便利な「alphabet=”sapi”」は Microsoft 固有の拡張(「Microsoft Speech API」の意味である)。以下の「1。」は、「解れて」を「ワカレテ」と読んでしまう:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='ja-JP'>
 5 <voice name='Microsoft Server Speech Text to Speech Voice (ja-JP, NanamiNeural)'>
 6 <p>
 7 <s>
 8 1。足の筋肉にかかっていた緊張が解れていきます。
 9 </s>
10 <s>
11 2。足の筋肉にかかっていた緊張が<phoneme alphabet="sapi" ph="ホグ'レ+">解れ</phoneme>ていきます。
12 </s>
13 </p>
14 </voice>
15 </speak>

アポストロフィやプラスサインがアクセントの指示になってる。これが「言うことを聞いてくれるかくれないかがフィフティ・フィフティ」なヤツね。もうこれは、諦めるしかないと思うんだ。後述の prosody にもこれに近い責務を果たす機能があるんだけれど、そっちは更に輪をかけて言うこと聞かないからね、つまりは「抑揚の完璧なコントロールは事実上不可能」というのが現状。

prosody の range は無視される模様

例のごとくというか、「効いていないのだとしたら、単に無視されている模様」という非常に不確かな「試行錯誤に基づく観測結果」。何の作用もしないよなこれ:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='en-US'>
 5 <voice
 6     name='Microsoft Server Speech Text to Speech Voice (en-US, AriaNeural)'>
 7 <p>
 8 <s>That'd be just amazing!</s>
 9 <s>
10 <prosody range="10Hz">
11 That'd be just amazing!
12 </prosody>
13 </s>
14 <s>
15 <prosody range="880Hz">
16 That'd be just amazing!
17 </prosody>
18 </s>
19 </p>
20 </voice>
21 </speak>

これが仮に使えたとすれば、部分的に mstts:express-as style の代替品として使えたんだろうと思うんだよなぁ、少なくとも「棒読み」の表現はきっとこれで出来たんじゃないかと。けど、ご覧の通り「何の影響も及ぼさない」みたいなの。何度も言うけどよ、使えないものはエラーにしてくれんかね…、この試行錯誤時間が非常に無駄なんだよな…。

prosody の rate、pitch、volume

一つは既に何度も言っている「全体として自然な読み方になるように調整する」「前後の文脈が加味され、読み方が変化する」の影響の話。これは、volume についてすら真で、以下の「2。」はきっとあなたが狙ったのと違うことをする:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='ja-JP'>
 5 <voice name='Microsoft Server Speech Text to Speech Voice (ja-JP, NanamiNeural)'>
 6 <p>
 7 <s>
 8 1。お湯を注いで、よく混ぜてください。
 9 </s>
10 <s>
11 2。お湯を注いで、<prosody volume="5">よく</prosody>混ぜてください。
12 </s>
13 <s>
14 <prosody volume="5">3。お湯を注いで、よく混ぜてください。</prosody>
15 </s>
16 </p>
17 </voice>
18 </speak>

rate、pitch は、ゆえに、センテンス全体に対しての指示であれば、意図した振る舞いをしてくれる:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='ja-JP'>
 5 <voice name='Microsoft Server Speech Text to Speech Voice (ja-JP, NanamiNeural)'>
 6 <p>
 7 <s>
 8 1。お湯を注いで、よく混ぜてください。
 9 </s>
10 <s>
11 <prosody rate="-30%">2。お湯を注いで、よく混ぜてください。</prosody>
12 </s>
13 <s>
14 <prosody pitch="+440Hz">3。お湯を注いで、よく混ぜてください。</prosody>
15 </s>
16 </p>
17 </voice>
18 </speak>

ただし、ちょっと注意深く結果を聴いて欲しい。ワレワレが期待する rate の振る舞いは、「ピッチなどを変えずにスピードだけ遅くする」ことだと思うが、実際はそれとは少し違うように思う。具体的には、僅かながらピッチが変化しているように感じるし、例えば遅くした場合は、ビブラートがかかったような音声になってしまう。これは感覚的な話なので、違ってたらゴメン。少なくともワタシはそう感じる…んだけど。

prosody の contour

「全体として自然な読み方になるように調整する」「前後の文脈が加味され、読み方が変化する」の影響の話は同じね。そして、「同じ」であることこそが、この contour の用途が非常に限られてしまう元凶。

これはつまり抑揚・イントネーションをコントロールするもの、と考えたいわけだ。けれども「全体として自然な読み方になるように調整する」「前後の文脈が加味され、読み方が変化する」の影響のせいで「個々のワードに対する抑揚・イントネーションを制御する目的には使えない」。つまり「マッチを擦って火を灯す」の「マッチ」が「マ↑ッチ↓」という抑揚であって「マ↓ッチ↑」ではない、のをどうにかするための道具にはなりえない、てこと。このための道具は確かに phoneme があるのだけれど、前述の通り、そちらもコントロールが効かないことが多いので、だったら contour で制御出来ないだろうか、という期待が外れる…、というハナシ。つまり以下では目的を果たせない:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='ja-JP'>
 5 <voice name='Microsoft Server Speech Text to Speech Voice (ja-JP, KeitaNeural)'>
 6 <p>
 7 <s>
 8 マッチを買ってください、マッチを買ってください。
 9 </s>
10 <s>
11 <prosody contour="(0%,+110Hz) (80%,-110Hz)">マッチ</prosody>を買ってください、<prosody contour="(0%,+110Hz) (80%,-110Hz)">マッチ</prosody>を買ってください。
12 </s>
13 <s>
14 
15 </s>
16 </p>
17 </voice>
18 </speak>

いや、問題は「これで目的を果たせる場合もある」という不統一の問題、と思う。一箇所で偶然意図通りの結果を生成出来る場合もあるのだ。それが全体に通用しない、ゆえに読み方が不統一になる、ということ。ダメなケースでは本当に何をしてもダメなのよこれ。

そして、これに関しては、「じゃぁ、センテンス単位なら何か価値あることが出来るのか」が何ともいえない、というのがね、かなりツラいのよ。そもそもどうも MSTTS は疑問形など「?」で語尾を上げることすら弱めになってて、疑問形に聞こえないことが多いんだけれど、だからといって以下で措置出来るかと:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='ja-JP'>
 5 <voice name='Microsoft Server Speech Text to Speech Voice (ja-JP, KeitaNeural)'>
 6 <p>
 7 <s>
 8 疑問形ですか?
 9 </s>
10 <s>
11 <prosody contour="(0%,+0Hz) (70%,+0Hz) (80%,+0Hz) (90%,+220Hz) (100%,+220Hz)">疑問形ですか?</prosody>
12 </s>
13 <s>
14 
15 </s>
16 </p>
17 </voice>
18 </speak>

やってみればわかる通り、全く措置にはならない。

もっと上手にコントロールする調整はありうるのかもしれない。けれども少なくとも今のワタシにとっては「contour は全く言うことを聞いてくれないので、1ビットとて役に立ってはくれない」という評価を下している。ちゃんと狙い通りの結果を得るのに、きっと何千回もの試行錯誤が必要なんだと思うよ、…、それって「使える」ことになるか? ならんよね。それが今時点でのワタシの感想。

長音の調整は無理ゲー

息切れの話とも通ずる。以下は、「指示した通りにとても不気味な読み上げをしてくれる」:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='ja-JP'>
 5 <voice name='Microsoft Server Speech Text to Speech Voice (ja-JP, KeitaNeural)'>
 6 <p>
 7 <s>
 8 <prosody rate="-150%">
 9 <phoneme alphabet="sapi" ph="ス"></phoneme>ってぇーぇーぇーぇーぇーぇーぇーぇーぇー吐いてぇーぇーぇー
10 </prosody>
11 </s>
12 </p>
13 </voice>
14 </speak>

「自然」だし「賢い」んだよ、気持ち悪いテキストをその通りに読んでくれるわけだから。けど少なくともこのテキストを書いた意図は、普通は「長く伸ばす」ことだけをして欲しいわけであって、「伸ばすと声が震える」ことまでは望んでないはずでしょ。でもこれはコントロール出来ない。

これは「深呼吸を指示する」テキストの例なのね、で、深呼吸の「良いやり方」てのがあって、それってたとえば「6秒かけて鼻から吸って、8秒かけて口からゆっくり吐く」みたいなものなのね。だからそのタイミング取りが読み上げでとても重要になるわけなんだけれど、ご覧の通りで、長音の伸ばす長さの調整は至難の業なの。ゆえに、これは「適当なところで諦める」のが肝要。かわりに break で間を取るように考えることをオススメする:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='ja-JP'>
 5 <voice name='Microsoft Server Speech Text to Speech Voice (ja-JP, KeitaNeural)'>
 6 <p>
 7 <s>
 8 <prosody rate="-30%">
 9 <phoneme alphabet="sapi" ph="ス"></phoneme>ってー<break time="5s"/><break time="1s"/>10 吐いてー<break time="5s"/><break time="3s"/>
11 <phoneme alphabet="sapi" ph="ス"></phoneme>ってー<break time="5s"/><break time="1s"/>12 吐いてー<break time="5s"/><break time="3s"/>
13 <phoneme alphabet="sapi" ph="ス"></phoneme>ってー<break time="5s"/><break time="1s"/>14 吐いてー<break time="5s"/><break time="3s"/>
15 </prosody>
16 </s>
17 </p>
18 </voice>
19 </speak>

bookmark は期待したのとは違うらしい

bookmark はそもそも在り方からして不穏。mstts: でないので MS 拡張でないと思いたいのだが、SSML の仕様には見当たらない。なんなんだこれは。

Microsoft による説明:

You can use the bookmark element to insert custom markers in SSML to get the offset of each marker in the audio stream. We won’t read out the bookmark elements. The bookmark element can be used to reference a specific location in the text or tag sequence. Bookmark is available for all languages and voices.

結果の音声ストリームにメタデータを挿入してくれるもの、という機能であることを期待したのだよね。だとすると、audiomstts:backgroundaudio が不自由なので諦めた場合に、MSTTS による処理の後処理としてそのブックマークの位置を頼りに効果音を追加する、などへの道が開くだろう、と。

けれども:

 1 <speak version='1.0'
 2     xmlns='http://www.w3.org/2001/10/synthesis'
 3     xmlns:mstts='https://www.w3.org/2001/mstts'
 4     xml:lang='ja-JP'>
 5 <voice name='Microsoft Server Speech Text to Speech Voice (ja-JP, KeitaNeural)'>
 6 <p>
 7 <bookmark mark="paragraph1-start"/>
 8 <s>
 9 ホットの場合。
10 <bookmark mark="p1s1-end"/>
11 </s>
12 <s>
13 ティーカップに、ティースプーン約3杯半を入れます。
14 <bookmark mark="p1s2-end"/>
15 </s>
16 <s>
17 約120ミリリットルのお湯を注いでよく混ぜてください。
18 <bookmark mark="p1s3-end"/>
19 </s>
20 <bookmark mark="paragraph1-end"/>
21 </p>
22 <p>
23 <bookmark mark="paragraph2-start"/>
24 <s>
25 アイスの場合。
26 <bookmark mark="p2s1-end"/>
27 </s>
28 <s>
29 グラスに、ティースプーン約3杯半を入れます。
30 <bookmark mark="p2s2-end"/>
31 </s>
32 <s>
33 約100ミリリットルの冷水を注ぎ、よくかき混ぜて氷を入れてください。
34 <bookmark mark="p2s3-end"/>
35 </s>
36 <bookmark mark="paragraph2-end"/>
37 </p>
38 </voice>
39 </speak>

これに対して Microsoft Edge’s TTS Server は何の不平も言わずに処理してくれる。けれども、結果の mp3 には何の影響も与えていない模様。

どうもこれは「SSML 文書処理の入力イベント」になるだけで、それに対して「出力に何かしらするのは貴様の責務」のためのもののようだね。本来のこの機能における「貴様」はワタシでありあなたのハズなのだが、Microsoft Edge’s TTS Server の場合の「貴様」は Microsoft Edge’s TTS Server 自身なので、その「貴様」は SSML 文書読み上げにおける bookmarkReached イベントに対して「何もしない」という親切を実行してくれた模様。うげぇ…。

その他読み上げの癖についてのメモ

数字

VOICEROID でもそうらしいんだけど、0~10だと、よく問題を起こすのは「0」「4」「5」「7」「10」の5つ。たぶんカウントダウンなどで単独で現れる場合に顕著なんだと思うんだけれど、まずは「0」のゼロ/レイ、「4」のヨン/シ、そして「5」「7」「10」はいずれも「(ご)っ」「(し)ち」「(じゅ)っ」みたいに、一文字かすれて聞こえない、みたいなことがよく起こる。

これへの措置は prosodyphoneme で読みを与えるか、「ごー」「ごっ」「ナナ」「ジュウ」みたいにテキスト自体変えてしまうかのどちらか。

勝手に叫んでしまうことがある

再現パターンが今ひとつわからないんだけれど、例えば「会いたくて会いたくて。震える。」みたいに「震える。」のように浮いたワードがあると、なんだか叫びがち。叫ぶ、というのは、ピッチが高く、スピードも若干速くなる状態。当然ワレワレがコントロール出来ないところで起こる。これが問題の場合はテキストを変えるしかないんだよね、mstts:express-as も使えないから…。

実例を示したいところなのだけれど、部分だけ取り出しても再現せんのよ。結局これも「全体に対する調整」の結果だから。人工知能が賢過ぎるのよね…。