Files
storyteller-ml/tts/nemo/collections/asr/data/vocabs.py
T
2022-02-28 05:43:54 -05:00

353 lines
11 KiB
Python

# Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import abc
import itertools
import re
import string
import unicodedata
from builtins import str as unicode
from typing import List
import nltk
from nemo.collections.common.parts.preprocessing import parsers
try:
import g2p_en # noqa
_g2p = g2p_en.G2p()
_g2p.variables = None
HAVE_G2P = True
except (FileNotFoundError, LookupError):
HAVE_G2P = False
_words_re = re.compile("([a-z\-]+'[a-z\-]+|[a-z\-]+)|([^a-z{}]+)")
def _text_preprocessing(text):
text = unicode(text)
text = ''.join(char for char in unicodedata.normalize('NFD', text) if unicodedata.category(char) != 'Mn')
text = text.lower()
text = re.sub("[^ a-z'\".,?!()\[\]:;\-]", "", text)
return text
def _word_tokenize(text):
words = _words_re.findall(text)
words = [re.sub(r'\s(\d)', r'\1', word[1].upper()) if word[0] == '' else word[0] for word in words]
return words
class G2p:
def __init__(
self,
phoneme_dict_path=None,
use_seq2seq_for_oov=False,
ignore_ambiguous_words=True,
text_preprocessing_func=_text_preprocessing,
word_tokenize_func=_word_tokenize,
):
# Download NLTK datasets if this class is to be instantiated
try:
nltk.data.find('taggers/averaged_perceptron_tagger.zip')
except LookupError:
nltk.download('averaged_perceptron_tagger', quiet=True)
try:
nltk.data.find('corpora/cmudict.zip')
except LookupError:
nltk.download('cmudict', quiet=True)
self.homograph2features = _g2p.homograph2features
self.g2p_dict = self._construct_grapheme2phoneme_dict(phoneme_dict_path)
self.use_seq2seq_for_oov = use_seq2seq_for_oov
self.ignore_ambiguous_words = ignore_ambiguous_words
self.text_preprocessing_func = text_preprocessing_func
self.word_tokenize_func = word_tokenize_func
@staticmethod
def _construct_grapheme2phoneme_dict(phoneme_dict_path=None, encoding='latin-1'):
if phoneme_dict_path is None:
from nltk.corpus import cmudict
return cmudict.dict()
_alt_re = re.compile(r'\([0-9]+\)')
g2p_dict = {}
with open(phoneme_dict_path, encoding=encoding) as file:
for line in file:
if len(line) and ('A' <= line[0] <= 'Z' or line[0] == "'"):
parts = line.split(' ')
word = re.sub(_alt_re, '', parts[0])
word = word.lower()
pronunciation = parts[1].strip().split(" ")
if word in g2p_dict:
g2p_dict[word].append(pronunciation)
else:
g2p_dict[word] = [pronunciation]
return g2p_dict
def handle_ambiguous(self, word):
if not self.ignore_ambiguous_words or len(self.g2p_dict[word]) == 1:
return True
return False
def __call__(self, text):
text = self.text_preprocessing_func(text)
words = self.word_tokenize_func(text)
words_and_pos_tags = nltk.pos_tag(words)
prons = []
for word, pos in words_and_pos_tags:
word_by_hyphen = word.split("-")
# punctuation
if re.search("[a-zA-Z]", word) is None:
pron = list(word)
# homograph
elif word in self.homograph2features:
pron1, pron2, pos1 = self.homograph2features[word]
if pos.startswith(pos1):
pron = pron1
else:
pron = pron2
# `'s` suffix
elif (
len(word) > 2
and word.endswith("'s")
and (word not in self.g2p_dict)
and (word[:-2] in self.g2p_dict)
and self.handle_ambiguous(word[:-2])
):
pron = self.g2p_dict[word[:-2]][0] + ["Z"]
# `s` suffix
elif (
len(word) > 1
and word.endswith("s")
and (word not in self.g2p_dict)
and (word[:-1] in self.g2p_dict)
and self.handle_ambiguous(word[:-1])
):
pron = self.g2p_dict[word[:-1]][0] + ["Z"]
# g2p dict
elif word in self.g2p_dict and self.handle_ambiguous(word):
pron = self.g2p_dict[word][0]
# word with hyphens
elif len(word_by_hyphen) > 1 and all(
[sub_word in self.g2p_dict and self.handle_ambiguous(sub_word) for sub_word in word_by_hyphen]
):
pron = []
for sub_word in word_by_hyphen:
pron.extend(self.g2p_dict[sub_word][0])
pron.extend(["-"])
pron.pop()
else:
if self.use_seq2seq_for_oov:
# run gru-based seq2seq model from _g2p
pron = _g2p.predict(word)
else:
pron = word
prons.extend(pron)
return prons
class Base(abc.ABC):
"""Vocabulary for turning str text to list of int tokens."""
# fmt: off
PUNCT = ( # Derived from LJSpeech and "/" additionally
',', '.', '!', '?', '-',
':', ';', '/', '"', '(',
')', '[', ']', '{', '}',
)
# fmt: on
PAD, BLANK, OOV = '<pad>', '<blank>', '<oov>'
def __init__(self, labels, *, pad=PAD, blank=BLANK, oov=OOV, sep='', add_blank_at="last_but_one"):
super().__init__()
labels = list(labels)
self.pad, labels = len(labels), labels + [pad] # Padding
if add_blank_at is not None:
self.blank, labels = len(labels), labels + [blank] # Reserved for blank from QN
else:
# use add_blank_at=None only for ASR where blank is added automatically
self.blank = -1
self.oov, labels = len(labels), labels + [oov] # Out Of Vocabulary
if add_blank_at == "last":
labels[-1], labels[-2] = labels[-2], labels[-1]
self.oov, self.blank = self.blank, self.oov
self.labels = labels
self.sep = sep
self._util_ids = {self.pad, self.blank, self.oov}
self._label2id = {l: i for i, l in enumerate(labels)}
self._id2label = labels
def __call__(self, text: str) -> List[int]:
return self.encode(text)
@abc.abstractmethod
def encode(self, text: str) -> List[int]:
"""Turns str text into int tokens."""
pass
def decode(self, tokens: List[int]) -> str:
"""Turns ints tokens into str text."""
return self.sep.join(self._id2label[t] for t in tokens if t not in self._util_ids)
class Chars(Base):
"""Chars vocabulary."""
def __init__(
self, punct=True, spaces=False, apostrophe=True, add_blank_at="last_but_one",
):
labels = []
self.space, labels = len(labels), labels + [' '] # Space
labels.extend(string.ascii_lowercase)
if apostrophe:
labels.append("'") # Apostrophe for saving "don't" and "Joe's"
if punct:
labels.extend(self.PUNCT)
super().__init__(labels, add_blank_at=add_blank_at)
self.punct = punct
self.spaces = spaces
self._parser = parsers.ENCharParser(labels)
def encode(self, text):
"""See base class."""
text = self._parser._normalize(text) # noqa
if self.spaces:
for p in set(text) & set(self.PUNCT):
text = text.replace(p, f' {p} ')
text = text.strip().replace(' ', ' ')
return self._parser._tokenize(text) # noqa
class Phonemes(Base):
"""Phonemes vocabulary."""
# fmt: off
VOWELS = (
'AA', 'AE', 'AH', 'AO', 'AW',
'AY', 'EH', 'ER', 'EY', 'IH',
'IY', 'OW', 'OY', 'UH', 'UW',
)
CONSONANTS = (
'B', 'CH', 'D', 'DH', 'F', 'G',
'HH', 'JH', 'K', 'L', 'M', 'N',
'NG', 'P', 'R', 'S', 'SH', 'T',
'TH', 'V', 'W', 'Y', 'Z', 'ZH',
)
# fmt: on
def __init__(
self,
punct=True,
stresses=False,
spaces=True,
chars=False,
*,
space=' ',
silence=None,
apostrophe=True,
oov=Base.OOV,
sep='|', # To be able to distinguish between 2/3 letters codes.
add_blank_at="last_but_one",
pad_with_space=False,
improved_version_g2p=False,
phoneme_dict_path=None,
):
labels = []
self.space, labels = len(labels), labels + [space] # Space
if silence is not None:
self.silence, labels = len(labels), labels + [silence] # Silence
labels.extend(self.CONSONANTS)
vowels = list(self.VOWELS)
if stresses:
vowels = [f'{p}{s}' for p, s in itertools.product(vowels, (0, 1, 2))]
labels.extend(vowels)
if chars:
labels.extend(string.ascii_lowercase)
if apostrophe:
labels.append("'") # Apostrophe
if punct:
labels.extend(self.PUNCT)
super().__init__(labels, oov=oov, sep=sep, add_blank_at=add_blank_at)
self.punct = punct
self.stresses = stresses
self.spaces = spaces
self.pad_with_space = pad_with_space
if improved_version_g2p:
self.g2p = G2p(phoneme_dict_path)
else:
self.g2p = _g2p
def encode(self, text):
"""See base class."""
ps, space, labels = [], self.labels[self.space], set(self.labels)
for p in self.g2p(text): # noqa
# Remove stress
if p.isalnum() and len(p) == 3 and not self.stresses:
p = p[:2]
# Add space if last one isn't one
if p == space and len(ps) > 0 and ps[-1] != space:
ps.append(p)
# Add next phoneme
if (p.isalnum() or p == "'") and p in labels:
ps.append(p)
# Add punct and remove space if needed
if (p in self.PUNCT) and self.punct:
if not self.spaces and len(ps) > 0 and ps[-1] == space:
ps.pop()
ps.append(p)
# Remove trailing spaces
while ps[-1] == space:
ps.pop()
if self.pad_with_space:
ps = [space] + ps + [space]
return [self._label2id[p] for p in ps]