cloudlanguagetools 11.3.4__tar.gz → 11.5.0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/PKG-INFO +1 -1
  2. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/amazon.py +27 -7
  3. cloudlanguagetools-11.5.0/cloudlanguagetools/audio_processing.py +29 -0
  4. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/azure.py +76 -49
  5. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/constants.py +2 -0
  6. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/elevenlabs.py +27 -2
  7. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/google.py +3 -1
  8. cloudlanguagetools-11.5.0/cloudlanguagetools/keys.py +1 -0
  9. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/naver.py +19 -14
  10. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/openai.py +8 -5
  11. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/service.py +11 -2
  12. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/watson.py +21 -2
  13. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools.egg-info/PKG-INFO +1 -1
  14. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools.egg-info/SOURCES.txt +1 -0
  15. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/setup.py +1 -1
  16. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/tests/test_audio.py +90 -11
  17. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/tests/test_translation.py +51 -9
  18. cloudlanguagetools-11.3.4/cloudlanguagetools/keys.py +0 -1
  19. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/LICENSE +0 -0
  20. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/README.rst +0 -0
  21. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/__init__.py +0 -0
  22. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/argostranslate.py +0 -0
  23. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/cereproc.py +0 -0
  24. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/chatapi.py +0 -0
  25. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/deepl.py +0 -0
  26. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/dictionarylookup.py +0 -0
  27. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/easypronunciation.py +0 -0
  28. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/encryption.py +0 -0
  29. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/epitran.py +0 -0
  30. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/errors.py +0 -0
  31. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/forvo.py +0 -0
  32. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/fptai.py +0 -0
  33. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/languages.py +0 -0
  34. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/libretranslate.py +0 -0
  35. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/mandarincantonese.py +0 -0
  36. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/options.py +0 -0
  37. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/pythainlp.py +0 -0
  38. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/servicemanager.py +0 -0
  39. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/spacy.py +0 -0
  40. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/test_services.py +0 -0
  41. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/tokenization.py +0 -0
  42. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/translationlanguage.py +0 -0
  43. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/transliterationlanguage.py +0 -0
  44. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/ttsvoice.py +0 -0
  45. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/vocalware.py +0 -0
  46. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/voicen.py +0 -0
  47. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools/wenlin.py +0 -0
  48. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools.egg-info/dependency_links.txt +0 -0
  49. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools.egg-info/requires.txt +0 -0
  50. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/cloudlanguagetools.egg-info/top_level.txt +0 -0
  51. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/setup.cfg +0 -0
  52. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/tests/test_breakdown.py +0 -0
  53. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/tests/test_chatapi.py +0 -0
  54. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/tests/test_dictionary_lookup.py +0 -0
  55. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/tests/test_llm.py +0 -0
  56. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/tests/test_mock_services.py +0 -0
  57. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.5.0}/tests/test_servicemanager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloudlanguagetools
3
- Version: 11.3.4
3
+ Version: 11.5.0
4
4
  Summary: Interface with various cloud APIs for language processing such as translation, text to speech
5
5
  Home-page: https://github.com/Language-Tools/cloud-language-tools-core
6
6
  Author: Luc
@@ -6,6 +6,7 @@ import boto3
6
6
  import botocore.exceptions
7
7
  import contextlib
8
8
 
9
+
9
10
  import cloudlanguagetools.service
10
11
  import cloudlanguagetools.constants
11
12
  import cloudlanguagetools.options
@@ -14,6 +15,9 @@ import cloudlanguagetools.ttsvoice
14
15
  import cloudlanguagetools.translationlanguage
15
16
  import cloudlanguagetools.transliterationlanguage
16
17
  import cloudlanguagetools.errors
18
+ import cloudlanguagetools.audio_processing
19
+
20
+ from cloudlanguagetools.options import AudioFormat
17
21
 
18
22
  DEFAULT_VOICE_PITCH = 0
19
23
  DEFAULT_VOICE_RATE = 100
@@ -73,6 +77,7 @@ class AmazonVoice(cloudlanguagetools.ttsvoice.TtsVoice):
73
77
  'values': [
74
78
  cloudlanguagetools.options.AudioFormat.mp3.name,
75
79
  cloudlanguagetools.options.AudioFormat.ogg_vorbis.name,
80
+ cloudlanguagetools.options.AudioFormat.wav.name
76
81
  ],
77
82
  'default': cloudlanguagetools.options.AudioFormat.mp3.name
78
83
  }
@@ -114,13 +119,14 @@ class AmazonService(cloudlanguagetools.service.Service):
114
119
  return result.get('TranslatedText')
115
120
 
116
121
  def get_tts_audio(self, text, voice_key, options):
117
- audio_format_str = options.get(cloudlanguagetools.options.AUDIO_FORMAT_PARAMETER, cloudlanguagetools.options.AudioFormat.mp3.name)
118
- audio_format = cloudlanguagetools.options.AudioFormat[audio_format_str]
122
+ response_format_parameter, audio_format = self.get_request_audio_format({
123
+ AudioFormat.mp3: 'mp3',
124
+ AudioFormat.ogg_vorbis: 'ogg_vorbis',
125
+ AudioFormat.wav: 'pcm'
126
+ }, options, AudioFormat.mp3)
119
127
 
120
- audio_format_map = {
121
- cloudlanguagetools.options.AudioFormat.mp3: 'mp3',
122
- cloudlanguagetools.options.AudioFormat.ogg_vorbis: 'ogg_vorbis'
123
- }
128
+ # wav, we need to convert as described here:
129
+ # https://aws.amazon.com/blogs/machine-learning/integrating-amazon-polly-with-legacy-ivr-systems-by-converting-output-to-wav-format/
124
130
 
125
131
  output_temp_file = tempfile.NamedTemporaryFile()
126
132
  output_temp_filename = output_temp_file.name
