files: # Copyright 2017 Mycroft AI Inc.
  #
  # 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 re
  import time
  from threading import Lock
  
  from mycroft.configuration import Configuration
  from mycroft.metrics import report_timing, Stopwatch
  from mycroft.tts import TTSFactory
  from mycroft.util import check_for_signal
  from mycroft.util.log import LOG
  from mycroft.messagebus.message import Message
  from mycroft.tts.remote_tts import RemoteTTSTimeoutException
  from mycroft.tts.mimic_tts import Mimic
  
  bus = None  # Mycroft messagebus connection
  config = None
  tts = None
  tts_hash = None
  lock = Lock()
  mimic_fallback_obj = None
  
  _last_stop_signal = 0
  
  
  def handle_speak(event):
      """Handle "speak" message
  
      Parse sentences and invoke text to speech service.
      """
      config = Configuration.get()
      Configuration.set_config_update_handlers(bus)
      global _last_stop_signal
  
      # Get conversation ID
      if event.context and 'ident' in event.context:
          ident = event.context['ident']
      else:
          ident = 'unknown'
  
      start = time.time()  # Time of speech request
      with lock:
          stopwatch = Stopwatch()
          stopwatch.start()
          utterance = event.data['utterance']
          listen = event.data.get('expect_response', False)
          # This is a bit of a hack for Picroft.  The analog audio on a Pi blocks
          # for 30 seconds fairly often, so we don't want to break on periods
          # (decreasing the chance of encountering the block).  But we will
          # keep the split for non-Picroft installs since it give user feedback
          # faster on longer phrases.
          #
          # TODO: Remove or make an option?  This is really a hack, anyway,
          # so we likely will want to get rid of this when not running on Mimic
          if (config.get('enclosure', {}).get('platform') != "picroft" and
                  len(re.findall('<[^>]*>', utterance)) == 0):
              # Remove any whitespace present after the period,
              # if a character (only alpha) ends with a period
              # ex: A. Lincoln -> A.Lincoln
              # so that we don't split at the period
              utterance = re.sub(r'\b([A-za-z][\.])(\s+)', r'\g<1>', utterance)
              chunks = re.split(r'(?<!\w\.\w.)(?<![A-Z][a-z]\.)(?<=\.|\;|\?)\s',
                                utterance)
              # Apply the listen flag to the last chunk, set the rest to False
              chunks = [(chunks[i], listen if i == len(chunks) - 1 else False)
                        for i in range(len(chunks))]
              for chunk, listen in chunks:
                  # Check if somthing has aborted the speech
                  if (_last_stop_signal > start or
                          check_for_signal('buttonPress')):
                      # Clear any newly queued speech
                      tts.playback.clear()
                      break
                  try:
                      mute_and_speak(chunk, ident, listen)
                  except KeyboardInterrupt:
                      raise
                  except Exception:
                      LOG.error('Error in mute_and_speak', exc_info=True)
          else:
              mute_and_speak(utterance, ident)
  
          stopwatch.stop()
      report_timing(ident, 'speech', stopwatch, {'utterance': utterance,
                                                 'tts': tts.__class__.__name__})
  
  
  def mute_and_speak(utterance, ident, listen=False):
      """Mute mic and start speaking the utterance using selected tts backend.
  
      Arguments:
          utterance:  The sentence to be spoken
          ident:      Ident tying the utterance to the source query
      """
      global tts_hash
  
      # update TTS object if configuration has changed
      if tts_hash != hash(str(config.get('tts', ''))):
          global tts
          # Stop tts playback thread
          tts.playback.stop()
          tts.playback.join()
          # Create new tts instance
          tts = TTSFactory.create()
          tts.init(bus)
          tts_hash = hash(str(config.get('tts', '')))
  
      LOG.info("Speak: " + utterance)
      try:
          tts.execute(utterance, ident, listen)
      except RemoteTTSTimeoutException as e:
          LOG.error(e)
          mimic_fallback_tts(utterance, ident)
      except Exception as e:
          LOG.error('TTS execution failed ({})'.format(repr(e)))
  
  
  def mimic_fallback_tts(utterance, ident):
      global mimic_fallback_obj
      # fallback if connection is lost
      config = Configuration.get()
      tts_config = config.get('tts', {}).get("mimic", {})
      lang = config.get("lang", "en-us")
      if not mimic_fallback_obj:
          mimic_fallback_obj = Mimic(lang, tts_config)
      tts = mimic_fallback_obj
      LOG.debug("Mimic fallback, utterance : " + str(utterance))
      tts.init(bus)
      tts.execute(utterance, ident)
  
  
  def handle_stop(event):
      """Handle stop message.
  
      Shutdown any speech.
      """
      global _last_stop_signal
      if check_for_signal("isSpeaking", -1):
          _last_stop_signal = time.time()
          tts.playback.clear()  # Clear here to get instant stop
          bus.emit(Message("mycroft.stop.handled", {"by": "TTS"}))
  
  
  def init(messagebus):
      """Start speech related handlers.
  
      Arguments:
          messagebus: Connection to the Mycroft messagebus
      """
  
      global bus
      global tts
      global tts_hash
      global config
  
      bus = messagebus
      Configuration.set_config_update_handlers(bus)
      config = Configuration.get()
      bus.on('mycroft.stop', handle_stop)
      bus.on('mycroft.audio.speech.stop', handle_stop)
      bus.on('speak', handle_speak)
  
      tts = TTSFactory.create()
      tts.init(bus)
      tts_hash = hash(str(config.get('tts', '')))
  
  
  def shutdown():
      """Shutdown the audio service cleanly.
  
      Stop any playing audio and make sure threads are joined correctly.
      """
      if tts:
          tts.playback.stop()
          tts.playback.join()
      if mimic_fallback_obj:
          mimic_fallback_obj.playback.stop()
          mimic_fallback_obj.playback.join()
  
  
  [... the rest is omitted ...]
