22 from boto3
import Session
23 from botocore.credentials
import CredentialProvider, RefreshableCredentials
24 from botocore.session
import get_session
25 from botocore.exceptions
import UnknownServiceError
26 from contextlib
import closing
27 from optparse
import OptionParser
30 from tts.srv
import Polly, PollyRequest, PollyResponse
35 key = rospy.search_param(param)
36 return default
if key
is None else rospy.get_param(key, default)
37 except Exception
as e:
38 rospy.logwarn(
'Failed to get ros param {}, will use default {}. Exception: '.format(param, default, e))
44 CANONICAL_NAME =
'customIoTwithCertificate' 46 DEFAULT_AUTH_CONNECT_TIMEOUT_MS = 5000
47 DEFAULT_AUTH_TOTAL_TIMEOUT_MS = 10000
50 super(AwsIotCredentialProvider, self).
__init__()
64 thing_name = self.
get_param(
'thing_name',
'')
66 if any(v
is None for v
in (cert_file, key_file, endpoint, role_alias, thing_name)):
69 headers = {
'x-amzn-iot-thingname': thing_name}
if len(thing_name) > 0
else None 70 url =
'https://{}/role-aliases/{}/credentials'.format(endpoint, role_alias)
71 timeout = (connect_timeout, total_timeout - connect_timeout)
73 response = requests.get(url, cert=(cert_file, key_file), headers=headers, timeout=timeout)
74 d = response.json()[
'credentials']
76 rospy.loginfo(
'Credentials expiry time: {}'.format(d[
'expiration']))
79 'access_key': d[
'accessKeyId'],
80 'secret_key': d[
'secretAccessKey'],
81 'token': d[
'sessionToken'],
82 'expiry_time': d[
'expiration'],
84 except Exception
as e:
85 rospy.logwarn(
'Failed to fetch credentials from AWS IoT: {}'.format(e))
89 return RefreshableCredentials.create_from_metadata(
92 'aws-iot-with-certificate' 97 """A TTS engine that can be used in two different ways. 102 1. It can run as a ROS service node. 106 $ rosrun tts polly_node.py 108 Call the service from command line:: 110 $ rosservice call /polly SynthesizeSpeech 'hello polly' '' '' '' '' '' '' '' '' [] [] 0 '' '' '' '' '' '' false 112 Call the service programmatically:: 114 from tts.srv import Polly 115 rospy.wait_for_service('polly') 116 polly = rospy.ServiceProxy('polly', Polly) 119 2. It can also be used as a normal python class:: 121 AmazonPolly().synthesize(text='hi polly') 123 PollyRequest supports many parameters, but the majority of the users can safely ignore most of them and just 124 use the vanilla version which involves only one argument, ``text``. 126 If in some use cases more control is needed, SSML will come handy. Example:: 128 AmazonPolly().synthesize( 129 text='<speak>Mary has a <amazon:effect name="whispered">little lamb.</amazon:effect></speak>', 133 A user can also control the voice, output format and so on. Example:: 135 AmazonPolly().synthesize( 136 text='<speak>Mary has a <amazon:effect name="whispered">little lamb.</amazon:effect></speak>', 140 output_path='/tmp/blah' 147 Among the parameters defined in Polly.srv, the following are supported while others are reserved for future. 149 * polly_action : currently only ``SynthesizeSpeech`` is supported 150 * text : the text to speak 151 * text_type : can be either ``text`` (default) or ``ssml`` 152 * voice_id : any voice id supported by Amazon Polly, default is Joanna 153 * output_format : ogg (default), mp3 or pcm 154 * output_path : where the audio file is saved 155 * sample_rate : default is 16000 for pcm or 22050 for mp3 and ogg 157 The following are the reserved ones. Note that ``language_code`` is rarely needed (this may seem counter-intuitive). 158 See official Amazon Polly documentation for details (link can be found below). 170 * output_s3_bucket_name 171 * output_s3_key_prefix 172 * include_additional_language_codes 178 Amazon Polly documentation: https://docs.aws.amazon.com/polly/latest/dg/API_SynthesizeSpeech.html 182 def __init__(self, aws_access_key_id=None, aws_secret_access_key=None, aws_session_token=None, region_name=None):
183 if region_name
is None:
184 region_name =
get_ros_param(
'aws_client_configuration/region', default=
'us-west-2')
193 def _get_polly_client(self, aws_access_key_id=None, aws_secret_access_key=None, aws_session_token=None,
194 region_name=
None, with_service_model_patch=
False):
195 """Note we get a new botocore session each time this function is called. 196 This is to avoid potential problems caused by inner state of the session. 198 botocore_session = get_session()
200 if with_service_model_patch:
203 current_dir = os.path.dirname(os.path.abspath(__file__))
204 service_model_path = os.path.join(current_dir,
'data',
'models')
205 botocore_session.set_config_variable(
'data_path', service_model_path)
206 rospy.loginfo(
'patching service model data path: {}'.format(service_model_path))
212 session = Session(aws_access_key_id=aws_access_key_id, aws_secret_access_key=aws_secret_access_key,
213 aws_session_token=aws_session_token, region_name=region_name,
214 botocore_session=botocore_session)
217 return session.client(
"polly")
218 except UnknownServiceError:
220 if not with_service_model_patch:
221 return self.
_get_polly_client(aws_access_key_id, aws_secret_access_key, aws_session_token, region_name,
222 with_service_model_patch=
True)
225 rospy.logerr(
'Amazon Polly is not available. Please install the latest boto3.')
229 exec_env =
get_ros_param(
'exec_env',
'AWS_RoboMaker').strip()
230 if 'AWS_RoboMaker' in exec_env:
233 exec_env +=
'-' + ver.strip()
234 ros_distro =
get_ros_param(
'rosdistro',
'Unknown_ROS_DISTRO').strip()
235 ros_version =
get_ros_param(
'rosversion',
'Unknown_ROS_VERSION').strip()
236 return 'exec-env/{} ros-{}/{}'.format(exec_env, ros_distro, ros_version)
238 def _pcm2wav(self, audio_data, wav_filename, sample_rate):
239 """per Amazon Polly official doc, the pcm in a signed 16-bit, 1 channel (mono), little-endian format.""" 240 wavf = wave.open(wav_filename,
'w')
241 wavf.setframerate(int(sample_rate))
244 wavf.writeframes(audio_data)
248 """Makes a full path for audio file based on given output path and format. 250 If ``output_path`` doesn't have a path, current path is used. 252 :param output_path: the output path received 253 :param output_format: the audio format, e.g., mp3, ogg_vorbis, pcm 254 :return: a full path for the output audio file. File ext will be constructed from audio format. 256 head, tail = os.path.split(output_path)
262 file_ext = {
'pcm':
'.wav',
'mp3':
'.mp3',
'ogg_vorbis':
'.ogg'}[output_format.lower()]
263 if not tail.endswith(file_ext):
266 return os.path.realpath(os.path.join(head, tail))
269 """Calls Amazon Polly and writes the returned audio data to a local file. 271 To make it practical, three things will be returned in a JSON form string, which are audio file path, 272 audio type and Amazon Polly response metadata. 274 If the Amazon Polly call fails, audio file name will be an empty string and audio type will be "N/A". 276 Please see https://boto3.readthedocs.io/reference/services/polly.html#Polly.Client.synthesize_speech 277 for more details on Amazon Polly API. 279 :param request: an instance of PollyRequest 280 :return: a string in JSON form with two attributes, "Audio File" and "Amazon Polly Response". 283 'LexiconNames': request.lexicon_names
if request.lexicon_names
else [],
285 'SampleRate': request.sample_rate,
286 'SpeechMarkTypes': request.speech_mark_types
if request.speech_mark_types
else [],
287 'Text': request.text,
292 if not kws[
'SampleRate']:
293 kws[
'SampleRate'] =
'16000' if kws[
'OutputFormat'].lower() ==
'pcm' else '22050' 295 rospy.loginfo(
'Amazon Polly Request: {}'.format(kws))
296 response = self.polly.synthesize_speech(**kws)
297 rospy.loginfo(
'Amazon Polly Response: {}'.format(response))
299 if "AudioStream" in response:
301 rospy.loginfo(
'will save audio as {}'.format(audiofile))
303 with closing(response[
"AudioStream"])
as stream:
304 if kws[
'OutputFormat'].lower() ==
'pcm':
305 self.
_pcm2wav(stream.read(), audiofile, kws[
'SampleRate'])
307 with open(audiofile,
"wb")
as f:
308 f.write(stream.read())
310 audiotype = response[
'ContentType']
316 'Audio File': audiofile,
317 'Audio Type': audiotype,
318 'Amazon Polly Response Metadata': str(response[
'ResponseMetadata'])
322 """Amazon Polly supports a number of APIs. This will call the right one based on the content of request. 324 Currently "SynthesizeSpeech" is the only recognized action. Basically this method just delegates the work 325 to ``self._synthesize_speech_and_save`` and returns the result as is. It will simply raise if a different 328 :param request: an instance of PollyRequest 329 :return: whatever returned by the delegate 336 if request.polly_action
not in actions:
337 raise RuntimeError(
'bad or unsupported Amazon Polly action: "' + request.polly_action +
'".')
339 return actions[request.polly_action](request)
342 """The callback function for processing service request. 344 It never raises. If anything unexpected happens, it will return a PollyResponse with details of the exception. 346 :param request: an instance of PollyRequest 347 :return: a PollyResponse 349 rospy.loginfo(
'Amazon Polly Request: {}'.format(request))
353 rospy.loginfo(
'will return {}'.format(response))
354 return PollyResponse(result=response)
355 except Exception
as e:
356 current_dir = os.path.dirname(os.path.abspath(__file__))
357 exc_type = sys.exc_info()[0]
361 error_ogg_filename =
'connerror.ogg' if 'ConnectionError' in exc_type.__name__
else 'error.ogg' 364 'Audio File': os.path.join(current_dir,
'data', error_ogg_filename),
367 'Type': str(exc_type),
368 'Module': exc_type.__module__,
369 'Name': exc_type.__name__,
372 'Traceback': traceback.format_exc()
375 error_str = json.dumps(error_details)
376 rospy.logerr(error_str)
377 return PollyResponse(result=error_str)
380 """Call this method if you want to use polly but don't want to start a node. 382 :param kws: input as defined in Polly.srv 383 :return: a string in JSON form with detailed information, success or failure 385 req = PollyRequest(polly_action=
'SynthesizeSpeech', **kws)
388 def start(self, node_name='polly_node', service_name='polly'):
389 """The entry point of a ROS service node. 391 Details of the service API can be found in Polly.srv. 393 :param node_name: name of ROS node 394 :param service_name: name of ROS service 395 :return: it doesn't return 397 rospy.init_node(node_name)
401 rospy.loginfo(
'polly running: {}'.format(service.uri))
407 usage =
'''usage: %prog [options] 410 parser = OptionParser(usage)
412 parser.add_option(
"-n",
"--node-name", dest=
"node_name", default=
'polly_node',
413 help=
"name of the ROS node",
415 parser.add_option(
"-s",
"--service-name", dest=
"service_name", default=
'polly',
416 help=
"name of the ROS service",
417 metavar=
"SERVICE_NAME")
419 (options, args) = parser.parse_args()
421 node_name = options.node_name
422 service_name = options.service_name
424 AmazonPolly().start(node_name=node_name, service_name=service_name)
427 if __name__ ==
"__main__":
def _get_polly_client(self, aws_access_key_id=None, aws_secret_access_key=None, aws_session_token=None, region_name=None, with_service_model_patch=False)
int DEFAULT_AUTH_TOTAL_TIMEOUT_MS
def _dispatch(self, request)
def _node_request_handler(self, request)
def start(self, node_name='polly_node', service_name='polly')
def _synthesize_speech_and_save(self, request)
def _generate_user_agent_suffix(self)
def get_ros_param(param, default=None)
def synthesize(self, kws)
int DEFAULT_AUTH_CONNECT_TIMEOUT_MS
def retrieve_credentials(self)
def get_param(self, param, default=None)
def _make_audio_file_fullpath(self, output_path, output_format)
def _pcm2wav(self, audio_data, wav_filename, sample_rate)
default_output_file_basename
def __init__(self, aws_access_key_id=None, aws_secret_access_key=None, aws_session_token=None, region_name=None)