mimic2_tts.py
Go to the documentation of this file.
1 # Copyright 2017 Mycroft AI Inc.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 #
15 
16 from mycroft.tts import TTS, TTSValidator
17 from mycroft.tts.remote_tts import RemoteTTSTimeoutException
18 from mycroft.util.log import LOG
19 from mycroft.util.format import pronounce_number
20 from mycroft.tts import cache_handler
21 from mycroft.util import play_wav, get_cache_directory
22 from requests_futures.sessions import FuturesSession
23 from requests.exceptions import (
24  ReadTimeout, ConnectionError, ConnectTimeout, HTTPError
25 )
26 from urllib import parse
27 from .mimic_tts import VISIMES
28 import math
29 import base64
30 import os
31 import re
32 import json
33 
34 
35 # Heuristic value, caps character length of a chunk of text to be spoken as a
36 # work around for current Mimic2 implementation limits.
37 _max_sentence_size = 170
38 
39 
40 def _break_chunks(l, n):
41  """ Yield successive n-sized chunks
42 
43  Args:
44  l (list): text (str) to split
45  chunk_size (int): chunk size
46  """
47  for i in range(0, len(l), n):
48  yield " ".join(l[i:i + n])
49 
50 
51 def _split_by_chunk_size(text, chunk_size):
52  """ Split text into word chunks by chunk_size size
53 
54  Args:
55  text (str): text to split
56  chunk_size (int): chunk size
57 
58  Returns:
59  list: list of text chunks
60  """
61  text_list = text.split()
62 
63  if len(text_list) <= chunk_size:
64  return [text]
65 
66  if chunk_size < len(text_list) < (chunk_size * 2):
67  return list(_break_chunks(
68  text_list,
69  int(math.ceil(len(text_list) / 2))
70  ))
71  elif (chunk_size * 2) < len(text_list) < (chunk_size * 3):
72  return list(_break_chunks(
73  text_list,
74  int(math.ceil(len(text_list) / 3))
75  ))
76  elif (chunk_size * 3) < len(text_list) < (chunk_size * 4):
77  return list(_break_chunks(
78  text_list,
79  int(math.ceil(len(text_list) / 4))
80  ))
81  else:
82  return list(_break_chunks(
83  text_list,
84  int(math.ceil(len(text_list) / 5))
85  ))
86 
87 
88 def _split_by_punctuation(chunks, puncs):
89  """splits text by various punctionations
90  e.g. hello, world => [hello, world]
91 
92  Args:
93  chunks (list or str): text (str) to split
94  puncs (list): list of punctuations used to split text
95 
96  Returns:
97  list: list with split text
98  """
99  if isinstance(chunks, str):
100  out = [chunks]
101  else:
102  out = chunks
103 
104  for punc in puncs:
105  splits = []
106  for t in out:
107  # Split text by punctuation, but not embedded punctuation. E.g.
108  # Split: "Short sentence. Longer sentence."
109  # But not at: "I.B.M." or "3.424", "3,424" or "what's-his-name."
110  splits += re.split(r'(?<!\.\S)' + punc + r'\s', t)
111  out = splits
112  return [t.strip() for t in out]
113 
114 
116  """ Add punctuation at the end of each chunk.
117 
118  Mimic2 expects some form of punctuation at the end of a sentence.
119  """
120  punctuation = ['.', '?', '!', ';']
121  if len(text) >= 1 and text[-1] not in punctuation:
122  return text + '.'
123  else:
124  return text
125 
126 
128  """ Split text into smaller chunks for TTS generation.
129 
130  NOTE: The smaller chunks are needed due to current Mimic2 TTS limitations.
131  This stage can be removed once Mimic2 can generate longer sentences.
132 
133  Args:
134  text (str): text to split
135  chunk_size (int): size of each chunk
136  split_by_punc (bool, optional): Defaults to True.
137 
138  Returns:
139  list: list of text chunks
140  """
141  if len(text) <= _max_sentence_size:
142  return [_add_punctuation(text)]
143 
144  # first split by punctuations that are major pauses
145  first_splits = _split_by_punctuation(
146  text,
147  puncs=[r'\.', r'\!', r'\?', r'\:', r'\;']
148  )
149 
150  # if chunks are too big, split by minor pauses (comma, hyphen)
151  second_splits = []
152  for chunk in first_splits:
153  if len(chunk) > _max_sentence_size:
154  second_splits += _split_by_punctuation(chunk,
155  puncs=[r'\,', '--', '-'])
156  else:
157  second_splits.append(chunk)
158 
159  # if chunks are still too big, chop into pieces of at most 20 words
160  third_splits = []
161  for chunk in second_splits:
162  if len(chunk) > _max_sentence_size:
163  third_splits += _split_by_chunk_size(chunk, 20)
164  else:
165  third_splits.append(chunk)
166 
167  return [_add_punctuation(chunk) for chunk in third_splits]
168 
169 
170 class Mimic2(TTS):
171 
172  def __init__(self, lang, config):
173  super(Mimic2, self).__init__(
174  lang, config, Mimic2Validator(self)
175  )
176  try:
177  LOG.info("Getting Pre-loaded cache")
178  cache_handler.main(config['preloaded_cache'])
179  LOG.info("Successfully downloaded Pre-loaded cache")
180  except Exception as e:
181  LOG.error("Could not get the pre-loaded cache ({})"
182  .format(repr(e)))
183  self.url = config['url']
184  self.session = FuturesSession()
185 
186  def _save(self, data):
187  """ Save WAV files in tmp
188 
189  Args:
190  data (byes): WAV data
191  """
192  with open(self.filename, 'wb') as f:
193  f.write(data)
194 
195  def _play(self, req):
196  """ Play WAV file after saving to tmp
197 
198  Args:
199  req (object): requests object
200  """
201  if req.status_code == 200:
202  self._save(req.content)
203  play_wav(self.filename).communicate()
204  else:
205  LOG.error(
206  '%s Http Error: %s for url: %s' %
207  (req.status_code, req.reason, req.url))
208 
209  def _requests(self, sentence):
210  """create asynchronous request list
211 
212  Args:
213  chunks (list): list of text to synthesize
214 
215  Returns:
216  list: list of FutureSession objects
217  """
218  url = self.url + parse.quote(sentence)
219  req_route = url + "&visimes=True"
220  return self.session.get(req_route, timeout=5)
221 
222  def viseme(self, phonemes):
223  """ Maps phonemes to appropriate viseme encoding
224 
225  Args:
226  phonemes (list): list of tuples (phoneme, time_start)
227 
228  Returns:
229  list: list of tuples (viseme_encoding, time_start)
230  """
231  visemes = []
232  for pair in phonemes:
233  if pair[0]:
234  phone = pair[0].lower()
235  else:
236  # if phoneme doesn't exist use
237  # this as placeholder since it
238  # is the most common one "3"
239  phone = 'z'
240  vis = VISIMES.get(phone)
241  vis_dur = float(pair[1])
242  visemes.append((vis, vis_dur))
243  return visemes
244 
245  def _prepocess_sentence(sentence):
246  """ Split sentence in chunks better suited for mimic2. """
247  return _sentence_chunker(sentence)
248 
249  def get_tts(self, sentence, wav_file):
250  """ Generate (remotely) and play mimic2 WAV audio
251 
252  Args:
253  sentence (str): Phrase to synthesize to audio with mimic2
254  wav_file (str): Location to write audio output
255  """
256  LOG.debug("Generating Mimic2 TSS for: " + str(sentence))
257  try:
258  req = self._requests(sentence)
259  results = req.result().json()
260  audio = base64.b64decode(results['audio_base64'])
261  vis = results['visimes']
262  with open(wav_file, 'wb') as f:
263  f.write(audio)
264  except (ReadTimeout, ConnectionError, ConnectTimeout, HTTPError):
266  "Mimic 2 server request timed out. Falling back to mimic")
267  return (wav_file, vis)
268 
269  def save_phonemes(self, key, phonemes):
270  """
271  Cache phonemes
272 
273  Args:
274  key: Hash key for the sentence
275  phonemes: phoneme string to save
276  """
277  cache_dir = get_cache_directory("tts/" + self.tts_name)
278  pho_file = os.path.join(cache_dir, key + ".pho")
279  try:
280  with open(pho_file, "w") as cachefile:
281  cachefile.write(json.dumps(phonemes))
282  except Exception:
283  LOG.exception("Failed to write {} to cache".format(pho_file))
284 
285  def load_phonemes(self, key):
286  """
287  Load phonemes from cache file.
288 
289  Args:
290  Key: Key identifying phoneme cache
291  """
292  pho_file = os.path.join(get_cache_directory("tts/" + self.tts_name),
293  key + ".pho")
294  if os.path.exists(pho_file):
295  try:
296  with open(pho_file, "r") as cachefile:
297  phonemes = json.load(cachefile)
298  return phonemes
299  except Exception as e:
300  LOG.error("Failed to read .PHO from cache ({})".format(e))
301  return None
302 
303 
305 
306  def __init__(self, tts):
307  super(Mimic2Validator, self).__init__(tts)
308 
309  def validate_lang(self):
310  # TODO
311  pass
312 
314  # TODO
315  pass
316 
317  def get_tts_class(self):
318  return Mimic2
def save_phonemes(self, key, phonemes)
Definition: mimic2_tts.py:269
def _requests(self, sentence)
Definition: mimic2_tts.py:209
def _split_by_chunk_size(text, chunk_size)
Definition: mimic2_tts.py:51
def _break_chunks(l, n)
Definition: mimic2_tts.py:40
def viseme(self, phonemes)
Definition: mimic2_tts.py:222
def load_phonemes(self, key)
Definition: mimic2_tts.py:285
def _split_by_punctuation(chunks, puncs)
Definition: mimic2_tts.py:88
def get_tts(self, sentence, wav_file)
Definition: mimic2_tts.py:249
def _prepocess_sentence(sentence)
Definition: mimic2_tts.py:245
def _add_punctuation(text)
Definition: mimic2_tts.py:115
def __init__(self, lang, config)
Definition: mimic2_tts.py:172
def get_cache_directory(domain=None)
def _sentence_chunker(text)
Definition: mimic2_tts.py:127


mycroft_ros
Author(s):
autogenerated on Mon Apr 26 2021 02:35:40