@@ -143,7 +149,15 @@ class AmazonService(cloudlanguagetools.service.Service):
143
149
  </speak>"""
144
150
 
145
151
  try:
146
- response = self.polly_client.synthesize_speech(Text=ssml_str, TextType="ssml", OutputFormat=audio_format_map[audio_format], VoiceId=voice_key['voice_id'], Engine=voice_key['engine'])
152
+ if audio_format == cloudlanguagetools.options.AudioFormat.wav:
153
+ response = self.polly_client.synthesize_speech(Text=ssml_str,
154
+ TextType="ssml",
155
+ OutputFormat=response_format_parameter,
156
+ VoiceId=voice_key['voice_id'],
157
+ Engine=voice_key['engine'],
158
+ SampleRate="16000")
159
+ else:
160
+ response = self.polly_client.synthesize_speech(Text=ssml_str, TextType="ssml", OutputFormat=response_format_parameter, VoiceId=voice_key['voice_id'], Engine=voice_key['engine'])
147
161
  except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as error:
148
162
  raise cloudlanguagetools.errors.RequestError(str(error))
149
163
 
@@ -155,6 +169,12 @@ class AmazonService(cloudlanguagetools.service.Service):
155
169
  with contextlib.closing(response["AudioStream"]) as stream:
156
170
  with open(output_temp_filename, 'wb') as audio:
157
171
  audio.write(stream.read())
172
+
173
+ if audio_format == cloudlanguagetools.options.AudioFormat.wav:
174
+ return cloudlanguagetools.audio_processing.wrap_pcm_data_wave(output_temp_file,
175
+ num_channels=1,
176
+ sample_width=2,
177
+ framerate=16000)
158
178
  return output_temp_file
159
179
 
160
180
  else:
@@ -0,0 +1,29 @@
1
+ import wave
2
+ import tempfile
3
+
4
+ # audio utilities
5
+
6
+
7
+ # take PCM audio and wrap it in a WAV container
8
+ def wrap_pcm_data_wave(audio_temp_file: tempfile.NamedTemporaryFile,
9
+ num_channels,
10
+ sample_width,
11
+ framerate) -> tempfile.NamedTemporaryFile:
12
+ # read the audio data
13
+ f = open(audio_temp_file.name, 'rb')
14
+ data = f.read()
15
+ f.close()
16
+
17
+ wav_frames = []
18
+ wav_frames.append(data)
19
+
20
+ wav_audio_temp_file = tempfile.NamedTemporaryFile(prefix='clt_wav_audio_', suffix='.wav')
21
+
22
+ WAVEFORMAT = wave.open(wav_audio_temp_file.name,'wb')
23
+ WAVEFORMAT.setnchannels(num_channels) # one channel, mono
24
+ WAVEFORMAT.setsampwidth(sample_width) # Polly's output is a stream of 16-bits (2 bytes) samples
25
+ WAVEFORMAT.setframerate(framerate)
26
+ WAVEFORMAT.writeframes(b''.join(wav_frames))
27
+ WAVEFORMAT.close()
28
+
29
+ return wav_audio_temp_file
@@ -6,6 +6,8 @@ import operator
6
6
  import pydub
7
7
  import logging
8
8
  import pprint
9
+ import time
10
+ import cachetools
9
11
  from typing import List
10
12
 
11
13
  import cloudlanguagetools.service
@@ -17,6 +19,7 @@ import cloudlanguagetools.translationlanguage
17
19
  import cloudlanguagetools.transliterationlanguage
18
20
  import cloudlanguagetools.dictionarylookup
19
21
  import cloudlanguagetools.errors
22
+ from cloudlanguagetools.options import AudioFormat
20
23
 
21
24
 
22
25
  import azure.cognitiveservices.speech
@@ -52,6 +55,7 @@ VOICE_OPTIONS = {
52
55
  'values': [
53
56
  cloudlanguagetools.options.AudioFormat.mp3.name,
54
57
  cloudlanguagetools.options.AudioFormat.ogg_opus.name,
58
+ cloudlanguagetools.options.AudioFormat.wav.name,
55
59
  ],
56
60
  'default': cloudlanguagetools.options.AudioFormat.mp3.name
57
61
  }
@@ -182,7 +186,7 @@ class AzureTranslationLanguage(cloudlanguagetools.translationlanguage.Translatio
182
186
  return self.language_id
183
187
 
184
188
  class AzureTransliterationLanguage(cloudlanguagetools.transliterationlanguage.TransliterationLanguage):
185
- def __init__(self, language_id, from_script, to_script, from_script_name, to_script_name):
189
+ def __init__(self, language_id, from_script, to_script, from_script_name, to_script_name, from_native_name, to_native_name):
186
190
  self.service = cloudlanguagetools.constants.Service.Azure
187
191
  self.service_fee = cloudlanguagetools.constants.ServiceFee.paid
188
192
  self.language_id = language_id
@@ -191,13 +195,15 @@ class AzureTransliterationLanguage(cloudlanguagetools.transliterationlanguage.Tr
191
195
  self.to_script = to_script
192
196
  self.from_script_name = from_script_name
193
197
  self.to_script_name = to_script_name
198
+ self.from_native_name = from_native_name
199
+ self.to_native_name = to_native_name
194
200
 
195
201
  def get_transliteration_name(self):
196
- result = f'{self.language.lang_name} ({self.from_script_name} to {self.to_script_name}), {self.service.name}'
202
+ result = f'{self.language.lang_name} ({self.from_script_name}/{self.from_native_name} to {self.to_script_name}/{self.to_native_name}), {self.service.name}'
197
203
  return result
198
204
 
199
205
  def get_transliteration_shortname(self):
200
- result = f'{self.from_script_name} to {self.to_script_name}, {self.service.name}'
206
+ result = f'{self.from_script_name}/{self.from_native_name} to {self.to_script_name}/{self.to_native_name}, {self.service.name}'
201
207
  return result
202
208
 
203
209
  def get_transliteration_key(self):
@@ -258,22 +264,18 @@ class AzureService(cloudlanguagetools.service.Service):
258
264
  return headers
259
265
 
260
266
  def get_tts_audio(self, text, voice_key, options):
261
-
262
- audio_format_str = options.get(cloudlanguagetools.options.AUDIO_FORMAT_PARAMETER, cloudlanguagetools.options.AudioFormat.mp3.name)
263
- audio_format = cloudlanguagetools.options.AudioFormat[audio_format_str]
264
-
265
267
  # https://learn.microsoft.com/en-us/azure/ai-services/speech-service/rest-text-to-speech?tabs=streaming#audio-outputs
266
268
  # https://learn.microsoft.com/en-us/python/api/azure-cognitiveservices-speech/azure.cognitiveservices.speech.speechsynthesisoutputformat?view=azure-python
267
- audio_format_map = {
268
- cloudlanguagetools.options.AudioFormat.mp3: 'Audio24Khz96KBitRateMonoMp3',
269
- cloudlanguagetools.options.AudioFormat.ogg_opus: 'Ogg48Khz16BitMonoOpus',
270
- cloudlanguagetools.options.AudioFormat.wav: 'Riff48Khz16BitMonoPcm',
271
- }
269
+ response_format_parameter, audio_format = self.get_request_audio_format({
270
+ AudioFormat.mp3: 'Audio24Khz96KBitRateMonoMp3',
271
+ AudioFormat.ogg_opus: 'Ogg48Khz16BitMonoOpus',
272
+ AudioFormat.wav: 'Riff48Khz16BitMonoPcm'
273
+ }, options, AudioFormat.mp3)
272
274
 
273
275
  output_temp_file = tempfile.NamedTemporaryFile(prefix=f'cloudlanguage_tools_{self.__class__.__name__}_audio', suffix=f'.{audio_format.name}')
274
276
  output_temp_filename = output_temp_file.name
275
277
  speech_config = azure.cognitiveservices.speech.SpeechConfig(subscription=self.key, region=self.region)
276
- speech_config.set_speech_synthesis_output_format(azure.cognitiveservices.speech.SpeechSynthesisOutputFormat[audio_format_map[audio_format]])
278
+ speech_config.set_speech_synthesis_output_format(azure.cognitiveservices.speech.SpeechSynthesisOutputFormat[response_format_parameter])
277
279
  synthesizer = azure.cognitiveservices.speech.SpeechSynthesizer(speech_config=speech_config, audio_config=None)
278
280
 
279
281
  default_pitch = 0
@@ -330,17 +332,18 @@ class AzureService(cloudlanguagetools.service.Service):
330
332
  headers = {
331
333
  'Authorization': 'Bearer ' + token,
332
334
  }
333
- response = requests.get(constructed_url, headers=headers)
334
- if response.status_code == 200:
335
- voice_list = json.loads(response.content)
336
- result = []
337
- for voice_data in voice_list:
338
- # print(voice_data['Status'])
339
- try:
340
- result.append(AzureVoice(voice_data))
341
- except KeyError:
342
- logging.error(f'could not process voice for {voice_data}', exc_info=True)
343
- return result
335
+ response = requests.get(constructed_url, headers=headers,
336
+ timeout=cloudlanguagetools.constants.RequestTimeout)
337
+ response.raise_for_status()
338
+ voice_list = json.loads(response.content)
339
+ result = []
340
+ for voice_data in voice_list:
341
+ # print(voice_data['Status'])
342
+ try:
343
+ result.append(AzureVoice(voice_data))
344
+ except KeyError:
345
+ logging.error(f'could not process voice for {voice_data}', exc_info=True)
346
+ return result
344
347
 
345
348
  def get_tts_voice_list_v3(self) -> List[cloudlanguagetools.ttsvoice.TtsVoice_v3]:
346
349
  # returns list of TtsVoice_v3
@@ -353,17 +356,18 @@ class AzureService(cloudlanguagetools.service.Service):
353
356
  headers = {
354
357
  'Authorization': 'Bearer ' + token,
355
358
  }
356
- response = requests.get(constructed_url, headers=headers)
357
- if response.status_code == 200:
358
- voice_list = json.loads(response.content)
359
- result = []
360
- for voice_data in voice_list:
361
- # print(voice_data['Status'])
362
- try:
363
- result.append(build_tts_voice_v3(voice_data))
364
- except:
365
- logger.exception(f'could not process voice for {voice_data}')
366
- return result
359
+ response = requests.get(constructed_url, headers=headers,
360
+ timeout=cloudlanguagetools.constants.RequestTimeout)
361
+ response.raise_for_status()
362
+ voice_list = json.loads(response.content)
363
+ result = []
364
+ for voice_data in voice_list:
365
+ # print(voice_data['Status'])
366
+ try:
367
+ result.append(build_tts_voice_v3(voice_data))
368
+ except:
369
+ logger.exception(f'could not process voice for {voice_data}')
370
+ return result
367
371
 
368
372
  def get_translation(self, text, from_language_key, to_language_key):
369
373
  base_url = f'{self.url_translator_base}/translate?api-version=3.0'
@@ -400,22 +404,33 @@ class AzureService(cloudlanguagetools.service.Service):
400
404
  result = []
401
405
  azure_data = self.get_supported_languages()
402
406
  for language_id, data in azure_data['transliteration'].items():
403
- # get the first script
404
- first_script = data['scripts'][0]
405
- from_script = first_script['code']
406
- to_script = first_script['toScripts'][0]['code']
407
- from_script_name = first_script['name']
408
- to_script_name = first_script['toScripts'][0]['name']
409
- # print(language_id, from_script, to_script)
410
- # assert(to_script == 'Latn')
411
407
  try:
412
- result.append(AzureTransliterationLanguage(language_id, from_script, to_script, from_script_name, to_script_name))
408
+ # get the first script
409
+ for from_script_data in data['scripts']:
410
+ from_script = from_script_data['code']
411
+ from_native_name = from_script_data['nativeName']
412
+ for to_script_data in from_script_data['toScripts']:
413
+ to_script = to_script_data['code']
414
+ from_script_name = from_script_data['name']
415
+ to_script_name = to_script_data['name']
416
+ to_native_name = to_script_data['nativeName']
417
+ result.append(AzureTransliterationLanguage(
418
+ language_id,
419
+ from_script,
420
+ to_script,
421
+ from_script_name,
422
+ to_script_name,
423
+ from_native_name,
424
+ to_native_name))
413
425
  except KeyError:
414
426
  logging.error(f'could not process transliteration language for {language_id}, {data}', exc_info=True)
415
427
  return result
416
428
 
417
429
 
430
+ @cachetools.cached(cache=cachetools.TTLCache(maxsize=1024, ttl=cloudlanguagetools.constants.TTLCacheTimeout))
418
431
  def get_supported_languages(self):
432
+ max_retries = 3
433
+ retry_delay = 5 # seconds
419
434
  url = 'https://api.cognitive.microsofttranslator.com/languages?api-version=3.0'
420
435
 
421
436
  # If you encounter any issues with the base_url or path, make sure
@@ -427,10 +442,20 @@ class AzureService(cloudlanguagetools.service.Service):
427
442
  'X-ClientTraceId': str(uuid.uuid4())
428
443
  }
429
444
 
430
- request = requests.get(url, headers=headers)
431
- response = request.json()
432
-
433
- return response
445
+ for attempt in range(max_retries):
446
+ try:
447
+ request = requests.get(url, headers=headers,
448
+ timeout=cloudlanguagetools.constants.RequestTimeoutLong)
449
+ request.raise_for_status()
450
+ response = request.json()
451
+ return response
452
+ except requests.exceptions.RequestException as e:
453
+ if attempt < max_retries - 1: # If not the last attempt
454
+ logger.warning(f"Request failed. Retrying in {retry_delay} seconds... (Attempt {attempt + 1}/{max_retries})")
455
+ time.sleep(retry_delay)
456
+ else:
457
+ logger.error(f"Max retries reached. Unable to get supported languages.")
458
+ raise # Re-raise the last exception if all retries failed
434
459
 
435
460
  # print(json.dumps(response, sort_keys=True, indent=4, ensure_ascii=False, separators=(',', ': ')))
436
461
 
@@ -475,6 +500,8 @@ class AzureService(cloudlanguagetools.service.Service):
475
500
  sound = pydub.AudioSegment.from_mp3(mp3_filepath)
476
501
  elif audio_format in [cloudlanguagetools.options.AudioFormat.ogg_opus, cloudlanguagetools.options.AudioFormat.ogg_vorbis]:
477
502
  sound = pydub.AudioSegment.from_ogg(mp3_filepath)
503
+ elif audio_format == cloudlanguagetools.options.AudioFormat.wav:
504
+ sound = pydub.AudioSegment.from_wav(mp3_filepath)
478
505
  wav_filepath = tempfile.NamedTemporaryFile(suffix='.wav').name
479
506
  sound.export(wav_filepath, format="wav")
480
507
 
@@ -647,4 +674,4 @@ class AzureService(cloudlanguagetools.service.Service):
647
674
 
648
675
  # return response[0]['translations'][0]['text']
649
676
 
650
-
677
+
@@ -55,6 +55,8 @@ class APIVersion(enum.Enum):
55
55
  # ======================================
56
56
 
57
57
  RequestTimeout = 10 # 10 seconds max
58
+ # need to change timeout for long requests, such as retrieving list of services
59
+ RequestTimeoutLong = 20 # 10 seconds max
58
60
  ReadTimeout = 3 # 3 seconds read timeout
59
61
 
60
62
  TTLCacheTimeout = 86400 # 24 hours
@@ -5,6 +5,7 @@ import tempfile
5
5
  import os
6
6
  import contextlib
7
7
  import logging
8
+ import urllib.parse
8
9
  from typing import List
9
10
 
10
11
  import cloudlanguagetools.service
@@ -15,6 +16,7 @@ import cloudlanguagetools.ttsvoice
15
16
  import cloudlanguagetools.translationlanguage
16
17
  import cloudlanguagetools.transliterationlanguage
17
18
  import cloudlanguagetools.errors
19
+ from cloudlanguagetools.options import AudioFormat
18
20
 
19
21
  logger = logging.getLogger(__name__)
20
22
 
@@ -39,7 +41,15 @@ VOICE_OPTIONS = {
39
41
  'min': 0.0,
40
42
  'max': 1.0,
41
43
  'default': DEFAULT_SIMILARITY_BOOST
42
- },
44
+ },
45
+ cloudlanguagetools.options.AUDIO_FORMAT_PARAMETER: {
46
+ 'type': cloudlanguagetools.options.ParameterType.list.name,
47
+ 'values': [
48
+ cloudlanguagetools.options.AudioFormat.mp3.name,
49
+ cloudlanguagetools.options.AudioFormat.wav.name
50
+ ],
51
+ 'default': cloudlanguagetools.options.AudioFormat.mp3.name
52
+ }
43
53
  }
44
54
 
45
55
  class ElevenLabsVoice(cloudlanguagetools.ttsvoice.TtsVoice):
@@ -84,6 +94,11 @@ class ElevenLabsService(cloudlanguagetools.service.Service):
84
94
  voice_id = voice_key['voice_id']
85
95
  url = f'https://api.elevenlabs.io/v1/text-to-speech/{voice_id}'
86
96
 
97
+ response_format_parameter, audio_format = self.get_request_audio_format({
98
+ AudioFormat.mp3: 'mp3_44100_128',
99
+ AudioFormat.wav: 'pcm_44100'
100
+ }, options, AudioFormat.mp3)
101
+
87
102
  headers = self.get_headers()
88
103
  headers['Accept'] = "audio/mpeg"
89
104
 
@@ -96,7 +111,17 @@ class ElevenLabsService(cloudlanguagetools.service.Service):
96
111
  }
97
112
  }
98
113
 
99
- return self.get_tts_audio_base_post_request(url, json=data, headers=headers)
114
+ query_params = {
115
+ 'output_format': response_format_parameter
116
+ }
117
+ full_url = f'{url}?{urllib.parse.urlencode(query_params)}'
118
+
119
+ if audio_format == cloudlanguagetools.options.AudioFormat.wav:
120
+ return cloudlanguagetools.audio_processing.wrap_pcm_data_wave(self.get_tts_audio_base_post_request(full_url, json=data, headers=headers),
121
+ num_channels=1,
122
+ sample_width=2,
123
+ framerate=44100) # pcm_44100 - PCM format (S16LE) with 44.1kHz sample rate.
124
+ return self.get_tts_audio_base_post_request(full_url, json=data, headers=headers)
100
125
 
101
126
 
102
127
 
@@ -74,6 +74,7 @@ class GoogleVoice(cloudlanguagetools.ttsvoice.TtsVoice):
74
74
  'values': [
75
75
  cloudlanguagetools.options.AudioFormat.mp3.name,
76
76
  cloudlanguagetools.options.AudioFormat.ogg_opus.name,
77
+ cloudlanguagetools.options.AudioFormat.wav.name
77
78
  ],
78
79
  'default': cloudlanguagetools.options.AudioFormat.mp3.name
79
80
  }
@@ -146,7 +147,8 @@ class GoogleService(cloudlanguagetools.service.Service):
146
147
 
147
148
  audio_format_map = {
148
149
  cloudlanguagetools.options.AudioFormat.mp3: google.cloud.texttospeech.AudioEncoding.MP3,
149
- cloudlanguagetools.options.AudioFormat.ogg_opus: google.cloud.texttospeech.AudioEncoding.OGG_OPUS
150
+ cloudlanguagetools.options.AudioFormat.ogg_opus: google.cloud.texttospeech.AudioEncoding.OGG_OPUS,
151
+ cloudlanguagetools.options.AudioFormat.wav: google.cloud.texttospeech.AudioEncoding.LINEAR16
150
152
  }
151
153
 
152
154
  try:
@@ -0,0 +1 @@
1
+ KEYS='gAAAAABnCcYzERlg56peIVaJoWPgT7m06v9-gZ-xuDco6PWxZEu-BRxgHR9UaqN3g8HzKcfVSS-j1rBf-x0SHZlFQwcRlIQIY1S9mE3Wm8I6_xCGkFfqeuLkc8c9ScDIFnMt5MtLevVchJWklkl86HH6qoUZyzmzpQm5ihwdShaCwqryQZ95UDzuFis0rPDLX63gvmdWvXSiYRjXb4hO53ajE380bmjjp9E9mbH01Y6aKAz1hBpGWjEmepnajkkopM3uw-mCz0ABq9Ysdhs0yd9UklwvbOgUgdKL1BC_st0xt2piEShHtu0lQL5LOB1ZXBYHzFhk8wLlpftvDhYcFXapoZYBlg6d3tTKoOonPumN78CSIjQdxvTcG6mcA0p1VlAurcN4-YgtbDvtObsN5clpmDXXPupwMwf7Wddmut4sGGvMJXEba4EP5G6R_3Se8F6GRfawOgLVBAJ-SSpdKszJBu9w-BPIOzXkZ0D_ddQwC1CKURM4Kpjv0phHbqulykHgHe1l8mDr8ycC87647jowIz55An2ZttpRo9mdJztPGeuEjoz5RvQHJQvjmhAT7vajWnXv7sB2y50qUnWTF5ip1rde3GfDgmNzrOd7jSe8dxonAtFziKBCiCqc3pFfh7YgeHzWCR7lJcF7wIZvGJ-goLFiUT9uayXXVAVbPFku6et1DnUpDU48nyrO7lUvmwqRprTM706UWbbdZk8dBn7n91KzzjR4AbQhQ3hl6QuvA4WoJYdpzAaUN_4LUiWHNFWtd3uBLjHqTBLOHUlAETkkPkssm48pBqqALSJBWOrNSBIcLhAQI-JcJoer_fgH4KG4pDaeKgmGF7X26xi4rE48GsYwd7Tf2kRMQrHfyLefYDocMTCqH2I7NHs4i66LDvj_Zn9nmkSyuAbO1o-cnWvGmmrEymA39Jn1oiEG-f4gTv7kTkURSZu-FtBie9dKXTWG-EHy8qr7acFitEyuLnBfUpA75OWCEJG7wB4D5DfNti3zagUTq_DSjTBmTGSLRIlS0TVBEiyQ9Vjfpy4QaCd5AFXSXSI-sUzH_wIbY5kDysM8OJ_8rD1MeTDuXRJWLMJmokcOkjR8AXpVPxWCBvDmFnYhwjyA1AkvqpIUCw0H2W59ZQFG-k_AmKJXc3ZGMwEQQuE-fSlZr9jbwAGNTWJPvOVvZWil_yoT8Rb6P_KuA5px6CtfIERCEf-lpag_j6Rn9dGXAFtPrY1kCFCL6pFG9pgi8pHzSHpIkzCfYN1sGytUz6iiWeEV_ua9OCOyoTmuwHJBmMzD2SNMXaNmH3tcniIalbG817u0RUTgsgc79FlMnJV8fgub5kxw4Kedh9S8J6P4P2Z7JyF7x44IQxc9nGPLfT37wb8LibO_esJ-d8hI4XMyApoupt-hzCRNvPbmHRGyzoty9ubtkx_1mGLOMLjCDAEZqBpI1utKYL3j-U1eJDOJGBBugkvTCok9HkbMwWqsEUwMITfwpgz1Oc2ce-DK65Hg75KO55larQlJUU5QkjU65MRduSreCNT-8kLdWTSpyeReUGA1YhbmPhGNd3yzmEVnJV0VDxhrVjljabRpQIbYJ-13OXyRsrNWv386BDW5Wt8RH3MgyluTPh3azuaQkVn3u9C37cy1CgV67BaW0VPevpKFzAclVTQHcFI2zT4byccJSS8sOztnQ2Gzwl1ACFMGwAoso68JENQhEE8jMBLbI6Q-xENbSEaXj7E24oiTFTKsI--b8e1-dyLlTYl1aa-sdj6KKyx0Yr1MTWTZfzqo0Ep2-9JRJAeOBR_5_QMQBoRRwjJXmUeQGMSDynXaExHdn-lkggGxF6Rx_1iXwsvY_7n6uSM_Qg96KHS1eDFhyev1vBZ34MIgRFiYo8Ehr4uGJeXZqwAwFBQERkYR_xFPPgJ1ESk3J5xDa-WtvTgr7iEIulNfvbmx8rCZxvDwh0t6GhUmuLDf2xV0JS_hi7wRGSOLZ1Q4VtbDUr3q11FbKYIvoD_pHYKdqUovVn0oYUmwysWIKaZ-05GUF7Tu9SYluv5JB4cKf8r-Wi2GUQBC-KUnlfYsrGcLlje_puy8XGsU09NUzDDYeRkfeB3M6ZEk3agyfRf15_3813L05MoswWpQ2Kt5UUgY1SVgwxKAJltwN4tHzO2SUrxxalf7eBNV8PO8bPcIXMJwNh_jq6jPqzYbSBk2LRSuUWz_oQl2yUo2uBoL8kPAcR8WMp2uGymgHwPz9-msey2Vk1rfYcwK3yQY3P9KYN55heTdEua-QnS9d_rREqtsdToUH6WtPzMB9zwTrXe7wThP6u9FbqYkgzlCEqzdG-5OnOTZdRcQGhroxpL6fy17_B5RsTvBcjqvBiuR9X4C0mVgae1lwMQWF95ljH3j81oeUhFESdWUy94bluNAL-dHfTRoBwJQ9oaSWAT84sS6GyW98TUIM1M0pOphxLkFKndaD0lsHVxKQObKML0xb3QAACwt3lbcCAqA6_r33zBxreL4_GUGcPkG3eu-dTTztT-aQeL4DqgaOskOPdPuSi3ygUYMBLZ1slWpq6R9sT-pk9LtRMl192vy77fenhW2EEaPp3lgbwAzTsSmwL8Abtd_vEQ9gIAKHdZq0wBy0KUKQjPmho4Ry6tfsjJs2X-qpX0c-fsP_CCqZ8EMuMFVLd1xqtlA59Utp7lj9K-AObYX2ohAALIO4X146csvOyV0BgUoHu5ycNk6T93JcyeocSvOlgLjgn9YgAvgHw0tFSq8EyYGUb9ewrKyjNHuzFffjzvwgfupVJ1PiA8tO7TZuLMSY1NYnSQE3yLluKQNlH2oq9JN2XQ4wtr71yQq2R47GvcqpVqPn_VPCdcKnWqNRo4qlB8dRtfzyACWYRkLzeqWtQ5gEU5vipViECyBnQurpCBNVuMOckfff_dnBhMSkuhsvGbnecX7ThH5As8gDQG23TWb1agL-cnOKS6OPvZty1MiL0lB0jcvTNSHsgE2X1c9d_B_-yII9_EKnEoOh36y7YEWHN66sZE0qG2muawG31EnaYCtBU8FBUILcOI7xKwLZfZm2LxIdSd4oNd6fEWaRtYD-z4V0MkOhpWh2oUuxtpeB9tVMKv8UHBFifRrR87-3xykOEUSBDvTUb33tahvfSeDsEk24FEwa0ZJfq0NXma4a-MdpnvHDSblDPCrpw7AKY1wWWg-oEy4vMsO14qAIcFKgl6A4vWlGPO_rkunrgm9JlTXISRUqHDkgjxdFxdwlsDu-x1oIXvLpTbWveFM9E066Z7Rk0VKvaw8EPOw6FgqYH81yaHhhMzY8BMYiPgzteqtEwUceGoU2H3LXOK5-xw6_24c14nftqLjk0ujhY23HC8I3C8gA42LZsX6gD21rTVafSaS-XD4x2RMOfuhqsKvwODTu7tBx-HRBEekZRhVe_HNYHTi24XyDhQsCfLRZ4OtkI3j_-uGL34Am_qtapr3aQLTs0jLtBkeqmtpRVhw6Z3t2jcoHAzjhzlPUyTn_Pjc9RXWZ_mBNk_Fjl-waZZNii0kRqqvr99yjNZqopeweKfLtVLjHObpyL5DC9KTw7VUIAQgywFxFj7pgWmlPIdjQsBBIsqGFElOZmS-7UZ-ti79A2PEboqJYmDKOQXLMAdOt07O0GPornqeh-IYmaXWQ-BWdaMuaXVdq0Z1aRdi-mryOpcvy02N3L__vUdLXBQZIXG7iPA-Lu2t3S1ixn5ytVLPvCTmWPCl9BmCSZzy5vJK2BvSTF8spGW4sO2EAS_JktsvdnqH2RbTtenf7QcZP1c6aqyQTz44aW0KDnEMyTDBrwGwGesA4iVAZv6oxwSmD-d075paAXb5F9lu_9h8wkjllZyVDsYxpu2tRXNBynROQzSF4nvhdy3PGGSXKz0bPf5Ia9iZuUBg5ndH2SXgtFzW23lgbjNujgFfFi5bVqikyh8xJuIhMeRio8YwZP8IqSZwnezsIdoDEvihcclssrsyK4D9z9jx4UEHgK3dYTc5IpINmv4AnzKCEmMYgeHPnpTNWE7z7wk42rTQ--DuDe3mdwvAdqYfxSYMFu8MM-N1ZOqG2f_UljIbYnFQpYCiiXt8peffWYNx0NvKgRTuBzPZDJlZwGoi5H5Do_sht5kxX3fY4K9UegHcnnUPYvSm6aXOTNkHBC4fy3KGw-qxCrFpW8tx8twc0m2aYMXTR2XmHReLWOjrM6wFvGqf6JyDAieQEAJLCmAFjAgmeDHPpUymRvs4Cd9Xj821aCXoqDbE1MKNmr5OtIx8l8ktJzzzArrnfebu-S138_yD5ZtxddIx4D6jrUrdeTAb_IpSq5Ov9R4u7xZLhl19WhXDUQFcafVS19eyAZkh-JWkQKuVHuXg66rPCw4zFxeAHGnmu-Y7p7nsN303DGMD5b1ug4ameHGc9JpvGnfNEpw7dbbevmFwuAjkBOkutC1v2kVvmO4tnR5t3kEgJyBbgRMT4IldfFhWpMO0fdVjLAJCcTuJqm1jtE9xwbzL-OVmnNpjrG4Nh-kuOFEww8MjF9OgH8I1cGUqZpd9QSi6QfoVDA7Rb6DYcpRjgeSM2LVxD1nZT01ta1mD7AW-aprakQWk7MqVtU9RUbprmkNnL1td5AT7y9JoRukC0QhB-QtM8z2VwrX5k92bk7SoXXbmBpVysm-FKrzuY8yhYXCuyK996Pp7orkuZLbtAma1mRVOboRxx25GDKwPwZQN3vzBGxgcrTmr5Bw7uzaRIkPIO6kCLrRfgPHE0BPJcAI8jlDws1SSGrLZ1kpeOMwpoVtXij9jUFttwQWx0RGfXLYKK30H9cvIxrgaAwkIaq7J5-BeUmzPvZxZENTgyAgqFQWf_CbKgCDr8BTCtjCCsXsxk0QdgRL4B7ceBZ4orFjeCgWh2fEHg9YVNgegTL5vR5YH8hCnz4vBjA2diOU4ZMUvMdHQsIIbSK4Y29Lz3L27qsK8V55Go16-1u_ExHC5o5cV25cce4gzzJUugoHctrLWa73C8K5dlqh16W9gP_jekpfUPj52sfoi-aF9CteSvB8gyBjRtYNCEBjxNgAx_rTNZHL1jhEnZiGUmvs2B8ZSzdX8eWzr-NL5WN5xrysnMgPqCMBwnIRZrqgaccUGsuYJ9qag5WUcvhVS2QIwUj7_be41soNj1_5tA6Q-3dGdVNS8P7iPi79tSgP3FVcGCnW9-P2t8b6nBSn7FsbHLH-ytSNOVRcKyCt0jEa8x0-PRsnza67tWz_logEmPrRPrSaC4r7eCD04IIoZuSM4EcqaFWMIXEm_BNU9vcHSTRfZioxafIQAR7sMjZy0vxUZPIAsaodSu-QfmqJfH0eSXaEpfCngFiidD0YUz2pB8lQMRAvl_eXPynXccgyk7PsjtrMAjh-l48_C3t7hw1BYAPl8BLQzvOBB5__GOAofxItQa50VIx_FK2aT95n_6u-CcYSYP37y_y84AzuTEmPafqN0Y-0m-pjlq56P63RJXmH84VOHv1cfxV-5v4wf7vbrpU2WZOVrgudO0uvsU4dDjuzMkjiTSvqGNSnvp4HUrpZ0YSq8pwegHYTx8TpTFA8oo5UsM0jJY4nTDk9zPe2hRH_RDomNCJKYoTKNjcWaTn0apqq-0LyNtOBx2qAtYN9HKny-WMlI4EGIO4ZoU2j_Krln9NGXSo_CwuW58NBirfQ7RkMmByJFml7KymuhuCaN5SzKb3rTaOgU7Y-RI16c14YgC__8QDzDJeXKNDnlKs83qQWoFijrBAuSFw49G-OUpYEpx3pt7bmYpirL_NOaKfJ7_simblVpMJp6B3bbBnmg5YcnTNHwLOcW99CwTIximeASfGYuMqELYKnLw1nPTFOuZbw8M5IjacSwMiIWD0Oj0ZgiRaNzUWpubLXpRsk5Ap8fzVmIuQrN2BoyKdmvrvN3Ibwx8LolfWjE0I72R2bliQndIDbYkMVKUsZidigbCszLeQTm2wb8DOi4sryp3m7o6R-kEcw0aoFHnWezRqjA-h7oLUzwyM-Vd7o33HwxrxCiq5LbGvNmtQ4Y0uLndqWy4BaiIPKLXPx8hicexxyG_nb0TZ7MX2iDl0nmYvwe_-owxmw-0GQ7dVZfeLCJhjcR6R1ltjSe6KAeoOWBPymHwf2y7O_K31vw1VXfBG_Z8IP9LjMtNz9xNbDMat0V_MTCxDCUNLpKfRjjCr3jQOZQ8cEaBnIUC7ZXuqI6uZVNrAbc0zNSyC_PESFDV2-f1aQ426GpMZqXX2yFI5ieF2G9cPbpWgDez_J12EHTBFiUPoHxFi_0dSVz3qCWOgUCnbgBlQqyy1VXl38gJnKGSED6knhOrNZz0Y3CT1b_B1110EScTeePvzQRIx7R39ob1sxN'
@@ -13,6 +13,7 @@ import cloudlanguagetools.ttsvoice
13
13
  import cloudlanguagetools.translationlanguage
14
14
  import cloudlanguagetools.transliterationlanguage
15
15
  import cloudlanguagetools.errors
16
+ from cloudlanguagetools.options import AudioFormat
16
17
 
17
18
  NAVER_VOICE_SPEED_DEFAULT = 0
18
19
  NAVER_VOICE_PITCH_DEFAULT = 0
@@ -48,6 +49,14 @@ class NaverVoice(cloudlanguagetools.ttsvoice.TtsVoice):
48
49
  'min': -5,
49
50
  'max': 5,
50
51
  'default': NAVER_VOICE_PITCH_DEFAULT
52
+ },
53
+ cloudlanguagetools.options.AUDIO_FORMAT_PARAMETER: {
54
+ 'type': cloudlanguagetools.options.ParameterType.list.name,
55
+ 'values': [
56
+ cloudlanguagetools.options.AudioFormat.mp3.name,
57
+ cloudlanguagetools.options.AudioFormat.wav.name
58
+ ],
59
+ 'default': cloudlanguagetools.options.AudioFormat.mp3.name
51
60
  }
52
61
  }
53
62
 
@@ -63,7 +72,7 @@ class NaverTranslationLanguage(cloudlanguagetools.translationlanguage.Translatio
63
72
 
64
73
  class NaverService(cloudlanguagetools.service.Service):
65
74
  def __init__(self):
66
- pass
75
+ self.service = cloudlanguagetools.constants.Service.Naver
67
76
 
68
77
  def configure(self, config):
69
78
  self.client_id = config['client_id']
@@ -93,8 +102,10 @@ class NaverService(cloudlanguagetools.service.Service):
93
102
 
94
103
 
95
104
  def get_tts_audio(self, text, voice_key, options):
96
- output_temp_file = tempfile.NamedTemporaryFile()
97
- output_temp_filename = output_temp_file.name
105
+ response_format_parameter, audio_format = self.get_request_audio_format({
106
+ AudioFormat.mp3: 'mp3',
107
+ AudioFormat.wav: 'wav'
108
+ }, options, AudioFormat.mp3)
98
109
 
99
110
  url = 'https://naveropenapi.apigw.ntruss.com/tts-premium/v1/tts'
100
111
  headers = {
@@ -107,19 +118,13 @@ class NaverService(cloudlanguagetools.service.Service):
107
118
  'text': text,
108
119
  'speaker': voice_key['name'],
109
120
  'speed': options.get('speed', NAVER_VOICE_SPEED_DEFAULT),
110
- 'pitch': options.get('pitch', NAVER_VOICE_PITCH_DEFAULT)
121
+ 'pitch': options.get('pitch', NAVER_VOICE_PITCH_DEFAULT),
122
+ 'format': response_format_parameter
111
123
  }
124
+ if audio_format == cloudlanguagetools.options.AudioFormat.wav:
125
+ data['sampling-rate'] = 48000
112
126
 
113
- # alternate_data = 'speaker=clara&text=vehicle&volume=0&speed=0&pitch=0&format=mp3'
114
- response = requests.post(url, data=data, headers=headers, timeout=cloudlanguagetools.constants.RequestTimeout)
115
- if response.status_code == 200:
116
- with open(output_temp_filename, 'wb') as audio:
117
- audio.write(response.content)
118
- return output_temp_file
119
-
120
- response_data = response.json()
121
- error_message = f'Status code: {response.status_code}: {response_data}'
122
- raise cloudlanguagetools.errors.RequestError(error_message)
127
+ return self.get_tts_audio_base_post_request(url, data=data, headers=headers)
123
128
 
124
129
  def get_tts_voice_list(self):
125
130
  # returns list of TtSVoice
@@ -12,6 +12,7 @@ import cloudlanguagetools.languages
12
12
  import cloudlanguagetools.options
13
13
 
14
14
  from cloudlanguagetools.languages import AudioLanguage
15
+ from cloudlanguagetools.options import AudioFormat
15
16
 
16
17
  logger = logging.getLogger(__name__)
17
18
 
@@ -29,6 +30,7 @@ VOICE_OPTIONS = {
29
30
  'values': [
30
31
  cloudlanguagetools.options.AudioFormat.mp3.name,
31
32
  cloudlanguagetools.options.AudioFormat.ogg_opus.name,
33
+ cloudlanguagetools.options.AudioFormat.wav.name
32
34
  ],
33
35
  'default': cloudlanguagetools.options.AudioFormat.mp3.name
34
36
  }
@@ -217,16 +219,17 @@ class OpenAIService(cloudlanguagetools.service.Service):
217
219
  output_temp_file = tempfile.NamedTemporaryFile()
218
220
 
219
221
  speed = options.get('speed', DEFAULT_TTS_SPEED)
220
- response_format = options.get(cloudlanguagetools.options.AUDIO_FORMAT_PARAMETER,
221
- cloudlanguagetools.options.AudioFormat.mp3.name)
222
- if response_format == cloudlanguagetools.options.AudioFormat.ogg_opus.name:
223
- response_format = 'opus'
222
+ response_format_parameter, audio_format = self.get_request_audio_format({
223
+ AudioFormat.mp3: 'mp3',
224
+ AudioFormat.ogg_opus: 'opus',
225
+ AudioFormat.wav: 'wav'
226
+ }, options, AudioFormat.mp3)
224
227
 
225
228
  response = self.client.audio.speech.create(
226
229
  model='tts-1-hd',
227
230
  voice=voice_key['name'],
228
231
  input=text,
229
- response_format=response_format,
232
+ response_format=response_format_parameter,
230
233
  speed=speed
231
234
  )
232
235
  response.stream_to_file(output_temp_file.name)
@@ -1,10 +1,11 @@
1
1
  import requests
2
2
  import tempfile
3
3
  import logging
4
- from typing import List
4
+ from typing import List, Dict
5
5
 
6
6
  import cloudlanguagetools.constants
7
7
  import cloudlanguagetools.ttsvoice
8
+ import cloudlanguagetools.options
8
9
 
9
10
  logger = logging.getLogger(__name__)
10
11
 
@@ -21,9 +22,10 @@ class Service():
21
22
 
22
23
  def get_tts_audio_base_post_request(self, url, **kwargs):
23
24
  try:
25
+ kwargs['timeout'] = cloudlanguagetools.constants.RequestTimeout
24
26
  response = self.post_request(url, **kwargs)
25
27
  response.raise_for_status()
26
- output_temp_file = tempfile.NamedTemporaryFile()
28
+ output_temp_file = tempfile.NamedTemporaryFile(prefix='clt_audio_')
27
29
  output_temp_filename = output_temp_file.name
28
30
  with open(output_temp_filename, 'wb') as audio:
29
31
  audio.write(response.content)
@@ -35,6 +37,13 @@ class Service():
35
37
  logger.exception(error_message)
36
38
  raise cloudlanguagetools.errors.RequestError(error_message)
37
39
 
40
+ def get_request_audio_format(self, format_map: Dict, options: Dict, default_format: cloudlanguagetools.options.AudioFormat):
41
+ response_format_str = options.get(cloudlanguagetools.options.AUDIO_FORMAT_PARAMETER,
42
+ default_format.name)
43
+ response_format = cloudlanguagetools.options.AudioFormat[response_format_str]
44
+ response_format_parameter = format_map[response_format]
45
+ return response_format_parameter, response_format
46
+
38
47
  # used for pre-loading models
39
48
  def load_data(self):
40
49
  pass
@@ -11,6 +11,7 @@ import cloudlanguagetools.ttsvoice
11
11
  import cloudlanguagetools.translationlanguage
12
12
  import cloudlanguagetools.transliterationlanguage
13
13
  import cloudlanguagetools.errors
14
+ from cloudlanguagetools.options import AudioFormat
14
15
 
15
16
  logger = logging.getLogger(__name__)
16
17
 
@@ -71,7 +72,18 @@ class WatsonVoice(cloudlanguagetools.ttsvoice.TtsVoice):
71
72
  return self.description.split(':')[0] + is_dnn
72
73
 
73
74
  def get_options(self):
74
- return {}
75
+ return {
76
+ cloudlanguagetools.options.AUDIO_FORMAT_PARAMETER: {
77
+ 'type': cloudlanguagetools.options.ParameterType.list.name,
78
+ 'values': [
79
+ cloudlanguagetools.options.AudioFormat.mp3.name,
80
+ cloudlanguagetools.options.AudioFormat.ogg_opus.name,
81
+ cloudlanguagetools.options.AudioFormat.ogg_vorbis.name,
82
+ cloudlanguagetools.options.AudioFormat.wav.name
83
+ ],
84
+ 'default': cloudlanguagetools.options.AudioFormat.mp3.name
85
+ }
86
+ }
75
87
 
76
88
  class WatsonService(cloudlanguagetools.service.Service):
77
89
  def __init__(self):
@@ -124,6 +136,13 @@ class WatsonService(cloudlanguagetools.service.Service):
124
136
  return result
125
137
 
126
138
  def get_tts_audio(self, text, voice_key, options):
139
+ response_format_parameter, audio_format = self.get_request_audio_format({
140
+ AudioFormat.mp3: 'audio/mp3',
141
+ AudioFormat.ogg_opus: 'audio/ogg;codecs=opus',
142
+ AudioFormat.ogg_vorbis: 'audio/ogg;codecs=vorbis',
143
+ AudioFormat.wav: 'audio/wav;rate=48000'
144
+ }, options, AudioFormat.mp3)
145
+
127
146
  output_temp_file = tempfile.NamedTemporaryFile()
128
147
  output_temp_filename = output_temp_file.name
129
148
 
@@ -133,7 +152,7 @@ class WatsonService(cloudlanguagetools.service.Service):
133
152
  constructed_url = base_url + url_path + f'?voice={voice_name}'
134
153
  headers = {
135
154
  'Content-Type': 'application/json',
136
- 'Accept': 'audio/mp3'
155
+ 'Accept': response_format_parameter
137
156
  }
138
157
 
139
158
  data = {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cloudlanguagetools
3
- Version: 11.3.4
3
+ Version: 11.5.0
4
4
  Summary: Interface with various cloud APIs for language processing such as translation, text to speech
5
5
  Home-page: https://github.com/Language-Tools/cloud-language-tools-core
6
6
  Author: Luc
@@ -4,6 +4,7 @@ setup.py
4
4
  cloudlanguagetools/__init__.py
5
5
  cloudlanguagetools/amazon.py
6
6
  cloudlanguagetools/argostranslate.py
7
+ cloudlanguagetools/audio_processing.py
7
8
  cloudlanguagetools/azure.py
8
9
  cloudlanguagetools/cereproc.py
9
10
  cloudlanguagetools/chatapi.py
@@ -6,7 +6,7 @@ from setuptools.command.install import install
6
6
  # twine upload dist/*
7
7
 
8
8
  setup(name='cloudlanguagetools',
9
- version='11.3.4',
9
+ version='11.5.0',
10
10
  description='Interface with various cloud APIs for language processing such as translation, text to speech',
11
11
  long_description=open('README.rst', encoding='utf-8').read(),
12
12
  url='https://github.com/Language-Tools/cloud-language-tools-core',
@@ -11,6 +11,7 @@ import pytest
11
11
  import json
12
12
  import time
13
13
  import pprint
14
+ import functools
14
15
 
15
16
  import audio_utils
16
17
 
@@ -33,13 +34,26 @@ def get_manager():
33
34
 
34
35
  return manager
35
36
 
37
+
38
+
39
+ def skip_unreliable_clt_test():
40
+ def decorator(func):
41
+ @functools.wraps(func)
42
+ def wrapper(*args, **kwargs):
43
+ if not CLOUDLANGUAGETOOLS_CORE_TEST_UNRELIABLE:
44
+ pytest.skip(f'you must set CLOUDLANGUAGETOOLS_CORE_TEST_UNRELIABLE=yes')
45
+ return func(*args, **kwargs)
46
+ return wrapper
47
+ return decorator
48
+
49
+
36
50
  class TestAudio(unittest.TestCase):
37
51
 
38
52
  ENGLISH_INPUT_TEXT = 'This is the best restaurant in town.'
39
- FRENCH_INPUT_TEXT = "On a volé mes affaires."
40
- JAPANESE_INPUT_TEXT = 'おはようございます'
41
- CHINESE_INPUT_TEXT = '老人家'
42
- KOREAN_INPUT_TEXT = '여보세요'
53
+ FRENCH_INPUT_TEXT = 'Bonjour'
54
+ JAPANESE_INPUT_TEXT = 'こんにちは'
55
+ CHINESE_INPUT_TEXT = '你好'
56
+ KOREAN_INPUT_TEXT = '안녕하세요'
43
57
 
44
58
  @classmethod
45
59
  def setUpClass(cls):
@@ -73,6 +87,18 @@ class TestAudio(unittest.TestCase):
73
87
  subset = [x for x in self.voice_list if x['audio_language_code'] == audio_language.name and x['service'] == service.name]
74
88
  return subset
75
89
 
90
+ def get_voice_by_service_and_name(self, service: Service, voice_name) -> cloudlanguagetools.ttsvoice.TtsVoice_v3:
91
+ subset = [x for x in self.voice_list_v3 if voice_name in x.name and x.service == service]
92
+ self.assertEqual(len(subset), 1)
93
+ return subset[0]
94
+
95
+ def get_voice_by_lambda(self, service: Service, filter_func, assert_unique=True):
96
+ service_voices = [x for x in self.voice_list_v3 if x.service == service]
97
+ subset = [x for x in service_voices if filter_func(x)]
98
+ if assert_unique:
99
+ self.assertEqual(len(subset), 1, pprint.pformat(subset))
100
+ return subset[0]
101
+
76
102
  def verify_voice(self, voice, text, recognition_language):
77
103
  return self.verify_voice_internal(voice['voice_key'], voice['service'], text, recognition_language)
78
104
 
@@ -117,6 +143,11 @@ class TestAudio(unittest.TestCase):
117
143
  # logging.info(f'verify_service_audio: service: {service} audio_language: {audio_language}')
118
144
  voices = self.get_voice_list_service_audio_language(service, audio_language)
119
145
  self.assertGreaterEqual(len(voices), 1, f'at least one voice for service {service}, language {audio_language}')
146
+
147
+ if service == Service.Google:
148
+ # exclude Journey voices, they don't use the standard interface
149
+ voices = [x for x in voices if 'Journey' not in x['voice_name']]
150
+
120
151
  # pick 3 random voices
121
152
  max_voices = 3
122
153
  if len(voices) > max_voices:
@@ -153,7 +184,8 @@ class TestAudio(unittest.TestCase):
153
184
  # pprint.pprint(mandarin_azure_voices)
154
185
 
155
186
  xiaochen = [x for x in mandarin_azure_voices if 'Xiaochen' in x.name]
156
- self.assertTrue(len(xiaochen) == 2) # there is a regular and a multilingual
187
+ self.assertEqual(len(xiaochen), 3, str(xiaochen)) # there is a regular and a multilingual
188
+ # and also a DragonHD
157
189
 
158
190
  xiaochen_single_language = [x for x in xiaochen if len(x.audio_languages) == 1][0]
159
191
  self.assertEquals(xiaochen_single_language.audio_languages, [AudioLanguage.zh_CN])
@@ -204,11 +236,11 @@ class TestAudio(unittest.TestCase):
204
236
  self.verify_service_audio_language(source_text, Service.Amazon, AudioLanguage.fr_FR, 'fr-FR')
205
237
 
206
238
  def test_mandarin_google(self):
207
- source_text = '老人家'
239
+ source_text = self.CHINESE_INPUT_TEXT
208
240
  self.verify_service_audio_language(source_text, Service.Google, AudioLanguage.zh_CN, 'zh-CN')
209
241
 
210
242
  def test_mandarin_azure(self):
211
- source_text = '你好'
243
+ source_text = self.CHINESE_INPUT_TEXT
212
244
  self.verify_service_audio_language(source_text, Service.Azure, AudioLanguage.zh_CN, 'zh-CN')
213
245
 
214
246
  def test_azure_standard_voice_deprecated(self):
@@ -231,7 +263,7 @@ class TestAudio(unittest.TestCase):
231
263
  mandarin_azure_voices = [x for x in azure_voices if AudioLanguage.zh_CN in x.audio_languages]
232
264
 
233
265
  xiaochen = [x for x in mandarin_azure_voices if 'Xiaochen' in x.name]
234
- self.assertTrue(len(xiaochen) == 2) # there is a regular and a multilingual
266
+ self.assertEqual(len(xiaochen), 3) # there is a regular and a multilingual, and dragonhd
235
267
 
236
268
  xiaochen_single_language = [x for x in xiaochen if len(x.audio_languages) == 1][0]
237
269
  xiaochen_multilingual = [x for x in xiaochen if len(x.audio_languages) > 1][0]
@@ -285,16 +317,15 @@ class TestAudio(unittest.TestCase):
285
317
  source_text = '老人家'
286
318
  self.verify_service_audio_language(source_text, Service.CereProc, AudioLanguage.zh_CN, 'zh-CN')
287
319
 
320
+ @skip_unreliable_clt_test()
288
321
  def test_mandarin_vocalware(self):
289
322
  # pytest test_audio.py -k test_mandarin_vocalware
290
- if not CLOUDLANGUAGETOOLS_CORE_TEST_UNRELIABLE:
291
- pytest.skip('you must set CLOUDLANGUAGETOOLS_CORE_TEST_UNRELIABLE=yes')
292
323
 
293
324
  source_text = '你好'
294
325
  self.verify_service_audio_language(source_text, Service.VocalWare, AudioLanguage.zh_CN, 'zh-CN')
295
326
 
296
327
  def test_cantonese_google(self):
297
- source_text = '天氣預報'
328
+ source_text = self.CHINESE_INPUT_TEXT
298
329
  self.verify_service_audio_language(source_text, Service.Google, AudioLanguage.zh_HK, 'zh-HK')
299
330
 
300
331
  def test_cantonese_azure(self):
@@ -354,6 +385,7 @@ class TestAudio(unittest.TestCase):
354
385
  source_text = 'hello <break time="50ms"/>world'
355
386
  self.verify_service_audio_language(source_text, Service.Google, AudioLanguage.en_US, 'en-US')
356
387
 
388
+ @skip_unreliable_clt_test()
357
389
  def test_ssml_english_azure(self):
358
390
  # pytest tests/test_audio.py -k test_ssml_english_azure
359
391
  source_text = 'hello <break time="200ms"/>world'
@@ -475,6 +507,50 @@ class TestAudio(unittest.TestCase):
475
507
  audio_text = audio_utils.speech_to_text(self.manager, audio_temp_file, 'fr-FR', audio_format=cloudlanguagetools.options.AudioFormat.ogg_opus)
476
508
  self.assertEqual(audio_utils.sanitize_recognized_text(source_text), audio_utils.sanitize_recognized_text(audio_text))
477
509
 
510
+ def verify_wav_voice(self, voice: cloudlanguagetools.ttsvoice.TtsVoice_v3, text: str, recognition_language: str):
511
+ # assert that the wav format is in the list of supported formats
512
+ self.assertTrue(cloudlanguagetools.options.AudioFormat.wav.name in
513
+ voice.options[cloudlanguagetools.options.AUDIO_FORMAT_PARAMETER]['values'])
514
+ options = {cloudlanguagetools.options.AUDIO_FORMAT_PARAMETER: cloudlanguagetools.options.AudioFormat.wav.name}
515
+ audio_temp_file = self.manager.get_tts_audio(text, voice.service, voice.voice_key, options)
516
+ audio_utils.assert_is_wav_format(self, audio_temp_file.name)
517
+ audio_text = audio_utils.speech_to_text(self.manager, audio_temp_file, recognition_language, audio_format=cloudlanguagetools.options.AudioFormat.wav)
518
+ self.assertEqual(audio_utils.sanitize_recognized_text(text), audio_utils.sanitize_recognized_text(audio_text))
519
+
520
+ def test_azure_format_wav(self):
521
+ fr_voice = self.get_voice_by_service_and_name(Service.Azure, 'Denise')
522
+ self.verify_wav_voice(fr_voice, self.FRENCH_INPUT_TEXT, 'fr-FR')
523
+
524
+ def test_amazon_format_wav(self):
525
+ fr_voice = self.get_voice_by_service_and_name(Service.Amazon, 'Mathieu')
526
+ self.verify_wav_voice(fr_voice, self.FRENCH_INPUT_TEXT, 'fr-FR')
527
+
528
+ def test_elevenlabs_format_wav(self):
529
+ fr_voice = self.get_voice_by_lambda(Service.ElevenLabs,
530
+ lambda x: 'Charlotte' in x.name and x.voice_key['model_id'] == 'eleven_multilingual_v2')
531
+ self.verify_wav_voice(fr_voice, self.FRENCH_INPUT_TEXT, 'fr-FR')
532
+
533
+ def test_google_format_wav(self):
534
+ fr_voice = self.get_voice_by_lambda(Service.Google,
535
+ lambda x: AudioLanguage.fr_FR in x.audio_languages and 'Journey' not in x.name, assert_unique=False)
536
+ self.verify_wav_voice(fr_voice, self.FRENCH_INPUT_TEXT, 'fr-FR')
537
+
538
+ def test_naver_format_wav(self):
539
+ ko_voice = self.get_voice_by_lambda(Service.Naver,
540
+ lambda x: AudioLanguage.ko_KR in x.audio_languages, assert_unique=False)
541
+ self.verify_wav_voice(ko_voice, self.KOREAN_INPUT_TEXT, 'ko-KR')
542
+
543
+ def test_openai_format_wav(self):
544
+ ko_voice = self.get_voice_by_lambda(Service.OpenAI,
545
+ lambda x: AudioLanguage.en_US in x.audio_languages, assert_unique=False)
546
+ self.verify_wav_voice(ko_voice, self.ENGLISH_INPUT_TEXT, 'en-US')
547
+
548
+ def test_watson_format_wav(self):
549
+ en_voice = self.get_voice_by_lambda(Service.Watson,
550
+ lambda x: AudioLanguage.en_US in x.audio_languages, assert_unique=False)
551
+ self.verify_wav_voice(en_voice, self.ENGLISH_INPUT_TEXT, 'en-US')
552
+
553
+
478
554
  def test_google_voice_journey(self):
479
555
  service = 'Google'
480
556
  source_text = self.ENGLISH_INPUT_TEXT
@@ -592,12 +668,15 @@ class TestAudio(unittest.TestCase):
592
668
  source_text = 'Ich mag das Essen nicht.'
593
669
  self.verify_service_audio_language(source_text, Service.ElevenLabs, AudioLanguage.de_DE, 'de-DE')
594
670
 
671
+ @skip_unreliable_clt_test()
595
672
  def test_elevenlabs_japanese(self):
596
673
  self.verify_service_japanese(Service.ElevenLabs)
597
674
 
675
+ @skip_unreliable_clt_test()
598
676
  def test_elevenlabs_chinese(self):
599
677
  self.verify_service_chinese(Service.ElevenLabs)
600
678
 
679
+ @skip_unreliable_clt_test()
601
680
  def test_elevenlabs_korean(self):
602
681
  self.verify_service_korean(Service.ElevenLabs)
603
682
 
@@ -5,6 +5,8 @@ import unittest
5
5
  import pytest
6
6
  import json
7
7
  import pprint
8
+ import time
9
+ import requests
8
10
 
9
11
  sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
10
12
 
@@ -21,11 +23,24 @@ def get_manager():
21
23
 
22
24
  class TestTranslation(unittest.TestCase):
23
25
  def setUp(self):
24
- self.manager = get_manager()
25
- self.language_list = self.manager.get_language_list()
26
- self.translation_language_list = self.manager.get_translation_language_list_json()
27
- self.transliteration_language_list = self.manager.get_transliteration_language_list_json()
28
- self.tokenization_options = self.manager.get_tokenization_options_json()
26
+ max_retries = 3
27
+ retry_delay = 5 # seconds
28
+
29
+ for attempt in range(max_retries):
30
+ try:
31
+ self.manager = get_manager()
32
+ self.language_list = self.manager.get_language_list()
33
+ self.translation_language_list = self.manager.get_translation_language_list_json()
34
+ self.transliteration_language_list = self.manager.get_transliteration_language_list_json()
35
+ self.tokenization_options = self.manager.get_tokenization_options_json()
36
+ break # If successful, break out of the retry loop
37
+ except requests.exceptions.RequestException as e:
38
+ if attempt < max_retries - 1: # If not the last attempt
39
+ logging.warning(f"Request failed. Retrying in {retry_delay} seconds... (Attempt {attempt + 1}/{max_retries})")
40
+ time.sleep(retry_delay)
41
+ else:
42
+ logging.error(f"Max retries reached. Unable to set up the test environment.")
43
+ raise # Re-raise the last exception if all retries failed
29
44
 
30
45
  def test_language_list(self):
31
46
  self.assertTrue(len(self.language_list) > 0)
@@ -140,7 +155,7 @@ class TestTranslation(unittest.TestCase):
140
155
  self.assertRaises(cloudlanguagetools.errors.RequestError, self.translate_text, Service.Naver, 'Veuillez parler lentement.', Language.fr, Language.th, 'Please speak slowly.')
141
156
 
142
157
  def test_translate_deepl(self):
143
- # pytest test_translation.py -rPP -k test_translate_deepl
158
+ # pytest tests/test_translation.py -rPP -k test_translate_deepl
144
159
  self.translate_text(Service.DeepL, 'Please speak slowly', Language.en, Language.fr, 'Veuillez parler lentement')
145
160
  self.translate_text(Service.DeepL, 'Je ne suis pas intéressé.', Language.fr, Language.en, ["""I'm not interested.""", 'i am not interested'])
