diff --git a/app.py b/app.py index d4d08ff..1bdb09d 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,9 @@ import argparse import datetime +import json import os import sys +from typing import Optional import gradio as gr import torch @@ -21,6 +23,8 @@ from common.constants import ( ) from common.log import logger from common.tts_model import ModelHolder +from infer import InvalidToneError +from text.japanese import g2kata_tone, kata_tone2phone_tone, text_normalize # Get path settings with open(os.path.join("configs", "paths.yml"), "r", encoding="utf-8") as f: @@ -46,36 +50,76 @@ def tts_fn( use_assist_text, style, style_weight, - given_tone, + kata_tone_json_str, + use_tone, ): assert model_holder.current_model is not None - if given_tone == "": - given_tone = None - else: - given_tone = [int(i) for i in given_tone] + + wrong_tone_message = "" + kata_tone: Optional[list[tuple[str, int]]] = None + if use_tone and kata_tone_json_str != "": + if language != "JP": + logger.warning("Only Japanese is supported for tone generation.") + wrong_tone_message = "アクセント指定は現在日本語のみ対応しています。" + if line_split: + logger.warning("Tone generation is not supported for line split.") + wrong_tone_message = "アクセント指定は改行で分けて生成を使わない場合のみ対応しています。" + try: + kata_tone = [] + json_data = json.loads(kata_tone_json_str) + # tupleを使うように変換 + for kana, tone in json_data: + assert isinstance(kana, str) and isinstance(tone, int) + kata_tone.append((kana, tone)) + except Exception as e: + logger.warning(f"Error occurred when parsing kana_tone_json: {e}") + wrong_tone_message = f"アクセント指定が不正です: {e}" + kata_tone = None + + # toneは実際に音声合成に代入される際のみnot Noneになる + tone: Optional[list[int]] = None + if kata_tone is not None: + phone_tone = kata_tone2phone_tone(kata_tone) + tone = [t for _, t in phone_tone] + start_time = datetime.datetime.now() - sr, audio = model_holder.current_model.infer( - text=text, - language=language, - reference_audio_path=reference_audio_path, - sdp_ratio=sdp_ratio, - noise=noise_scale, - noisew=noise_scale_w, - length=length_scale, - line_split=line_split, - split_interval=split_interval, - assist_text=assist_text, - assist_text_weight=assist_text_weight, - use_assist_text=use_assist_text, - style=style, - style_weight=style_weight, - given_tone=given_tone, - ) + try: + sr, audio = model_holder.current_model.infer( + text=text, + language=language, + reference_audio_path=reference_audio_path, + sdp_ratio=sdp_ratio, + noise=noise_scale, + noisew=noise_scale_w, + length=length_scale, + line_split=line_split, + split_interval=split_interval, + assist_text=assist_text, + assist_text_weight=assist_text_weight, + use_assist_text=use_assist_text, + style=style, + style_weight=style_weight, + given_tone=tone, + ) + except InvalidToneError as e: + logger.error(f"Tone error: {e}") + return f"Error: アクセント指定が不正です:\n{e}", None, kata_tone_json_str end_time = datetime.datetime.now() duration = (end_time - start_time).total_seconds() - return f"Success, time: {duration} seconds.", (sr, audio) + + if tone is None and language == "JP" and not line_split: + # アクセント指定に使えるようにアクセント情報を返す + norm_text = text_normalize(text) + kata_tone = g2kata_tone(norm_text) + kata_tone_json_str = json.dumps(kata_tone, ensure_ascii=False, indent=2) + elif tone is None: + kata_tone_json_str = "" + message = f"Success, time: {duration} seconds." + if wrong_tone_message != "": + message = wrong_tone_message + "\n" + message + return message, (sr, audio), kata_tone_json_str initial_text = "こんにちは、初めまして。あなたの名前はなんていうの?" @@ -243,7 +287,10 @@ if __name__ == "__main__": step=0.1, label="分けた場合に挟む無音の長さ(秒)", ) - given_tone = gr.Textbox("トーン、0と1の数値列") + tone = gr.Textbox(label="アクセント指定") + use_tone = gr.Checkbox( + label="アクセント指定を使う", info="改行で分けない場合のみ使えます", value=False + ) language = gr.Dropdown(choices=languages, value="JP", label="Language") with gr.Accordion(label="詳細設定", open=False): sdp_ratio = gr.Slider( @@ -340,9 +387,10 @@ if __name__ == "__main__": use_assist_text, style, style_weight, - given_tone, + tone, + use_tone, ], - outputs=[text_output, audio_output], + outputs=[text_output, audio_output, tone], ) model_name.change( diff --git a/common/tts_model.py b/common/tts_model.py index 5fdf551..5e52064 100644 --- a/common/tts_model.py +++ b/common/tts_model.py @@ -71,9 +71,9 @@ class Model: def get_style_vector_from_audio( self, audio_path: str, weight: float = 1.0 ) -> np.ndarray: - from style_gen import extract_style_vector + from style_gen import get_style_vector - xvec = extract_style_vector(audio_path) + xvec = get_style_vector(audio_path) mean = self.style_vectors[0] xvec = mean + (xvec - mean) * weight return xvec @@ -190,7 +190,7 @@ class ModelHolder: if f.endswith(".pth") or f.endswith(".pt") or f.endswith(".safetensors") ] if len(model_files) == 0: - logger.info( + logger.warning( f"No model files found in {self.root_dir}/{model_name}, so skip it" ) continue diff --git a/infer.py b/infer.py index fb0e670..e9a592a 100644 --- a/infer.py +++ b/infer.py @@ -11,6 +11,10 @@ from common.log import logger # latest_version = "1.0" +class InvalidToneError(ValueError): + pass + + def get_net_g(model_path: str, version: str, device: str, hps): net_g = SynthesizerTrn( len(symbols), @@ -41,9 +45,11 @@ def get_text( ): # 在此处实现当前版本的get_text norm_text, phone, tone, word2ph = clean_text(text, language_str) - logger.info(f"Original tone: {''.join(str(num) for num in tone)}") if given_tone is not None: - logger.debug(f"Tone given: {given_tone}") + if len(given_tone) != len(phone): + raise InvalidToneError( + f"Length of given_tone ({len(given_tone)}) != length of phone ({len(phone)})" + ) tone = given_tone phone, tone, language = cleaned_text_to_sequence(phone, tone, language_str) diff --git a/style_gen.py b/style_gen.py index 5bd7b2a..12dab20 100644 --- a/style_gen.py +++ b/style_gen.py @@ -20,9 +20,14 @@ device = torch.device(config.style_gen_config.device) inference.to(device) +# 推論時にインポートするために短いが関数を書く +def get_style_vector(wav_path): + return inference(wav_path) + + def save_style_vector(wav_path): try: - style_vec = inference(wav_path) + style_vec = get_style_vector(wav_path) except Exception as e: logger.error(f"\nError occurred with file: {wav_path}, Details:\n{e}\n") raise diff --git a/text/japanese.py b/text/japanese.py index 717b169..b053625 100644 --- a/text/japanese.py +++ b/text/japanese.py @@ -3,13 +3,29 @@ import re import unicodedata -import jaconv import pyopenjtalk from num2words import num2words from transformers import AutoTokenizer from common.log import logger from text import punctuation +from text.japanese_mora_list import ( + mora_kata_to_mora_phonemes, + mora_phonemes_to_mora_kata, +) + +# 子音の集合 +COSONANTS = set( + [ + cosonant + for cosonant, _ in mora_kata_to_mora_phonemes.values() + if cosonant is not None + ] +) + +# 母音の集合 +VOWELS = {"a", "i", "u", "e", "o"} + # 正規化で記号を変換するための辞書 rep_map = { @@ -145,14 +161,14 @@ def g2p(norm_text: str) -> tuple[list[str], list[int], list[int]]: # アクセント割当をしなおすことによってpunctuationを含めた音素とアクセントのリストを作る。 # punctuationがすべて消えた、音素とアクセントのタプルのリスト - phone_tone_list_wo_punct = g2phone_tone_list(norm_text) + phone_tone_list_wo_punct = g2phone_tone_wo_punct(norm_text) # sep_text: 単語単位の単語のリスト # sep_kata: 単語単位の単語のカタカナ読みのリスト sep_text, sep_kata = text2sep_kata(norm_text) # sep_phonemes: 各単語ごとの音素のリストのリスト - sep_phonemes = handle_long([kata2phoneme(i) for i in sep_kata]) + sep_phonemes = handle_long([kata2phoneme_list(i) for i in sep_kata]) # phone_w_punct: sep_phonemesを結合した、punctuationを元のまま保持した音素列 phone_w_punct: list[str] = [] @@ -192,10 +208,71 @@ def g2p(norm_text: str) -> tuple[list[str], list[int], list[int]]: return phones, tones, word2ph -def g2phone_tone_list(text: str) -> list[tuple[str, int]]: +def g2kata_tone(norm_text: str) -> list[tuple[str, int]]: + phones, tones, _ = g2p(norm_text) + return phone_tone2kata_tone(list(zip(phones, tones))) + + +def phone_tone2kata_tone(phone_tone: list[tuple[str, int]]) -> list[tuple[str, int]]: + """phone_toneをのphone部分をカタカナに変換する。ただし最初と最後の("_", 0)は無視""" + phone_tone = phone_tone[1:] # 最初の("_", 0)を無視 + phones = [phone for phone, _ in phone_tone] + tones = [tone for _, tone in phone_tone] + logger.debug(f"phones: {phones}") + logger.debug(f"tones: {tones}") + result: list[tuple[str, int]] = [] + current_mora = "" + for phone, next_phone, tone, next_tone in zip(phones, phones[1:], tones, tones[1:]): + # zipの関係で最後の("_", 0)は無視されている + if phone in punctuation: + result.append((phone, tone)) + continue + if phone in COSONANTS and phone != "n": # n以外の子音の場合 + assert current_mora == "", f"Unexpected {phone} after {current_mora}" + assert tone == next_tone, f"Unexpected {phone} tone {tone} != {next_tone}" + current_mora = phone + elif phone == "n": + logger.debug(f"current_mora: {current_mora}") + logger.debug(f"next_phone: {next_phone}") + assert current_mora == "", f"Unexpected {phone} after {current_mora}" + if next_phone not in VOWELS: # 次の音素が母音でない場合 + result.append(("ン", tone)) + else: + # 子音のnの場合 + assert ( + tone == next_tone + ), f"Unexpected {phone} tone {tone} != {next_tone}" + current_mora = "n" + else: + # phoneが母音 + current_mora += phone + result.append((mora_phonemes_to_mora_kata[current_mora], tone)) + current_mora = "" + logger.debug(f"result: {result}") + return result + + +def kata_tone2phone_tone(kata_tone: list[tuple[str, int]]) -> list[tuple[str, int]]: + """`phone_tone2kata_tone()`の逆。""" + result: list[tuple[str, int]] = [("_", 0)] + for mora, tone in kata_tone: + if mora in punctuation: + result.append((mora, tone)) + else: + cosonant, vowel = mora_kata_to_mora_phonemes[mora] + if cosonant is None: + result.append((vowel, tone)) + else: + result.append((cosonant, tone)) + result.append((vowel, tone)) + result.append(("_", 0)) + return result + + +def g2phone_tone_wo_punct(text: str) -> list[tuple[str, int]]: """ テキストに対して、音素とアクセント(0か1)のペアのリストを返す。 - ただし「!」「.」「?」等の非音素記号は全て消える(ポーズ記号も残さない)。 + ただし「!」「.」「?」等の非音素記号(punctuation)は全て消える(ポーズ記号も残さない)。 非音素記号を含める処理は`align_tones()`で行われる。 また「っ」は「q」に、「ん」は「n」に変換される。 例: "こんにちは、世界ー。。元気?!" → @@ -272,8 +349,7 @@ def text2sep_kata(norm_text: str) -> tuple[list[str], list[str]]: """ assert yomi != "", f"Empty yomi: {word}" if yomi == "、": - assert len(word) == 1, f"yomi `、` comes from: {word}" - # wordは1文字の記号。正規化されているので、`.`, `,`, `!`, `'`, `-`のいずれか + # wordは正規化されているので、`.`, `,`, `!`, `'`, `-`のいずれか if word not in ( ".", ",", @@ -281,6 +357,7 @@ def text2sep_kata(norm_text: str) -> tuple[list[str], list[str]]: "'", "-", ): + # ここはpyopenjtalkが読めない文字等のときに起こる raise ValueError(f"Cannot read: {word} in:\n{norm_text}") # yomiは元の記号のままに変更 yomi = word @@ -292,35 +369,6 @@ def text2sep_kata(norm_text: str) -> tuple[list[str], list[str]]: return sep_text, sep_kata -def kata2phoneme(text: str) -> list[str]: - """ - 原則カタカナの`text`を受け取り、それをそのままいじらずに音素記号のリストに変換。 - 注意点: - - punctuationが来た場合(punctuationが1文字の場合がありうる)、処理せず1文字のリストを返す - - 冒頭に続く「ー」はそのまま「ー」のままにする(`handle_long()`で処理される) - - 文中の「ー」は前の音素記号の最後の音素記号に変換される。 - 例: - `ーーソーナノカーー` → ["ー", "ー", "s", "o", "o", "n", "a", "n", "o", "k", "a", "a", "a"] - `?` → ["?"] - """ - if text in punctuation: - return [text] - # `text`がカタカナ(`ー`含む)のみからなるかどうかをチェック - if re.fullmatch(r"[\u30A0-\u30FF]+", text) is None: - raise ValueError(f"Non-punctuation input must be katakana only: {text}") - # 以降`text`はカタカナのみからなる - if text == "ー": - return ["ー"] - elif text.startswith("ー"): - return ["ー"] + kata2phoneme(text[1:]) - res: list[str] = [] - while text: - # カタカナをひらがなに変換してから`hiragana2p`をかける - res += hiragana2p(jaconv.kata2hira(text)).split(" ") - break - return res - - # ESPnetの実装から引用、変更点無し # https://github.com/espnet/espnet/blob/master/espnet2/text/phoneme_tokenizer.py def pyopenjtalk_g2p_prosody(text: str, drop_unvoiced_vowels: bool = True) -> list[str]: @@ -492,346 +540,38 @@ def align_tones( return result -# jaconvから借りて修正 -# https://github.com/ikegami-yukino/jaconv/blob/master/jaconv/jaconv.py -def hiragana2p(text: str) -> str: +def kata2phoneme_list(text: str) -> list[str]: """ - Modification of `jaconv.hiragana2julius`. - - avoid using `:`, instead, `あーーー` -> `a a a a`. - - avoid converting `o u` to `o o` (because the input is already actual `yomi`). - - avoid using `N` for `ん` (for compatibility) - - use `v` for `ゔ` related text. - - add bare `ゃ` `ゅ` `ょ` to `y a` `y u` `y o` (for compatibility). + 原則カタカナの`text`を受け取り、それをそのままいじらずに音素記号のリストに変換。 + 注意点: + - punctuationが来た場合(punctuationが1文字の場合がありうる)、処理せず1文字のリストを返す + - 冒頭に続く「ー」はそのまま「ー」のままにする(`handle_long()`で処理される) + - 文中の「ー」は前の音素記号の最後の音素記号に変換される。 + 例: + `ーーソーナノカーー` → ["ー", "ー", "s", "o", "o", "n", "a", "n", "o", "k", "a", "a", "a"] + `?` → ["?"] """ - # 3文字以上からなる変換規則 - text = text.replace("う゛ぁ", " v a") - text = text.replace("う゛ぃ", " v i") - text = text.replace("う゛ぇ", " v e") - text = text.replace("う゛ぉ", " v o") - text = text.replace("う゛ゅ", " by u") + if text in punctuation: + return [text] + # `text`がカタカナ(`ー`含む)のみからなるかどうかをチェック + if re.fullmatch(r"[\u30A0-\u30FF]+", text) is None: + raise ValueError(f"Input must be katakana only: {text}") + sorted_keys = sorted(mora_kata_to_mora_phonemes.keys(), key=len, reverse=True) + pattern = "|".join(map(re.escape, sorted_keys)) - # ゔ等の処理を追加 - text = text.replace("ゔぁ", " v a") - text = text.replace("ゔぃ", " v i") - text = text.replace("ゔぇ", " v e") - text = text.replace("ゔぉ", " v o") - text = text.replace("ゔゅ", " by u") + def mora2phonemes(mora: str) -> str: + cosonant, vowel = mora_kata_to_mora_phonemes[mora] + if cosonant is None: + return f" {vowel}" + return f" {cosonant} {vowel}" - # 2文字からなる変換規則 - text = text.replace("ぅ゛", " v u") + spaced_phonemes = re.sub(pattern, lambda m: mora2phonemes(m.group()), text) - text = text.replace("あぁ", " a a") - text = text.replace("いぃ", " i i") - text = text.replace("いぇ", " i e") - text = text.replace("いゃ", " y a") - text = text.replace("うぅ", " u:") - text = text.replace("えぇ", " e e") - text = text.replace("おぉ", " o:") - text = text.replace("かぁ", " k a:") - text = text.replace("きぃ", " k i:") - text = text.replace("くぅ", " k u:") - text = text.replace("くゃ", " ky a") - text = text.replace("くゅ", " ky u") - text = text.replace("くょ", " ky o") - text = text.replace("けぇ", " k e:") - text = text.replace("こぉ", " k o:") - text = text.replace("がぁ", " g a:") - text = text.replace("ぎぃ", " g i:") - text = text.replace("ぐぅ", " g u:") - text = text.replace("ぐゃ", " gy a") - text = text.replace("ぐゅ", " gy u") - text = text.replace("ぐょ", " gy o") - text = text.replace("げぇ", " g e:") - text = text.replace("ごぉ", " g o:") - text = text.replace("さぁ", " s a:") - text = text.replace("しぃ", " sh i:") - text = text.replace("すぅ", " s u:") - text = text.replace("すゃ", " sh a") - text = text.replace("すゅ", " sh u") - text = text.replace("すょ", " sh o") - text = text.replace("せぇ", " s e:") - text = text.replace("そぉ", " s o:") - text = text.replace("ざぁ", " z a:") - text = text.replace("じぃ", " j i:") - text = text.replace("ずぅ", " z u:") - text = text.replace("ずゃ", " zy a") - text = text.replace("ずゅ", " zy u") - text = text.replace("ずょ", " zy o") - text = text.replace("ぜぇ", " z e:") - text = text.replace("ぞぉ", " z o:") - text = text.replace("たぁ", " t a:") - text = text.replace("ちぃ", " ch i:") - text = text.replace("つぁ", " ts a") - text = text.replace("つぃ", " ts i") - text = text.replace("つぅ", " ts u:") - text = text.replace("つゃ", " ch a") - text = text.replace("つゅ", " ch u") - text = text.replace("つょ", " ch o") - text = text.replace("つぇ", " ts e") - text = text.replace("つぉ", " ts o") - text = text.replace("てぇ", " t e:") - text = text.replace("とぉ", " t o:") - text = text.replace("だぁ", " d a:") - text = text.replace("ぢぃ", " j i:") - text = text.replace("づぅ", " d u:") - text = text.replace("づゃ", " zy a") - text = text.replace("づゅ", " zy u") - text = text.replace("づょ", " zy o") - text = text.replace("でぇ", " d e:") - text = text.replace("どぉ", " d o:") - text = text.replace("なぁ", " n a:") - text = text.replace("にぃ", " n i:") - text = text.replace("ぬぅ", " n u:") - text = text.replace("ぬゃ", " ny a") - text = text.replace("ぬゅ", " ny u") - text = text.replace("ぬょ", " ny o") - text = text.replace("ねぇ", " n e:") - text = text.replace("のぉ", " n o:") - text = text.replace("はぁ", " h a:") - text = text.replace("ひぃ", " h i:") - text = text.replace("ふぅ", " f u:") - text = text.replace("ふゃ", " hy a") - text = text.replace("ふゅ", " hy u") - text = text.replace("ふょ", " hy o") - text = text.replace("へぇ", " h e:") - text = text.replace("ほぉ", " h o:") - text = text.replace("ばぁ", " b a:") - text = text.replace("びぃ", " b i:") - text = text.replace("ぶぅ", " b u:") - text = text.replace("ふゃ", " hy a") - text = text.replace("ぶゅ", " by u") - text = text.replace("ふょ", " hy o") - text = text.replace("べぇ", " b e:") - text = text.replace("ぼぉ", " b o:") - text = text.replace("ぱぁ", " p a:") - text = text.replace("ぴぃ", " p i:") - text = text.replace("ぷぅ", " p u:") - text = text.replace("ぷゃ", " py a") - text = text.replace("ぷゅ", " py u") - text = text.replace("ぷょ", " py o") - text = text.replace("ぺぇ", " p e:") - text = text.replace("ぽぉ", " p o:") - text = text.replace("まぁ", " m a:") - text = text.replace("みぃ", " m i:") - text = text.replace("むぅ", " m u:") - text = text.replace("むゃ", " my a") - text = text.replace("むゅ", " my u") - text = text.replace("むょ", " my o") - text = text.replace("めぇ", " m e:") - text = text.replace("もぉ", " m o:") - text = text.replace("やぁ", " y a:") - text = text.replace("ゆぅ", " y u:") - text = text.replace("ゆゃ", " y a:") - text = text.replace("ゆゅ", " y u:") - text = text.replace("ゆょ", " y o:") - text = text.replace("よぉ", " y o:") - text = text.replace("らぁ", " r a:") - text = text.replace("りぃ", " r i:") - text = text.replace("るぅ", " r u:") - text = text.replace("るゃ", " ry a") - text = text.replace("るゅ", " ry u") - text = text.replace("るょ", " ry o") - text = text.replace("れぇ", " r e:") - text = text.replace("ろぉ", " r o:") - text = text.replace("わぁ", " w a:") - text = text.replace("をぉ", " o:") - - text = text.replace("う゛", " b u") - text = text.replace("でぃ", " d i") - text = text.replace("でぇ", " d e:") - text = text.replace("でゃ", " dy a") - text = text.replace("でゅ", " dy u") - text = text.replace("でょ", " dy o") - text = text.replace("てぃ", " t i") - text = text.replace("てぇ", " t e:") - text = text.replace("てゃ", " ty a") - text = text.replace("てゅ", " ty u") - text = text.replace("てょ", " ty o") - text = text.replace("すぃ", " s i") - text = text.replace("ずぁ", " z u a") - text = text.replace("ずぃ", " z i") - text = text.replace("ずぅ", " z u") - text = text.replace("ずゃ", " zy a") - text = text.replace("ずゅ", " zy u") - text = text.replace("ずょ", " zy o") - text = text.replace("ずぇ", " z e") - text = text.replace("ずぉ", " z o") - text = text.replace("きゃ", " ky a") - text = text.replace("きゅ", " ky u") - text = text.replace("きょ", " ky o") - text = text.replace("しゃ", " sh a") - text = text.replace("しゅ", " sh u") - text = text.replace("しぇ", " sh e") - text = text.replace("しょ", " sh o") - text = text.replace("ちゃ", " ch a") - text = text.replace("ちゅ", " ch u") - text = text.replace("ちぇ", " ch e") - text = text.replace("ちょ", " ch o") - text = text.replace("とぅ", " t u") - text = text.replace("とゃ", " ty a") - text = text.replace("とゅ", " ty u") - text = text.replace("とょ", " ty o") - text = text.replace("どぁ", " d o a") - text = text.replace("どぅ", " d u") - text = text.replace("どゃ", " dy a") - text = text.replace("どゅ", " dy u") - text = text.replace("どょ", " dy o") - text = text.replace("どぉ", " d o:") - text = text.replace("にゃ", " ny a") - text = text.replace("にゅ", " ny u") - text = text.replace("にょ", " ny o") - text = text.replace("ひゃ", " hy a") - text = text.replace("ひゅ", " hy u") - text = text.replace("ひょ", " hy o") - text = text.replace("みゃ", " my a") - text = text.replace("みゅ", " my u") - text = text.replace("みょ", " my o") - text = text.replace("りゃ", " ry a") - text = text.replace("りゅ", " ry u") - text = text.replace("りょ", " ry o") - text = text.replace("ぎゃ", " gy a") - text = text.replace("ぎゅ", " gy u") - text = text.replace("ぎょ", " gy o") - text = text.replace("ぢぇ", " j e") - text = text.replace("ぢゃ", " j a") - text = text.replace("ぢゅ", " j u") - text = text.replace("ぢょ", " j o") - text = text.replace("じぇ", " j e") - text = text.replace("じゃ", " j a") - text = text.replace("じゅ", " j u") - text = text.replace("じょ", " j o") - text = text.replace("びゃ", " by a") - text = text.replace("びゅ", " by u") - text = text.replace("びょ", " by o") - text = text.replace("ぴゃ", " py a") - text = text.replace("ぴゅ", " py u") - text = text.replace("ぴょ", " py o") - text = text.replace("うぁ", " u a") - text = text.replace("うぃ", " w i") - text = text.replace("うぇ", " w e") - text = text.replace("うぉ", " w o") - text = text.replace("ふぁ", " f a") - text = text.replace("ふぃ", " f i") - text = text.replace("ふぅ", " f u") - text = text.replace("ふゃ", " hy a") - text = text.replace("ふゅ", " hy u") - text = text.replace("ふょ", " hy o") - text = text.replace("ふぇ", " f e") - text = text.replace("ふぉ", " f o") - - # 1音からなる変換規則 - text = text.replace("あ", " a") - text = text.replace("い", " i") - text = text.replace("う", " u") - text = text.replace("ゔ", " v u") # ゔの処理を追加 - text = text.replace("え", " e") - text = text.replace("お", " o") - text = text.replace("か", " k a") - text = text.replace("き", " k i") - text = text.replace("く", " k u") - text = text.replace("け", " k e") - text = text.replace("こ", " k o") - text = text.replace("さ", " s a") - text = text.replace("し", " sh i") - text = text.replace("す", " s u") - text = text.replace("せ", " s e") - text = text.replace("そ", " s o") - text = text.replace("た", " t a") - text = text.replace("ち", " ch i") - text = text.replace("つ", " ts u") - text = text.replace("て", " t e") - text = text.replace("と", " t o") - text = text.replace("な", " n a") - text = text.replace("に", " n i") - text = text.replace("ぬ", " n u") - text = text.replace("ね", " n e") - text = text.replace("の", " n o") - text = text.replace("は", " h a") - text = text.replace("ひ", " h i") - text = text.replace("ふ", " f u") - text = text.replace("へ", " h e") - text = text.replace("ほ", " h o") - text = text.replace("ま", " m a") - text = text.replace("み", " m i") - text = text.replace("む", " m u") - text = text.replace("め", " m e") - text = text.replace("も", " m o") - text = text.replace("ら", " r a") - text = text.replace("り", " r i") - text = text.replace("る", " r u") - text = text.replace("れ", " r e") - text = text.replace("ろ", " r o") - text = text.replace("が", " g a") - text = text.replace("ぎ", " g i") - text = text.replace("ぐ", " g u") - text = text.replace("げ", " g e") - text = text.replace("ご", " g o") - text = text.replace("ざ", " z a") - text = text.replace("じ", " j i") - text = text.replace("ず", " z u") - text = text.replace("ぜ", " z e") - text = text.replace("ぞ", " z o") - text = text.replace("だ", " d a") - text = text.replace("ぢ", " j i") - text = text.replace("づ", " z u") - text = text.replace("で", " d e") - text = text.replace("ど", " d o") - text = text.replace("ば", " b a") - text = text.replace("び", " b i") - text = text.replace("ぶ", " b u") - text = text.replace("べ", " b e") - text = text.replace("ぼ", " b o") - text = text.replace("ぱ", " p a") - text = text.replace("ぴ", " p i") - text = text.replace("ぷ", " p u") - text = text.replace("ぺ", " p e") - text = text.replace("ぽ", " p o") - text = text.replace("や", " y a") - text = text.replace("ゆ", " y u") - text = text.replace("よ", " y o") - text = text.replace("わ", " w a") - text = text.replace("ゐ", " i") - text = text.replace("ゑ", " e") - text = text.replace("ん", " N") - text = text.replace("っ", " q") - # ここまでに処理されてない ぁぃぅぇぉ はそのまま大文字扱い - text = text.replace("ぁ", " a") - text = text.replace("ぃ", " i") - text = text.replace("ぅ", " u") - text = text.replace("ぇ", " e") - text = text.replace("ぉ", " o") - text = text.replace("ゎ", " w a") - text = text.replace("ぉ", " o") - - # ここまでに処理されていないゅ等もそのまま大文字扱い(追加) - text = text.replace("ゃ", " y a") - text = text.replace("ゅ", " y u") - text = text.replace("ょ", " y o") - - # 長音の処理 - # for (pattern, replace_str) in JULIUS_LONG_VOWEL: - # text = pattern.sub(replace_str, text) - # text = text.replace("o u", "o:") # おう -> おーの音便 - text = text.replace("ー", ":") - text = text.replace("〜", ":") - text = text.replace("−", ":") - text = text.replace("-", ":") - - # その他特別な処理 - text = text.replace("を", " o") - - text = text.strip() - - text = text.replace(":+", ":") - - # ここまで`jaconv.hiragana2julius`と音便処理と長音処理をのぞいて同じ - # ここから`k a:: k i:`→`k a a a k i i`のように`:`の数だけ繰り返す処理 - pattern = r"(\w)(:*)" - replacement = lambda m: m.group(1) + (" " + m.group(1)) * len(m.group(2)) - - text = re.sub(pattern, replacement, text) - text = text.replace("N", "n") # 「ん」のNをnに変換 - return text + # 長音記号「ー」の処理 + long_pattern = r"(\w)(ー*)" + long_replacement = lambda m: m.group(1) + (" " + m.group(1)) * len(m.group(2)) + spaced_phonemes = re.sub(long_pattern, long_replacement, spaced_phonemes) + return spaced_phonemes.strip().split(" ") if __name__ == "__main__": diff --git a/text/japanese_mora_list.py b/text/japanese_mora_list.py new file mode 100644 index 0000000..57d09e7 --- /dev/null +++ b/text/japanese_mora_list.py @@ -0,0 +1,229 @@ +""" +VOICEVOXのソースコードからお借りして最低限に改造したコード。 +https://github.com/VOICEVOX/voicevox_engine/blob/master/voicevox_engine/tts_pipeline/mora_list.py +""" +""" +以下のモーラ対応表はOpenJTalkのソースコードから取得し、 +カタカナ表記とモーラが一対一対応するように改造した。 +ライセンス表記: +----------------------------------------------------------------- + The Japanese TTS System "Open JTalk" + developed by HTS Working Group + http://open-jtalk.sourceforge.net/ +----------------------------------------------------------------- + + Copyright (c) 2008-2014 Nagoya Institute of Technology + Department of Computer Science + +All rights reserved. + +Redistribution and use in source and binary forms, with or +without modification, are permitted provided that the following +conditions are met: + +- Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. +- Neither the name of the HTS working group nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +""" + +# カタカナ, 子音, 母音)の順。子音がない場合はNoneを入れる。 +# 但し「ン」と「ッ」は母音のみという扱いで、「ン」は「N」、「ッ」は「q」に変更 +# (元々はそれぞれ「N」「cl」) +_mora_list_minimum: list[tuple[str, str | None, str]] = [ + ("ヴォ", "v", "o"), + ("ヴェ", "v", "e"), + ("ヴィ", "v", "i"), + ("ヴァ", "v", "a"), + ("ヴ", "v", "u"), + ("ン", None, "n"), # 「N」から「n」に変更 + ("ワ", "w", "a"), + ("ロ", "r", "o"), + ("レ", "r", "e"), + ("ル", "r", "u"), + ("リョ", "ry", "o"), + ("リュ", "ry", "u"), + ("リャ", "ry", "a"), + ("リェ", "ry", "e"), + ("リ", "r", "i"), + ("ラ", "r", "a"), + ("ヨ", "y", "o"), + ("ユ", "y", "u"), + ("ヤ", "y", "a"), + ("モ", "m", "o"), + ("メ", "m", "e"), + ("ム", "m", "u"), + ("ミョ", "my", "o"), + ("ミュ", "my", "u"), + ("ミャ", "my", "a"), + ("ミェ", "my", "e"), + ("ミ", "m", "i"), + ("マ", "m", "a"), + ("ポ", "p", "o"), + ("ボ", "b", "o"), + ("ホ", "h", "o"), + ("ペ", "p", "e"), + ("ベ", "b", "e"), + ("ヘ", "h", "e"), + ("プ", "p", "u"), + ("ブ", "b", "u"), + ("フォ", "f", "o"), + ("フェ", "f", "e"), + ("フィ", "f", "i"), + ("ファ", "f", "a"), + ("フ", "f", "u"), + ("ピョ", "py", "o"), + ("ピュ", "py", "u"), + ("ピャ", "py", "a"), + ("ピェ", "py", "e"), + ("ピ", "p", "i"), + ("ビョ", "by", "o"), + ("ビュ", "by", "u"), + ("ビャ", "by", "a"), + ("ビェ", "by", "e"), + ("ビ", "b", "i"), + ("ヒョ", "hy", "o"), + ("ヒュ", "hy", "u"), + ("ヒャ", "hy", "a"), + ("ヒェ", "hy", "e"), + ("ヒ", "h", "i"), + ("パ", "p", "a"), + ("バ", "b", "a"), + ("ハ", "h", "a"), + ("ノ", "n", "o"), + ("ネ", "n", "e"), + ("ヌ", "n", "u"), + ("ニョ", "ny", "o"), + ("ニュ", "ny", "u"), + ("ニャ", "ny", "a"), + ("ニェ", "ny", "e"), + ("ニ", "n", "i"), + ("ナ", "n", "a"), + ("ドゥ", "d", "u"), + ("ド", "d", "o"), + ("トゥ", "t", "u"), + ("ト", "t", "o"), + ("デョ", "dy", "o"), + ("デュ", "dy", "u"), + ("デャ", "dy", "a"), + ("デェ", "dy", "e"), + ("ディ", "d", "i"), + ("デ", "d", "e"), + ("テョ", "ty", "o"), + ("テュ", "ty", "u"), + ("テャ", "ty", "a"), + ("ティ", "t", "i"), + ("テ", "t", "e"), + ("ツォ", "ts", "o"), + ("ツェ", "ts", "e"), + ("ツィ", "ts", "i"), + ("ツァ", "ts", "a"), + ("ツ", "ts", "u"), + ("ッ", None, "q"), # 「cl」から「q」に変更 + ("チョ", "ch", "o"), + ("チュ", "ch", "u"), + ("チャ", "ch", "a"), + ("チェ", "ch", "e"), + ("チ", "ch", "i"), + ("ダ", "d", "a"), + ("タ", "t", "a"), + ("ゾ", "z", "o"), + ("ソ", "s", "o"), + ("ゼ", "z", "e"), + ("セ", "s", "e"), + ("ズィ", "z", "i"), + ("ズ", "z", "u"), + ("スィ", "s", "i"), + ("ス", "s", "u"), + ("ジョ", "j", "o"), + ("ジュ", "j", "u"), + ("ジャ", "j", "a"), + ("ジェ", "j", "e"), + ("ジ", "j", "i"), + ("ショ", "sh", "o"), + ("シュ", "sh", "u"), + ("シャ", "sh", "a"), + ("シェ", "sh", "e"), + ("シ", "sh", "i"), + ("ザ", "z", "a"), + ("サ", "s", "a"), + ("ゴ", "g", "o"), + ("コ", "k", "o"), + ("ゲ", "g", "e"), + ("ケ", "k", "e"), + ("グヮ", "gw", "a"), + ("グ", "g", "u"), + ("クヮ", "kw", "a"), + ("ク", "k", "u"), + ("ギョ", "gy", "o"), + ("ギュ", "gy", "u"), + ("ギャ", "gy", "a"), + ("ギェ", "gy", "e"), + ("ギ", "g", "i"), + ("キョ", "ky", "o"), + ("キュ", "ky", "u"), + ("キャ", "ky", "a"), + ("キェ", "ky", "e"), + ("キ", "k", "i"), + ("ガ", "g", "a"), + ("カ", "k", "a"), + ("オ", None, "o"), + ("エ", None, "e"), + ("ウォ", "w", "o"), + ("ウェ", "w", "e"), + ("ウィ", "w", "i"), + ("ウ", None, "u"), + ("イェ", "y", "e"), + ("イ", None, "i"), + ("ア", None, "a"), +] +_mora_list_additional: list[tuple[str, str | None, str]] = [ + ("ヴョ", "by", "o"), + ("ヴュ", "by", "u"), + ("ヴャ", "by", "a"), + ("ヲ", None, "o"), + ("ヱ", None, "e"), + ("ヰ", None, "i"), + ("ヮ", "w", "a"), + ("ョ", "y", "o"), + ("ュ", "y", "u"), + ("ヅ", "z", "u"), + ("ヂ", "j", "i"), + ("ヶ", "k", "e"), + ("ャ", "y", "a"), + ("ォ", None, "o"), + ("ェ", None, "e"), + ("ゥ", None, "u"), + ("ィ", None, "i"), + ("ァ", None, "a"), +] + +# 例: "vo" -> "ヴォ", "a" -> "ア" +mora_phonemes_to_mora_kata: dict[str, str] = { + (consonant or "") + vowel: kana for [kana, consonant, vowel] in _mora_list_minimum +} + +# 例: "ヴォ" -> ("v", "o"), "ア" -> (None, "a") +mora_kata_to_mora_phonemes: dict[str, tuple[str | None, str]] = { + kana: (consonant, vowel) + for [kana, consonant, vowel] in _mora_list_minimum + _mora_list_additional +}