files: mycroft/audio/speech.py
files: # Copyright 2017 Mycroft AI Inc.
  #
  # 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 re
  import time
  from threading import Lock
  
  from mycroft.configuration import Configuration
  from mycroft.metrics import report_timing, Stopwatch
  from mycroft.tts import TTSFactory
  from mycroft.util import check_for_signal
  from mycroft.util.log import LOG
  from mycroft.messagebus.message import Message
  from mycroft.tts.remote_tts import RemoteTTSTimeoutException
  from mycroft.tts.mimic_tts import Mimic
  
  bus = None  # Mycroft messagebus connection
  config = None
  tts = None
  tts_hash = None
  lock = Lock()
  mimic_fallback_obj = None
  
  _last_stop_signal = 0
  
  
  def _start_listener(_):
      """Force Mycroft to start listening (as if 'Hey Mycroft' was spoken)."""
      bus.emit(Message('mycroft.mic.listen'))
  
  
  def handle_speak(event):
      """Handle "speak" message
  
      Parse sentences and invoke text to speech service.
      """
      config = Configuration.get()
      Configuration.set_config_update_handlers(bus)
      global _last_stop_signal
  
      # Get conversation ID
      if event.context and 'ident' in event.context:
          ident = event.context['ident']
      else:
          ident = 'unknown'
  
      start = time.time()  # Time of speech request
      with lock:
          stopwatch = Stopwatch()
          stopwatch.start()
          utterance = event.data['utterance']
          if event.data.get('expect_response', False):
              # When expect_response is requested, the listener will be restarted
              # at the end of the next bit of spoken audio.
              bus.once('recognizer_loop:audio_output_end', _start_listener)
  
          # This is a bit of a hack for Picroft.  The analog audio on a Pi blocks
          # for 30 seconds fairly often, so we don't want to break on periods
          # (decreasing the chance of encountering the block).  But we will
          # keep the split for non-Picroft installs since it give user feedback
          # faster on longer phrases.
          #
          # TODO: Remove or make an option?  This is really a hack, anyway,
          # so we likely will want to get rid of this when not running on Mimic
          if (config.get('enclosure', {}).get('platform') != "picroft" and
                  len(re.findall('<[^>]*>', utterance)) == 0):
              # Remove any whitespace present after the period,
              # if a character (only alpha) ends with a period
              # ex: A. Lincoln -> A.Lincoln
              # so that we don't split at the period
              utterance = re.sub(r'\b([A-za-z][\.])(\s+)', r'\g<1>', utterance)
              chunks = re.split(r'(?<!\w\.\w.)(?<![A-Z][a-z]\.)(?<=\.|\;|\?)\s',
                                utterance)
              for chunk in chunks:
                  # Check if somthing has aborted the speech
                  if (_last_stop_signal > start or
                          check_for_signal('buttonPress')):
                      # Clear any newly queued speech
                      tts.playback.clear()
                      break
                  try:
                      mute_and_speak(chunk, ident)
                  except KeyboardInterrupt:
                      raise
                  except Exception:
                      LOG.error('Error in mute_and_speak', exc_info=True)
          else:
              mute_and_speak(utterance, ident)
  
          stopwatch.stop()
      report_timing(ident, 'speech', stopwatch, {'utterance': utterance,
                                                 'tts': tts.__class__.__name__})
  
  
  def mute_and_speak(utterance, ident):
      """Mute mic and start speaking the utterance using selected tts backend.
  
      Arguments:
          utterance:  The sentence to be spoken
          ident:      Ident tying the utterance to the source query
      """
      global tts_hash
  
      # update TTS object if configuration has changed
      if tts_hash != hash(str(config.get('tts', ''))):
          global tts
          # Stop tts playback thread
          tts.playback.stop()
          tts.playback.join()
          # Create new tts instance
          tts = TTSFactory.create()
          tts.init(bus)
          tts_hash = hash(str(config.get('tts', '')))
  
      LOG.info("Speak: " + utterance)
      try:
          tts.execute(utterance, ident)
      except RemoteTTSTimeoutException as e:
          LOG.error(e)
          mimic_fallback_tts(utterance, ident)
      except Exception as e:
          LOG.error('TTS execution failed ({})'.format(repr(e)))
  
  
  def mimic_fallback_tts(utterance, ident):
      global mimic_fallback_obj
      # fallback if connection is lost
      config = Configuration.get()
      tts_config = config.get('tts', {}).get("mimic", {})
      lang = config.get("lang", "en-us")
      if not mimic_fallback_obj:
          mimic_fallback_obj = Mimic(lang, tts_config)
      tts = mimic_fallback_obj
      LOG.debug("Mimic fallback, utterance : " + str(utterance))
      tts.init(bus)
      tts.execute(utterance, ident)
  
  
  def handle_stop(event):
      """Handle stop message.
  
      Shutdown any speech.
      """
      global _last_stop_signal
      if check_for_signal("isSpeaking", -1):
          _last_stop_signal = time.time()
          tts.playback.clear()  # Clear here to get instant stop
          bus.emit(Message("mycroft.stop.handled", {"by": "TTS"}))
  
  
  def init(messagebus):
      """Start speech related handlers.
  
      Arguments:
          messagebus: Connection to the Mycroft messagebus
      """
  
      global bus
      global tts
      global tts_hash
      global config
  
      bus = messagebus
      Configuration.set_config_update_handlers(bus)
      config = Configuration.get()
      bus.on('mycroft.stop', handle_stop)
      bus.on('mycroft.audio.speech.stop', handle_stop)
      bus.on('speak', handle_speak)
  
      tts = TTSFactory.create()
      tts.init(bus)
      tts_hash = hash(str(config.get('tts', '')))
  
  
  def shutdown():
      """Shutdown the audio service cleanly.
  
      Stop any playing audio and make sure threads are joined correctly.
      """
      if tts:
          tts.playback.stop()
          tts.playback.join()
      if mimic_fallback_obj:
          mimic_fallback_obj.playback.stop()
          mimic_fallback_obj.playback.join()
  
  
  [... the rest is omitted ...]
