カニ食べたい。どうも、だーやまんです。
これは、SLP KBIT Advent Calendar 2019 - Adventarの15日目の記事です。
始めに
Text To Speech APIを利用する際、API側で読み仮名が正しく認識されないことがある。造語などに関しては、いうまでもなく。そのような時、どのようにして送信した文字列を思い通りに読んでもらうか、という処理をPythonで行った場合を紹介する。
とりあえず
以下の通りに読んで欲しい文字列があるとする。ここでは、鉤括弧や句読点等は読み飛ばされるものとする。
文字列: 僕の禁断の過負荷 『却本作り』!! 読み: ぼくのはじまりのまいなす、ぶっくめーかー
( めだかボックス第11巻 p25 )
この場合、正しくない読み仮名と造語が入り混じった文になっている。まずはこれらの辞書を作成する
u_dict = { '禁断':'はじまり', '過負荷':'まいなす', '却本作り':'ブックメーカー' }
あとは、元の文字列に対して、辞書の登録数だけ一致する部分を変換するだけである。辞書に対して、 items()
を使うことでキーと値のペアのタプルのリストを取得できる。あとは置換してやるだけである。
read_text = ' 僕の禁断の過負荷 『却本作り』!!' for word, read in u_dict.items(): read_text = read_text.replace(word, read) print(read_text)
結果
僕のはじまりのまいなす 『ぶっくめーかー』!!
これで思い通りに変換ができた。これがとりあえずの実装になる。
問題点
以上の実装で、以下のような辞書と文字列の時、どうなるだろうか。
u_dict = {'蜂':'びー', 'びー':'ハエ'} read_text = 'ぶんぶんぶん、蜂がとぶ。びーは蠅である。'
for文の1巡目で文中の「蜂」が「びー」に変換され、2巡目で「びー」が「ハエ」に変換される。その結果、
ぶんぶんぶん、ハエがとぶ。ハエは蠅である。
になる。
変換された読み仮名がさらに変換されるのは問題である。自分の思わぬ文字列に変換される恐れがあるし、文字列爆発による攻撃も可能になる。
文字列爆発の例
u_dict = { 'a':'bbb', 'b':'ccc'} read_text = 'aaa'
変換結果
ccccccccccccccccccccccccccc
悪意のある文字列を登録された場合、文字列は指数関数的に伸ばすことが可能になるので、メモリへの攻撃が可能になる。
解決策
str型の組み込み関数、 format()
を用いる。一度、文字列の変換該当部分を{}と変換先の文字列の引数番号にする。そうすることで二重の変換を防ぐことができる。コードを見てみよう。
u_dict = {'蜂':'びー', 'びー':'ハエ'} read_text = 'ぶんぶんぶん、蜂がとぶ。びーは蠅である。' print(read_text) read_list = [] # あとでまとめて変換するときの読み仮名リスト for i, one_dic in enumerate(u_dict.items()): # one_dicは単語と読みのタプル。添字はそれぞれ0と1。 read_text = read_text.replace(one_dic[0], '{'+str(i)+'}') read_list.append(one_dic[1]) # 変換が発生した順に読みがなリストに追加 print(read_text) read_text = read_text.format(*read_list) #読み仮名リストを引数にとる print(read_text)
実行結果
ぶんぶんぶん、蜂がとぶ。びーは蠅である。 ぶんぶんぶん、{0}がとぶ。{1}は蠅である。 ぶんぶんぶん、びーがとぶ。ハエは蠅である。
いい感じに変換されました。
終わりに
辞書の登録順に変換されてしまうので、優先度等の重みつけはできていない。辞書型を使うのではなく、新しくクラスを定義することで可能になるかな〜。私が運用している喋太郎でも、今回紹介した方法を用いている。誰かの参考になれば幸い。