cloudlanguagetools 11.3.4__tar.gz → 11.4.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.4.0}/PKG-INFO +1 -1
  2. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/amazon.py +27 -7
  3. cloudlanguagetools-11.4.0/cloudlanguagetools/audio_processing.py +29 -0
  4. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/azure.py +54 -37
  5. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/constants.py +2 -0
  6. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/elevenlabs.py +27 -2
  7. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/google.py +3 -1
  8. cloudlanguagetools-11.4.0/cloudlanguagetools/keys.py +1 -0
  9. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/naver.py +19 -14
  10. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/openai.py +8 -5
  11. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/service.py +11 -2
  12. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/watson.py +21 -2
  13. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools.egg-info/PKG-INFO +1 -1
  14. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools.egg-info/SOURCES.txt +1 -0
  15. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/setup.py +1 -1
  16. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/tests/test_audio.py +82 -9
  17. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/tests/test_translation.py +21 -6
  18. cloudlanguagetools-11.3.4/cloudlanguagetools/keys.py +0 -1
  19. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/LICENSE +0 -0
  20. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/README.rst +0 -0
  21. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/__init__.py +0 -0
  22. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/argostranslate.py +0 -0
  23. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/cereproc.py +0 -0
  24. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/chatapi.py +0 -0
  25. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/deepl.py +0 -0
  26. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/dictionarylookup.py +0 -0
  27. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/easypronunciation.py +0 -0
  28. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/encryption.py +0 -0
  29. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/epitran.py +0 -0
  30. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/errors.py +0 -0
  31. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/forvo.py +0 -0
  32. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/fptai.py +0 -0
  33. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/languages.py +0 -0
  34. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/libretranslate.py +0 -0
  35. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/mandarincantonese.py +0 -0
  36. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/options.py +0 -0
  37. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/pythainlp.py +0 -0
  38. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/servicemanager.py +0 -0
  39. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/spacy.py +0 -0
  40. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/test_services.py +0 -0
  41. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/tokenization.py +0 -0
  42. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/translationlanguage.py +0 -0
  43. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/transliterationlanguage.py +0 -0
  44. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/ttsvoice.py +0 -0
  45. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/vocalware.py +0 -0
  46. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/voicen.py +0 -0
  47. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools/wenlin.py +0 -0
  48. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools.egg-info/dependency_links.txt +0 -0
  49. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools.egg-info/requires.txt +0 -0
  50. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/cloudlanguagetools.egg-info/top_level.txt +0 -0
  51. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/setup.cfg +0 -0
  52. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/tests/test_breakdown.py +0 -0
  53. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/tests/test_chatapi.py +0 -0
  54. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/tests/test_dictionary_lookup.py +0 -0
  55. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/tests/test_llm.py +0 -0
  56. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.0}/tests/test_mock_services.py +0 -0
  57. {cloudlanguagetools-11.3.4 → cloudlanguagetools-11.4.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.4.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
  }
@@ -258,22 +262,18 @@ class AzureService(cloudlanguagetools.service.Service):
258
262
  return headers
259
263
 
260
264
  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
265
  # https://learn.microsoft.com/en-us/azure/ai-services/speech-service/rest-text-to-speech?tabs=streaming#audio-outputs
266
266
  # 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
- }
267
+ response_format_parameter, audio_format = self.get_request_audio_format({
268
+ AudioFormat.mp3: 'Audio24Khz96KBitRateMonoMp3',
269
+ AudioFormat.ogg_opus: 'Ogg48Khz16BitMonoOpus',
270
+ AudioFormat.wav: 'Riff48Khz16BitMonoPcm'
271
+ }, options, AudioFormat.mp3)
272
272
 
273
273
  output_temp_file = tempfile.NamedTemporaryFile(prefix=f'cloudlanguage_tools_{self.__class__.__name__}_audio', suffix=f'.{audio_format.name}')
274
274
  output_temp_filename = output_temp_file.name
275
275
  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]])