146
161
  self.translate_text(Service.DeepL, '送外卖的人', Language.zh_cn, Language.en, ['delivery person', 'takeaway delivery people'])
@@ -211,14 +226,41 @@ class TestTranslation(unittest.TestCase):
211
226
  self.assertTrue
212
227
  self.assertEqual(result['Watson'], "<b><span style=\"font-weight: 400;\">What's wrong with you?</span></b>")
213
228
 
214
- def test_transliteration(self):
215
- # pytest test_translation.py -k test_transliteration
229
+ def test_transliteration_azure_custom(self):
230
+ # pytest tests/test_translation.py -k test_transliteration_azure_custom
231
+
232
+ # chinese
233
+ source_text = '成本很低'
234
+ service = 'Azure'
235
+ # transliteration_key = transliteration_option['transliteration_key']
236
+ transliteration_key = {
237
+ 'language_id': 'zh-Hans',
238
+ 'from_script': 'Hans',
239
+ 'to_script': 'Latn',
240
+ }
241
+ result = self.manager.get_transliteration(source_text, service, transliteration_key)
242
+ self.assertIn(result, ['chéng běn hěn dī', 'chéngběn hěndī'])
243
+
244
+ transliteration_key = {
245
+ 'language_id': 'zh-Hant',
246
+ 'from_script': 'Hans',
247
+ 'to_script': 'Hant',
248
+ }
249
+ result = self.manager.get_transliteration('讲话', service, transliteration_key)
250
+ self.assertEqual(result, '講話')
251
+
252
+ def test_transliteration_azure(self):
253
+ # pytest tests/test_translation.py -k test_transliteration_azure
216
254
 
