NeuronModule.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. # coding: utf8
  2. import logging
  3. import random
  4. import sys
  5. import six
  6. from jinja2 import Template
  7. from kalliope.core import OrderListener
  8. from kalliope.core.ConfigurationManager import SettingLoader, BrainLoader
  9. from kalliope.core.Cortex import Cortex
  10. from kalliope.core.Models.MatchedSynapse import MatchedSynapse
  11. from kalliope.core.OrderAnalyser import OrderAnalyser
  12. from kalliope.core.SynapseLauncher import SynapseLauncher
  13. from kalliope.core.Utils.RpiUtils import RpiUtils
  14. from kalliope.core.Utils.Utils import Utils
  15. logging.basicConfig()
  16. logger = logging.getLogger("kalliope")
  17. class InvalidParameterException(Exception):
  18. """
  19. Some Neuron parameters are invalid.
  20. """
  21. pass
  22. class MissingParameterException(Exception):
  23. """
  24. Some Neuron parameters are missing.
  25. """
  26. pass
  27. class NoTemplateException(Exception):
  28. """
  29. You must specify a say_template or a file_template
  30. """
  31. pass
  32. class TemplateFileNotFoundException(Exception):
  33. """
  34. Template file can not be found. Check the provided path.
  35. """
  36. pass
  37. class TTSModuleNotFound(Exception):
  38. """
  39. TTS module can not be find. It must be configured in the settings file.
  40. """
  41. pass
  42. class NeuronModule(object):
  43. """
  44. This Abstract Class is representing main Class for Neuron.
  45. Each Neuron must implement this Class.
  46. """
  47. def __init__(self, **kwargs):
  48. """
  49. Class used by neuron for talking
  50. :param kwargs: Same parameter as the Child. Can contain info about the tts to use instead of the
  51. default one
  52. """
  53. # get the child who called the class
  54. child_name = self.__class__.__name__
  55. self.neuron_name = child_name
  56. sl = SettingLoader()
  57. self.settings = sl.settings
  58. brain_loader = BrainLoader()
  59. self.brain = brain_loader.brain
  60. # a dict of overridden TTS parameters if provided by the user
  61. self.override_tts_parameters = kwargs.get('tts', None)
  62. # create the TTS instance
  63. self.tts = None
  64. if self.override_tts_parameters is None or not isinstance(self.override_tts_parameters, dict):
  65. # we get the default TTS
  66. self.tts = self._get_tts_object(settings=self.settings)
  67. else:
  68. for key, value in self.override_tts_parameters.items():
  69. tts_name = key
  70. tts_parameters = value
  71. self.tts = self._get_tts_object(tts_name=tts_name,
  72. override_parameter=tts_parameters,
  73. settings=self.settings)
  74. # get templates if provided
  75. # Check if there is a template associate to the output message
  76. self.say_template = kwargs.get('say_template', None)
  77. # check if there is a template file associate to the output message
  78. self.file_template = kwargs.get('file_template', None)
  79. # keep the generated message
  80. self.tts_message = None
  81. # if the current call is api one
  82. self.is_api_call = kwargs.get('is_api_call', False)
  83. # if the current call want to mute kalliope
  84. self.no_voice = kwargs.get('no_voice', False)
  85. # boolean to know id the synapse is waiting for an answer
  86. self.is_waiting_for_answer = False
  87. # the synapse name to add the the buffer
  88. self.pending_synapse = None
  89. # a dict of parameters the user ask to save in short term memory
  90. self.kalliope_memory = kwargs.get('kalliope_memory', None)
  91. # parameters loaded from the order can be save now
  92. Cortex.save_parameter_from_order_in_memory(self.kalliope_memory)
  93. def __str__(self):
  94. retuned_string = ""
  95. retuned_string += self.tts_message
  96. return retuned_string
  97. def serialize(self):
  98. """
  99. This method allows to serialize in a proper way this object
  100. :return: A dict of name and parameters
  101. :rtype: Dict
  102. """
  103. self.tts_message = Utils.encode_text_utf8(self.tts_message)
  104. return {
  105. 'neuron_name': self.neuron_name,
  106. 'generated_message': self.tts_message
  107. }
  108. def say(self, message):
  109. """
  110. USe TTS to speak out loud the Message.
  111. A message can be a string, a list or a dict
  112. If it's a string, simply use the TTS with the message
  113. If it's a list, we select randomly a string in the list and give it to the TTS
  114. If it's a dict, we use the template given in parameter to create a string that we give to the TTS
  115. :param message: Can be a String or a dict or a list
  116. .. raises:: TTSModuleNotFound
  117. """
  118. logger.debug("[NeuronModule] Say() called with message: %s" % message)
  119. tts_message = None
  120. # we can save parameters from the neuron in memory
  121. Cortex.save_neuron_parameter_in_memory(self.kalliope_memory, message)
  122. if isinstance(message, str) or isinstance(message, six.text_type):
  123. logger.debug("[NeuronModule] message is string")
  124. tts_message = message
  125. if isinstance(message, list):
  126. logger.debug("[NeuronModule] message is list")
  127. tts_message = random.choice(message)
  128. if isinstance(message, dict):
  129. logger.debug("[NeuronModule] message is dict")
  130. tts_message = self._get_message_from_dict(message)
  131. if tts_message is not None:
  132. logger.debug("[NeuronModule] tts_message to say: %s" % tts_message)
  133. self.tts_message = tts_message
  134. Utils.print_success(tts_message)
  135. # process the audio only if the no_voice flag is false
  136. if self.no_voice:
  137. logger.debug("[NeuronModule] no_voice is True, Kalliope is muted")
  138. else:
  139. logger.debug("[NeuronModule] no_voice is False, make Kalliope speaking")
  140. # get the instance of the TTS module
  141. tts_folder = None
  142. if self.settings.resources:
  143. tts_folder = self.settings.resources.tts_folder
  144. tts_module_instance = Utils.get_dynamic_class_instantiation(package_name="tts",
  145. module_name=self.tts.name,
  146. parameters=self.tts.parameters,
  147. resources_dir=tts_folder)
  148. # Kalliope will talk, turn on the LED
  149. self.switch_on_led_talking(rpi_settings=self.settings.rpi_settings, on=True)
  150. # generate the audio file and play it
  151. tts_module_instance.say(tts_message)
  152. # Kalliope has finished to talk, turn off the LED
  153. self.switch_on_led_talking(rpi_settings=self.settings.rpi_settings, on=False)
  154. def _get_message_from_dict(self, message_dict):
  155. """
  156. Generate a message that can be played by a TTS engine from a dict of variable and the jinja template
  157. :param message_dict: the dict of message
  158. :return: The message to say
  159. .. raises:: TemplateFileNotFoundException
  160. """
  161. returned_message = None
  162. # the user chooses a say_template option
  163. if self.say_template is not None:
  164. returned_message = self._get_say_template(self.say_template, message_dict)
  165. # trick to remove unicode problem when loading jinja template with non ascii char
  166. if sys.version_info[0] == 2:
  167. reload(sys)
  168. sys.setdefaultencoding('utf-8')
  169. # the user chooses a file_template option
  170. if self.file_template is not None: # the user choose a file_template option
  171. returned_message = self._get_file_template(self.file_template, message_dict)
  172. return returned_message
  173. @staticmethod
  174. def _get_say_template(list_say_template, message_dict):
  175. if isinstance(list_say_template, list):
  176. # then we pick randomly one template
  177. list_say_template = random.choice(list_say_template)
  178. t = Template(list_say_template)
  179. return t.render(**message_dict)
  180. @classmethod
  181. def _get_file_template(cls, file_template, message_dict):
  182. real_file_template_path = Utils.get_real_file_path(file_template)
  183. if real_file_template_path is None:
  184. raise TemplateFileNotFoundException("Template file %s not found in templates folder"
  185. % real_file_template_path)
  186. # load the content of the file as template
  187. t = Template(cls._get_content_of_file(real_file_template_path))
  188. returned_message = t.render(**message_dict)
  189. return returned_message
  190. def run_synapse_by_name(self, synapse_name, user_order=None, synapse_order=None):
  191. """
  192. call the lifo for adding a synapse to execute in the list of synapse list to process
  193. :param synapse_name: The name of the synapse to run
  194. :param user_order: The user order
  195. :param synapse_order: The synapse order
  196. """
  197. synapse = BrainLoader().get_brain().get_synapse_by_name(synapse_name)
  198. matched_synapse = MatchedSynapse(matched_synapse=synapse,
  199. matched_order=synapse_order,
  200. user_order=user_order)
  201. self.pending_synapse = matched_synapse
  202. @staticmethod
  203. def is_order_matching(order_said, order_match):
  204. return OrderAnalyser().spelt_order_match_brain_order_via_table(order_to_analyse=order_match,
  205. user_said=order_said)
  206. @staticmethod
  207. def _get_content_of_file(real_file_template_path):
  208. """
  209. Return the content of a file in path <real_file_template_path>
  210. :param real_file_template_path: path of the file to return the content
  211. :return: file content str
  212. """
  213. with open(real_file_template_path, 'r') as content_file:
  214. return content_file.read()
  215. @staticmethod
  216. def get_audio_from_stt(callback):
  217. """
  218. Call the default STT to get an audio sample and return it into the callback method
  219. :param callback: A callback function
  220. """
  221. # call the order listener
  222. ol = OrderListener(callback=callback)
  223. ol.start()
  224. ol.join()
  225. # wait that the STT engine has finish his job (or the neurotransmitter neuron will be killed)
  226. if ol.stt_instance is not None:
  227. ol.stt_instance.join()
  228. def get_neuron_name(self):
  229. """
  230. Return the name of the neuron who call the mother class
  231. :return:
  232. """
  233. return self.neuron_name
  234. @staticmethod
  235. def _get_tts_object(tts_name=None, override_parameter=None, settings=None):
  236. """
  237. Return a TTS model object
  238. If no tts name provided, return the default TTS defined in the settings
  239. If the TTS name is provided, get the default configuration for this TTS in settings and override each parameters
  240. with parameters provided in override_parameter
  241. :param tts_name: name of the TTS to load
  242. :param override_parameter: dict of parameter to override the default configuration of the TTS
  243. :param settings: current settings
  244. :return: Tts model object
  245. """
  246. # if the tts_name is not provided, we get the default tts from settings
  247. if tts_name is None:
  248. tts_name = settings.default_tts_name
  249. # create a tts object from the tts the user want to use
  250. tts_object = next((x for x in settings.ttss if x.name == tts_name), None)
  251. if tts_object is None:
  252. raise TTSModuleNotFound("[NeuronModule] The tts module name %s does not exist in settings file" % tts_name)
  253. if override_parameter is not None: # the user want to override the default TTS configuration
  254. logger.debug("[NeuronModule] args for TTS plugin before update: %s" % str(tts_object.parameters))
  255. for key, value in override_parameter.items():
  256. tts_object.parameters[key] = value
  257. logger.debug("[NeuronModule] args for TTS plugin after update: %s" % str(tts_object.parameters))
  258. logger.debug("[NeuronModule] TTS args: %s" % tts_object)
  259. return tts_object
  260. @staticmethod
  261. def switch_on_led_talking(rpi_settings, on):
  262. """
  263. Call the Rpi utils class to switch the led talking if the setting has been specified by the user
  264. :param rpi_settings: Rpi
  265. :param on: True if the led need to be switched to on
  266. """
  267. if rpi_settings:
  268. if rpi_settings.pin_led_talking:
  269. if on:
  270. RpiUtils.switch_pin_to_on(rpi_settings.pin_led_talking)
  271. else:
  272. RpiUtils.switch_pin_to_off(rpi_settings.pin_led_talking)
  273. def start_synapse_by_name(self, synapse_name, overriding_parameter_dict=None):
  274. """
  275. Used to run a synapse by name by calling directly the SynapseLauncher class.
  276. The Lifo buffer is not aware of this call and so the user cannot get the result
  277. :param synapse_name: name of the synapse to run
  278. :param overriding_parameter_dict: dict of parameter to pass to the synapse
  279. """
  280. # received parameters are not coded with utf-8 on python 2 by default.
  281. if sys.version_info[0] == 2:
  282. reload(sys)
  283. sys.setdefaultencoding('utf-8')
  284. SynapseLauncher.start_synapse_by_name(synapse_name,
  285. brain=self.brain,
  286. overriding_parameter_dict=overriding_parameter_dict)