276
+ speech_config.set_speech_synthesis_output_format(azure.cognitiveservices.speech.SpeechSynthesisOutputFormat[response_format_parameter])
277
277
  synthesizer = azure.cognitiveservices.speech.SpeechSynthesizer(speech_config=speech_config, audio_config=None)
278
278
 
279
279
  default_pitch = 0
@@ -330,17 +330,18 @@ class AzureService(cloudlanguagetools.service.Service):
330
330
  headers = {
331
331
  'Authorization': 'Bearer ' + token,
332
332
  }
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
333
+ response = requests.get(constructed_url, headers=headers,
334
+ timeout=cloudlanguagetools.constants.RequestTimeout)
335
+ response.raise_for_status()
336
+ voice_list = json.loads(response.content)
337
+ result = []
338
+ for voice_data in voice_list:
339
+ # print(voice_data['Status'])
340
+ try:
341
+ result.append(AzureVoice(voice_data))
342
+ except KeyError:
343
+ logging.error(f'could not process voice for {voice_data}', exc_info=True)
344
+ return result
344
345
 
345
346
  def get_tts_voice_list_v3(self) -> List[cloudlanguagetools.ttsvoice.TtsVoice_v3]:
346
347
  # returns list of TtsVoice_v3
@@ -353,17 +354,18 @@ class AzureService(cloudlanguagetools.service.Service):
353
354
  headers = {
354
355
  'Authorization': 'Bearer ' + token,
355
356
  }
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
357
+ response = requests.get(constructed_url, headers=headers,
358
+ timeout=cloudlanguagetools.constants.RequestTimeout)
359
+ response.raise_for_status()
360
+ voice_list = json.loads(response.content)
361
+ result = []
362
+ for voice_data in voice_list:
363
+ # print(voice_data['Status'])
364
+ try:
365
+ result.append(build_tts_voice_v3(voice_data))
366
+ except:
367
+ logger.exception(f'could not process voice for {voice_data}')
368
+ return result
367
369
 
368
370
  def get_translation(self, text, from_language_key, to_language_key):
369
371
  base_url = f'{self.url_translator_base}/translate?api-version=3.0'
@@ -415,7 +417,10 @@ class AzureService(cloudlanguagetools.service.Service):
415
417
  return result
416
418
 
417
419
 
420
+ @cachetools.cached(cache=cachetools.TTLCache(maxsize=1024, ttl=cloudlanguagetools.constants.TTLCacheTimeout))
418
421
  def get_supported_languages(self):
422
+ max_retries = 3
423
+ retry_delay = 5 # seconds
419
424
  url = 'https://api.cognitive.microsofttranslator.com/languages?api-version=3.0'
420
425
 
421
426
  # If you encounter any issues with the base_url or path, make sure
@@ -427,10 +432,20 @@ class AzureService(cloudlanguagetools.service.Service):
427
432
  'X-ClientTraceId': str(uuid.uuid4())
428
433
  }
429
434
 
430
- request = requests.get(url, headers=headers)
431
- response = request.json()
432
-
433
- return response
435
+ for attempt in range(max_retries):
436
+ try:
437
+ request = requests.get(url, headers=headers,
438
+ timeout=cloudlanguagetools.constants.RequestTimeoutLong)
439
+ request.raise_for_status()
440
+ response = request.json()
441
+ return response
442
+ except requests.exceptions.RequestException as e:
443
+ if attempt < max_retries - 1: # If not the last attempt
444
+ logger.warning(f"Request failed. Retrying in {retry_delay} seconds... (Attempt {attempt + 1}/{max_retries})")
445
+ time.sleep(retry_delay)
446
+ else:
447
+ logger.error(f"Max retries reached. Unable to get supported languages.")
448
+ raise # Re-raise the last exception if all retries failed
434
449
 
435
450
  # print(json.dumps(response, sort_keys=True, indent=4, ensure_ascii=False, separators=(',', ': ')))
436
451
 
@@ -475,6 +490,8 @@ class AzureService(cloudlanguagetools.service.Service):
475
490
  sound = pydub.AudioSegment.from_mp3(mp3_filepath)
