This commit is contained in:
Brandon Thomas
2022-07-04 02:47:40 -04:00
parent c401cc5146
commit 1b9d44cf34
20 changed files with 37 additions and 37 deletions
@@ -0,0 +1,56 @@
"""
Parse raw text sequences into Arpabet and Grapheme sequences so that they can be
treated independently.
Inspired by and including code from original Tacotron2 by Keith Ito.
"""
# pyright: strict
import re
from typing import List, Union, Any
# Regular expression matching text enclosed in curly braces:
_curly_re = re.compile(r'(.*?)\{(.+?)\}(.*)')
class ArpabetSequence:
def __init__(self, sequence: str):
self.sequence = sequence
def __eq__(self, other: Any) -> bool:
return isinstance(other, ArpabetSequence) and self.sequence == other.sequence
class GraphemeSequence:
def __init__(self, sequence: str):
self.sequence = sequence
def __eq__(self, other: Any) -> bool:
return isinstance(other, GraphemeSequence) and self.sequence == other.sequence
ParsedSequence = List[Union[ArpabetSequence, GraphemeSequence]]
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 : ParsedSequence = []
# Check for curly braces and treat their contents as ARPAbet:
while len(input_text):
input_text = input_text.strip()
m = _curly_re.match(input_text)
if not m:
sequence.append(GraphemeSequence(input_text))
break
before = m.group(1).strip()
bracketed = m.group(2).strip()
after = m.group(3).strip()
if before:
sequence.append(GraphemeSequence(before))
if bracketed:
sequence.append(ArpabetSequence(bracketed))
input_text = after
return sequence
@@ -0,0 +1,45 @@
from tts.text.pipeline_components.input_parsing.parser import ArpabetSequence, GraphemeSequence, parse_grapheme_and_arpabet_sequence
class TestParser:
def test_only_text(self):
parsed = parse_grapheme_and_arpabet_sequence("This is a test")
assert len(parsed) == 1
assert isinstance(parsed[0], GraphemeSequence)
def test_only_arpabet(self):
parsed = parse_grapheme_and_arpabet_sequence("{T EH1 S T IH0 NG}")
assert len(parsed) == 1
assert isinstance(parsed[0], ArpabetSequence)
def test_text_and_arpabet(self):
parsed = parse_grapheme_and_arpabet_sequence("This is a {T EH1 S T}")
assert len(parsed) == 2
assert isinstance(parsed[0], GraphemeSequence)
assert isinstance(parsed[1], ArpabetSequence)
def test_text_and_arpabet_interleved_1(self):
parsed = parse_grapheme_and_arpabet_sequence("{DH IH1 S} is a {T EH1 S T}")
assert len(parsed) == 3
assert isinstance(parsed[0], ArpabetSequence)
assert isinstance(parsed[1], GraphemeSequence)
assert isinstance(parsed[2], ArpabetSequence)
def test_text_and_arpabet_interleved_2(self):
parsed = parse_grapheme_and_arpabet_sequence("Check out {DH IH1 S} {T EH1 S T} of the {S IH1 S T AH0 M}")
assert len(parsed) == 5
assert isinstance(parsed[0], GraphemeSequence)
assert isinstance(parsed[1], ArpabetSequence)
assert isinstance(parsed[2], ArpabetSequence)
assert isinstance(parsed[3], GraphemeSequence)
assert isinstance(parsed[4], ArpabetSequence)
def test_text_and_arpabet_interleved_2(self):
parsed = parse_grapheme_and_arpabet_sequence("Check out {DH IH1 S} {T EH1 S T} of the {S IH1 S T AH0 M}.")
assert len(parsed) == 6
assert isinstance(parsed[0], GraphemeSequence)
assert isinstance(parsed[1], ArpabetSequence)
assert isinstance(parsed[2], ArpabetSequence)
assert isinstance(parsed[3], GraphemeSequence)
assert isinstance(parsed[4], ArpabetSequence)
assert isinstance(parsed[5], GraphemeSequence)
@@ -0,0 +1,4 @@
class BaseNormalization:
def normalize(self, raw_input_text: str) -> str:
return raw_input_text
@@ -0,0 +1,32 @@
"""
Copied or adapted from Keith Ito's Tacotron2 code.
https://github.com/keithito/tacotron
"""
import re
from unidecode import unidecode
# Regular expression matching whitespace:
_whitespace_re = re.compile(r'\s+')
def lowercase(text: str) -> str:
return text.lower()
def collapse_whitespace(text) -> str:
return re.sub(_whitespace_re, ' ', text)
def convert_to_ascii(text: str) -> str:
return unidecode(text)
def basic_cleaners(text: str) -> str:
'''Basic pipeline that lowercases and collapses whitespace without transliteration.'''
text = lowercase(text)
text = collapse_whitespace(text)
return text
def transliteration_cleaners(text: str) -> str:
'''Pipeline for non-English text that transliterates to ASCII.'''
text = convert_to_ascii(text)
text = lowercase(text)
text = collapse_whitespace(text)
return text
@@ -0,0 +1,17 @@
from tts.text.pipeline_components.normalization.base_normalization import BaseNormalization
from tts.text.pipeline_components.normalization.common_cleaners import collapse_whitespace
from tts.text.pipeline_components.normalization.common_cleaners import convert_to_ascii
from tts.text.pipeline_components.normalization.common_cleaners import lowercase
class EnglishNormalization(BaseNormalization):
def normalize(self, raw_input_text: str) -> str:
text = convert_to_ascii(raw_input_text)
text = lowercase(text)
# TODO: Expand abbreviations
# TODO: Expand numbers
text = collapse_whitespace(text)
return text
@@ -0,0 +1,6 @@
from tts.text.pipeline_components.normalization.base_normalization import BaseNormalization
class SpanishNormalization(BaseNormalization):
def normalize(self, raw_input_text: str) -> str:
return raw_input_text
@@ -0,0 +1,13 @@
from tts.text.pipeline_components.normalization.english_normalization import EnglishNormalization
class TestEnglishNormalization:
def test_whitespace_collapse(self):
normalizer = EnglishNormalization()
output = normalizer.normalize(" \t \n normalized \n \t ")
assert output == " normalized "
def test_lowercase(self):
normalizer = EnglishNormalization()
output = normalizer.normalize("This is a test of Normalization.")
assert output == "this is a test of normalization."
@@ -0,0 +1,15 @@
import re
from typing import List
from tts.text.pipeline_components.phonetization.cmudict import CMUDict
# Regex taken from open source tacotron2 extensions
_arpabet_split_re = re.compile(r'[^\s]*{.*?}[^\s]*|[\w]+|[^\w\s]+')
class BasePhonetization:
def __init__(self, phoneme_dictionary: CMUDict):
self.phoneme_dictionary = phoneme_dictionary
def _split_grapheme_sentence_to_words(self, text_sequence: str) -> List[str]:
"""Split a grapheme word sequence to individual grapheme words"""
return [word for word in _arpabet_split_re.findall(text_sequence)]
@@ -0,0 +1,72 @@
"""
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'
valid_symbols = [
'AA', 'AA0', 'AA1', 'AA2', 'AE', 'AE0', 'AE1', 'AE2', 'AH', 'AH0', 'AH1', 'AH2',
'AO', 'AO0', 'AO1', 'AO2', 'AW', 'AW0', 'AW1', 'AW2', 'AY', 'AY0', 'AY1', 'AY2',
'B', 'CH', 'D', 'DH', 'EH', 'EH0', 'EH1', 'EH2', 'ER', 'ER0', 'ER1', 'ER2', 'EY',
'EY0', 'EY1', 'EY2', 'F', 'G', 'HH', 'IH', 'IH0', 'IH1', 'IH2', 'IY', 'IY0', 'IY1',
'IY2', 'JH', 'K', 'L', 'M', 'N', 'NG', 'OW', 'OW0', 'OW1', 'OW2', 'OY', 'OY0',
'OY1', 'OY2', 'P', 'R', 'S', 'SH', 'T', 'TH', 'UH', 'UH0', 'UH1', 'UH2', 'UW',
'UW0', 'UW1', 'UW2', 'V', 'W', 'Y', 'Z', 'ZH'
]
_valid_symbol_set = set(valid_symbols)
class CMUDict:
'''Thin wrapper around CMUDict data. http://www.speech.cs.cmu.edu/cgi-bin/cmudict'''
def __init__(self, file_or_path, keep_ambiguous=True):
if isinstance(file_or_path, str):
with open(file_or_path, encoding='latin-1') as f:
entries = _parse_cmudict(f)
else:
entries = _parse_cmudict(file_or_path)
if not keep_ambiguous:
entries = {word: pron for word, pron in entries.items() if len(pron) == 1}
self._entries = entries
def __len__(self):
return len(self._entries)
def lookup(self, word):
'''Returns list of ARPAbet pronunciations of the given word.'''
return self._entries.get(word.upper())
_alt_re = re.compile(r'\([0-9]+\)')
def _parse_cmudict(file):
cmudict = {}
for line in file:
if len(line) and (line[0] >= 'A' and line[0] <= 'Z' or line[0] == "'"):
parts = line.split(' ')
word = re.sub(_alt_re, '', parts[0])
pronunciation = _get_pronunciation(parts[1])
if pronunciation:
if word in cmudict:
cmudict[word].append(pronunciation)
else:
cmudict[word] = [pronunciation]
return cmudict
def _get_pronunciation(s):
parts = s.strip().split(' ')
for part in parts:
if part not in _valid_symbol_set:
return None
return ' '.join(parts)
@@ -0,0 +1,30 @@
from typing import List
from tts.text.pipeline_components.input_parsing.parser import ArpabetSequence, GraphemeSequence, ParsedSequence
from tts.text.pipeline_components.phonetization.base_phonetization import BasePhonetization
from tts.text.pipeline_components.phonetization.cmudict import CMUDict
class EnglishPhonetization(BasePhonetization):
def __init__(self, phoneme_dictionary: CMUDict):
super().__init__(phoneme_dictionary)
def try_convert_to_phonemes(self, grapheme_sequence: str) -> ParsedSequence:
"""
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 = []
for word in words:
maybe_phonemes = self.phoneme_dictionary.lookup(word)
if maybe_phonemes:
# NB: Dictionary returns a list of possible matches.
# We'll just use the first.
converted_words.append(ArpabetSequence(maybe_phonemes[0]))
else:
# TODO: Use g2p prediction to predict the phonemes.
converted_words.append(GraphemeSequence(word))
return converted_words
@@ -0,0 +1,15 @@
from tts.text.pipeline_components.phonetization.cmudict import CMUDict
from tts.text.pipeline_components.phonetization.cmudict import DEFAULT_CMUDICT_PATH
class TestCmuDict:
def test_lookup_success(self):
cmudict = CMUDict(DEFAULT_CMUDICT_PATH)
arpabet_sequence = cmudict.lookup('word')
assert arpabet_sequence == ['W ER1 D'] # TODO: Split the phonemes!
def test_lookup_failure(self):
cmudict = CMUDict(DEFAULT_CMUDICT_PATH)
arpabet_sequence = cmudict.lookup('fakeword')
assert arpabet_sequence == None
@@ -0,0 +1,73 @@
from tts.text.pipeline_components.phonetization.cmudict import DEFAULT_CMUDICT_PATH, CMUDict
from tts.text.pipeline_components.phonetization.english_phonetization import EnglishPhonetization
from tts.text.pipeline_components.input_parsing.parser import ArpabetSequence
from tts.text.pipeline_components.input_parsing.parser import GraphemeSequence
class TestEnglishPhonetization:
def test_no_input(self):
phoneme_dictionary = CMUDict(DEFAULT_CMUDICT_PATH)
phonetization = EnglishPhonetization(phoneme_dictionary)
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.try_convert_to_phonemes("this is a test")
assert output == [
ArpabetSequence("DH IH1 S"),
ArpabetSequence("IH1 Z"),
ArpabetSequence("AH0"),
ArpabetSequence("T EH1 S T"),
]
def test_most_words_in_dictionary(self):
phoneme_dictionary = CMUDict(DEFAULT_CMUDICT_PATH)
phonetization = EnglishPhonetization(phoneme_dictionary)
output = phonetization.try_convert_to_phonemes("nonword is a test")
assert output == [
GraphemeSequence("nonword"),
ArpabetSequence("IH1 Z"),
ArpabetSequence("AH0"),
ArpabetSequence("T EH1 S T"),
]
def test_punctuation(self):
phoneme_dictionary = CMUDict(DEFAULT_CMUDICT_PATH)
phonetization = EnglishPhonetization(phoneme_dictionary)
output = phonetization.try_convert_to_phonemes("this...")
assert output == [
ArpabetSequence("DH IH1 S"),
GraphemeSequence("..."),
]
output = phonetization.try_convert_to_phonemes("this!")
assert output == [
ArpabetSequence("DH IH1 S"),
GraphemeSequence("!"),
]
output = phonetization.try_convert_to_phonemes("this?")
assert output == [
ArpabetSequence("DH IH1 S"),
GraphemeSequence("?"),
]
def test_extra_spaces(self):
phoneme_dictionary = CMUDict(DEFAULT_CMUDICT_PATH)
phonetization = EnglishPhonetization(phoneme_dictionary)
output = phonetization.try_convert_to_phonemes(" this is a test ")
assert output == [
ArpabetSequence("DH IH1 S"),
ArpabetSequence("IH1 Z"),
ArpabetSequence("AH0"),
ArpabetSequence("T EH1 S T"),
]
def test_multi_non_word_extra_spaces(self):
phoneme_dictionary = CMUDict(DEFAULT_CMUDICT_PATH)
phonetization = EnglishPhonetization(phoneme_dictionary)
output = phonetization.try_convert_to_phonemes(" nonword anothernonword ")
assert output == [
GraphemeSequence("nonword"),
GraphemeSequence("anothernonword"),
]
@@ -0,0 +1,50 @@
"""
Originally adapted from https://github.com/keithito/tacotron
This defines the set of symbols used in text input to the model.
This is the set of character encodings that FakeYou and UberDuck models have
been trained on. We freeze it here as "V1", because these may change in future
notebooks.
Original documentation:
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.pipeline_components.phonetization import cmudict
from typing import List
# NB: DO NOT CHANGE THE ORDER OF SYMBOLS!
# Models are trained on and depend upon the order.
_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)}
# Spaces can be injected when joining words.
SPACE_INTEGER_ENCODING = _symbol_to_id[' ']
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 '~'
@@ -0,0 +1,29 @@
from tts.text.pipeline_components.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):
# NB: Models are trained on these integer encodings.
# If these tests break, the order of symbols may have changed.
output = arpabet_to_sequence('T EH1 S T')
assert output == [133, 94, 131, 133]
def test_grapheme_word(self):
# NB: Models are trained on these integer encodings.
# If these tests break, the order of symbols may have changed.
output = symbols_to_sequence('test')
assert output == [57, 42, 56, 57]
def test_grapheme_sentence(self):
# NB: Models are trained on these integer encodings.
# If these tests break, the order of symbols may have changed.
output = symbols_to_sequence('this is a test.')
assert output == [57, 45, 46, 56, 11, 46, 56, 11, 38, 11, 57, 42, 56, 57, 7]