files: mycroft/audio/speech.py
files: # Copyright 2017 Mycroft AI Inc.
  #
  # 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.
  #
  from copy import deepcopy
  import hashlib
  import os
  import random
  import re
  from abc import ABCMeta, abstractmethod
  from threading import Thread
  from time import time, sleep
  
  import os.path
  from os.path import dirname, exists, isdir, join
  
  import mycroft.util
  from mycroft.enclosure.api import EnclosureAPI
  from mycroft.configuration import Configuration
  from mycroft.messagebus.message import Message
  from mycroft.metrics import report_timing, Stopwatch
  from mycroft.util import (
      play_wav, play_mp3, check_for_signal, create_signal, resolve_resource_file
  )
  from mycroft.util.log import LOG
  from queue import Queue, Empty
  
  
  _TTS_ENV = deepcopy(os.environ)
  _TTS_ENV['PULSE_PROP'] = 'media.role=phone'
  
  
  def send_playback_metric(stopwatch, ident):
      """Send playback metrics in a background thread."""
  
      def do_send(stopwatch, ident):
          report_timing(ident, 'speech_playback', stopwatch)
  
      t = Thread(target=do_send, args=(stopwatch, ident))
      t.daemon = True
      t.start()
  
  
  class PlaybackThread(Thread):
      """Thread class for playing back tts audio and sending
      viseme data to enclosure.
      """
  
      def __init__(self, queue):
          super(PlaybackThread, self).__init__()
          self.queue = queue
          self._terminated = False
          self._processing_queue = False
          # Check if the tts shall have a ducking role set
          if Configuration.get().get('tts', {}).get('pulse_duck'):
              self.pulse_env = _TTS_ENV
          else:
              self.pulse_env = None
  
      def init(self, tts):
          self.tts = tts
  
      def clear_queue(self):
          """Remove all pending playbacks."""
          while not self.queue.empty():
              self.queue.get()
          try:
              self.p.terminate()
          except Exception:
              pass
  
      def run(self):
          """Thread main loop. get audio and viseme data from queue and play."""
          while not self._terminated:
              try:
                  snd_type, data, visemes, ident, listen = \
                      self.queue.get(timeout=2)
                  self.blink(0.5)
                  if not self._processing_queue:
                      self._processing_queue = True
                      self.tts.begin_audio()
  
                  stopwatch = Stopwatch()
                  with stopwatch:
                      if snd_type == 'wav':
                          self.p = play_wav(data, environment=self.pulse_env)
                      elif snd_type == 'mp3':
                          self.p = play_mp3(data, environment=self.pulse_env)
  
                      if visemes:
                          self.show_visemes(visemes)
                      self.p.communicate()
                      self.p.wait()
                  send_playback_metric(stopwatch, ident)
  
                  if self.queue.empty():
                      self.tts.end_audio()
                      self._processing_queue = False
                  self.blink(0.2)
              except Empty:
                  pass
              except Exception as e:
                  LOG.exception(e)
                  if self._processing_queue:
                      self.tts.end_audio(listen)
                      self._processing_queue = False
  
      def show_visemes(self, pairs):
          """Send viseme data to enclosure
  
          Arguments:
              pairs(list): Visime and timing pair
  
          Returns:
              True if button has been pressed.
          """
          if self.enclosure:
              self.enclosure.mouth_viseme(time(), pairs)
  
      def clear(self):
          """Clear all pending actions for the TTS playback thread."""
          self.clear_queue()
  
      def blink(self, rate=1.0):
          """Blink mycroft's eyes"""
          if self.enclosure and random.random() < rate:
              self.enclosure.eyes_blink("b")
  
      def stop(self):
          """Stop thread"""
          self._terminated = True
          self.clear_queue()
  
  
  class TTS(metaclass=ABCMeta):
      """TTS abstract class to be implemented by all TTS engines.
  
      It aggregates the minimum required parameters and exposes
      ``execute(sentence)`` and ``validate_ssml(sentence)`` functions.
  
      Arguments:
          lang (str):
          config (dict): Configuration for this specific tts engine
          validator (TTSValidator): Used to verify proper installation
          phonetic_spelling (bool): Whether to spell certain words phonetically
          ssml_tags (list): Supported ssml properties. Ex. ['speak', 'prosody']
      """
      def __init__(self, lang, config, validator, audio_ext='wav',
                   phonetic_spelling=True, ssml_tags=None):
          super(TTS, self).__init__()
          self.bus = None  # initalized in "init" step
          self.lang = lang or 'en-us'
          self.config = config
          self.validator = validator
          self.phonetic_spelling = phonetic_spelling
          self.audio_ext = audio_ext
          self.ssml_tags = ssml_tags or []
  
          self.voice = config.get("voice")
          self.filename = '/tmp/tts.wav'
          self.enclosure = None
          random.seed()
          self.queue = Queue()
          self.playback = PlaybackThread(self.queue)
          self.playback.start()
          self.clear_cache()
          self.spellings = self.load_spellings()
          self.tts_name = type(self).__name__
  
      def load_spellings(self):
          """Load phonetic spellings of words as dictionary"""
          path = join('text', self.lang, 'phonetic_spellings.txt')
          spellings_file = resolve_resource_file(path)
          if not spellings_file:
              return {}
          try:
              with open(spellings_file) as f:
                  lines = filter(bool, f.read().split('\n'))
              lines = [i.split(':') for i in lines]
              return {key.strip(): value.strip() for key, value in lines}
          except ValueError:
              LOG.exception('Failed to load phonetic spellings.')
              return {}
  
      def begin_audio(self):
          """Helper function for child classes to call in execute()"""
          # Create signals informing start of speech
          self.bus.emit(Message("recognizer_loop:audio_output_start"))
  
      def end_audio(self, listen):
          """Helper function for child classes to call in execute().
  
          Sends the recognizer_loop:audio_output_end message, indicating
          that speaking is done for the moment. It also checks if cache
          directory needs cleaning to free up disk space.
          """
  
          self.bus.emit(Message("recognizer_loop:audio_output_end"))
          if listen:
              self.bus.emit(Message('mycroft.mic.listen'))
          # Clean the cache as needed
          cache_dir = mycroft.util.get_cache_directory("tts/" + self.tts_name)
          mycroft.util.curate_cache(cache_dir, min_free_percent=100)
  
          # This check will clear the "signal"
          check_for_signal("isSpeaking")
  
      def init(self, bus):
          """Performs intial setup of TTS object.
  
          Arguments:
              bus:    Mycroft messagebus connection
          """
          self.bus = bus
          self.playback.init(self)
          self.enclosure = EnclosureAPI(self.bus)
          self.playback.enclosure = self.enclosure
  
      def get_tts(self, sentence, wav_file):
          """Abstract method that a tts implementation needs to implement.
  
          Should get data from tts.
  
          Arguments:
              sentence(str): Sentence to synthesize
              wav_file(str): output file
  
          Returns:
              tuple: (wav_file, phoneme)
          """
          pass
  
      def modify_tag(self, tag):
          """Override to modify each supported ssml tag"""
          return tag
  
      @staticmethod
      def remove_ssml(text):
          return re.sub('<[^>]*>', '', text).replace('  ', ' ')
  
      def validate_ssml(self, utterance):
          """Check if engine supports ssml, if not remove all tags.
  
          Remove unsupported / invalid tags
  
          Arguments:
              utterance(str): Sentence to validate
  
          Returns:
              validated_sentence (str)
          """
          # if ssml is not supported by TTS engine remove all tags
          if not self.ssml_tags:
              return self.remove_ssml(utterance)
  
          # find ssml tags in string
          tags = re.findall('<[^>]*>', utterance)
  
          for tag
  
  [... the rest is omitted ...]