476
491
  elif audio_format in [cloudlanguagetools.options.AudioFormat.ogg_opus, cloudlanguagetools.options.AudioFormat.ogg_vorbis]:
477
492
  sound = pydub.AudioSegment.from_ogg(mp3_filepath)
493
+ elif audio_format == cloudlanguagetools.options.AudioFormat.wav:
494
+ sound = pydub.AudioSegment.from_wav(mp3_filepath)
478
495
  wav_filepath = tempfile.NamedTemporaryFile(suffix='.wav').name
479
496
  sound.export(wav_filepath, format="wav")
480
497
 
@@ -647,4 +664,4 @@ class AzureService(cloudlanguagetools.service.Service):
647
664
 
648
665
  # return response[0]['translations'][0]['text']
649
666
 
650
-
667
+
@@ -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.4.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.4.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
 
@@ -204,11 +230,11 @@ class TestAudio(unittest.TestCase):
204
230
  self.verify_service_audio_language(source_text, Service.Amazon, AudioLanguage.fr_FR, 'fr-FR')
205
231
 
206
232
  def test_mandarin_google(self):
207
- source_text = '老人家'
233
+ source_text = self.CHINESE_INPUT_TEXT
208
234
  self.verify_service_audio_language(source_text, Service.Google, AudioLanguage.zh_CN, 'zh-CN')
209
235
 
210
236
  def test_mandarin_azure(self):
211
- source_text = '你好'
237
+ source_text = self.CHINESE_INPUT_TEXT
212
238
  self.verify_service_audio_language(source_text, Service.Azure, AudioLanguage.zh_CN, 'zh-CN')
213
239
 
214
240
  def test_azure_standard_voice_deprecated(self):
@@ -285,16 +311,15 @@ class TestAudio(unittest.TestCase):
285
311
  source_text = '老人家'
286
312
  self.verify_service_audio_language(source_text, Service.CereProc, AudioLanguage.zh_CN, 'zh-CN')
287
313
 
314
+ @skip_unreliable_clt_test()
288
315
  def test_mandarin_vocalware(self):
289
316
  # 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
317
 
293
318
  source_text = '你好'
294
319
  self.verify_service_audio_language(source_text, Service.VocalWare, AudioLanguage.zh_CN, 'zh-CN')
295
320
 
296
321
  def test_cantonese_google(self):
297
- source_text = '天氣預報'
322
+ source_text = self.CHINESE_INPUT_TEXT
298
323
  self.verify_service_audio_language(source_text, Service.Google, AudioLanguage.zh_HK, 'zh-HK')
299
324
 
300
325
  def test_cantonese_azure(self):
@@ -354,6 +379,7 @@ class TestAudio(unittest.TestCase):
354
379
  source_text = 'hello <break time="50ms"/>world'
355
380
  self.verify_service_audio_language(source_text, Service.Google, AudioLanguage.en_US, 'en-US')
356
381
 
382
+ @skip_unreliable_clt_test()
357
383
  def test_ssml_english_azure(self):
358
384
  # pytest tests/test_audio.py -k test_ssml_english_azure
359
385
  source_text = 'hello <break time="200ms"/>world'
@@ -475,6 +501,50 @@ class TestAudio(unittest.TestCase):
475
501
  audio_text = audio_utils.speech_to_text(self.manager, audio_temp_file, 'fr-FR', audio_format=cloudlanguagetools.options.AudioFormat.ogg_opus)
476
502
  self.assertEqual(audio_utils.sanitize_recognized_text(source_text), audio_utils.sanitize_recognized_text(audio_text))
477
503
 
