Initial commit
This commit is contained in:
commit
1eae034d8c
72 changed files with 4689 additions and 0 deletions
808
mimic3-tts/mimic3_tts/tts.py
Normal file
808
mimic3-tts/mimic3_tts/tts.py
Normal file
|
|
@ -0,0 +1,808 @@
|
|||
#!/usr/bin/env python3
|
||||
import dataclasses
|
||||
import logging
|
||||
import time
|
||||
import typing
|
||||
from abc import ABCMeta
|
||||
from dataclasses import dataclass, field
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from xml.sax.saxutils import escape as xmlescape
|
||||
|
||||
import gruut
|
||||
import numpy as np
|
||||
import onnxruntime
|
||||
import phonemes2ids
|
||||
from gruut.const import LookupPhonemes, WordRole
|
||||
from gruut_ipa import guess_phonemes, IPA, Phonemes, Phoneme
|
||||
|
||||
from opentts_abc import (
|
||||
TextToSpeechSystem,
|
||||
Voice,
|
||||
BaseToken,
|
||||
BaseResult,
|
||||
MarkResult,
|
||||
AudioResult,
|
||||
Word,
|
||||
Phonemes,
|
||||
SayAs,
|
||||
)
|
||||
|
||||
from mimic3_tts.config import TrainingConfig
|
||||
from mimic3_tts.utils import audio_float_to_int16
|
||||
|
||||
_DIR = Path(__file__).parent
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PHONEMES_LIST = typing.List[typing.List[str]]
|
||||
|
||||
DEFAULT_VOICE = "en_US/vctk_low"
|
||||
DEFAULT_LANGUAGE = "en_US"
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class Mimic3Settings:
|
||||
voice: typing.Optional[str] = None
|
||||
language: typing.Optional[str] = None
|
||||
voices_directories: typing.Optional[typing.Iterable[typing.Union[str, Path]]] = None
|
||||
speaker_id: typing.Optional[int] = None
|
||||
length_scale: float = 1.0
|
||||
noise_scale: float = 0.333
|
||||
noise_w: float = 1.0
|
||||
text_language: typing.Optional[str] = None
|
||||
sample_rate: int = 22050
|
||||
|
||||
|
||||
@dataclass
|
||||
class LoadedVoice:
|
||||
config: TrainingConfig
|
||||
onnx_model: onnxruntime.InferenceSession
|
||||
phoneme_to_id: typing.Mapping[str, int]
|
||||
phoneme_map: typing.Optional[typing.Dict[str, typing.List[str]]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Mimic3Phonemes:
|
||||
current_settings: Mimic3Settings
|
||||
phonemes: typing.List[typing.List[str]] = field(default_factory=list)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class Mimic3TextToSpeechSystem(TextToSpeechSystem):
|
||||
"""Convert text to speech using Mimic 3"""
|
||||
|
||||
def __init__(self, settings: Mimic3Settings):
|
||||
self.settings = settings
|
||||
|
||||
# self._current_voice: typing.Optional[LoadedVoice] = None
|
||||
# self._current_settings = self.settings
|
||||
|
||||
self._results: typing.List[typing.Union[BaseResult, Mimic3Phonemes]] = []
|
||||
|
||||
self.loaded_voices: typing.Dict[str, LoadedVoice] = {}
|
||||
|
||||
@property
|
||||
def voice(self) -> str:
|
||||
return self.settings.voice or DEFAULT_VOICE
|
||||
|
||||
@voice.setter
|
||||
def voice(self, new_voice: str):
|
||||
if new_voice != self.settings.voice:
|
||||
# Clear speaker id on voice change
|
||||
self.speaker_id = None
|
||||
|
||||
self.settings.voice = new_voice
|
||||
|
||||
if "#" in self.settings.voice:
|
||||
# Split
|
||||
voice, speaker_id_str = self.settings.voice.split("#", maxsplit=1)
|
||||
self.settings.voice = voice
|
||||
|
||||
# TODO: Use speaker map
|
||||
self.speaker_id = int(speaker_id_str)
|
||||
|
||||
# self._current_voice = self._get_or_load_voice(
|
||||
# self.settings.voice or DEFAULT_VOICE
|
||||
# )
|
||||
|
||||
@property
|
||||
def speaker_id(self) -> typing.Optional[int]:
|
||||
return self.settings.speaker_id
|
||||
|
||||
@speaker_id.setter
|
||||
def speaker_id(self, new_speaker_id: typing.Optional[int]):
|
||||
self.settings.speaker_id = new_speaker_id
|
||||
|
||||
@property
|
||||
def language(self) -> str:
|
||||
return self.settings.language or DEFAULT_LANGUAGE
|
||||
|
||||
@language.setter
|
||||
def language(self, new_language: str):
|
||||
self.settings.language = new_language
|
||||
|
||||
@staticmethod
|
||||
def get_default_voices_directories() -> typing.List[Path]:
|
||||
return [_DIR.parent.parent / "voices"]
|
||||
|
||||
# @property
|
||||
# def text_lang(self) -> str:
|
||||
# return (
|
||||
# self.settings.text_language
|
||||
# or self.settings.language
|
||||
# or (
|
||||
# self._current_voice.config.text_language
|
||||
# if self._current_voice
|
||||
# else None
|
||||
# )
|
||||
# or "en_US"
|
||||
# )
|
||||
|
||||
# @property
|
||||
# def sample_rate(self) -> int:
|
||||
# return (
|
||||
# self._current_voice.config.audio.sample_rate
|
||||
# if self._current_voice
|
||||
# else self.settings.sample_rate
|
||||
# )
|
||||
|
||||
def get_voices(self) -> typing.Iterable[Voice]:
|
||||
voices_dirs = (
|
||||
self.settings.voices_directories
|
||||
or Mimic3TextToSpeechSystem.get_default_voices_directories()
|
||||
)
|
||||
|
||||
# voices/<language>/<voice>/
|
||||
for voices_dir in voices_dirs:
|
||||
voices_dir = Path(voices_dir)
|
||||
|
||||
if not voices_dir.is_dir():
|
||||
continue
|
||||
|
||||
for lang_dir in voices_dir.iterdir():
|
||||
if not lang_dir.is_dir():
|
||||
continue
|
||||
|
||||
for voice_dir in lang_dir.iterdir():
|
||||
if not voice_dir.is_dir():
|
||||
continue
|
||||
|
||||
voice_lang = lang_dir.name
|
||||
voice_name = voice_dir.name
|
||||
|
||||
yield Voice(
|
||||
key=str(voice_dir.absolute()),
|
||||
name=voice_name,
|
||||
language=voice_lang,
|
||||
description="",
|
||||
)
|
||||
|
||||
def begin_utterance(self):
|
||||
self._results.clear()
|
||||
# self._current_settings = deepcopy(self.settings)
|
||||
|
||||
def speak_text(self, text: str, text_language: typing.Optional[str] = None):
|
||||
text_language = text_language or self.language
|
||||
for sentence in gruut.sentences(text, lang=text_language):
|
||||
sent_phonemes = [w.phonemes for w in sentence if w.phonemes]
|
||||
self._results.append(
|
||||
Mimic3Phonemes(
|
||||
current_settings=deepcopy(self.settings),
|
||||
phonemes=sent_phonemes,
|
||||
)
|
||||
)
|
||||
|
||||
def _speak_sentence_phonemes(
|
||||
self,
|
||||
sent_phonemes,
|
||||
text: typing.Optional[str] = None,
|
||||
settings: typing.Optional[Mimic3Settings] = None,
|
||||
) -> AudioResult:
|
||||
settings = settings or self.settings
|
||||
current_voice = self._get_or_load_voice(settings.voice or DEFAULT_VOICE)
|
||||
|
||||
config = current_voice.config
|
||||
onnx_model = current_voice.onnx_model
|
||||
phoneme_to_id = current_voice.phoneme_to_id
|
||||
phoneme_map = current_voice.phoneme_map or config.phonemes.phoneme_map
|
||||
|
||||
sent_phoneme_ids = phonemes2ids.phonemes2ids(
|
||||
word_phonemes=sent_phonemes,
|
||||
phoneme_to_id=phoneme_to_id,
|
||||
pad=config.phonemes.pad,
|
||||
bos=config.phonemes.bos,
|
||||
eos=config.phonemes.eos,
|
||||
auto_bos_eos=config.phonemes.auto_bos_eos,
|
||||
blank=config.phonemes.blank,
|
||||
blank_word=config.phonemes.blank_word,
|
||||
blank_between=config.phonemes.blank_between,
|
||||
blank_at_start=config.phonemes.blank_at_start,
|
||||
blank_at_end=config.phonemes.blank_at_end,
|
||||
simple_punctuation=config.phonemes.simple_punctuation,
|
||||
punctuation_map=config.phonemes.punctuation_map,
|
||||
separate=config.phonemes.separate,
|
||||
separate_graphemes=config.phonemes.separate_graphemes,
|
||||
separate_tones=config.phonemes.separate_tones,
|
||||
tone_before=config.phonemes.tone_before,
|
||||
phoneme_map=phoneme_map,
|
||||
fail_on_missing=False,
|
||||
)
|
||||
|
||||
if text:
|
||||
_LOGGER.debug("%s %s %s", text, sent_phonemes, sent_phoneme_ids)
|
||||
else:
|
||||
_LOGGER.debug("%s %s", sent_phonemes, sent_phoneme_ids)
|
||||
|
||||
# Create model inputs
|
||||
text_array = np.expand_dims(np.array(sent_phoneme_ids, dtype=np.int64), 0)
|
||||
text_lengths_array = np.array([text_array.shape[1]], dtype=np.int64)
|
||||
scales_array = np.array(
|
||||
[
|
||||
settings.noise_scale,
|
||||
settings.length_scale,
|
||||
settings.noise_w,
|
||||
],
|
||||
dtype=np.float32,
|
||||
)
|
||||
|
||||
inputs = {
|
||||
"input": text_array,
|
||||
"input_lengths": text_lengths_array,
|
||||
"scales": scales_array,
|
||||
}
|
||||
|
||||
if config.is_multispeaker:
|
||||
speaker_id = settings.speaker_id if settings.speaker_id is not None else 0
|
||||
speaker_id_array = np.array([speaker_id], dtype=np.int64)
|
||||
inputs["sid"] = speaker_id_array
|
||||
|
||||
# Infer audio from phonemes
|
||||
start_time = time.perf_counter()
|
||||
audio = onnx_model.run(None, inputs)[0].squeeze()
|
||||
audio = audio_float_to_int16(audio)
|
||||
end_time = time.perf_counter()
|
||||
|
||||
# Compute real-time factor
|
||||
audio_duration_sec = audio.shape[-1] / config.audio.sample_rate
|
||||
infer_sec = end_time - start_time
|
||||
real_time_factor = (
|
||||
infer_sec / audio_duration_sec if audio_duration_sec > 0 else 0.0
|
||||
)
|
||||
|
||||
_LOGGER.debug("RTF: %s", real_time_factor)
|
||||
|
||||
audio_bytes = audio.tobytes()
|
||||
return AudioResult(
|
||||
sample_rate_hz=config.audio.sample_rate,
|
||||
audio_bytes=audio_bytes,
|
||||
# 16-bit mono
|
||||
sample_width_bytes=2,
|
||||
num_channels=1,
|
||||
)
|
||||
|
||||
def speak_tokens(self, tokens: typing.Iterable[BaseToken]):
|
||||
token_phonemes: PHONEMES_LIST = []
|
||||
|
||||
for token in tokens:
|
||||
if isinstance(token, Word):
|
||||
word_role = xmlescape(token.role) if token.role else ""
|
||||
word_text = xmlescape(token.text)
|
||||
|
||||
sentence = next(
|
||||
iter(
|
||||
gruut.sentences(
|
||||
f'<w role="{word_role}">{word_text}</w>', ssml=True
|
||||
)
|
||||
)
|
||||
)
|
||||
token_phonemes.extend(w.phonemes for w in sentence if w.phonemes)
|
||||
elif isinstance(token, Phonemes):
|
||||
phoneme_str = token.text.strip()
|
||||
if " " in phoneme_str:
|
||||
token_phonemes.append(phoneme_str.split())
|
||||
else:
|
||||
token_phonemes.append(list(phoneme_str))
|
||||
elif isinstance(token, SayAs):
|
||||
word_text = xmlescape(token.text)
|
||||
interpret_as = xmlescape(token.interpret_as)
|
||||
format_attr = (
|
||||
f'format="{xmlescape(token.format)}"' if token.format else ""
|
||||
)
|
||||
|
||||
sentence = next(
|
||||
iter(
|
||||
gruut.sentences(
|
||||
f'<say-as interpret-as="{interpret_as}" {format_attr}>{word_text}</say-as>',
|
||||
ssml=True,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
token_phonemes.extend(w.phonemes for w in sentence if w.phonemes)
|
||||
|
||||
if token_phonemes:
|
||||
self._results.append(
|
||||
Mimic3Phonemes(
|
||||
current_settings=deepcopy(self.settings), phonemes=token_phonemes
|
||||
)
|
||||
)
|
||||
|
||||
def add_break(self, time_ms: int):
|
||||
# Generate silence (16-bit mono at sample rate)
|
||||
num_bytes = int((time_ms / 1000.0) * self.settings.sample_rate * 2)
|
||||
audio_bytes = bytes(num_bytes)
|
||||
|
||||
self._results.append(
|
||||
AudioResult(
|
||||
sample_rate_hz=self.settings.sample_rate,
|
||||
audio_bytes=audio_bytes,
|
||||
# 16-bit mono
|
||||
sample_width_bytes=2,
|
||||
num_channels=1,
|
||||
)
|
||||
)
|
||||
|
||||
def set_mark(self, name: str):
|
||||
self._results.append(MarkResult(name=name))
|
||||
|
||||
def end_utterance(self) -> typing.Iterable[BaseResult]:
|
||||
last_settings = self.settings
|
||||
|
||||
sent_phonemes: PHONEMES_LIST = []
|
||||
|
||||
for result in self._results:
|
||||
if isinstance(result, Mimic3Phonemes):
|
||||
if result.current_settings != last_settings:
|
||||
if sent_phonemes:
|
||||
yield self._speak_sentence_phonemes(
|
||||
sent_phonemes, settings=last_settings
|
||||
)
|
||||
sent_phonemes.clear()
|
||||
|
||||
sent_phonemes.extend(result.phonemes)
|
||||
last_settings = result.current_settings
|
||||
else:
|
||||
if sent_phonemes:
|
||||
yield self._speak_sentence_phonemes(
|
||||
sent_phonemes, settings=last_settings
|
||||
)
|
||||
sent_phonemes.clear()
|
||||
|
||||
yield result
|
||||
|
||||
if sent_phonemes:
|
||||
yield self._speak_sentence_phonemes(sent_phonemes)
|
||||
|
||||
def _get_or_load_voice(self, voice_key: str) -> LoadedVoice:
|
||||
existing_voice = self.loaded_voices.get(voice_key)
|
||||
if existing_voice is not None:
|
||||
return existing_voice
|
||||
|
||||
# Look up as substring of known voice
|
||||
model_dir: typing.Optional[Path] = None
|
||||
for maybe_voice in self.get_voices():
|
||||
if maybe_voice.key.endswith(voice_key):
|
||||
model_dir = Path(maybe_voice.key)
|
||||
break
|
||||
|
||||
assert model_dir is not None
|
||||
existing_voice = self.loaded_voices.get(str(model_dir.absolute()))
|
||||
if existing_voice is not None:
|
||||
# Alias
|
||||
self.loaded_voices[voice_key] = existing_voice
|
||||
|
||||
return existing_voice
|
||||
|
||||
_LOGGER.debug("Loading voice from %s", model_dir)
|
||||
|
||||
config_path = model_dir / "config.json"
|
||||
_LOGGER.debug("Loading model config from %s", config_path)
|
||||
|
||||
with open(config_path, "r", encoding="utf-8") as config_file:
|
||||
config = TrainingConfig.load(config_file)
|
||||
|
||||
# phoneme -> id
|
||||
phoneme_ids_path = model_dir / "phonemes.txt"
|
||||
_LOGGER.debug("Loading model phonemes from %s", phoneme_ids_path)
|
||||
with open(phoneme_ids_path, "r", encoding="utf-8") as ids_file:
|
||||
phoneme_to_id = phonemes2ids.load_phoneme_ids(ids_file)
|
||||
|
||||
generator_path = model_dir / "generator.onnx"
|
||||
_LOGGER.debug("Loading model from %s", generator_path)
|
||||
|
||||
sess_options = onnxruntime.SessionOptions()
|
||||
# sess_options.enable_cpu_mem_arena = False
|
||||
# sess_options.enable_mem_pattern = False
|
||||
# sess_options.enable_mem_reuse = False
|
||||
|
||||
onnx_model = onnxruntime.InferenceSession(
|
||||
str(generator_path), sess_options=sess_options
|
||||
)
|
||||
|
||||
voice = LoadedVoice(
|
||||
config=config, onnx_model=onnx_model, phoneme_to_id=phoneme_to_id
|
||||
)
|
||||
|
||||
# valid_phonemes = []
|
||||
# for phoneme_str in self._phoneme_to_id:
|
||||
# maybe_phoneme = Phoneme(phoneme_str)
|
||||
# if any(
|
||||
# [
|
||||
# maybe_phoneme.vowel,
|
||||
# maybe_phoneme.consonant,
|
||||
# maybe_phoneme.dipthong,
|
||||
# maybe_phoneme.schwa,
|
||||
# ]
|
||||
# ):
|
||||
# valid_phonemes.append(maybe_phoneme)
|
||||
|
||||
# self._voice_phonemes = Phonemes(phonemes=valid_phonemes)
|
||||
|
||||
# phoneme -> phoneme, phoneme, ...
|
||||
phoneme_map_path = model_dir / "phoneme_map.txt"
|
||||
if phoneme_map_path.is_file():
|
||||
_LOGGER.debug("Loading phoneme map from %s", phoneme_map_path)
|
||||
with open(phoneme_map_path, "r", encoding="utf-8") as map_file:
|
||||
voice.phoneme_map = phonemes2ids.utils.load_phoneme_map(map_file)
|
||||
|
||||
_LOGGER.info("Loaded voice from %s", model_dir)
|
||||
|
||||
# Add to cache
|
||||
self.loaded_voices[voice_key] = voice
|
||||
|
||||
return voice
|
||||
|
||||
# def start(self):
|
||||
# self.stop()
|
||||
|
||||
# self._thread = threading.Thread(target=self._thread_proc, daemon=True)
|
||||
# self._thread.start()
|
||||
|
||||
# def stop(self):
|
||||
# if self._thread is not None:
|
||||
# self._request_queue.put(None)
|
||||
# self._thread.join()
|
||||
# self._thread = None
|
||||
|
||||
# # Drain queues
|
||||
# while not self._request_queue.empty():
|
||||
# self._request_queue.get()
|
||||
|
||||
# while not self._result_queue.empty():
|
||||
# self._result_queue.get()
|
||||
|
||||
# def _thread_proc(self):
|
||||
# try:
|
||||
# self._load_model()
|
||||
# self._load_text_processor()
|
||||
|
||||
# while True:
|
||||
# message = self._request_queue.get()
|
||||
# if message is None:
|
||||
# break
|
||||
|
||||
# if isinstance(message, AddLexiconMessage):
|
||||
# self._add_lexicon(message.lexicon_file)
|
||||
# elif isinstance(message, TextToSpeechMessage):
|
||||
# result = self._text_to_speech(**dataclasses.asdict(message))
|
||||
# self._result_queue.put(result)
|
||||
|
||||
# except Exception:
|
||||
# _LOGGER.exception("_thread_proc")
|
||||
|
||||
# def _load_model(self):
|
||||
# """Load model configuration and generator"""
|
||||
|
||||
# if self._config is None:
|
||||
# config_path = self.model_dir / "config.json"
|
||||
# _LOGGER.debug("Loading model config from %s", config_path)
|
||||
|
||||
# with open(config_path, "r", encoding="utf-8") as config_file:
|
||||
# self._config = TrainingConfig.load(config_file)
|
||||
|
||||
# self.lang = self.lang or self._config.text_language or "en_US"
|
||||
|
||||
# if self._phoneme_to_id is None:
|
||||
# # phoneme -> id
|
||||
# phoneme_ids_path = self.model_dir / "phonemes.txt"
|
||||
# _LOGGER.debug("Loading model phonemes from %s", phoneme_ids_path)
|
||||
# with open(phoneme_ids_path, "r", encoding="utf-8") as ids_file:
|
||||
# self._phoneme_to_id = phonemes2ids.load_phoneme_ids(ids_file)
|
||||
|
||||
# valid_phonemes = []
|
||||
# for phoneme_str in self._phoneme_to_id:
|
||||
# maybe_phoneme = Phoneme(phoneme_str)
|
||||
# if any(
|
||||
# [
|
||||
# maybe_phoneme.vowel,
|
||||
# maybe_phoneme.consonant,
|
||||
# maybe_phoneme.dipthong,
|
||||
# maybe_phoneme.schwa,
|
||||
# ]
|
||||
# ):
|
||||
# valid_phonemes.append(maybe_phoneme)
|
||||
|
||||
# self._voice_phonemes = Phonemes(phonemes=valid_phonemes)
|
||||
|
||||
# if self._phoneme_map is None:
|
||||
# # phoneme -> phoneme, phoneme, ...
|
||||
# phoneme_map_path = self.model_dir / "phoneme_map.txt"
|
||||
# if phoneme_map_path.is_file():
|
||||
# _LOGGER.debug("Loading phoneme map from %s", phoneme_map_path)
|
||||
# with open(phoneme_map_path, "r", encoding="utf-8") as map_file:
|
||||
# self._phoneme_map = phonemes2ids.utils.load_phoneme_map(map_file)
|
||||
|
||||
# if self._onnx_model is None:
|
||||
# generator_path = self.model_dir / "generator.onnx"
|
||||
# _LOGGER.debug("Loading model from %s", generator_path)
|
||||
|
||||
# sess_options = onnxruntime.SessionOptions()
|
||||
# sess_options.enable_cpu_mem_arena = False
|
||||
# sess_options.enable_mem_pattern = False
|
||||
# sess_options.enable_mem_reuse = False
|
||||
|
||||
# self._onnx_model = onnxruntime.InferenceSession(
|
||||
# str(generator_path), sess_options=sess_options
|
||||
# )
|
||||
|
||||
# def _load_text_processor(self):
|
||||
# if self._text_processor is None:
|
||||
# self._text_processor = gruut.TextProcessor(default_lang=self.lang)
|
||||
|
||||
# def add_lexicon(self, lexicon_file: typing.Iterable[str]):
|
||||
# """Load a custom pronunciation lexicon from a file.
|
||||
|
||||
# Format is:
|
||||
# <word> <role> <phoneme> <phoneme> ...
|
||||
|
||||
# Role can be things like "gruut:VB" or "gruut:NN".
|
||||
# Use "_" for the default role (any part of speech).
|
||||
# """
|
||||
# self._request_queue.put(AddLexiconMessage(lexicon_file=list(lexicon_file)))
|
||||
|
||||
# def _add_lexicon(self, lexicon_file: typing.Iterable[str]):
|
||||
# self._load_text_processor()
|
||||
# assert self._text_processor is not None
|
||||
|
||||
# # word -> role -> [phoneme, phoneme, ...]
|
||||
# lexicon: typing.Dict[str, typing.Dict[str, typing.List[str]]] = {}
|
||||
|
||||
# for line in lexicon_file:
|
||||
# line = line.strip()
|
||||
# if not line:
|
||||
# continue
|
||||
|
||||
# word, role, *phonemes = line.split()
|
||||
# if (not word) or (not phonemes):
|
||||
# _LOGGER.warning("Empty word or pronunciation in lexicon: %s", line)
|
||||
# continue
|
||||
|
||||
# if role == "_":
|
||||
# role = WordRole.DEFAULT
|
||||
|
||||
# word_roles = lexicon.get(word)
|
||||
# if word_roles is None:
|
||||
# word_roles = {}
|
||||
# lexicon[word] = word_roles
|
||||
|
||||
# word_roles[role] = phonemes
|
||||
|
||||
# if lexicon:
|
||||
|
||||
# # Wrap the "lookup_phonemes" method in the gruut text processor.
|
||||
# # Our lexicon will be consulted first.
|
||||
# settings = self._text_processor.get_settings()
|
||||
# base_lookup = settings.lookup_phonemes
|
||||
|
||||
# def lookup_phonemes(word: str, role: typing.Optional[str] = None, **kwargs):
|
||||
# word_roles = lexicon.get(word)
|
||||
|
||||
# if not word_roles:
|
||||
# # Try lower case
|
||||
# word_roles = lexicon.get(word.lower())
|
||||
|
||||
# if word_roles:
|
||||
# if role is None:
|
||||
# role = WordRole.DEFAULT
|
||||
|
||||
# phonemes = word_roles.get(role)
|
||||
|
||||
# if (phonemes is None) and (role != WordRole.DEFAULT):
|
||||
# phonemes = word_roles.get(WordRole.DEFAULT)
|
||||
|
||||
# if phonemes:
|
||||
# return phonemes
|
||||
|
||||
# if base_lookup is not None:
|
||||
# return base_lookup(word, role, **kwargs)
|
||||
|
||||
# return None
|
||||
|
||||
# settings.lookup_phonemes = typing.cast(LookupPhonemes, lookup_phonemes)
|
||||
# _LOGGER.debug("Added custom pronunciations for %s word(s)", len(lexicon))
|
||||
|
||||
# def text_to_speech(
|
||||
# self,
|
||||
# text: str,
|
||||
# speaker_id: typing.Optional[int] = None,
|
||||
# length_scale: typing.Optional[float] = None,
|
||||
# noise_scale: typing.Optional[float] = None,
|
||||
# noise_w: typing.Optional[float] = None,
|
||||
# ssml: bool = False,
|
||||
# text_language: typing.Optional[str] = None,
|
||||
# ) -> Result:
|
||||
# self._request_queue.put(
|
||||
# TextToSpeechMessage(
|
||||
# text=text,
|
||||
# speaker_id=speaker_id,
|
||||
# length_scale=length_scale,
|
||||
# noise_scale=noise_scale,
|
||||
# noise_w=noise_w,
|
||||
# ssml=ssml,
|
||||
# text_language=text_language,
|
||||
# )
|
||||
# )
|
||||
|
||||
# result = typing.cast(Result, self._result_queue.get())
|
||||
|
||||
# return result
|
||||
|
||||
# def _text_to_speech(
|
||||
# self,
|
||||
# text: str,
|
||||
# speaker_id: typing.Optional[int] = None,
|
||||
# length_scale: typing.Optional[float] = None,
|
||||
# noise_scale: typing.Optional[float] = None,
|
||||
# noise_w: typing.Optional[float] = None,
|
||||
# ssml: bool = False,
|
||||
# text_language: typing.Optional[str] = None,
|
||||
# ) -> Result:
|
||||
# """Speak text and return WAV audio as bytes"""
|
||||
# text_language = text_language or self.lang
|
||||
# assert self._text_processor is not None
|
||||
|
||||
# # Ensure model is loaded
|
||||
# assert self.lang is not None
|
||||
# assert self._config is not None
|
||||
# assert self._phoneme_to_id is not None
|
||||
# assert self._onnx_model is not None
|
||||
|
||||
# # Resolve settings
|
||||
# if speaker_id is None:
|
||||
# speaker_id = self.speaker_id or 0
|
||||
|
||||
# if length_scale is None:
|
||||
# length_scale = self.length_scale
|
||||
|
||||
# if noise_scale is None:
|
||||
# noise_scale = self.noise_scale
|
||||
|
||||
# if noise_w is None:
|
||||
# noise_w = self.noise_w
|
||||
|
||||
# # Process text into sentences
|
||||
# result = Result(text=text)
|
||||
# audio_arrays: typing.List[np.ndarray] = []
|
||||
|
||||
# graph, root = self._text_processor.process(text, lang=text_language, ssml=ssml)
|
||||
# sentences = list(self._text_processor.sentences(graph, root))
|
||||
|
||||
# for sentence in sentences:
|
||||
# result.sentence_words.append([w.text for w in sentence])
|
||||
|
||||
# if text_language == self.lang:
|
||||
# sent_phonemes = [w.phonemes for w in sentence if w.phonemes]
|
||||
# else:
|
||||
# # Convert phonemes to ids to target language
|
||||
# other_sent_phonemes = [w.phonemes for w in sentence if w.phonemes]
|
||||
# _LOGGER.debug(other_sent_phonemes)
|
||||
|
||||
# sent_phonemes = []
|
||||
# for other_word_p in other_sent_phonemes:
|
||||
# word_p = []
|
||||
# for other_p in other_word_p:
|
||||
# if IPA.is_break(other_p):
|
||||
# # Keep breaks
|
||||
# word_p.append(other_p)
|
||||
# continue
|
||||
|
||||
# original_p = other_p
|
||||
# stress = ""
|
||||
# while other_p and IPA.is_stress(other_p[0]):
|
||||
# stress = other_p[0]
|
||||
# other_p = other_p[1:]
|
||||
|
||||
# if not other_p:
|
||||
# continue
|
||||
|
||||
# if other_p in self._phoneme_to_id:
|
||||
# word_p.append(original_p)
|
||||
# continue
|
||||
|
||||
# assert self._voice_phonemes is not None
|
||||
# guessed = guess_phonemes(
|
||||
# other_p, to_phonemes=self._voice_phonemes
|
||||
# )
|
||||
# if guessed.phonemes:
|
||||
# word_p.extend([p.text for p in guessed.phonemes])
|
||||
|
||||
# if word_p:
|
||||
# sent_phonemes.append(word_p)
|
||||
|
||||
# result.sentence_phonemes.append(sent_phonemes)
|
||||
|
||||
# sent_phoneme_ids = phonemes2ids.phonemes2ids(
|
||||
# word_phonemes=sent_phonemes,
|
||||
# phoneme_to_id=self._phoneme_to_id,
|
||||
# pad=self._config.phonemes.pad,
|
||||
# bos=self._config.phonemes.bos,
|
||||
# eos=self._config.phonemes.eos,
|
||||
# auto_bos_eos=self._config.phonemes.auto_bos_eos,
|
||||
# blank=self._config.phonemes.blank,
|
||||
# blank_word=self._config.phonemes.blank_word,
|
||||
# blank_between=self._config.phonemes.blank_between,
|
||||
# blank_at_start=self._config.phonemes.blank_at_start,
|
||||
# blank_at_end=self._config.phonemes.blank_at_end,
|
||||
# simple_punctuation=self._config.phonemes.simple_punctuation,
|
||||
# punctuation_map=self._config.phonemes.punctuation_map,
|
||||
# separate=self._config.phonemes.separate,
|
||||
# separate_graphemes=self._config.phonemes.separate_graphemes,
|
||||
# separate_tones=self._config.phonemes.separate_tones,
|
||||
# tone_before=self._config.phonemes.tone_before,
|
||||
# phoneme_map=self._phoneme_map or self._config.phonemes.phoneme_map,
|
||||
# fail_on_missing=False,
|
||||
# )
|
||||
|
||||
# result.sentence_phoneme_ids.append(sent_phonemes)
|
||||
|
||||
# _LOGGER.debug("%s %s %s", sentence.text, sent_phonemes, sent_phoneme_ids)
|
||||
|
||||
# # Create model inputs
|
||||
# text_array = np.expand_dims(np.array(sent_phoneme_ids, dtype=np.int64), 0)
|
||||
# text_lengths_array = np.array([text_array.shape[1]], dtype=np.int64)
|
||||
# scales_array = np.array(
|
||||
# [noise_scale, length_scale, noise_w], dtype=np.float32
|
||||
# )
|
||||
|
||||
# inputs = {
|
||||
# "input": text_array,
|
||||
# "input_lengths": text_lengths_array,
|
||||
# "scales": scales_array,
|
||||
# }
|
||||
|
||||
# if self._config.is_multispeaker:
|
||||
# speaker_id_array = np.array([speaker_id], dtype=np.int64)
|
||||
# inputs["sid"] = speaker_id_array
|
||||
|
||||
# # Infer audio from phonemes
|
||||
# start_time = time.perf_counter()
|
||||
# audio = self._onnx_model.run(None, inputs)[0].squeeze()
|
||||
# audio = audio_float_to_int16(audio)
|
||||
# end_time = time.perf_counter()
|
||||
|
||||
# # Compute real-time factor
|
||||
# audio_duration_sec = audio.shape[-1] / self._config.audio.sample_rate
|
||||
# infer_sec = end_time - start_time
|
||||
# real_time_factor = (
|
||||
# infer_sec / audio_duration_sec if audio_duration_sec > 0 else 0.0
|
||||
# )
|
||||
|
||||
# _LOGGER.debug("RTF: %s", real_time_factor)
|
||||
|
||||
# audio_arrays.append(audio)
|
||||
|
||||
# # Write to WAV and return bytes
|
||||
# with io.BytesIO() as wav_file:
|
||||
# write_wav(
|
||||
# wav_file, self._config.audio.sample_rate, np.concatenate(audio_arrays),
|
||||
# )
|
||||
|
||||
# result.wav_bytes = wav_file.getvalue()
|
||||
|
||||
# return result
|
||||
Loading…
Add table
Add a link
Reference in a new issue