Entirely reworked text handling pipeline

This breaks the text pipeline into the following pieces:

- input parsing (split known bracketed phoneme sections out first)

- normalization (fix spacing, numbers, abbreviations, etc.)

- phonetization (do CMUdict lookup or g2p prediction)

- sequencing (convert the symbolic representation to integer
  representation)

Each "pipeline" can have a unique combination of operations for each of
these pipeline steps.

This will grow over time.
This commit is contained in:
Brandon Thomas
2022-06-28 03:25:02 -04:00
parent 6a43c8d7ca
commit 76c6dbf12b
33 changed files with 135390 additions and 1 deletions
View File
+7
View File
@@ -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 []
+74
View File
@@ -0,0 +1,74 @@
from typing import List
from text.pipeline.base_pipeline import BasePipeline
from text.pipeline_components.input_parsing.parser import ArpabetSequence, GraphemeSequence, ParsedSequence, parse_grapheme_and_arpabet_sequence
from text.pipeline_components.normalization.english_normalization import normalize_english
from text.pipeline_components.phonetization.base_phonetization import BasePhonetization
from text.pipeline_components.phonetization.cmudict import DEFAULT_CMUDICT_PATH, CMUDict
from text.pipeline_components.phonetization.english_phonetization import EnglishPhonetization
from text.pipeline_components.sequencing.english_symbols_v1 import SPACE_INTEGER_ENCODING, arpabet_to_sequence, symbols_to_sequence
class EnglishPipelineV1(BasePipeline):
"""
This is a new text pipeline for FakeYou that will be introduced in July 2022.
It introduces Arpabet and individually upgradable components.
The code is not 1:1 with our notebooks, but we'll go to efforts to make sure that
the model input expectations on both sides match one another.
"""
def __init__(self, phonetization: BasePhonetization):
self.phonetization = phonetization
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)
parsed_sequence = self._convert_phonemes(parsed_sequence)
return self._convert_to_integer_sequence(parsed_sequence)
def _normalize_graphemes(self, parsed_sequence: ParsedSequence):
for i in range(len(parsed_sequence)):
subsequence = parsed_sequence[i]
if isinstance(subsequence, GraphemeSequence):
normalized = normalize_english(subsequence.sequence)
parsed_sequence[i] = GraphemeSequence(normalized)
def _convert_phonemes(self, parsed_sequence: ParsedSequence) -> ParsedSequence:
# NB: We will likely break grapheme runs into multiple arpabet sequences, extending the
# output list length.
updated_sequence = []
for subsequence in parsed_sequence:
if isinstance(subsequence, ArpabetSequence):
updated_sequence.append(subsequence)
elif isinstance(subsequence, GraphemeSequence):
converted_sequence = self.phonetization.try_convert_to_phonemes(subsequence.sequence)
updated_sequence.extend(converted_sequence)
return updated_sequence
def _convert_to_integer_sequence(self, parsed_sequence: ParsedSequence) -> List[int]:
integer_sequence = []
last_index = len(parsed_sequence) - 1
for i, subsequence in enumerate(parsed_sequence):
integer_subsequence = []
if isinstance(subsequence, ArpabetSequence):
integer_subsequence = arpabet_to_sequence(subsequence.sequence)
elif isinstance(subsequence, GraphemeSequence):
integer_subsequence = symbols_to_sequence(subsequence.sequence)
if integer_subsequence:
integer_sequence.extend(integer_subsequence)
if i < last_index and integer_sequence[-1] != SPACE_INTEGER_ENCODING:
# Separate words with single spaces if not already present.
integer_sequence.append(SPACE_INTEGER_ENCODING)
return integer_sequence
@@ -0,0 +1,74 @@
import re
from typing import List
from text.pipeline.base_pipeline import BasePipeline
from text.pipeline_components.normalization.common_cleaners import collapse_whitespace, convert_to_ascii, lowercase
from text.pipeline_components.normalization.english_cleaners import expand_abbreviations, expand_numbers
from text.pipeline_components.sequencing.english_symbols_v1 import arpabet_to_sequence, symbols_to_sequence
# Regular expression matching text enclosed in curly braces:
_curly_re = re.compile(r'(.*?)\{(.+?)\}(.*)')
class LegacyFakeYouPipeline(BasePipeline):
"""
This pipeline was in use from June 2021 to July 2022.
In contrast to legacy "vo.codes" (2020 - 2021), it does not
leverage CMUDict to look up arpabet even when models were known to
be trained with arpabet support. (It did support curly braces for
manual arpabet inclusion.)
"""
def user_input_to_sequence(self, input_text: str) -> List[int]:
"""Convert raw user input into a numerical sequence for ML inference."""
pre_sequence = self._preprocess_text(input_text)
return self._text_to_sequence(pre_sequence, ['english_cleaners'])
def _preprocess_text(self, input_text: str, is_raw_input: bool = True) -> str:
pre_sequence = ""
for line in input_text.split("\n"):
if len(line) < 1:
continue
if is_raw_input:
if line[-1] != ";":
line = line + ";"
else:
pass
pre_sequence += line
return pre_sequence
def _text_to_sequence(self, text, cleaner_names):
'''Converts a string of text to a sequence of IDs corresponding to the symbols in the text.
The text can optionally have ARPAbet sequences enclosed in curly braces embedded
in it. For example, "Turn left on {HH AW1 S S T AH0 N} Street."
Args:
text: string to convert to a sequence
cleaner_names: names of the cleaner functions to run the text through
Returns:
List of integers corresponding to the symbols in the text
'''
sequence = []
# Check for curly braces and treat their contents as ARPAbet:
while len(text):
m = _curly_re.match(text)
if not m:
sequence += symbols_to_sequence(english_cleaners(text))
break
sequence += symbols_to_sequence(english_cleaners(m.group(1)))
sequence += arpabet_to_sequence(m.group(2))
text = m.group(3)
return sequence
def english_cleaners(text):
'''Pipeline for English text, including number and abbreviation expansion.'''
text = convert_to_ascii(text)
text = lowercase(text)
text = expand_numbers(text)
text = expand_abbreviations(text)
text = collapse_whitespace(text)
return text
@@ -0,0 +1,2 @@
# TODO: In the future we may allow uploads of legacy vocodes models.
# These models utilize a very unique encoding scheme.
@@ -0,0 +1,65 @@
from text.pipeline.english_pipeline_v1 import EnglishPipelineV1
from text.pipeline_components.phonetization.cmudict import DEFAULT_CMUDICT_PATH, CMUDict
from text.pipeline_components.phonetization.english_phonetization import EnglishPhonetization
class TestEnglishPipelineV1:
def build_pipeline_instance(self):
# NB: CMUDict used in production may differ
cmudict = CMUDict(DEFAULT_CMUDICT_PATH)
phonetization = EnglishPhonetization(cmudict)
return EnglishPipelineV1(phonetization)
def test_no_input(self):
pipeline = self.build_pipeline_instance()
output = pipeline.user_input_to_sequence("")
assert output == []
def test_whitespace(self):
pipeline = self.build_pipeline_instance()
output = pipeline.user_input_to_sequence(" \n \t ")
assert output == []
def test_single_word(self):
pipeline = self.build_pipeline_instance()
output = pipeline.user_input_to_sequence("hello")
assert output == [106, 73, 117, 123] # NB: Arpabet encoded
def test_two_words(self):
pipeline = self.build_pipeline_instance()
output = pipeline.user_input_to_sequence("hello hello")
assert output == [
106, 73, 117, 123, # NB: Arpabet encoded
11, # Space
106, 73, 117, 123, # Arpabet encoded
]
def test_two_words_with_arpabet_segment(self):
pipeline = self.build_pipeline_instance()
output = pipeline.user_input_to_sequence("hello {HH AH0 L OW1}")
assert output == [
106, 73, 117, 123, # NB: Arpabet encoded
11, # Space
106, 73, 117, 123, # Manually arpabet encoded
]
def test_words_and_punctuation(self):
pipeline = self.build_pipeline_instance()
output = pipeline.user_input_to_sequence("hello friends, welcome!")
assert output == [
106, 73, 117, 123, # NB: Arpabet encoded
11, # Space
104, 130, 94, 119, 90, 146,
11, # Space (TODO: Get rid of extraneous spaces with richer parsing)
6, # Comma
11, # Space
144, 94, 117, 116, 73, 118,
11, # Space
2, # Exclamation mark
]
def test_apostrophe_word(self):
pipeline = self.build_pipeline_instance()
output = pipeline.user_input_to_sequence("you're")
assert output == [145, 137, 130] # YOU'RE Y UH1 R ~ 145 137 130
@@ -0,0 +1,42 @@
from text.pipeline.legacy_fakeyou_pipeline import LegacyFakeYouPipeline
class TestLegacyFakeYouPipeline():
def test_no_input(self):
pipeline = LegacyFakeYouPipeline()
output = pipeline.user_input_to_sequence("")
assert output == []
def test_single_word(self):
pipeline = LegacyFakeYouPipeline()
output = pipeline.user_input_to_sequence("hello")
# NB: Graphemes remains ASCII! No CMUDict lookup or phoneme prediction.
# Also note the ending ";".
assert output == [
45, 42, 49, 49, 52, # "h e l l o"
9, # Semicolon line ender
]
def test_two_words(self):
pipeline = LegacyFakeYouPipeline()
output = pipeline.user_input_to_sequence("hello hello")
# NB: Graphemes remains ASCII! No CMUDict lookup or phoneme prediction.
assert output == [
45, 42, 49, 49, 52, # "h e l l o"
11, # Space
45, 42, 49, 49, 52, # "h e l l o"
9, # Semicolon line ender
]
def test_two_words_with_arpabet_segment(self):
pipeline = LegacyFakeYouPipeline()
output = pipeline.user_input_to_sequence("hello {HH AH0 L OW1}")
# NB: Graphemes remains ASCII! No CMUDict lookup or phoneme prediction.
assert output == [
45, 42, 49, 49, 52, # "h e l l o"
11, # Space
106, 73, 117, 123, # Legacy FakeYou supports manual arpabet inclusion
9, # Semicolon line ender
]