mirror of
https://github.com/storytold/storyteller-ml.git
synced 2026-10-28 09:08:54 +00:00
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:
@@ -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 []
|
||||
@@ -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
|
||||
]
|
||||
Reference in New Issue
Block a user