Add manual minor/major breaks for eSpeak voices

This commit is contained in:
Michael Hansen 2022-04-05 10:50:47 -04:00
commit 9fd10f5949
6 changed files with 171 additions and 21 deletions

View file

@ -15,11 +15,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import argparse import argparse
import subprocess
import shlex
import shutil
import logging import logging
import os import os
import shlex
import shutil
import subprocess
import sys import sys
import tempfile import tempfile
import typing import typing
@ -118,16 +118,24 @@ def get_args() -> argparse.Namespace:
help="URL of mimic3 HTTP server (default: http://localhost:59125/api/tts)", help="URL of mimic3 HTTP server (default: http://localhost:59125/api/tts)",
) )
parser.add_argument( parser.add_argument(
"--voice", "-v", help="Name of voice (expected in <voices-dir>/<language>)", "--voice",
"-v",
help="Name of voice (expected in <voices-dir>/<language>)",
) )
parser.add_argument( parser.add_argument(
"--output", "-o", help="Path to write WAV file (default: play audio)", "--output",
"-o",
help="Path to write WAV file (default: play audio)",
) )
parser.add_argument( parser.add_argument(
"--stdout", action="store_true", help="Write WAV data to stdout", "--stdout",
action="store_true",
help="Write WAV data to stdout",
) )
parser.add_argument( parser.add_argument(
"--noise-scale", type=float, help="Noise scale [0-1], default is 0.667", "--noise-scale",
type=float,
help="Noise scale [0-1], default is 0.667",
) )
parser.add_argument( parser.add_argument(
"--length-scale", "--length-scale",
@ -135,7 +143,9 @@ def get_args() -> argparse.Namespace:
help="Length scale (1.0 is default speed, 0.5 is 2x faster)", help="Length scale (1.0 is default speed, 0.5 is 2x faster)",
) )
parser.add_argument( parser.add_argument(
"--noise-w", type=float, help="Variation in cadence [0-1], default is 0.8", "--noise-w",
type=float,
help="Variation in cadence [0-1], default is 0.8",
) )
parser.add_argument( parser.add_argument(
"--play-program", "--play-program",

View file

@ -8,3 +8,6 @@ ignore_missing_imports = True
[mypy-setuptools.*] [mypy-setuptools.*]
ignore_missing_imports = True ignore_missing_imports = True
[mypy-swagger_ui.*]
ignore_missing_imports = True

View file

@ -29,6 +29,8 @@ from phonemes2ids import BlankBetween
@dataclass @dataclass
class AudioConfig(DataClassJsonMixin): class AudioConfig(DataClassJsonMixin):
"""Audio input/output details"""
filter_length: int = 1024 filter_length: int = 1024
hop_length: int = 256 hop_length: int = 256
win_length: int = 1024 win_length: int = 1024
@ -109,6 +111,8 @@ class AudioConfig(DataClassJsonMixin):
@dataclass @dataclass
class ModelConfig(DataClassJsonMixin): class ModelConfig(DataClassJsonMixin):
"""TTS model hyperparameters"""
num_symbols: int = 0 num_symbols: int = 0
n_speakers: int = 1 n_speakers: int = 1
@ -141,6 +145,8 @@ class ModelConfig(DataClassJsonMixin):
@dataclass @dataclass
class PhonemesConfig(DataClassJsonMixin): class PhonemesConfig(DataClassJsonMixin):
"""Phonemes to ids configuration"""
phoneme_separator: str = " " phoneme_separator: str = " "
"""Separator between individual phonemes in CSV input""" """Separator between individual phonemes in CSV input"""
@ -185,21 +191,30 @@ class PhonemesConfig(DataClassJsonMixin):
class Phonemizer(str, Enum): class Phonemizer(str, Enum):
"""Method used to convert text to phonemes"""
SYMBOLS = "symbols" SYMBOLS = "symbols"
GRUUT = "gruut" GRUUT = "gruut"
ESPEAK = "espeak" ESPEAK = "espeak"
class Aligner(str, Enum): class Aligner(str, Enum):
"""Text/audio aligner"""
KALDI_ALIGN = "kaldi_align" KALDI_ALIGN = "kaldi_align"
"""https://github.com/rhasspy/kaldi-align"""
class TextCasing(str, Enum): class TextCasing(str, Enum):
"""Casing method applied to text"""
LOWER = "lower" LOWER = "lower"
UPPER = "upper" UPPER = "upper"
class MetadataFormat(str, Enum): class MetadataFormat(str, Enum):
"""Format of training metadata"""
TEXT = "text" TEXT = "text"
PHONEMES = "phonemes" PHONEMES = "phonemes"
PHONEME_IDS = "ids" PHONEME_IDS = "ids"
@ -207,6 +222,8 @@ class MetadataFormat(str, Enum):
@dataclass @dataclass
class DatasetConfig: class DatasetConfig:
"""Training dataset configuration"""
name: str name: str
metadata_format: MetadataFormat = MetadataFormat.TEXT metadata_format: MetadataFormat = MetadataFormat.TEXT
multispeaker: bool = False multispeaker: bool = False
@ -228,19 +245,28 @@ class DatasetConfig:
@dataclass @dataclass
class AlignerConfig: class AlignerConfig:
"""Text/audio alignment configuration"""
aligner: typing.Optional[Aligner] = None aligner: typing.Optional[Aligner] = None
casing: typing.Optional[TextCasing] = None casing: typing.Optional[TextCasing] = None
@dataclass @dataclass
class InferenceConfig: class InferenceConfig:
"""Inference configuration"""
length_scale: float = 1.0 length_scale: float = 1.0
noise_scale: float = 0.667 noise_scale: float = 0.667
noise_w: float = 0.8 noise_w: float = 0.8
minor_break_ms: typing.Optional[int] = None
major_break_ms: typing.Optional[int] = None
@dataclass @dataclass
class TrainingConfig(DataClassJsonMixin): class TrainingConfig(DataClassJsonMixin):
"""Master configuration for training"""
seed: int = 1234 seed: int = 1234
epochs: int = 10000 epochs: int = 10000
learning_rate: float = 2e-4 learning_rate: float = 2e-4

View file

@ -44,7 +44,7 @@ from .const import (
DEFAULT_VOICES_URL_FORMAT, DEFAULT_VOICES_URL_FORMAT,
) )
from .download import VoiceFile, download_voice from .download import VoiceFile, download_voice
from .voice import SPEAKER_TYPE, Mimic3Voice from .voice import SPEAKER_TYPE, BreakType, Mimic3Voice
_DIR = Path(__file__).parent _DIR = Path(__file__).parent
@ -293,15 +293,29 @@ class Mimic3TextToSpeechSystem(TextToSpeechSystem):
def speak_text(self, text: str, text_language: typing.Optional[str] = None): def speak_text(self, text: str, text_language: typing.Optional[str] = None):
voice = self._get_or_load_voice(self.voice) voice = self._get_or_load_voice(self.voice)
for sent_phonemes in voice.text_to_phonemes(text, text_language=text_language): minor_break_ms = voice.config.inference.major_break_ms
major_break_ms = voice.config.inference.major_break_ms
for sent_phonemes, break_type in voice.text_to_phonemes(
text, text_language=text_language
):
# Utterances have start/end meta phonemes (usually ^ and $)
is_utterance = break_type != BreakType.NONE
self._results.append( self._results.append(
Mimic3Phonemes( Mimic3Phonemes(
current_settings=deepcopy(self.settings), current_settings=deepcopy(self.settings),
phonemes=sent_phonemes, phonemes=sent_phonemes,
is_utterance=False, is_utterance=is_utterance,
) )
) )
# Add silence if using manual break intervals
if (break_type == BreakType.MAJOR) and (major_break_ms is not None):
self.add_break(major_break_ms)
elif (break_type == BreakType.MINOR) and (minor_break_ms is not None):
self.add_break(minor_break_ms)
# pylint: disable=arguments-differ # pylint: disable=arguments-differ
def speak_tokens( def speak_tokens(
self, self,

View file

@ -19,6 +19,7 @@ import platform
import time import time
import typing import typing
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from enum import Enum
from pathlib import Path from pathlib import Path
from xml.sax.saxutils import escape as xmlescape from xml.sax.saxutils import escape as xmlescape
@ -34,10 +35,19 @@ from mimic3_tts.utils import audio_float_to_int16
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
class BreakType(str, Enum):
NONE = "none"
MINOR = "minor"
MAJOR = "major"
UTTERANCE = "utterance"
PHONEME_TYPE = str PHONEME_TYPE = str
PHONEME_ID_TYPE = int PHONEME_ID_TYPE = int
WORD_PHONEMES_TYPE = typing.List[typing.List[PHONEME_TYPE]] WORD_PHONEMES_TYPE = typing.List[typing.List[PHONEME_TYPE]]
PHONEME_MAP_TYPE = typing.Dict[PHONEME_TYPE, typing.List[PHONEME_TYPE]] PHONEME_MAP_TYPE = typing.Dict[PHONEME_TYPE, typing.List[PHONEME_TYPE]]
TEXT_TO_PHONEMES_TYPE = typing.Iterable[typing.Tuple[WORD_PHONEMES_TYPE, BreakType]]
SPEAKER_NAME_TYPE = str SPEAKER_NAME_TYPE = str
SPEAKER_ID_TYPE = int SPEAKER_ID_TYPE = int
@ -48,6 +58,7 @@ DEFAULT_LANGUAGE = "en_US"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@ -71,7 +82,7 @@ class Mimic3Voice(metaclass=ABCMeta):
@abstractmethod @abstractmethod
def text_to_phonemes( def text_to_phonemes(
self, text: str, text_language: typing.Optional[str] = None self, text: str, text_language: typing.Optional[str] = None
) -> typing.Iterable[WORD_PHONEMES_TYPE]: ) -> TEXT_TO_PHONEMES_TYPE:
"""Convert text into phonemes""" """Convert text into phonemes"""
def word_to_phonemes( def word_to_phonemes(
@ -82,7 +93,7 @@ class Mimic3Voice(metaclass=ABCMeta):
) -> typing.List[PHONEME_TYPE]: ) -> typing.List[PHONEME_TYPE]:
"""Convert a single word (with optional role) into phonemes""" """Convert a single word (with optional role) into phonemes"""
word_phonemes = [] word_phonemes = []
for sent_phonemes in self.text_to_phonemes( for sent_phonemes, _break_type in self.text_to_phonemes(
word_text, text_language=text_language word_text, text_language=text_language
): ):
for sent_word_phonemes in sent_phonemes: for sent_word_phonemes in sent_phonemes:
@ -99,7 +110,9 @@ class Mimic3Voice(metaclass=ABCMeta):
) -> WORD_PHONEMES_TYPE: ) -> WORD_PHONEMES_TYPE:
"""Speak a word or phrase with a specific interpretation/format""" """Speak a word or phrase with a specific interpretation/format"""
word_phonemes = [] word_phonemes = []
for sent_phonemes in self.text_to_phonemes(text, text_language=text_language): for sent_phonemes, _break_type in self.text_to_phonemes(
text, text_language=text_language
):
word_phonemes.extend(sent_phonemes) word_phonemes.extend(sent_phonemes)
return word_phonemes return word_phonemes
@ -318,12 +331,12 @@ class GruutVoice(Mimic3Voice):
def text_to_phonemes( def text_to_phonemes(
self, text: str, text_language: typing.Optional[str] = None self, text: str, text_language: typing.Optional[str] = None
) -> typing.Iterable[WORD_PHONEMES_TYPE]: ) -> TEXT_TO_PHONEMES_TYPE:
text_language = text_language or self.config.text_language or DEFAULT_LANGUAGE text_language = text_language or self.config.text_language or DEFAULT_LANGUAGE
for sentence in gruut.sentences(text, lang=text_language): for sentence in gruut.sentences(text, lang=text_language):
sent_phonemes = [w.phonemes for w in sentence if w.phonemes] sent_phonemes = [w.phonemes for w in sentence if w.phonemes]
if sent_phonemes: if sent_phonemes:
yield sent_phonemes yield sent_phonemes, BreakType.NONE
def word_to_phonemes( def word_to_phonemes(
self, self,
@ -389,7 +402,7 @@ class EspeakVoice(Mimic3Voice):
def text_to_phonemes( def text_to_phonemes(
self, text: str, text_language: typing.Optional[str] = None self, text: str, text_language: typing.Optional[str] = None
) -> typing.Iterable[WORD_PHONEMES_TYPE]: ) -> TEXT_TO_PHONEMES_TYPE:
phoneme_separator = "" phoneme_separator = ""
word_separator = self.config.phonemes.word_separator word_separator = self.config.phonemes.word_separator
@ -406,11 +419,31 @@ class EspeakVoice(Mimic3Voice):
punctuation_separator=phoneme_separator, punctuation_separator=phoneme_separator,
) )
word_phonemes = [ all_word_phonemes = [
list(IPA.graphemes(wp_str)) for wp_str in phoneme_str.split(word_separator) list(IPA.graphemes(wp_str)) for wp_str in phoneme_str.split(word_separator)
] ]
yield word_phonemes minor_break = self.config.phonemes.minor_break
major_break = self.config.phonemes.major_break
if minor_break or major_break:
# Split on breaks
sent_phonemes = []
for word_phonemes in all_word_phonemes:
sent_phonemes.append(word_phonemes)
if minor_break and (word_phonemes[-1] == minor_break):
yield sent_phonemes, BreakType.MINOR
sent_phonemes = []
elif major_break and (word_phonemes[-1] == major_break):
yield sent_phonemes, BreakType.MAJOR
sent_phonemes = []
if sent_phonemes:
yield sent_phonemes, BreakType.MAJOR
else:
# No split
yield all_word_phonemes, BreakType.UTTERANCE
def word_to_phonemes( def word_to_phonemes(
self, self,
@ -486,9 +519,9 @@ class SymbolsVoice(Mimic3Voice):
def text_to_phonemes( def text_to_phonemes(
self, text: str, text_language: typing.Optional[str] = None self, text: str, text_language: typing.Optional[str] = None
) -> typing.Iterable[WORD_PHONEMES_TYPE]: ) -> TEXT_TO_PHONEMES_TYPE:
word_separator = self.config.phonemes.word_separator word_separator = self.config.phonemes.word_separator
word_phonemes = [ word_phonemes = [
list(IPA.graphemes(wp_str)) for wp_str in text.split(word_separator) list(IPA.graphemes(wp_str)) for wp_str in text.split(word_separator)
] ]
yield word_phonemes yield word_phonemes, BreakType.NONE

View file

@ -63,6 +63,36 @@
"speakers": [], "speakers": [],
"properties": {} "properties": {}
}, },
"en_UK/apope_low": {
"files": {
"LICENSE": {
"size_bytes": 46,
"sha256_sum": "2fb2b744e6d96a2e3137d58121670b005718c736d5dde1832c99ff9073f64c00"
},
"README.md": {
"size_bytes": 155,
"sha256_sum": "2e6c39454c35910c6b48518523429ec2e4f27ae8eb763e311877a017db23d4da"
},
"config.json": {
"size_bytes": 3434,
"sha256_sum": "1fdaa1124e02cc177eb776fbc6e08c838b56bd2e86c82d8d7fe434d9337806b0"
},
"generator.onnx": {
"size_bytes": 62792219,
"sha256_sum": "0b5a323500ebd022351db12da2b3aab8cdd47d0826d173e780a58b93604618c9"
},
"phoneme_map.txt": {
"size_bytes": 15,
"sha256_sum": "4003f421fc91ed1d5a343442659db6cf9d58bd1c6d8d771abc1999cc24d7694d"
},
"phonemes.txt": {
"size_bytes": 263,
"sha256_sum": "8f9c3e6ced14d7fc5426e4e1bc7f7cc1037a20a645ca34110abcb76148fa8bfd"
}
},
"speakers": [],
"properties": {}
},
"en_US/cmu-arctic_low": { "en_US/cmu-arctic_low": {
"files": { "files": {
"LICENSE": { "LICENSE": {
@ -502,6 +532,40 @@
"speakers": [], "speakers": [],
"properties": {} "properties": {}
}, },
"nl/nathalie_low": {
"files": {
"LICENSE": {
"size_bytes": 7049,
"sha256_sum": "7179683e8000e6bdc9bbc60d85edf0a4ac8e76f951857f54fcb775d5886f1309"
},
"README.md": {
"size_bytes": 180,
"sha256_sum": "15e74dbbdadc8bf7c3a84b1b130f6e5df5f37ffe82880703b27d92e11cff1af8"
},
"SOURCE": {
"size_bytes": 50,
"sha256_sum": "6422e7d891e0d77db7a7cd9643bc8cff4c26f47d652b83b651a0179682ce65c7"
},
"config.json": {
"size_bytes": 3604,
"sha256_sum": "e8e1acf414067657bd377965eb822c51f55483e2ad9a498398e473b0eae4c137"
},
"generator.onnx": {
"size_bytes": 62800663,
"sha256_sum": "1707e33a94538529009c28d0da2a99fe81aa15bd7bb4c2252b3ace0ab704fa71"
},
"phoneme_map.txt": {
"size_bytes": 21,
"sha256_sum": "18b9c5fe46201cd5b0b20e88dc78c64ef3bcaaf87a428386420757f2d0bf7bb3"
},
"phonemes.txt": {
"size_bytes": 336,
"sha256_sum": "355389fee04f97557232cdde7fb8d4cf03ae2aabd7b0b26ed5978ebbf6575dd4"
}
},
"speakers": [],
"properties": {}
},
"nl/rdh_low": { "nl/rdh_low": {
"files": { "files": {
"LICENSE": { "LICENSE": {