files: mycroft/tts/__init__.py
files: # Copyright 2017 Mycroft AI Inc.
  #
  # 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.
  #
  from copy import deepcopy
  import hashlib
  import os
  import random
  import re
  from abc import ABCMeta, abstractmethod
  from threading import Thread
  from time import time
  
  import os.path
  from os.path import dirname, exists, isdir, join
  
  import mycroft.util
  from mycroft.enclosure.api import EnclosureAPI
  from mycroft.configuration import Configuration
  from mycroft.messagebus.message import Message
  from mycroft.metrics import report_timing, Stopwatch
  from mycroft.util import (
      play_wav, play_mp3, check_for_signal, create_signal, resolve_resource_file
  )
  from mycroft.util.log import LOG
  from queue import Queue, Empty
  
  
  _TTS_ENV = deepcopy(os.environ)
  _TTS_ENV['PULSE_PROP'] = 'media.role=phone'
  
  
  def send_playback_metric(stopwatch, ident):
      """Send playback metrics in a background thread."""
  
      def do_send(stopwatch, ident):
          report_timing(ident, 'speech_playback', stopwatch)
  
      t = Thread(target=do_send, args=(stopwatch, ident))
      t.daemon = True
      t.start()
  
  
  class PlaybackThread(Thread):
      """Thread class for playing back tts audio and sending
      viseme data to enclosure.
      """
  
      def __init__(self, queue):
          super(PlaybackThread, self).__init__()
          self.queue = queue
          self._terminated = False
          self._processing_queue = False
          # Check if the tts shall have a ducking role set
          if Configuration.get().get('tts', {}).get('pulse_duck'):
              self.pulse_env = _TTS_ENV
          else:
              self.pulse_env = None
  
      def init(self, tts):
          self.tts = tts
  
      def clear_queue(self):
          """Remove all pending playbacks."""
          while not self.queue.empty():
              self.queue.get()
          try:
              self.p.terminate()
          except Exception:
              pass
  
      def run(self):
          """Thread main loop. get audio and viseme data from queue and play."""
          while not self._terminated:
              try:
                  snd_type, data, visemes, ident = self.queue.get(timeout=2)
                  self.blink(0.5)
                  if not self._processing_queue:
                      self._processing_queue = True
                      self.tts.begin_audio()
  
                  stopwatch = Stopwatch()
                  with stopwatch:
                      if snd_type == 'wav':
                          self.p = play_wav(data, environment=self.pulse_env)
                      elif snd_type == 'mp3':
                          self.p = play_mp3(data, environment=self.pulse_env)
  
                      if visemes:
                          self.show_visemes(visemes)
                      self.p.communicate()
                      self.p.wait()
                  send_playback_metric(stopwatch, ident)
  
                  if self.queue.empty():
                      self.tts.end_audio()
                      self._processing_queue = False
                  self.blink(0.2)
              except Empty:
                  pass
              except Exception as e:
                  LOG.exception(e)
                  if self._processing_queue:
                      self.tts.end_audio()
                      self._processing_queue = False
  
      def show_visemes(self, pairs):
          """Send viseme data to enclosure
  
          Arguments:
              pairs(list): Visime and timing pair
  
          Returns:
              True if button has been pressed.
          """
          if self.enclosure:
              self.enclosure.mouth_viseme(time(), pairs)
  
      def clear(self):
          """Clear all pending actions for the TTS playback thread."""
          self.clear_queue()
  
      def blink(self, rate=1.0):
          """Blink mycroft's eyes"""
          if self.enclosure and random.random() < rate:
              self.enclosure.eyes_blink("b")
  
      def stop(self):
          """Stop thread"""
          self._terminated = True
          self.clear_queue()
  
  
  class TTS(metaclass=ABCMeta):
      """TTS abstract class to be implemented by all TTS engines.
  
      It aggregates the minimum required parameters and exposes
      ``execute(sentence)`` and ``validate_ssml(sentence)`` functions.
  
      Arguments:
          lang (str):
          config (dict): Configuration for this specific tts engine
          validator (TTSValidator): Used to verify proper installation
          phonetic_spelling (bool): Whether to spell certain words phonetically
          ssml_tags (list): Supported ssml properties. Ex. ['speak', 'prosody']
      """
      def __init__(self, lang, config, validator, audio_ext='wav',
                   phonetic_spelling=True, ssml_tags=None):
          super(TTS, self).__init__()
          self.bus = None  # initalized in "init" step
          self.lang = lang or 'en-us'
          self.config = config
          self.validator = validator
          self.phonetic_spelling = phonetic_spelling
          self.audio_ext = audio_ext
          self.ssml_tags = ssml_tags or []
  
          self.voice = config.get("voice")
          self.filename = '/tmp/tts.wav'
          self.enclosure = None
          random.seed()
          self.queue = Queue()
          self.playback = PlaybackThread(self.queue)
          self.playback.start()
          self.clear_cache()
          self.spellings = self.load_spellings()
          self.tts_name = type(self).__name__
  
      def load_spellings(self):
          """Load phonetic spellings of words as dictionary"""
          path = join('text', self.lang, 'phonetic_spellings.txt')
          spellings_file = resolve_resource_file(path)
          if not spellings_file:
              return {}
          try:
              with open(spellings_file) as f:
                  lines = filter(bool, f.read().split('\n'))
              lines = [i.split(':') for i in lines]
              return {key.strip(): value.strip() for key, value in lines}
          except ValueError:
              LOG.exception('Failed to load phonetic spellings.')
              return {}
  
      def begin_audio(self):
          """Helper function for child classes to call in execute()"""
          # Create signals informing start of speech
          self.bus.emit(Message("recognizer_loop:audio_output_start"))
  
      def end_audio(self):
          """Helper function for child classes to call in execute().
  
          Sends the recognizer_loop:audio_output_end message, indicating
          that speaking is done for the moment. It also checks if cache
          directory needs cleaning to free up disk space.
          """
  
          self.bus.emit(Message("recognizer_loop:audio_output_end"))
          # Clean the cache as needed
          cache_dir = mycroft.util.get_cache_directory("tts/" + self.tts_name)
          mycroft.util.curate_cache(cache_dir, min_free_percent=100)
  
          # This check will clear the "signal"
          check_for_signal("isSpeaking")
  
      def init(self, bus):
          """Performs intial setup of TTS object.
  
          Arguments:
              bus:    Mycroft messagebus connection
          """
          self.bus = bus
          self.playback.init(self)
          self.enclosure = EnclosureAPI(self.bus)
          self.playback.enclosure = self.enclosure
  
      def get_tts(self, sentence, wav_file):
          """Abstract method that a tts implementation needs to implement.
  
          Should get data from tts.
  
          Arguments:
              sentence(str): Sentence to synthesize
              wav_file(str): output file
  
          Returns:
              tuple: (wav_file, phoneme)
          """
          pass
  
      def modify_tag(self, tag):
          """Override to modify each supported ssml tag"""
          return tag
  
      @staticmethod
      def remove_ssml(text):
          return re.sub('<[^>]*>', '', text).replace('  ', ' ')
  
      def validate_ssml(self, utterance):
          """Check if engine supports ssml, if not remove all tags.
  
          Remove unsupported / invalid tags
  
          Arguments:
              utterance(str): Sentence to validate
  
          Returns:
              validated_sentence (str)
          """
          # if ssml is not supported by TTS engine remove all tags
          if not self.ssml_tags:
              return self.remove_ssml(utterance)
  
          # find ssml tags in string
          tags = re.findall('<[^>]*>', utterance)
  
          for tag in tags:
              if any(supported in tag for supported in self.ssml_tags):
                  utterance = utterance.replace(tag, self.modify
  
  [... the rest is omitted ...]
files: mycroft/tts/__init__.py