やりたいこと
冒頭のタイトルに対して、VOICEVOXに英語を読ませる(カタカナ英語) #Python - Qiitaという記事が存在し、これによりカタカナ英語で読ませることが出来る。
なお、記事中では以下の問題が指摘されている。
先のライブラリでは、「アイ ラブ ユー」のように英語の発音を単語ごとに半角スペースで区切るようにしている。 これをVOICEVOXに読ませると、単語間の空白時間が長すぎる。(場所によっては0.5秒ほどの無音が入る)
記事の方法とは別に、英単語の前後で空白が入ること自体を防げる方法があれば、それでも対応可能だと思われる。例えば「この無線LANを」を「この無線」「ラン」「を」と分けずに「この無線ランを」に変更できれば、そもそも無音の時間は生じない。
ここで2つの希望が生じる。
- 上記の解決に変更したうえで、txtの文章をVOICEVOXに読ませたい。
- COEIROINKでの発話に対応させたい。
自動での改行に対応するようにもしたい。
解決案(VOICEVOX)
そこでまず、1つ目の希望を達成する。①eng_to_kana.py
を変更して期待通りカタカナ英語に変換させる。その後、②これを呼び出して音声合成するスクリプトを作成する。
①カタカナ英語への変換
まずは、eng_to_kana.py
を変更する。生成AIに尋ねつつ、以下の修正となった。
@@ -14,9 +14,6 @@ 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) @@ -27,21 +24,16 @@ # 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])] + # アルファベットの文字列を抽出 + words = re.findall(r"[A-Za-z0-9]+", text) + words.sort(key=len, reverse=True) + + # 辞書型の変数kana_dictから読み仮名を取り出し、置換 + for word in words: + # 大文字に変換 + upper_word = word.upper() + if upper_word in kana_dict: + # re.sub()を使用して、テキスト内のすべての出現を置換 + text = re.sub(word, kana_dict[upper_word], text) - return (" ".join(text_split)) + return text
変更後ではテキストを分割せず、全体を対象に置換を行う。これにより、元のテキストの構造を変えずに、英語だけをカタカナ英語に変換できる(ということらしい)。
なお「-」をマイナスとして読ませないようにしている。例えばEAP-TTLS
はイーエーピーティーティーエルエスと読んでほしいため。
②呼び出して音声合成
つぎに、これを呼び出してテキストをカタカナ英語に変換した後、engineにクエリを投げて音声を再生するスクリプトtalk.py
を作成する。
先記事のsave_voice()では保存してしまうので、これは再生させるように変更する。
# 英語をカタカナに変換するモジュールをインポート from eng_to_kana import eng_to_kana # 並行処理を行うためのモジュールをインポート from concurrent.futures import ThreadPoolExecutor import json import requests import winsound # スピーカーの詳細を取得する関数 def get_speaker(speaker_name) -> map: # スピーカーの情報を取得 res = requests.get("http://" + voice_server + "/speakers").json() # 指定したスピーカーの詳細のみ抽出 voice_details = list(filter(lambda x: x["name"] == speaker_name, res)) if len(voice_details) >= 1: return voice_details[0] else: raise Exception("There is no such a speaker.") # スピーカーの音声IDを取得する関数 def get_voiceid(speaker_name, style_name=None) -> int: # スピーカーの詳細を取得 voice_detail = get_speaker(speaker_name) # 指定したスタイルのボイスIDを抽出(未指定ならトップのボイスID) if style_name == None: voice_styles = voice_detail["styles"] else: voice_styles = list( filter(lambda x: x["name"] == style_name, voice_detail["styles"]) ) if len(voice_styles) >= 1: return voice_styles[0]["id"] else: raise Exception("There is no such a style.") # 音声データを取得する関数 def get_wave_voice(msg, voice_id, custom=[0, 1.1, 1]): # メッセージをカタカナに変換 msg = eng_to_kana(msg) # 音声合成クエリを作成 request = requests.post( "http://" + voice_server + "/audio_query?speaker=" + str(voice_id) + '&text="' + msg + '"' ).json() # ピッチ、スピード、イントネーションのスケールを設定 request["pitchScale"] = custom[0] request["speedScale"] = custom[1] request["intonationScale"] = custom[2] # 音声を合成 return requests.post( "http://" + voice_server + "/synthesis?speaker=" + str(voice_id), data=json.dumps(request, ensure_ascii=False).encode("utf-8"), ) # 音声サーバーのアドレスを設定 voice_server = "localhost:50021" def main(): # スピーカーを指定 speaker_name = "No.7" style_name = None # デフォルト指定ならNone msg_file = "message.txt" # 読むテキストを保存したファイル tasklist = [] # 各スレッドのタスクを保持するためのリスト # ボイスIDを取得 voice_id = get_voiceid(speaker_name, style_name) with open(msg_file, mode="r", encoding="utf-8") as f: # 句点でテキストを分割 lines = sum([item.split("。") for item in f.readlines()], []) with ThreadPoolExecutor() as executor: for line in lines: # スレッドを使って、各行を音声合成 tasklist.append(executor.submit(get_wave_voice, line, voice_id)) for task in tasklist: # 合成結果を取得して再生 wavdata = task.result() winsound.PlaySound(wavdata.content, winsound.SND_MEMORY) main()
解決案(COEIROINK)
つぎに、COEIROINKでの発話に対応させる。
もちろんeng_to_kana.py
は上記の変更と同じで良い。ただしリクエストの形式などが異なるので、呼び出し部talk.py
は変更する必要がある。
# 英語をカタカナに変換するモジュールをインポート from eng_to_kana import eng_to_kana # 並行処理を行うためのモジュールをインポート from concurrent.futures import ThreadPoolExecutor import json import requests import winsound # スピーカーの詳細を取得する関数 def get_speaker(speaker_name) -> map: # スピーカーの詳細を取得 res = requests.get("http://" + voice_server + "/v1/speakers").json() # 指定したスピーカーの詳細のみ抽出 voice_details = list(filter(lambda x: x["speakerName"] == speaker_name, res)) if len(voice_details) >= 1: return voice_details[0] else: raise Exception("There is no such a speaker.") # デフォルトのリクエストを作成する関数 def get_default_request(speaker_name, style_name, custom=[0, 1.2, 1]) -> dict: # スピーカーの情報を取得 speaker_info = get_speaker(speaker_name) # 指定したスタイルのスタイルIDを抽出(未指定ならトップのスタイルID) voice_styles = list( filter(lambda x: x["styleName"] == style_name, speaker_info["styles"]) ) if len(voice_styles) > 0: style_id = voice_styles[0]["styleId"] else: style_id = speaker_info["styles"][0]["styleId"] # リクエストの内容を作成 request = { "speakerUuid": speaker_info["speakerUuid"], "styleId": style_id, "text": "", "speedScale": custom[1], "volumeScale": 1, "pitchScale": custom[0], "intonationScale": custom[2], "prePhonemeLength": 0.1, "postPhonemeLength": 0.1, "outputSamplingRate": 44100 } return request # 音声データを取得する関数 def get_wave_voice(msg, request): # メッセージをカタカナに変換 msg = eng_to_kana(msg) # メッセージが存在すればリクエストのテキストに設定 request["text"] = msg if len(msg) > 0 else "" # 音声を合成 return requests.post( "http://" + voice_server + "/v1/synthesis", data=json.dumps(request, ensure_ascii=False).encode("utf-8"), ) # 音声サーバーのアドレスを設定 voice_server = "localhost:50032" def main(): # スピーカーを指定 speaker_name = "ディアちゃん" style_name = None # デフォルト指定ならNone msg_file = "message.txt" # 読むテキストを保存したファイル tasklist = [] # 各スレッドのタスクを保持するためのリスト # リクエストのベースを作成 default_request = get_default_request(speaker_name, style_name) with open(msg_file, mode="r", encoding="utf-8") as f: # 句点でテキストを分割 lines = sum([item.split("。") for item in f.readlines()], []) # 2スレッドで実施するとエラーっぽいのがengineに出力されるので、一応1 with ThreadPoolExecutor(max_workers=1) as executor: for line in lines: # 行に文字がなければスキップ if len(''.join(line.split())) == 0: continue # 音声データを作成するタスクを追加 tasklist.append(executor.submit(get_wave_voice, line, default_request)) for task in tasklist: # 音声合成結果を取得し、再生 wavdata = task.result() if wavdata.status_code != 200: continue winsound.PlaySound(wavdata.content, winsound.SND_MEMORY) main()
実際に発話させてみる
あとはrun.exe
もしくはengine.exe
を起動しておいて、talk.py
を実行すれば良い。
venv環境で実行が面倒であれば、activate.bat
をtalk.bat
などとしてコピーしておいて、末尾に以下を足せばこのバッチファイルから実行できる(適宜start /MIN /REALTIME
などとしてよい)。
start python talk.py
コード部分は多いに改善点がありそうだが、とりあえず用は満たしているので良しとする。