217
255
  # chinese
218
256
  source_text = '成本很低'
219
257
  from_language = Language.zh_cn.name
220
258
  service = 'Azure'
221
- transliteration_candidates = [x for x in self.transliteration_language_list if x['language_code'] == from_language and x['service'] == service]
259
+ transliteration_candidates = [
260
+ x for x in self.transliteration_language_list
261
+ if x['language_code'] == from_language
262
+ and x['service'] == service
263
+ and 'to Latin' in x['transliteration_shortname']]
222
264
  self.assertTrue(len(transliteration_candidates) == 1)
223
265
  transliteration_option = transliteration_candidates[0]
224
266
  service = transliteration_option['service']
@@ -1 +0,0 @@
1
- KEYS='gAAAAABkk5vaZm3izjIqmeoWzO8UiDRp8Riz0F2BmB5_tFmcc2Fgu0W-MXvC7y4-7vfN5ggR0hgVRKJVC2UbyJpNBt3yDuJAfBoccZ1327aDb2KfXb69akWXWrTnIqHTpfl1XbpVIbgMk3MT777H_0cz88GM4cgVtH9WuMQC20wjr3AQgHEhWUlDowIdHPbyb_UX-bom3bEF0DfZ__EkYS7tIBmMOH_B6oSayUSibUG1SJc7xfGvJqW4zXUeESJtBujRqVvbr5z0zf46eicNAMiQXEm9IpYIRnrldsUID-6KGw4eUCYx32PPYjldGkvapelHnpAv5WqqHQ6M_p4lu95CbgWwfQIF1qLW68cOOiAZBgjHZrPbY0DLqT-eEfB_xrek0U7ZqlpOisIfc3Q8QfoyGaW-mSchB5GYqXfp38xLlPGI3OSYnJl08MuJ6p7M5cxRTRkjvTj8UimShfvmPm5ltM-XW43uIsuy5rAkrHIbsKCkiJ-tXK1eEIySR092cl0qpZ3U2ztySbzX1eXsgzynYVfGdD7V4tGNpTMPhekOTpFLNKw6swA3EK1tC_7aWWvnqec6ccew7nAPHKE00Li7trYpvZeBdLQvOdVzZj77TTLdBE39_syeB4EDZfV6WXFu8_DhAuNiPF2LJHFIMWCA6RiAo1HtPUC8l3IDU3uSBljyZ1QTGNUBg975SljdrgLWocm-Hmyfvq0WfnTk19JnXFAp_hiIsPtsulyLkuGLXXO3aUHl2xcGy6u9cQE3NxtslnSXy8IRnv9MqCae54NwzNGkWiCiN29pvNvEppuHG-riexmh9MhUvHVOV68kwuB5Et2gm9kFgpKJlV0ZkUWDdJUZDYrgQ2oQ7TmBhez43duyJZ2k2uYHaZ8MFFcFxjgT-WkvfkI8jIUZ0ZIwZMKjcM5OCNLPiL-EbxVO2grithjZ_bCbnpxsahKLvWA4QYVy-3eWhhqutmNpsFIig05No1I4tXNQmcxJcFANGIG2p73DbgBIfDrJToxYIPw_bX2Ii93cKX0FNhzv1be0uUP3RZcuni_ESYnvrXgdNFZ_fIf-lci6lZcWiITFxejDFe1pqb9rd_BLEyJNIFfkvFou_eC8wWT0G-h8yPckuVCYlOEWAPfhL5BjFxQhFvnwyTm4vNHBLnhoOhsURc1NTwJ9EL1_ksYLCktLnlFjjg21Z-93aV4G6T6X2bwIdFpqebo5G-SIz-xKVo7Wln105uzUr2CJEliaufNOWZC6by2N2HJ2jqsajiJX2ZNOJCjkYtjH_-jln9taKF3Q5BFSlOeraam-ppZkBsABMewrUZ8d7lqqdyN7ntDHWiASjFm11KL_25hRT6tMhx4BpKlP40ovfY7XoJhRT-_Ss7Aeze6jBj-Qe58dRDEiIzZcko2F1SEvIZFNVPbN3FuEjhlrOP9N1IvhCYn_8NjXENetbjIVhkQx5nKGaLVeT9toRc4qwhg-4XdN620TnIxPusva9ncXUYXVz1iDVFtN5-RuJRAppuBj7_nfJOG8MngntJOV7B0o6fVDK8McVi3TQ9lyCl1GMPuoeB41xCKyY3Lc-lqzgXhRhiWbuSBg1G45jQ3BE8bWciFjQCXuMeGXmDZzEFtzJMPQWwxzDkuOB2Y74ITEzsbleSvFXhlgUpa692ckDK1z_mK2UPrLbAa629GzPsrdjVGzQ6vWiYiKkuJFHQUkxpBscIH7y9rPW6zc0jg7tVmYwuV94UCS61g2g-s1-UodQog9vcD4JGyPgr2AcetoBzSh-wAsBBzra3A_E-cWdD2uzB5Mv60hgOlInLbi8QUBppCig89IwnFLdXAP7QFamWtijflUpqRECMvokxaImhtKSwPy3BlyJAMr7T3lFC3XY74fA-3k2wKAC_0LLYxedED68-GtklIpfgSwQ9e5oxEzv927oF_uEibZizjJusNtnk4Wz76YMd-VHq-wPJcxFcRqkKdZCgyToLY2z4YP81tWVxB_9F5aR_yF9cvCPbAycRAxny9Qsno44VPjZ5v7zLj5G7xqhEza11EzrUdPk657s_a1M6G8ARehCblBChTUD6Gxz1Y2-dZ0ejpB81kGFqICPEw24dF1xq44GAwNJsx_zYd_0y5GldZSMCYpmLbJNDFx1wGOJwLQq9h0JaqRT5jAYl3uacpmApYLj8jMCdn1HAdJwKSIw-s17RyEZHBg4JHC3tAMzZhhco5k7gHLCCRI_jep9fAusZTjo3mdaJD_tJfAZsoQXx4kq9HJ8k71nyxYz18pwzIfT0hJgFsSbS-UyrhAk8mskTcw5UPKGcgzMOwafh0Giy69Bn4KfU_nHUDeHng_b-KCN7G_fF43xX7l3EB3qIyN9vUtPYrHteJt0LYS0yXtUM_Dc0jxeEdZwcpKYxbkMlTbK1DM6JZ2JitNGAPDvNv8ZjRQxPmKjjgVdvs8OzJZ4kdPzAfEVBvhYx3gKmggcnsQBgxFki3FOhXhPrCAyxXO5-_glom5KzEGqG2VQgJWU3ycCtmFypoNmx38wyd3eH7_M2ydfkIE6e350g8y7sFayK6vqO4GdANxTRbHThF7HfI-3nWtPJ4NpILiu71CtpuaU5MAhqD8qAJjQkUUifANeY6i-aqQZXBPDWw7VaYpQWGAoiPBuFJc-a906yU-FzViRToZIyBbunOgJO0GOy4iJRJSnAQY3Cd3Xrk7VenUnBuYBLpUUNiVCZw6MnWTAhY1BujnDf-Pxrj8jJYf2Y0KipSxkCK4Hkjnc13bTkBovA8YM7HECTIVkkIlsjvV440eyqLpO1r4yD_lecvExcSNsF-EBwSbp0hRps3wTynY0yeuWPM3NTQ4dEmO6fW1ewpsBKicDu0sHWAhK_WOpYiydTT3rAGSgDolNSKKYUpFlHS92g5-Nb_nLW4qwOV5F8yDKbfYtaBKylCNrmDu8aXc5GIdIeK7eGC5JF8uaUCsidPOh7JuOpn-hhKym4Aq6Hb5FlnLf9eNkimCYDm3F1a91CcJwhPRN_FZNLVfpBqw8pOfFK9RkBEttStWxvgWvW-C5K5EzTiBGlZBR2j4LHazDJR8_bjJ6uBCw-8fFQsPWGgliic4_k1ioRVwhJDNfw6VN0mR6iqt9D7duTxU8nuNC3u5idCJuwDC4aJC9vgvpbU2TledADfaqeBujUn8DhkOEhGJsE3WEcwaidsqI3NFSKjtoxiDEOfWmXVmFAjDo_tENvjRnriFPrWR5vMvmluAlncg16ok0qqpLBpgnGYDYhfv4IC3HsYfSwmyCDrmoMUBqv5dUtbEBMbQB1BcWH1hOahCN60AO3Emedq2jsM853UI6_EuZA19TIhLreNMpEL1sSWbB3cVlLfGEvoSzga1MdtKsxCZmBIWa2_Eez9lBtdpuqG4phfpFs5sM6mWKza0ZLr8v5N4-oIRqApAAr2JNscXeehiZPhWxjaXmA_69N0mdmzkjwznH6sQ3bkX7b9UWOQPblEwPfk4iXktxiIOBGxDh0SgWMpEvtEO9MxFnOYoIsmeyTVNOG73iFKQ6gybMoAiCFBeGSq24scq1Tf8ydBkpaM7Z3SzNj3b4u5feRTb1dZCVsjQ3kCP657INEpRWx6GfVAZBH2-GGVuDNrr7-YY7XcLees9h5jSI5JdelsuFwfI8U1D3gH2F_gZenLaR5RlFpH-yma-XPOuCpbflzs-MoEO8A55L2zs6EZTxcjEGt8v-J8rKEIPt8sr0u80YklSYs0m5q8AwDjgzwRG9UPoaZJYclGo4U5N_P-fixUtF2cr7rHWxDZLzuK7jF8KNUW8pWRgYn5Zn6BfKN8ro-49xbqnDQ7CORKEiFxjcSUP4D0ocyFIhHf3V4YiyS860KHqbhkyRm4d765skBe5DqrJn2EeGwh_ehBMw30uqYai-ws1920xFGPYCGuHC2uY5cgY_8QF1HWsDsr-Ei4jtsE24_AA8oJTx2wIbchQIu7oJl9gElYbIRXaoHHyuaFIkxpV4CMpTXtiWTwIjQeCBbQmESnj9s5vBdKvqbqRDZQ0LigkqwCoO8nizJ8kNse601UN5Nci9UkH5vcOUmmc1RCj8kQLiaCWMTQ_lT3bzGXgUxRgUdrgVyKYbSSigWZ98WzT_YjsbjLufIY5uz1ix8e_mz7JK2MxFywSt_JJ2O-ry6NNVOUHig10XJBUEIj2hE4X7ziecWMAH93axfiwk9GxLy_vFxA5Nl0KbZGbvwixN4rrAH1yFChh83vF819kjhe0H4oCsGWvxq3OrxdVSbuF95p6lhxhpcy0hPM4rEQ5EcM3Gd_iFUxuZMBgZzvnjDgvvwGbX97Sx6WjIqEfNHFLbvID1nmUMXROPoMmnr_P14QptBvDCgE1SxDhavEOr8EMTxxzyrBEFeKwZ2Sw8QukuOceDMpwmR5ksAk0QiiWgvdzu_LkgUgayKhp8hQMmtt0wsTlmizkG5tATqGCri1dENCcKM5TqCqd3FEAPlm07VqPJrjwq2OHt5xR90HF8uizO9pdBXyY5_M5asChPsUyeBm5dYLfhl7JKLwVdj8wcZLsVojlunIUKppFdOSx9RW_jzqa20jvq1i7-f43kcMsjzuklAY4a3QoTh9gYOrOxzecko6lij6W0jVC0EuplsbUi75rwNzYVjRV1s_3Ze96S9Ym-dNf3murhzPTOvCL1v3Ugb0vtW5cEK0MwMEM7jN-K1GNprILjVMky15f5hiJEKlmsNuvILi6-F911Zu3s6Rl_a2P6-7zWa7crlrgsrCUoCmGWLpVW6fsscfGyBgSp74NV1qOz_062V9Fg7-LBcYWgR3VILw3VbFQ1ZvLggLuEpxbxwOIWgb_jhWjo5wbQ9pdGziHY1bR7ejh3n_Dc1YwfIKEo0yNG6nnLchcThTggRZhLozHpx-ElamXM7hMgotm1XUbjLlPEgDHfAQjazFPI0h_i_4N2MiJ_OSXvF3IN2U8nKivvfcCn3lVrWLzzHtiAxBoSOfGrwD6S46Um1cSV0_cRCJQAvbUsn2iT8CNVbOli6xzCceWkUdVjpZYhklwV8SMs839PTNN2CU9Poxm2t2PIeQcT_A-KFBqC6MYwGy_wLO_uibXHY1oxcL8FKl2JUD7JbXEWItFydDLQVz3KpumP43D_zJ3pK7kin5sA3CX6ultD94PKZegIpe1gc2SUh2bte31HwGZh96IuFNoEuYVumlSMrJhtpOvpKC_ScSW6uw_f1WHCnBotYXWX_kkmmig6H-Dzv4zOgLmMcSE_R8QhYc3XrUXL_0lpl85xsJHv2sWx-evCWhGeIF-LkYmP8mwCHXkfcigbJV5bPVX8XRXx6X4_ZKSTKmKmsyRaBLmxpWtg6bZRi8D2d1Oz9sNTSISWxJlChqcL5VVqKN2EAaGAupGqmRvn7dPt5JQqGARx14PGQGOcHygif0eCrGv3x78sG2Vi_mNZKbBszJNu-yePTDStZ_LPjKya87DEyIghwe-S4fFrx8X-oSz6PYd26bhCUfYfkPfsSShcfJ1aHXCY6b9In8EXd0-G5um6Ng0eIYlc6Y0Mr--cpc0xXEubTslRvUhwLumZ59Lr-4oN75NFra427iErxLrzhhFCSbcVc7mh-qYa-z_6mQqdbUfiPVwfE2iDEtGiz8-keZUfEysxh_nkKQSJO-dmie8031UpF2TEJTMQ4onnyKNmLLSFltmqT5xa6CRpHXPrhuoCx7J-lH9L5_qbOFiCNwNhMtEKuhMp-CBRQsK3KPLtzKx01haclk4uR36_fXLdLkY3GPG2jrkEoc3sWIlibQA5EtcNAT5Y7vVHVm1hW_P9xVOjGe_uokHLdln9rAOZayJkJid1hqikXjQ2uYdpk9rSNtxeWw-9YB6fGL0o3IFL01MngtMvSfvwXSWK2fbWkPEzSgPv8qpqQuDa8QzIh5IQ_r5IH7jHBY1zwn4IPhb6qxU7WvCmQ0zD0mt3b2XxNsL5qNbmGM0L63Pc-6ICwN5HMIxzo7T5rVzdqZn1sWu00T6pRQzzOXfhJIqS_WvmZe8X1dcTUo9mdrSjjYj5F5GR9k4M6MDih3M6ndfKJWUdE1-l7eD6BvES5faIuDicoU0HCOrU8oK82v7NApultvIu9LkZTBNQc2mv2Tc7nNx0jjoO1N3Kw4LPaFJxfSp3xEEn0eL1R7c-bKmVCCOT1rtbbJx2QCB_LWXqJnQtgZbX7x72jTokiqooKQTBlEc_YiqV4GTDCHQL-mW6XDm96YzhYhY6RxCfcABDx6Ot22uqZQ1g9gbLXV1raTUXC2zowuV49ehW8RWKDq6vVR94l9doLLjhO29cGk5LuQ2oYLA8pO9dDs6HMWtILj5msrcKYyjWR7hYQnnEyG4'