504
+ def verify_wav_voice(self, voice: cloudlanguagetools.ttsvoice.TtsVoice_v3, text: str, recognition_language: str):
505
+ # assert that the wav format is in the list of supported formats
506
+ self.assertTrue(cloudlanguagetools.options.AudioFormat.wav.name in
507
+ voice.options[cloudlanguagetools.options.AUDIO_FORMAT_PARAMETER]['values'])
508
+ options = {cloudlanguagetools.options.AUDIO_FORMAT_PARAMETER: cloudlanguagetools.options.AudioFormat.wav.name}
509
+ audio_temp_file = self.manager.get_tts_audio(text, voice.service, voice.voice_key, options)
510
+ audio_utils.assert_is_wav_format(self, audio_temp_file.name)
511
+ audio_text = audio_utils.speech_to_text(self.manager, audio_temp_file, recognition_language, audio_format=cloudlanguagetools.options.AudioFormat.wav)
512
+ self.assertEqual(audio_utils.sanitize_recognized_text(text), audio_utils.sanitize_recognized_text(audio_text))
513
+
514
+ def test_azure_format_wav(self):
515
+ fr_voice = self.get_voice_by_service_and_name(Service.Azure, 'Denise')
516
+ self.verify_wav_voice(fr_voice, self.FRENCH_INPUT_TEXT, 'fr-FR')
517
+
518
+ def test_amazon_format_wav(self):
519
+ fr_voice = self.get_voice_by_service_and_name(Service.Amazon, 'Mathieu')
520
+ self.verify_wav_voice(fr_voice, self.FRENCH_INPUT_TEXT, 'fr-FR')
521
+
522
+ def test_elevenlabs_format_wav(self):
523
+ fr_voice = self.get_voice_by_lambda(Service.ElevenLabs,
524
+ lambda x: 'Charlotte' in x.name and x.voice_key['model_id'] == 'eleven_multilingual_v2')
525
+ self.verify_wav_voice(fr_voice, self.FRENCH_INPUT_TEXT, 'fr-FR')
526
+
527
+ def test_google_format_wav(self):
528
+ fr_voice = self.get_voice_by_lambda(Service.Google,
529
+ lambda x: AudioLanguage.fr_FR in x.audio_languages, assert_unique=False)
530
+ self.verify_wav_voice(fr_voice, self.FRENCH_INPUT_TEXT, 'fr-FR')
531
+
532
+ def test_naver_format_wav(self):
533
+ ko_voice = self.get_voice_by_lambda(Service.Naver,
534
+ lambda x: AudioLanguage.ko_KR in x.audio_languages, assert_unique=False)
535
+ self.verify_wav_voice(ko_voice, self.KOREAN_INPUT_TEXT, 'ko-KR')
536
+
537
+ def test_openai_format_wav(self):
538
+ ko_voice = self.get_voice_by_lambda(Service.OpenAI,
539
+ lambda x: AudioLanguage.en_US in x.audio_languages, assert_unique=False)
540
+ self.verify_wav_voice(ko_voice, self.ENGLISH_INPUT_TEXT, 'en-US')
541
+
542
+ def test_watson_format_wav(self):
543
+ en_voice = self.get_voice_by_lambda(Service.Watson,
544
+ lambda x: AudioLanguage.en_US in x.audio_languages, assert_unique=False)
545
+ self.verify_wav_voice(en_voice, self.ENGLISH_INPUT_TEXT, 'en-US')
546
+
547
+
478
548
  def test_google_voice_journey(self):
479
549
  service = 'Google'
480
550
  source_text = self.ENGLISH_INPUT_TEXT
@@ -592,12 +662,15 @@ class TestAudio(unittest.TestCase):
592
662
  source_text = 'Ich mag das Essen nicht.'
593
663
  self.verify_service_audio_language(source_text, Service.ElevenLabs, AudioLanguage.de_DE, 'de-DE')
594
664
 
665
+ @skip_unreliable_clt_test()
595
666
  def test_elevenlabs_japanese(self):
596
667
  self.verify_service_japanese(Service.ElevenLabs)
597
668
 
669
+ @skip_unreliable_clt_test()
598
670
  def test_elevenlabs_chinese(self):
599
671
  self.verify_service_chinese(Service.ElevenLabs)
600
672
 
673
+ @skip_unreliable_clt_test()
601
674
  def test_elevenlabs_korean(self):
602
675
  self.verify_service_korean(Service.ElevenLabs)
603
676
 
@@ -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'])
@@ -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'