From 5abbe566427c4e12b21577e825a40c5bf63150ce Mon Sep 17 00:00:00 2001 From: harmlessman <87223285+harmlessman@users.noreply.github.com> Date: Thu, 8 Sep 2022 10:06:07 +0000 Subject: [PATCH] Korean Phonemizer (#1822) * Update requirements.txt install jamo for korean * Update formatters.py add KSS formatter KSS is a korean single speech dataset (12hours) * Add files via upload add phonemizer for korean * Add files via upload add korean phonemizer * Update requirements.txt * change code style with `black` and `pylint` * reflecting pylint's Evaluation * reflecting pylint's Evaluation * reflecting pylint's Evaluation-2 * isort * edit about separator write test case and add 'nltk' for requirements.txt * add korean g2p (g2pkk) * isort * TTS/tts/utils/text/phonemizers/ko_kr_phonemizer.py:43:24: W0621: Redefining name 'text' from outer scope (line 58) (redefined-outer-name) TTS/tts/utils/text/korean/korean.py:28:8: R1705: Unnecessary "else" after "return" (no-else-return) * black --- TTS/tts/datasets/formatters.py | 14 +++ TTS/tts/utils/text/korean/__init__.py | 0 TTS/tts/utils/text/korean/ko_dictionary.py | 44 +++++++ TTS/tts/utils/text/korean/korean.py | 32 +++++ TTS/tts/utils/text/korean/phonemizer.py | 32 +++++ TTS/tts/utils/text/phonemizers/__init__.py | 110 +++++++++--------- .../text/phonemizers/ko_kr_phonemizer.py | 65 +++++++++++ requirements.txt | 4 + tests/text_tests/test_korean_phonemizer.py | 31 +++++ 9 files changed, 279 insertions(+), 53 deletions(-) create mode 100644 TTS/tts/utils/text/korean/__init__.py create mode 100644 TTS/tts/utils/text/korean/ko_dictionary.py create mode 100644 TTS/tts/utils/text/korean/korean.py create mode 100644 TTS/tts/utils/text/korean/phonemizer.py create mode 100644 TTS/tts/utils/text/phonemizers/ko_kr_phonemizer.py create mode 100644 tests/text_tests/test_korean_phonemizer.py diff --git a/TTS/tts/datasets/formatters.py b/TTS/tts/datasets/formatters.py index a4be2b33..8b3603f4 100644 --- a/TTS/tts/datasets/formatters.py +++ b/TTS/tts/datasets/formatters.py @@ -578,3 +578,17 @@ def kokoro(root_path, meta_file, **kwargs): # pylint: disable=unused-argument text = cols[2].replace(" ", "") items.append({"text": text, "audio_file": wav_file, "speaker_name": speaker_name, "root_path": root_path}) return items + + +def kss(root_path, meta_file, **kwargs): # pylint: disable=unused-argument + """Korean single-speaker dataset from https://www.kaggle.com/datasets/bryanpark/korean-single-speaker-speech-dataset""" + txt_file = os.path.join(root_path, meta_file) + items = [] + speaker_name = "kss" + with open(txt_file, "r", encoding="utf-8") as ttf: + for line in ttf: + cols = line.split("|") + wav_file = os.path.join(root_path, cols[0]) + text = cols[2] # cols[1] => 6월, cols[2] => 유월 + items.append({"text": text, "audio_file": wav_file, "speaker_name": speaker_name}) + return items diff --git a/TTS/tts/utils/text/korean/__init__.py b/TTS/tts/utils/text/korean/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/TTS/tts/utils/text/korean/ko_dictionary.py b/TTS/tts/utils/text/korean/ko_dictionary.py new file mode 100644 index 00000000..9b739339 --- /dev/null +++ b/TTS/tts/utils/text/korean/ko_dictionary.py @@ -0,0 +1,44 @@ +# coding: utf-8 +# Add the word you want to the dictionary. +etc_dictionary = {"1+1": "원플러스원", "2+1": "투플러스원"} + + +english_dictionary = { + "KOREA": "코리아", + "IDOL": "아이돌", + "IT": "아이티", + "IQ": "아이큐", + "UP": "업", + "DOWN": "다운", + "PC": "피씨", + "CCTV": "씨씨티비", + "SNS": "에스엔에스", + "AI": "에이아이", + "CEO": "씨이오", + "A": "에이", + "B": "비", + "C": "씨", + "D": "디", + "E": "이", + "F": "에프", + "G": "지", + "H": "에이치", + "I": "아이", + "J": "제이", + "K": "케이", + "L": "엘", + "M": "엠", + "N": "엔", + "O": "오", + "P": "피", + "Q": "큐", + "R": "알", + "S": "에스", + "T": "티", + "U": "유", + "V": "브이", + "W": "더블유", + "X": "엑스", + "Y": "와이", + "Z": "제트", +} diff --git a/TTS/tts/utils/text/korean/korean.py b/TTS/tts/utils/text/korean/korean.py new file mode 100644 index 00000000..423aeed3 --- /dev/null +++ b/TTS/tts/utils/text/korean/korean.py @@ -0,0 +1,32 @@ +# coding: utf-8 +# Code based on https://github.com/carpedm20/multi-speaker-tacotron-tensorflow/blob/master/text/korean.py +import re + +from TTS.tts.utils.text.korean.ko_dictionary import english_dictionary, etc_dictionary + + +def normalize(text): + text = text.strip() + text = re.sub("[⺀-⺙⺛-⻳⼀-⿕々〇〡-〩〸-〺〻㐀-䶵一-鿃豈-鶴侮-頻並-龎]", "", text) + text = normalize_with_dictionary(text, etc_dictionary) + text = normalize_english(text) + text = text.lower() + return text + + +def normalize_with_dictionary(text, dic): + if any(key in text for key in dic.keys()): + pattern = re.compile("|".join(re.escape(key) for key in dic.keys())) + return pattern.sub(lambda x: dic[x.group()], text) + return text + + +def normalize_english(text): + def fn(m): + word = m.group() + if word in english_dictionary: + return english_dictionary.get(word) + return word + + text = re.sub("([A-Za-z]+)", fn, text) + return text diff --git a/TTS/tts/utils/text/korean/phonemizer.py b/TTS/tts/utils/text/korean/phonemizer.py new file mode 100644 index 00000000..7c48ef58 --- /dev/null +++ b/TTS/tts/utils/text/korean/phonemizer.py @@ -0,0 +1,32 @@ +from g2pkk import G2p +from jamo import hangul_to_jamo + +from TTS.tts.utils.text.korean.korean import normalize + +g2p = G2p() + + +def korean_text_to_phonemes(text, character: str = "hangeul") -> str: + """ + + The input and output values look the same, but they are different in Unicode. + + example : + + input = '하늘' (Unicode : \ud558\ub298), (하 + 늘) + output = '하늘' (Unicode :\u1112\u1161\u1102\u1173\u11af), (ᄒ + ᅡ + ᄂ + ᅳ + ᆯ) + + """ + + if character == "english": + from anyascii import anyascii + + text = normalize(text) + text = g2p(text) + text = anyascii(text) + return text + + text = normalize(text) + text = g2p(text) + text = list(hangul_to_jamo(text)) # '하늘' --> ['ᄒ', 'ᅡ', 'ᄂ', 'ᅳ', 'ᆯ'] + return "".join(text) diff --git a/TTS/tts/utils/text/phonemizers/__init__.py b/TTS/tts/utils/text/phonemizers/__init__.py index 374d0c8a..a5a341e9 100644 --- a/TTS/tts/utils/text/phonemizers/__init__.py +++ b/TTS/tts/utils/text/phonemizers/__init__.py @@ -1,53 +1,57 @@ -from TTS.tts.utils.text.phonemizers.base import BasePhonemizer -from TTS.tts.utils.text.phonemizers.espeak_wrapper import ESpeak -from TTS.tts.utils.text.phonemizers.gruut_wrapper import Gruut -from TTS.tts.utils.text.phonemizers.ja_jp_phonemizer import JA_JP_Phonemizer -from TTS.tts.utils.text.phonemizers.zh_cn_phonemizer import ZH_CN_Phonemizer - -PHONEMIZERS = {b.name(): b for b in (ESpeak, Gruut, JA_JP_Phonemizer)} - - -ESPEAK_LANGS = list(ESpeak.supported_languages().keys()) -GRUUT_LANGS = list(Gruut.supported_languages()) - - -# Dict setting default phonemizers for each language -# Add Gruut languages -_ = [Gruut.name()] * len(GRUUT_LANGS) -DEF_LANG_TO_PHONEMIZER = dict(list(zip(GRUUT_LANGS, _))) - - -# Add ESpeak languages and override any existing ones -_ = [ESpeak.name()] * len(ESPEAK_LANGS) -_new_dict = dict(list(zip(list(ESPEAK_LANGS), _))) -DEF_LANG_TO_PHONEMIZER.update(_new_dict) - -# Force default for some languages -DEF_LANG_TO_PHONEMIZER["en"] = DEF_LANG_TO_PHONEMIZER["en-us"] -DEF_LANG_TO_PHONEMIZER["ja-jp"] = JA_JP_Phonemizer.name() -DEF_LANG_TO_PHONEMIZER["zh-cn"] = ZH_CN_Phonemizer.name() - - -def get_phonemizer_by_name(name: str, **kwargs) -> BasePhonemizer: - """Initiate a phonemizer by name - - Args: - name (str): - Name of the phonemizer that should match `phonemizer.name()`. - - kwargs (dict): - Extra keyword arguments that should be passed to the phonemizer. - """ - if name == "espeak": - return ESpeak(**kwargs) - if name == "gruut": - return Gruut(**kwargs) - if name == "zh_cn_phonemizer": - return ZH_CN_Phonemizer(**kwargs) - if name == "ja_jp_phonemizer": - return JA_JP_Phonemizer(**kwargs) - raise ValueError(f"Phonemizer {name} not found") - - -if __name__ == "__main__": - print(DEF_LANG_TO_PHONEMIZER) +from TTS.tts.utils.text.phonemizers.base import BasePhonemizer +from TTS.tts.utils.text.phonemizers.espeak_wrapper import ESpeak +from TTS.tts.utils.text.phonemizers.gruut_wrapper import Gruut +from TTS.tts.utils.text.phonemizers.ja_jp_phonemizer import JA_JP_Phonemizer +from TTS.tts.utils.text.phonemizers.ko_kr_phonemizer import KO_KR_Phonemizer +from TTS.tts.utils.text.phonemizers.zh_cn_phonemizer import ZH_CN_Phonemizer + +PHONEMIZERS = {b.name(): b for b in (ESpeak, Gruut, JA_JP_Phonemizer)} + + +ESPEAK_LANGS = list(ESpeak.supported_languages().keys()) +GRUUT_LANGS = list(Gruut.supported_languages()) + + +# Dict setting default phonemizers for each language +# Add Gruut languages +_ = [Gruut.name()] * len(GRUUT_LANGS) +DEF_LANG_TO_PHONEMIZER = dict(list(zip(GRUUT_LANGS, _))) + + +# Add ESpeak languages and override any existing ones +_ = [ESpeak.name()] * len(ESPEAK_LANGS) +_new_dict = dict(list(zip(list(ESPEAK_LANGS), _))) +DEF_LANG_TO_PHONEMIZER.update(_new_dict) + +# Force default for some languages +DEF_LANG_TO_PHONEMIZER["en"] = DEF_LANG_TO_PHONEMIZER["en-us"] +DEF_LANG_TO_PHONEMIZER["ja-jp"] = JA_JP_Phonemizer.name() +DEF_LANG_TO_PHONEMIZER["zh-cn"] = ZH_CN_Phonemizer.name() +DEF_LANG_TO_PHONEMIZER["ko-kr"] = KO_KR_Phonemizer.name() + + +def get_phonemizer_by_name(name: str, **kwargs) -> BasePhonemizer: + """Initiate a phonemizer by name + + Args: + name (str): + Name of the phonemizer that should match `phonemizer.name()`. + + kwargs (dict): + Extra keyword arguments that should be passed to the phonemizer. + """ + if name == "espeak": + return ESpeak(**kwargs) + if name == "gruut": + return Gruut(**kwargs) + if name == "zh_cn_phonemizer": + return ZH_CN_Phonemizer(**kwargs) + if name == "ja_jp_phonemizer": + return JA_JP_Phonemizer(**kwargs) + if name == "ko_kr_phonemizer": + return KO_KR_Phonemizer(**kwargs) + raise ValueError(f"Phonemizer {name} not found") + + +if __name__ == "__main__": + print(DEF_LANG_TO_PHONEMIZER) diff --git a/TTS/tts/utils/text/phonemizers/ko_kr_phonemizer.py b/TTS/tts/utils/text/phonemizers/ko_kr_phonemizer.py new file mode 100644 index 00000000..c4aeb354 --- /dev/null +++ b/TTS/tts/utils/text/phonemizers/ko_kr_phonemizer.py @@ -0,0 +1,65 @@ +from typing import Dict + +from TTS.tts.utils.text.korean.phonemizer import korean_text_to_phonemes +from TTS.tts.utils.text.phonemizers.base import BasePhonemizer + +_DEF_KO_PUNCS = "、.,[]()?!〽~『』「」【】" + + +class KO_KR_Phonemizer(BasePhonemizer): + """🐸TTS ko_kr_phonemizer using functions in `TTS.tts.utils.text.korean.phonemizer` + + TODO: Add Korean to character (ᄀᄁᄂᄃᄄᄅᄆᄇᄈᄉᄊᄋᄌᄍᄎᄏᄐᄑ하ᅢᅣᅤᅥᅦᅧᅨᅩᅪᅫᅬᅭᅮᅯᅰᅱᅲᅳᅴᅵᆨᆩᆪᆫᆬᆭᆮᆯᆰᆱᆲᆳᆴᆵᆶᆷᆸᆹᆺᆻᆼᆽᆾᆿᇀᇁᇂ) + + Example: + + >>> from TTS.tts.utils.text.phonemizers import KO_KR_Phonemizer + >>> phonemizer = KO_KR_Phonemizer() + >>> phonemizer.phonemize("이 문장은 음성합성 테스트를 위한 문장입니다.", separator="|") + 'ᄋ|ᅵ| |ᄆ|ᅮ|ᆫ|ᄌ|ᅡ|ᆼ|ᄋ|ᅳ| |ᄂ|ᅳ|ᆷ|ᄉ|ᅥ|ᆼ|ᄒ|ᅡ|ᆸ|ᄊ|ᅥ|ᆼ| |ᄐ|ᅦ|ᄉ|ᅳ|ᄐ|ᅳ|ᄅ|ᅳ| |ᄅ|ᅱ|ᄒ|ᅡ|ᆫ| |ᄆ|ᅮ|ᆫ|ᄌ|ᅡ|ᆼ|ᄋ|ᅵ|ᆷ|ᄂ|ᅵ|ᄃ|ᅡ|.' + + >>> from TTS.tts.utils.text.phonemizers import KO_KR_Phonemizer + >>> phonemizer = KO_KR_Phonemizer() + >>> phonemizer.phonemize("이 문장은 음성합성 테스트를 위한 문장입니다.", separator="|", character='english') + 'I| |M|u|n|J|a|n|g|E|u| |N|e|u|m|S|e|o|n|g|H|a|b|S|s|e|o|n|g| |T|e|S|e|u|T|e|u|L|e|u| |L|w|i|H|a|n| |M|u|n|J|a|n|g|I|m|N|i|D|a|.' + + """ + + language = "ko-kr" + + def __init__(self, punctuations=_DEF_KO_PUNCS, keep_puncs=True, **kwargs): # pylint: disable=unused-argument + super().__init__(self.language, punctuations=punctuations, keep_puncs=keep_puncs) + + @staticmethod + def name(): + return "ko_kr_phonemizer" + + def _phonemize(self, text: str, separator: str = "", character: str = "hangeul") -> str: + ph = korean_text_to_phonemes(text, character=character) + if separator is not None or separator != "": + return separator.join(ph) + return ph + + def phonemize(self, text: str, separator: str = "", character: str = "hangeul") -> str: + return self._phonemize(text, separator, character) + + @staticmethod + def supported_languages() -> Dict: + return {"ko-kr": "hangeul(korean)"} + + def version(self) -> str: + return "0.0.2" + + def is_available(self) -> bool: + return True + + +if __name__ == "__main__": + texts = "이 문장은 음성합성 테스트를 위한 문장입니다." + e = KO_KR_Phonemizer() + print(e.supported_languages()) + print(e.version()) + print(e.language) + print(e.name()) + print(e.is_available()) + print(e.phonemize(texts)) diff --git a/requirements.txt b/requirements.txt index a9416112..ad6404be 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,3 +36,7 @@ mecab-python3==1.0.5 unidic-lite==1.0.8 # gruut+supported langs gruut[cs,de,es,fr,it,nl,pt,ru,sv]==2.2.3 +# deps for korean +jamo +nltk +g2pkk>=0.1.1 \ No newline at end of file diff --git a/tests/text_tests/test_korean_phonemizer.py b/tests/text_tests/test_korean_phonemizer.py new file mode 100644 index 00000000..7d651d1d --- /dev/null +++ b/tests/text_tests/test_korean_phonemizer.py @@ -0,0 +1,31 @@ +import unittest + +from TTS.tts.utils.text.korean.phonemizer import korean_text_to_phonemes + +_TEST_CASES = """ +포상은 열심히 한 아이에게만 주어지기 때문에 포상인 것입니다./포상으 녈심히 하 나이에게만 주어지기 때무네 포상인 거심니다. +오늘은 8월 31일 입니다./오느른 파뤌 삼시비리 림니다. +친구 100명 만들기가 목표입니다./친구 뱅명 만들기가 목표임니다. +A부터 Z까지 입니다./에이부터 제트까지 임니다. +이게 제 마음이에요./이게 제 마으미에요. +""" +_TEST_CASES_EN = """ +이제야 이쪽을 보는구나./IJeYa IJjoGeul BoNeunGuNa. +크고 맛있는 cake를 부탁해요./KeuGo MaSinNeun KeIKeuLeul BuTaKaeYo. +전부 거짓말이야./JeonBu GeoJinMaLiYa. +좋은 노래를 찾았어요./JoEun NoLaeLeul ChaJaSseoYo. +""" + + +class TestText(unittest.TestCase): + def test_korean_text_to_phonemes(self): + for line in _TEST_CASES.strip().split("\n"): + text, phone = line.split("/") + self.assertEqual(korean_text_to_phonemes(text), phone) + for line in _TEST_CASES_EN.strip().split("\n"): + text, phone = line.split("/") + self.assertEqual(korean_text_to_phonemes(text, character="english"), phone) + + +if __name__ == "__main__": + unittest.main()