VOICEVOXに英語を読ませる(カタカナ英語)

VOICEVOXは英語の発音ができないが、さすがに「Cisco」を「シーアイエスシーオー」と発音するのはさすがに困る。とはいえ、膨大な単語1つずつを辞書化していくのは骨が折れる。

ということで、多少妥協して、片言でも良いので発音してもらう。
とりあえず、英単語をカタカナに変える辞書を用意。
[bep-eng.dic](https://fastapi.metacpan.org/source/MASH/Lingua-JA-Yomi-0.01/lib/Lingua/JA/bep-eng.dic)あたりをベースに、自分好みの単語を追加すれば良いんじゃないかな。

```
[root@ip-10-100-0-11 app]# tail eng.dic
PYTORCH パイトーチ
TWITTER ツイッター
SAAS サーズ
KAGGLE カグル
DROPBOX ドロップボックス
SPLUNK スプランク
SALESFORCE セールスフォース
DREAMS ドリームズ
AZURE アジュール
LEADERS リーダーズ
[root@ip-10-100-0-11 app]#
```

先のリストを元に、アルファベットをカタカナに置換する処理を入れた eng_to_kana.py を用意

```python

#!/usr/local/bin/python
# -*- coding: utf-8 -*-
import re

dic_file = 'eng.dic'
kana_dict = {}
with open(dic_file, mode='r', encoding='utf-8') as f:
  lines = f.readlines()
  for i, line in enumerate(lines):
    line_list = line.replace('\n', '').split(' ')
    kana_dict[line_list[0]] = line_list[1]

reduction=[["It\'s","イッツ"],["I\'m","アイム"],["You\'re","ユーァ"],["He\'s","ヒーィズ"],["She\'s","シーィズ"],["We\'re","ウィーアー"],["They\'re","ゼァー"],["That\'s","ザッツ"],["Who\'s","フーズ"],["Where\'s","フェアーズ"],["I\'d","アイドゥ"],["You\'d","ユードゥ"],["I\'ve","アイブ"],["I\'ll","アイル"],["You\'ll","ユール"],["He\'ll","ヒール"],["She\'ll","シール"],["We\'ll","ウィール"]]

def eng_to_kana(text):
  # 読みたい記号。他の単語と混ざらないように、前後に半角スペースを挟む
  text = text.replace("+"," プラス ").replace("＋"," プラス ").replace("-"," マイナス ").replace("="," イコール ").replace("＝"," イコール ")

  # No.2、No6みたいに、No.の後に数字が続く場合はノーではなくナンバーと読む
  text = re.sub(r'No\.([0-9])',"ナンバー\\1",text)
  text = re.sub(r'No([0-9])',"ナンバー\\1",text)

  # 短縮形の処理
  for red in reduction: text = text.replace(red[0]," "+red[1]+" ")

  # this is a pen.のように、aの後に半角スペース、続いてアルファベットの場合、エーではなくアッと呼ぶ
  text = re.sub(r'a ([a-zA-Z])',"アッ \\1",text)

  # 文を区切る文字は消してはダメなので、前後に半角スペースを挟む
  text = text.replace("."," . ").replace("。"," 。 ").replace("!"," ! ").replace("！"," ！ ")

  # アルファベットとアルファベット以外が近接している時、その間に半角スペースを挟む（この後、英単語を単語ごとに区切るための前準備）
  text_l=list(text)
  for i in range(len(text))[::-1][:-1]:
    if re.compile("[a-zA-Z]").search(text_l[i]) and re.compile("[^a-zA-Z]").search(text_l[i-1]): text_l.insert(i," ")
    elif re.compile("[^a-zA-Z]").search(text_l[i]) and re.compile("[a-zA-Z]").search(text_l[i+-1]): text_l.insert(i," ")

  text = "".join(text_l)

  # 半角スペースや読まなくて良い文字で区切り、各単語の英語をカタカナに変換
  text_split = re.split('[ \,\*\-\_\=\(\)\[\]\'\"\&\$　]',text)
  for i in range(len(text_split)):
    if str.upper(text_split[i]) in kana_dict:
      text_split[i] = kana_dict[str.upper(text_split[i])]

  return (" ".join(text_split))
```

上記ライブラリを使って、あとは変換したい文字列を突っ込む。

```python
from eng_to_kana import eng_to_kana

msg = eng_to_kana(msg)
```

先のライブラリでは、「アイ ラブ ユー」のように英語の発音を単語ごとに半角スペースで区切るようにしている。
これをVOICEVOXに読ませると、単語間の空白時間が長すぎる。（場所によっては0.5秒ほどの無音が入る）

なので無理やり詰める。

以下関数はAIチャットの応答をVOICEVOXで音声出力する仕組みの一部として使っているものなので色々余計なこともしているが、英語に関するところとしては、for i in request['accent_phrases']の部分。
pause_moraのvowel_lengthが単語間の空白なので、これが0.2を超えている場合は、問答無用で0.2にしている。
句読点も0.2秒に縮まるが、自分はもともとプリセットを早口で調整しているので、あまり違和感がない。

```python
def save_voice(msg,voiceid,fname,custom=[0,1,1]):
    msg = eng_to_kana(msg)
    request = requests.post('http://'+voice_server+'/audio_query_from_preset?preset_id='+voiceid+'&text="'+msg+'"').json()
    request["pitchScale"]=custom[0]
    request["speedScale"]=custom[1]
    request["intonationScale"]=custom[2]
    for i in request['accent_phrases']: # 単語と単語の間の空白を最大0.2秒にする
      if i['pause_mora'] is not None and i['pause_mora']['vowel_length'] > 0.2: i['pause_mora']['vowel_length']=0.2
    wavdata = requests.post('http://'+voice_server+'/synthesis?speaker='+str(dict_presets[voiceid]["style_id"]), data=json.dumps(request,ensure_ascii=False).encode("utf-8"))
    wavf = open('static/tmp/'+fname+'.wav', 'wb')
    try: wavf.write(wavdata.content)
    finally: wavf.close()
```

それ以外の部分も一応説明すると、
voice_serverには、voicevoxを実行しているコンテナのIP、msgが発音したい文字列、fnameが出力wavファイルの名前、voiceidがプリセットID、customでピッチ、スピード、イントネーションを調整できるようにしている。
事前に色々なプリセットを用意して、dict_presets[voiceid]["style_id"]でstyle_idを取得できるようにしている点には注意。