TTSModule.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. # coding: utf8
  2. import hashlib
  3. import logging
  4. import os
  5. import subprocess
  6. import six
  7. from kalliope.core.ConfigurationManager import SettingLoader
  8. from kalliope.core.PlayerLauncher import PlayerLauncher
  9. from kalliope.core.Utils.FileManager import FileManager
  10. from kalliope.core import Utils
  11. logging.basicConfig()
  12. logger = logging.getLogger("kalliope")
  13. class MissingTTSParameter(Exception):
  14. """
  15. Some TTS Parameters are missing in the settings.yml file.
  16. .. seealose:: Settings
  17. """
  18. pass
  19. class TtsGenerateAudioFunctionNotFound(Exception):
  20. """
  21. You must provide a callBack to the TTS
  22. """
  23. pass
  24. class FailToLoadSoundFile(Exception):
  25. """
  26. Fail while truing to load the sound file.
  27. """
  28. pass
  29. class TTSModule(object):
  30. """
  31. Mother class of TTS module. Handle:
  32. - Cache: call cache object to create file, delete file, check if file exist
  33. - Player: call the default player to play the generated file
  34. """
  35. def __init__(self, **kwargs):
  36. # set parameter from what we receive from the settings
  37. self.cache = kwargs.get('cache', False)
  38. self.language = kwargs.get('language', "default")
  39. self.voice = kwargs.get('voice', "default")
  40. # the name of the TSS is the name of the Tss module that have instantiated TTSModule
  41. self.tts_caller_name = self.__class__.__name__
  42. # we don't know yet the words that will be converted to an audio and so we don't have the audio path yet too
  43. self.words = None
  44. self.file_path = None
  45. self.base_cache_path = None
  46. # load settings
  47. sl = SettingLoader()
  48. self.settings = sl.settings
  49. self.player = PlayerLauncher.get_player(settings=self.settings)
  50. # create the path in the tmp folder
  51. base_path = os.path.join(self.settings.cache_path, self.tts_caller_name, self.language, self.voice)
  52. FileManager.create_directory(base_path)
  53. logger.debug("Class TTSModule called from module %s, cache: %s, language: %s, voice: %s" % (self.tts_caller_name,
  54. self.cache,
  55. self.language,
  56. self.voice))
  57. def play_audio(self):
  58. """
  59. Play the audio file
  60. """
  61. # Mplayer.play(self.file_path)
  62. self.player.play(self.file_path)
  63. def generate_and_play(self, words, generate_audio_function_from_child=None):
  64. """
  65. Generate an audio file from <words> if not already in cache and call the Player to play it
  66. :param words: Sentence text from which we want to generate an audio file
  67. :type words: String
  68. :param generate_audio_function_from_child: The child function to generate a file if necessary
  69. :type generate_audio_function_from_child; Callback function
  70. .. raises:: TtsGenerateAudioFunctionNotFound
  71. """
  72. if generate_audio_function_from_child is None:
  73. raise TtsGenerateAudioFunctionNotFound
  74. self.words = words
  75. # we can generate the file path from info we have
  76. self.file_path = self._get_path_to_store_audio()
  77. if not self.cache:
  78. # no cache, we need to generate the file
  79. generate_audio_function_from_child()
  80. else:
  81. # we check if the file already exist. If not we generate it with the TTS engine
  82. if not self._is_file_already_in_cache(self.base_cache_path, self.file_path):
  83. generate_audio_function_from_child()
  84. # then play the generated audio file
  85. self.play_audio()
  86. # if the user don't want to keep the cache we remove the file
  87. if not self.cache:
  88. FileManager.remove_file(self.file_path)
  89. def _get_path_to_store_audio(self):
  90. """
  91. Get a sentence (a text) an return the full path of the file
  92. Path syntax:
  93. </path/in/settings>/<tts.name>/tts.parameter["language"]/tts.parameter["voice"]/<md5_of_sentence.tts
  94. E.g:
  95. /tmp/kalliope/acapela/fr/abcd12345.tts
  96. :return: path String
  97. """
  98. md5 = self.generate_md5_from_words(self.words)+".tts"
  99. self.base_cache_path = os.path.join(self.settings.cache_path, self.tts_caller_name, self.language, self.voice)
  100. returned_path = os.path.join(self.base_cache_path, md5)
  101. logger.debug("get_path_to_store_audio return: %s" % returned_path)
  102. return returned_path
  103. @staticmethod
  104. def generate_md5_from_words(words):
  105. """
  106. Generate a md5 hash from received text
  107. :param words: Text to convert into md5 hash
  108. :return: String md5 hash from the received words
  109. """
  110. if isinstance(words, six.text_type):
  111. words = words.encode('utf-8')
  112. return hashlib.md5(words).hexdigest()
  113. @staticmethod
  114. def _is_file_already_in_cache(base_cache_path, file_path):
  115. """
  116. Return true if the file to generate has already been generated before
  117. """
  118. # generate sub folder
  119. FileManager.create_directory(base_cache_path)
  120. # check if the audio file exist
  121. exist_in_cache = os.path.exists(file_path)
  122. if exist_in_cache:
  123. logger.debug("TTSModule, File already in cache: %s" % file_path)
  124. else:
  125. logger.debug("TTSModule, File not yet in cache: %s" % file_path)
  126. return exist_in_cache
  127. @staticmethod
  128. def convert_mp3_to_wav(file_path_mp3):
  129. """
  130. PyAudio does not support mp3 files
  131. MP3 files must be converted to a wave in order to be played
  132. This function assumes ffmpeg is available on the system
  133. :param file_path_mp3: the file path to convert from mp3 to wav
  134. """
  135. logger.debug("Converting mp3 file to wav file: %s" % file_path_mp3)
  136. fnull = open(os.devnull, 'w')
  137. # temp file
  138. tmp_file_wav = file_path_mp3 + ".wav"
  139. # Convert mp3 to wave
  140. subprocess.call(['ffmpeg', '-y', '-i', file_path_mp3, tmp_file_wav],
  141. stdout=fnull, stderr=fnull)
  142. # remove the original file
  143. FileManager.remove_file(file_path_mp3)
  144. # rename the temp file with the same name as the original file
  145. os.rename(tmp_file_wav, file_path_mp3)