diff --git a/tts/text/input_parsing/parser.py b/tts/text/input_parsing/parser.py index 9fad52d..26d73a3 100644 --- a/tts/text/input_parsing/parser.py +++ b/tts/text/input_parsing/parser.py @@ -21,12 +21,14 @@ class GraphemeSquence: def __init__(self, sequence: str): self.sequence = sequence -def parse_grapheme_and_arpabet_sequence(input_text: str) -> List[Union[ArpabetSequence, GraphemeSquence]]: +ParsedSequence = List[Union[ArpabetSequence, GraphemeSquence]] + +def parse_grapheme_and_arpabet_sequence(input_text: str) -> ParsedSequence: """ Parse a string into a list of ArpabetSequence and GraphemeSequence so that each subsequence can be treated independently. """ - sequence : List[Union[ArpabetSequence, GraphemeSquence]] = [] + sequence : ParsedSequence = [] # Check for curly braces and treat their contents as ARPAbet: while len(input_text): diff --git a/tts/text/phonetization/cmudict.py b/tts/text/phonetization/cmudict.py index 13cb736..0a07bf7 100644 --- a/tts/text/phonetization/cmudict.py +++ b/tts/text/phonetization/cmudict.py @@ -4,6 +4,8 @@ Contents of this file are mostly from https://github.com/keithito/tacotron import re +# TODO: Don't use strings for arpabet phoneme representation, and share the memory. + # Relative to TTS codebase root # This does not include any CMUDict extensions or alternatives (eg. for other languages). DEFAULT_CMUDICT_PATH = 'dictionaries/cmudict-0.7b' diff --git a/tts/text/phonetization/english_phonetization.py b/tts/text/phonetization/english_phonetization.py index 5c48aac..60674aa 100644 --- a/tts/text/phonetization/english_phonetization.py +++ b/tts/text/phonetization/english_phonetization.py @@ -7,7 +7,12 @@ class EnglishPhonetization(BasePhonetization): def __init__(self, phoneme_dictionary: CMUDict): super().__init__(phoneme_dictionary) - def convert_to_phonemes(self, grapheme_sequence: str) -> List[str]: + def try_convert_to_phonemes(self, grapheme_sequence: str) -> List[str]: + """ + Attempt to enrich a grapheme sequence with phonemes where word lookup succeeds. + Grapheme symbols will be left in place. + The output list will need to be space-separated before inference. + """ words = self._split_grapheme_sentence_to_words(grapheme_sequence) converted_words = [] diff --git a/tts/text/phonetization/test_english_phonetization.py b/tts/text/phonetization/test_english_phonetization.py index f3c46db..deef8d5 100644 --- a/tts/text/phonetization/test_english_phonetization.py +++ b/tts/text/phonetization/test_english_phonetization.py @@ -6,27 +6,27 @@ class TestEnglishPhonetization: def test_no_input(self): phoneme_dictionary = CMUDict(DEFAULT_CMUDICT_PATH) phonetization = EnglishPhonetization(phoneme_dictionary) - output = phonetization.convert_to_phonemes("") + output = phonetization.try_convert_to_phonemes("") assert output == [] def test_all_words_in_dictionary(self): phoneme_dictionary = CMUDict(DEFAULT_CMUDICT_PATH) phonetization = EnglishPhonetization(phoneme_dictionary) - output = phonetization.convert_to_phonemes("this is a test") + output = phonetization.try_convert_to_phonemes("this is a test") assert output == ["DH IH1 S", "IH1 Z", "AH0", "T EH1 S T"] def test_most_words_in_dictionary(self): phoneme_dictionary = CMUDict(DEFAULT_CMUDICT_PATH) phonetization = EnglishPhonetization(phoneme_dictionary) - output = phonetization.convert_to_phonemes("nonword is a test") + output = phonetization.try_convert_to_phonemes("nonword is a test") assert output == ["nonword", "IH1 Z", "AH0", "T EH1 S T"] def test_punctuation(self): phoneme_dictionary = CMUDict(DEFAULT_CMUDICT_PATH) phonetization = EnglishPhonetization(phoneme_dictionary) - output = phonetization.convert_to_phonemes("this...") + output = phonetization.try_convert_to_phonemes("this...") assert output == ["DH IH1 S", "..."] - output = phonetization.convert_to_phonemes("this!") + output = phonetization.try_convert_to_phonemes("this!") assert output == ["DH IH1 S", "!"] - output = phonetization.convert_to_phonemes("this?") + output = phonetization.try_convert_to_phonemes("this?") assert output == ["DH IH1 S", "?"] diff --git a/tts/text/pipeline.py b/tts/text/pipeline.py deleted file mode 100644 index de486a6..0000000 --- a/tts/text/pipeline.py +++ /dev/null @@ -1,46 +0,0 @@ -from enum import Enum, auto - -from tts.text.input_parsing.parser import GraphemeSquence, parse_grapheme_and_arpabet_sequence -from tts.text.normalization.english_normalization import EnglishNormalization -from tts.text.normalization.spanish_normalization import SpanishNormalization - - -class PipelineType(Enum): - LEGACY_FAKEYOU = auto() - ENGLISH_FORCED_ARPABET_V1 = auto() - SPANISH_FORCED_ARPABET_V1 = auto() - -_ENGLISH_NORMALIZER = EnglishNormalization() -_SPANISH_NORMALIZER = SpanishNormalization() - -def _legacy_fakeyou_pipeline(input_text): - """ - Legacy FakeYou.com did not do Grapheme -> Arpabet conversions automatically. - """ - pass - - -def _english_forced_arpabet_v1(input_text): - parsed_sequence = parse_grapheme_and_arpabet_sequence(input_text) - for i in range(len(parsed_sequence)): - subsequence = parsed_sequence[i] - if isinstance(subsequence, GraphemeSquence): - normalized = _ENGLISH_NORMALIZER.normalize(subsequence.sequence) - parsed_sequence[i] = GraphemeSquence(normalized) - - integer_sequence = [] - - - -def _spanish_forced_arpabet_v1(input_text): - parsed_sequence = parse_grapheme_and_arpabet_sequence(input_text) - - -def text_to_sequence(input_text: str, pipeline_type: PipelineType): - if pipeline_type == PipelineType.ENGLISH_FORCED_ARPABET_V1: - return _english_forced_arpabet_v1(input_text) - elif pipeline_type == PipelineType.SPANISH_FORCED_ARPABET_V1: - return _spanish_forced_arpabet_v1(input_text) - else: - return _legacy_fakeyou_pipeline(input_text) - diff --git a/tts/text/pipeline/__init__.py b/tts/text/pipeline/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tts/text/pipeline/base_pipeline.py b/tts/text/pipeline/base_pipeline.py new file mode 100644 index 0000000..584aa34 --- /dev/null +++ b/tts/text/pipeline/base_pipeline.py @@ -0,0 +1,7 @@ +from typing import List + +class BasePipeline: + + def user_input_to_sequence(self, input_text: str) -> List[int]: + """Convert raw user input into a numerical sequence for ML inference.""" + return [] \ No newline at end of file diff --git a/tts/text/pipeline/english_pipeline_v1.py b/tts/text/pipeline/english_pipeline_v1.py new file mode 100644 index 0000000..38e405f --- /dev/null +++ b/tts/text/pipeline/english_pipeline_v1.py @@ -0,0 +1,42 @@ +from typing import List +from tts.text.input_parsing.parser import GraphemeSquence, ParsedSequence, parse_grapheme_and_arpabet_sequence +from tts.text.normalization.english_normalization import EnglishNormalization +from tts.text.phonetization.cmudict import CMUDict +from tts.text.phonetization.english_phonetization import EnglishPhonetization +from tts.text.pipeline.base_pipeline import BasePipeline + +_ENGLISH_NORMALIZER = EnglishNormalization() + +_ENGLISH_DICTIONARY = CMUDict() +_ENGLISH_PHONETIZER = EnglishPhonetization(_ENGLISH_DICTIONARY) + +class EnglishPipelineV1(BasePipeline): + + def user_input_to_sequence(self, input_text: str) -> List[int]: + """Convert raw user input into a numerical sequence for ML inference.""" + + parsed_sequence = parse_grapheme_and_arpabet_sequence(input_text) + + self._normalize_graphemes(parsed_sequence) + self._convert_phonemes(parsed_sequence) + + + integer_sequence = [] + + return integer_sequence + + def _normalize_graphemes(self, parsed_sequence: ParsedSequence): + for i in range(len(parsed_sequence)): + subsequence = parsed_sequence[i] + if isinstance(subsequence, GraphemeSquence): + normalized = _ENGLISH_NORMALIZER.normalize(subsequence.sequence) + parsed_sequence[i] = GraphemeSquence(normalized) + + + def _convert_phonemes(self, parsed_sequence: ParsedSequence): + for i in range(len(parsed_sequence)): + subsequence = parsed_sequence[i] + if isinstance(subsequence, GraphemeSquence): + converted = _ENGLISH_PHONETIZER.try_convert_to_phonemes(subsequence.sequence) + converted = ' '.join(converted) + parsed_sequence[i] = GraphemeSquence(converted) diff --git a/tts/text/pipeline/legacy_fakeyou_pipeline.py b/tts/text/pipeline/legacy_fakeyou_pipeline.py new file mode 100644 index 0000000..e69de29 diff --git a/tts/text/pipeline/legacy_vocodes_pipeline.py b/tts/text/pipeline/legacy_vocodes_pipeline.py new file mode 100644 index 0000000..e69de29 diff --git a/tts/text/pipeline/spanish_pipeline.py b/tts/text/pipeline/spanish_pipeline.py new file mode 100644 index 0000000..e69de29 diff --git a/tts/text/sequencing/english_symbols_v1.py b/tts/text/sequencing/english_symbols_v1.py new file mode 100644 index 0000000..df4753a --- /dev/null +++ b/tts/text/sequencing/english_symbols_v1.py @@ -0,0 +1,38 @@ +"""Adapted from https://github.com/keithito/tacotron """ + +''' +Defines the set of symbols used in text input to the model. + +The default is a set of ASCII characters that works well for English or text that has been run through Unidecode. +For other data, you can modify _characters. See TRAINING_DATA.md for details. +''' +from tts.text.phonetization import cmudict +from typing import List + +_pad = '_' +_punctuation = '!\'(),.:;? ' #NB: Includes space +_special = '-' +_letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' + +# Prepend "@" to ARPAbet symbols to ensure uniqueness (some are the same as uppercase letters): +_arpabet = ['@' + s for s in cmudict.valid_symbols] + +# Export all symbols: +symbols = [_pad] + list(_special) + list(_punctuation) + list(_letters) + _arpabet + +# Mappings from symbol to numeric ID and vice versa: +# The ordering **CANNOT** change. +_symbol_to_id = {s: i for i, s in enumerate(symbols)} +_id_to_symbol = {i: s for i, s in enumerate(symbols)} + + +def symbols_to_sequence(symbols: str) -> List[int]: + return [_symbol_to_id[s] for s in symbols if _should_keep_symbol(s)] + + +def arpabet_to_sequence(arpabet_string: str) -> List[int]: + return symbols_to_sequence(['@' + s for s in arpabet_string.split()]) + + +def _should_keep_symbol(s): + return s in _symbol_to_id and s is not '_' and s is not '~' diff --git a/tts/text/sequencing/test_english_symbols_v1.py b/tts/text/sequencing/test_english_symbols_v1.py new file mode 100644 index 0000000..2f5b170 --- /dev/null +++ b/tts/text/sequencing/test_english_symbols_v1.py @@ -0,0 +1,19 @@ +from tts.text.sequencing.english_symbols_v1 import arpabet_to_sequence, symbols_to_sequence + +class TestEnglishSymbols: + + def test_no_input_arpabet(self): + output = arpabet_to_sequence('') + assert output == [] + + def test_no_input_symbols(self): + output = symbols_to_sequence('') + assert output == [] + + def test_arpabet_word(self): + output = arpabet_to_sequence('T EH1 S T') + assert output == [133, 94, 131, 133] + + def test_grapheme_word(self): + output = symbols_to_sequence('test') + assert output == [57, 42, 56, 57] \ No newline at end of file