From cfccf90382fbc8168683583498f196e170a8cfc1 Mon Sep 17 00:00:00 2001 From: rubynguyen1510 <50710444+rubynguyen1510@users.noreply.github.com> Date: Fri, 21 Jul 2023 21:17:36 +0000 Subject: [PATCH 01/11] Implement Text To Speech base --- python/aws-sandbox/.gitignore | 1 + python/aws-sandbox/polly.py | 20 ++ python/text-to-speech/.gitignore | 1 + python/text-to-speech/README.md | 72 ++++++ python/text-to-speech/main.py | 242 ++++++++++++++++++++ python/text-to-speech/requirements.txt | 3 + python/text-to-speech/results/aws.txt | 0 python/text-to-speech/results/azure.txt | 1 + python/text-to-speech/results/google.txt | 1 + python/text-to-speech/test_main.py | 276 +++++++++++++++++++++++ 10 files changed, 617 insertions(+) create mode 100644 python/aws-sandbox/.gitignore create mode 100644 python/aws-sandbox/polly.py create mode 100644 python/text-to-speech/.gitignore create mode 100644 python/text-to-speech/README.md create mode 100644 python/text-to-speech/main.py create mode 100644 python/text-to-speech/requirements.txt create mode 100644 python/text-to-speech/results/aws.txt create mode 100644 python/text-to-speech/results/azure.txt create mode 100644 python/text-to-speech/results/google.txt create mode 100644 python/text-to-speech/test_main.py diff --git a/python/aws-sandbox/.gitignore b/python/aws-sandbox/.gitignore new file mode 100644 index 00000000..b47515cb --- /dev/null +++ b/python/aws-sandbox/.gitignore @@ -0,0 +1 @@ +secret.py \ No newline at end of file diff --git a/python/aws-sandbox/polly.py b/python/aws-sandbox/polly.py new file mode 100644 index 00000000..c53cb71b --- /dev/null +++ b/python/aws-sandbox/polly.py @@ -0,0 +1,20 @@ +import boto3 +import secret +import base64 + +polly_client = boto3.Session( + aws_access_key_id=secret.aws_access_key_id, + aws_secret_access_key=secret.aws_secret_access_key, + region_name='us-west-2').client('polly') + +t = "Hello, this is a sample text to be converted into speech." + +response = polly_client.synthesize_speech( + VoiceId='Joanna', + OutputFormat='mp3', + SampleRate='8000', + Text=t, + TextType='text', + ) + +print(base64.b64encode(response['AudioStream'].read().decode())) \ No newline at end of file diff --git a/python/text-to-speech/.gitignore b/python/text-to-speech/.gitignore new file mode 100644 index 00000000..b47515cb --- /dev/null +++ b/python/text-to-speech/.gitignore @@ -0,0 +1 @@ +secret.py \ No newline at end of file diff --git a/python/text-to-speech/README.md b/python/text-to-speech/README.md new file mode 100644 index 00000000..fdcaf5f0 --- /dev/null +++ b/python/text-to-speech/README.md @@ -0,0 +1,72 @@ +# 🌐 Send HTTP request + +A Python Cloud Function for sending an HTTP request. Supported options are method, url, body, and headers. Status code, response body, and response headers are returned. + +_Example input:_ + +```json +{ + "url":"https://demo.appwrite.io/v1/locale/countries/eu", + "method":"GET", + "headers": + { + "x-client-version":"1.0.0" + }, + "body":"" +} +``` + +_Example success output:_ + + +```json +{ + "success":true, + "response": + { + "headers":{}, + "code":200, + "body":"{\"total\":27,\"countries\":[]}" + } +} +``` + +_Example failure output:_ + + +```json +{ + "success":false, + "message":"URL could not be reached" +} +``` + +## 📝 Environment Variables + +No environment variables are required to run this function. + +## 🚀 Deployment + +1. Clone this repository, and enter this function folder: + +``` +$ git clone https://github.com/open-runtimes/examples.git && cd examples +$ cd python/send-http-request +``` + +2. Enter this function folder and build the code: +``` +docker run --rm --interactive --tty --volume $PWD:/usr/code openruntimes/python:3.10 sh /usr/local/src/build.sh +``` +As a result, a `code.tar.gz` file will be generated. + +3. Start the Open Runtime: +``` +docker run -p 3000:3000 -e INTERNAL_RUNTIME_KEY=secret-key -e INTERNAL_RUNTIME_ENTRYPOINT=main.py --rm --interactive --tty --volume $PWD/code.tar.gz:/tmp/code.tar.gz:ro openruntimes/python:3.10 sh /usr/local/src/start.sh +``` + +Your function is now listening on port `3000`, and you can execute it by sending `POST` request with appropriate authorization headers. To learn more about runtime, you can visit Python runtime [README](https://github.com/open-runtimes/open-runtimes/tree/main/runtimes/python-3.10). + +## 📝 Notes + - This function is designed for use with Appwrite Cloud Functions. You can learn more about it in [Appwrite docs](https://appwrite.io/docs/functions). + - This example is compatible with Python 3.10. Other versions may work but are not guaranteed to work as they haven't been tested. \ No newline at end of file diff --git a/python/text-to-speech/main.py b/python/text-to-speech/main.py new file mode 100644 index 00000000..7b72e9df --- /dev/null +++ b/python/text-to-speech/main.py @@ -0,0 +1,242 @@ +# Standard library +import base64 +import abc +import json + +# Third party +import requests +from google.cloud import texttospeech +import azure.cognitiveservices.speech as speechsdk +import boto3 + +# Local imports +import secret + + +class TextToSpeech(): + """Base class for Text to Speech.""" + def __init__(self, req: requests) -> None: + """Initialize class method.""" + self.validate_request(req) + + @abc.abstractmethod + def validate_request(self, req: requests): + """Abstract validate request method for providers.""" + + @abc.abstractmethod + def speech(self, text: str, language: str) -> bytes: + """Abstract speech method for providers.""" + + +class Google(TextToSpeech): + """This class represents the implementation of Google text to speech.""" + api_key = None + project_id = None + + def validate_request(self, req: requests) -> None: + """ + This method validates the request data for Google text to speech. + + Input: + req (request): The request provided by the user. + + Raises: + ValueError: If any required value is missing or invalid. + """ + if not req.variables.get("API_KEY"): + raise ValueError("Missing API_KEY.") + if not req.variables.get("PROJECT_ID"): + raise ValueError("Missing PROJECT_ID.") + self.api_key = req.variables.get("API_KEY") + self.project_id = req.variables.get("PROJECT_ID") + + def speech(self, text, language) -> bytes: + """ + Converts the given text into speech with the Google text to speech API. + + Input: + text: The text to be converted into speech. + language: The language code (BCP-47 format). + + Returns: + bytes: The synthezied speech in bytes. + """ + # Instantiate a client. + client = texttospeech.TextToSpeechClient(client_options={"api_key": self.api_key, "quota_project_id": self.project_id}) + # Set the text input to be synthesized. + synthesis_input = texttospeech.SynthesisInput(text=text) + # Build the voice request, select the language code ("en-US") and the ssml voice gender is neutral. + voice = texttospeech.VoiceSelectionParams(language_code=language, ssml_gender=texttospeech.SsmlVoiceGender.NEUTRAL) + # Select the type of audio file you want returned. + audio_config = texttospeech.AudioConfig(audio_encoding=texttospeech.AudioEncoding.MP3) + # Perform the text-to-speech request on the text input with the selected voice parameters and audio file type. + response = client.synthesize_speech(input=synthesis_input, voice=voice, audio_config=audio_config) + return response.audio_content + + +class Azure(TextToSpeech): + """ + This class represents the implementation of Azure text to speech. + """ + api_key = None + region_key = None + + def validate_request(self, req: requests) -> None: + """ + This method validates the request data for Azure text to speech. + + Input: + req (request): The request provided by the user. + Raises: + ValueError: If any required value is missing or invalid. + """ + if not req.variables.get("API_KEY"): + raise ValueError("Missing API_KEY") + if not req.variables.get("REGION_KEY"): + raise ValueError("Missing region") + self.api_key = req.variables.get("API_KEY") + self.region_key = req.variables.get("REGION_KEY") + + def speech(self, text, language) -> bytes: + """ + Converts the given text into speech with the Google text to speech API. + + Input: + text: The text to be converted into speech. + language: The language code (BCP-47 format). + + Returns: + bytes: The synthezied speech in bytes. + """ + # Set the speech configuration to speech key and region key. + speech_config = speechsdk.SpeechConfig(subscription=self.api_key, region=self.region_key) + # The language of the voice that speaks. + speech_config.speech_synthesis_language = language + # Set the speech. + speech_synthesizer = speechsdk.SpeechSynthesizer(speech_config=speech_config, audio_config=None) + # Response for the speech synthesizer. + response = speech_synthesizer.speak_text_async(text).get().audio_data + return response + + +class AWS(TextToSpeech): + """ + This class represents the implementation of AWS text to speech. + """ + api_key = None + secret_api_key = None + + def validate_request(self, req: requests) -> None: + """ + This method validates the request data for AWS text to speech. + + Input: + req (request): The request provided by the user. + Raises: + ValueError: If any required value is missing or invalid. + """ + if not req.payload.get("API_KEY"): + raise ValueError("Missing API_KEY.") + if not req.payload.get("SECRET_API_KEY"): + raise ValueError("Missing SECRET_API_KEY.") + self.api_key = req.payload.get("API_KEY") + self.secret_api_key = req.payload.get("SECRET_API_KEY") + + def speech(self, text, language) -> bytes: + """ + Converts the given text into speech with the AWS text to speech API. + + Input: + text: The text to be converted into speech. + language: The language code (BCP-47 format). + + Returns: + bytes: The synthezied speech in bytes. + """ + polly_client = boto3.Session(aws_access_key_id=self.api_key, aws_secret_access_key=self.secret_api_key, region_name="us-west-2").client("polly") + response = polly_client.synthesize_speech(VoiceId="Joanna", OutputFormat="mp3", Text=text, LanguageCode=language) + return response["AudioStream"].read().decode() + + +def validate_common(req: requests) -> tuple: + """ + This function validates the common fields in the request data + that are independent of the text-to-speech provider. + + Input: + req (request): The request provided by the user. + + Returns: + (tuple): A tuple containing the text and language from the request. + + Raises: + ValueError: If any of the common fields (provider, text, language) + are missing in the request payload. + """ + # Check if the payload is empty. + if not req.payload: + raise ValueError("Missing payload") + + # Check if variables is empty. + if not req.variables: + raise ValueError("Missing variables.") + + # Check if provider is empty. + if not req.payload.get("provider"): + raise ValueError("Missing provider") + + # Check if text is empty. + if not req.payload.get("text"): + raise ValueError("Missing Text.") + + # Check if language is empty. + if not req.payload.get("language"): + raise ValueError("Missing Language.") + + # Return the text and langage. + return (req.payload.get("text"), req.payload.get("language")) + + +IMPLEMENTATIONS = { + "google": Google, + "azure": Azure, + "aws": AWS, +} + + +def main(req: requests, res: json) -> json: + """ + Main Function for Text to Speech. + + Input: + req(request): The request from the user. + res(json): The response for the user. + + Returns: + (json): JSON representing the success value of the text to speech api + containing the synthesized audio in base64 encoded format. + """ + try: + text, language = validate_common(req) + provider_class = IMPLEMENTATIONS[req.payload.get("provider")](req) + + except (ValueError) as value_error: + return res.json({ + "success": False, + "error": f"{value_error}", + }) + try: + audio_stream = provider_class.speech(text, language) + except Exception as error: + return res.json({ + "success": False, + "error": f"{type(error).__name__}: {error}", + }) + + # f = open("python/text-to-speech/results/azure.txt", "w") + # f.write(base64.b64encode(audio_stream).decode()) + + return res.json({ + "success": True, + "audio_stream": base64.b64encode(audio_stream).decode(), + }) \ No newline at end of file diff --git a/python/text-to-speech/requirements.txt b/python/text-to-speech/requirements.txt new file mode 100644 index 00000000..2105adb5 --- /dev/null +++ b/python/text-to-speech/requirements.txt @@ -0,0 +1,3 @@ +boto3==1.28.9 +azure-cognitiveservices-speech==1.24.0 +google-cloud-texttospeech==2.14.1 \ No newline at end of file diff --git a/python/text-to-speech/results/aws.txt b/python/text-to-speech/results/aws.txt new file mode 100644 index 00000000..e69de29b diff --git a/python/text-to-speech/results/azure.txt b/python/text-to-speech/results/azure.txt new file mode 100644 index 00000000..03ba5215 --- /dev/null +++ b/python/text-to-speech/results/azure.txt @@ -0,0 +1 @@ +UklGRqavAABXQVZFZm10IBIAAAABAAEAgD4AAAB9AAACABAAAABkYXRhgK8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAQAAAAAAAQABAAAAAAAAAP////8AAP///////wAA////////////////AAAAAP////////////8AAAAAAAAAAAAAAAD//wAA//8AAP////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABAAEAAQABAAAAAQABAAAAAQABAAEAAQABAAEAAQABAAEAAgABAAEAAQABAAAAAAAAAAAA/////wAA/////wAAAAAAAAAAAAAAAAAA/////////////////////wAAAAAAAP//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABAAAAAAAAAAAAAQABAAEAAQAAAAEAAAAAAAAAAAAAAAAA////////////////AAABAAAAAAAAAAAAAAAAAAEAAQAAAAAAAAD+//7//v/+/////v/+//3//v////7//v/+//////////////8AAAAAAAAAAAEA/////wAAAAABAAAAAAAAAAIAAgACAAMAAwAFAAMAAwAEAAMAAwADAAMAAgACAAEAAQABAAAAAQAAAAAA///////////////////////////+//7//f/9//3//f/9//3//f/8//3//f/9//3//v/+//3//v/9//7//v////////8AAAAAAAD/////AAAAAAEAAQABAAEAAQABAAEAAQACAAIAAgADAAMAAgACAAIAAgABAAEAAAABAAIAAgACAAIAAQABAAIAAgACAAIAAgABAAEAAQACAAEAAQACAAIAAQACAAEA//////7////+//3//f/8//z//f/+//3//f/9//z/+//8//z//P/9//3//v/+//7//v/////////+////AAD//wAAAAAAAAAAAAAAAP//AAAAAAAAAAAAAAEAAAAAAAEAAQABAAIAAQABAAAAAQABAAEAAQABAAAAAAAAAP///v/+//7//v/+//7//v/+//7//////////////wAAAAAAAAAA//8AAAAAAAABAAAAAQABAAAAAAAAAAEAAQABAAEAAQAAAAEAAQABAAEAAAAAAAAAAQAAAP///v////7//f/9//z//P/7//z//f/9//7//v/+////AAAAAAAAAAAAAAEAAQACAAIAAQABAAAAAAD///////8AAAEAAAAAAAAAAAAAAAIAAgADAAMAAwAEAAMAAwAEAAMAAwAEAAMAAQAAAAAAAAAAAAAAAAD//wAA//8AAP////////3//v/9//3//f/9//3//v/+//3//v/+//3//v/9//3//P/8//3//v////////////7//v/+//3//f/+//3//f/9//7//v/+/wAAAAAAAAEAAQAAAAEAAAAAAAAAAAABAAAAAAAAAAEAAQABAAEAAAABAAAAAQABAAAAAAAAAAEAAQACAAEAAQABAP///////////v////7//v///////////wAAAAAAAAEAAQAAAAAAAAD///7///////////////3//v/+//3//f/9//7/AAABAAEAAQABAAEAAgACAAIAAgACAAIAAgADAAIAAwADAAMAAwACAAEAAAABAAEAAQABAAIAAgABAAIAAgABAAIAAQAAAP////////7//f/+//3//f////7////+//7//f/8//z//P/9//z//f/+//7//f/9//7//f/9//3//f/+//3//f/9//3///8AAAAAAAAAAAAAAAD//wAAAQAAAAAAAQAAAAAAAAAAAP//AAABAAAAAQAAAAAAAAD+////AAAAAAAA//8BAAEAAAACAAEAAQACAAEAAQABAAAAAQABAAAAAQABAAIAAQABAAEAAQABAAEAAQAAAAAAAAAAAP7//v////7//f/+//3//P/9//3//f/9//z//f/9//3////9//3//P/+/////v8AAAEAAgABAAEAAQAAAAAAAAABAAEAAgABAAIAAwADAAUABAAEAAQABAAFAAQABQAEAAMAAwADAAMAAQABAAAAAAAAAP///v/8//3//f/9/////v/+/////v/+/////v/8//7//f/9//3//P/9//7//P/9//3/+//9//3//f/+//7/////////////////AAAAAP7/////////AAD///7//v/+//7//f/9//7//v/+/////////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAgABAAIAAgACAAMAAgADAAIAAQABAAAAAQABAAEAAAABAAAAAAD///7//v/+/////v/+//7//v///wAAAAD//wAAAQAAAAAA/////////v8AAAEA//8AAAAAAQAAAAAAAAD//wEAAQADAAQAAwADAAMABAADAAMAAwACAAIAAAAAAP///v/+//3//v/+//3//P/8//3//v/9//7/AAD///3//v/+//3//f/9//7//v/////////+////AAAAAAAA/////wAA///+/wAAAAAAAAAAAAABAP/////+//7////9//7//v/+/wAAAAD//wAA///////////+//////////7//P/+/////v///wAAAAD//wAAAAAAAAAA//8BAAEAAQACAAIAAwAEAAMAAwADAAMABAADAAIAAQABAAIAAgABAAEAAAAAAAAA//8AAP///f/+//3//f/9//3//v/+//7//f/9//3//v////7///8AAP////8AAAAAAAAAAAAAAAAAAAAAAAABAAEAAQACAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQACAAEAAQABAAEAAAAAAAAAAQAAAP//AAD//wAAAQD//////v/+//z/+//+//3//P/9//3//f/9//3//P/8//z//P/9//z/+//8//z/+//5//r/+v/6//v/+//7//z//P/9//7//v8AAAAAAAABAAIAAQABAAMAAgACAAMAAwADAAIAAwAFAAUABAAFAAQABQAEAAQABQAEAAQAAwADAAUABQADAAQAAwACAAEAAQABAAEAAQAAAP///f/9//7//v/+//z//P/8//3////+//////////7//P/8//v/+v/7//3//P/8//7//P/9//3//P/9//3//v/+//3//f//////AAD/////AAD+//////////7///8BAP////8AAAEAAAACAAMAAAABAP7///8AAAAAAQD+/wAAAAAAAAIAAQABAP/////////////+/wAAAAAAAAEAAAAAAAIAAQABAAAA/v/+//7//v/7//r/+v/7//3//f8AAAEA//8AAAAAAAABAP///f8AAAAA//8BAAEAAAABAAAA//8CAAIAAwADAAEAAQAAAAAA//8AAP//AAABAP//AQAAAP//AgACAAAA///+/wAAAQD+//7////9//z//f/9//z//v/+//3//v/+/////v/+//7//v8AAAEA//8AAAEAAAACAAEAAAD+//7////8//3//f/6//r//P8AAAAAAQACAAIAAAABAAIAAAAAAP//AAAAAAAA///9/wEA///+/////v/9////AQD+/wAAAwADAAEA//8AAAIAAgABAAMAAQADAAIAAAABAP//AAAAAAAAAAD//wIAAgACAAEA/f/+/////P/9/////f//////+//9//3//P/7//z//v8AAAAAAQABAP///f/8//r/+v/+//3//P/+//z//v8CAAEAAQACAAIAAgADAAAAAAACAAIABAACAAEAAwAFAAQABQADAAEAAgAAAAMA/v/+/wEAAAADAAEAAwAFAAMAAQD//wEA/////wEAAQAAAAAAAAD+///////9//z//P/7//z/+//7//z/+/8BAAEAAQADAP//AAD///z/+//8//r/+//9//z//v/9//z////+/////v/8//3//P////3/+//+////AwACAAAAAgAAAAEAAwADAAEAAwADAP//BQADAAMABAAAAAIA/v///wEAAAAFAAUABAACAAEA/v/9//3//v8AAP3///8AAP///P/+/////f/+//z/AAD//wAAAQD9//z/+v/8//z//v8EAAAA//8CAAAA//8CAAEAAQABAAAAAgD/////AQAAAP////8BAAEAAgABAAIABQAEAAYAAwD//wAA///9//3///8AAAEAAAD+/wEA/f/5//v//P8AAAAAAwAFAAQABAD+/wAAAQD9/////v8CAAIAAgADAP7/AAAAAP3//f////////8DAAMAAQAAAP///f8BAAAA/v8CAP3////+//v////7//3//v/5//3////+/////v/+/wAAAAD7//3/AwACAAgAEgAPAAkACAAEAAEA/f8AAP//AAACAPr///////j/+P/8//7/AwAEAAEAAQD6//7/+//9/wQAAAAIAAUAAgADAAAADQAHAP//AQDz//P/7v/0/wAA+/8BAPX/9f/0//b////z//r//P///wIABAALAAsADgABAPX/+v/8//H//v8JAAAAAwAIAAgA9v/v//f/9/8AAAQADQATAPz/9f/9/wYADwADAAQA/v8KABoADQASAAgA9/8LABEAAwAGAP3/+v/z//D/+//v//7/AAAGAPb/3f8AAPj/+P8VAAUA/P/9/+j/6//z/wAACgD//woADQAKAAMABgAGAP3/6P/r/wwA9P/4/wIAEAAeAAoAFgAIAPf/7v/t/wMACwAMAAIA9P/d/9f/AQAGAPH/BQAcACYADQDy/+j/5v/r//z/EwAgADkAHgAKAAIA8f/r/9//BQACAP3/EAAmACwA8//T/9v/+f/+/wU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/of/C/7L/vP/k/93/3v/K/9j/5f/S/9X/zf/J/8P/9f84ABYAAwAeADQAQQBSAGMATwAvACsASABSAFkAUwBrAHIAUABmAH0AugDWAMUArAB6AI8A0ADUAI4ATgApAEgAcgBvAEAABwAMAD8AbABBACUAMQBIAF4AVgAsAPD/7v/0/w4A+/+9/6X/vf/H/67/xv/S/9r/wf+y/8j/3//+/wMA4f+Y/6j/6P/9/87/m/+s/87/2f+6/6//sv/O/+b/3v+3/5b/wf/s/+v/w/+m/6L/sv/Z//b/1/+Z/6P/3/8HAPv/0v+k/7H/8P8fAPz/uv+5/+L/IQApAAwA5f/f/xgATgBaADEAEgAcAEQAQwBEAEUAFgAVAEUAYwA3ABAAGAAqADMAMgAyABgA9P8SAE8ATAAmAAQAKgBHAD8AVwBMADYAHgAyAEgALQALAAAADADm/8L/vf/M/8X/uf/C/6r/r//T/////v/Z/+L/9P8TAC8AGAD5/+j/4v/m/+r/2v/I/8j/tv/C/+3/+P/3/+7/9f/1/wUAKgAkABQA8P/p/wEA/v8IAPr/0//A/9//DgAKAPX/9v/1//D/AwAkABgA5//f/+n/2//g//3/EAD5/9L/5f8WADYAPgA1ACAA8/8PAGMAYgAiAO//7/8JAB0AIwAZAA0AAAAPACAAFwAbADIANgAYAAMABwAaADIAKQD//97/4P/7/wkA/P/e/8T/y//i/+z/7P/l/9z/4P/t//H/9f/9//7/8v/e/9n/8v8SABEA+f/f/+X/AAARACIADgDg/97/AgAhABgA9P/l/+b/9P8eAC4AFAANACAAQgBSADwANAA5AD4ARgAzABUAAQD+////6//b/9P/z//H/7//zP/h/+//9P/2/+7/7f8NABoACQAAAP//DQANAAQACAABAAIABAD//wsABgACAAsABQAEAPb/4v/i/+P/6f/p/+D/3f/f/+v/8//y//H/9v8MABwAEQAFAAsAHwAmAA4A8f/m/+3/9/////n/4f/U/+X/9//3//P//P8MABEADQALAAgADwAUABQADwD//wUAHgApACcAGAAXACoAOQA/ACcADwAMABYAHAD///L/7//f//L/9P/k/9//1f/k/+z/6P/5//j/7P/p/+z/+P/1/+v/6v/v////BQAEAAQA/P8DAA0ABgACAAEA+P/w//X/+//2/+n/5P/p/+v/BAAdABMABwAPACIAIAATABQADgAGAPn/7f/r/+f//P8NAP//+P/8/xEAJwAjAB4AIQARAAUAEgATAAMA9f/n/+L/5//1//r/9//0//L/AQALAAAA//8IAAYA+//s/+f/7//z//r//P/x/+j/8P8PABsADAACAAUAFwAkAB4ACwD3//P/AgARAAMA6P/k//f/CgAHAAAA9v/v/wkAGAAPAAcA/f///w8AIAAYAAoADgAUABwAFQAKABUAIQAbAA4A/v/2/wcAGQAWAPr/4f/x/xYAHgANAP3/8P/y/wUACwD9/+X/4P/y//T/7P/p/+f/4v/j/+b/4v/o//T/8v/u/+n/4//t//3/+//z/+r/5P/1//7/9//3//H/8////wsAFgAOAAEABAAOABMAEgANAAIA+//5/wAA///0//r/BQAJAAcABQAIABAAFAAOAAkADAATABAAAgD6//7/CAAMAAcAAAABAAsAFwAZABEAEQAUABUAGAASAA0ACwABAPz/+//3//n/+v/6//j/9//3//7/AgD4//H/9f8BAAIA9v/x/+7/9/8DAP7/9v/0//r/AwAGAAYABAACAAMAAgABAAYADAAMAAoACAACAAEABwAMABMADQACAP7/AAAOABgAEQADAP7/AQACAAgABwD8//v/+f/4//7//f/6//v//f/7//T/9//+//7/+f/w//D/9P/y//D/8P/w//D/7//2//v/9P/0//7//v/7//3/AAD6//X/9//5//n/+f/z/+//7//x//r//////wAA+/8AAAsACgAFAAcADQANAAYACAAIAAEABAAGAAEA+//2/wAABwAFAAQABAACAAAABQANAAoABwAMAAwACwAHAAgADQAFAAEAAwAHAAgA/////wQAAAD+/wAAAAD9//7/BwAJAAgACAADAAUACgAIAAgAAwD//wAAAwAAAP3//v/9//7////8//7/AwACAAIAAwAFAAYAAAD9//7///8BAAAAAQD///z///8CAAIAAAD///7//P///wAA///+//3//f/8//n/+v/7//7//f/7//v/+f/5//r//P////z/+v/7////AQACAAIA///9//3//f/9/wAAAQD///z/+//7//v//P/9//z/9//3//b/9v/9//v/9f/1//b/+P/6//z//P/7//z//f/+/wAAAgACAAUABgAEAAcADgAPAA4ADAAJAAwACwALAAsABQAEAAcABwAFAAIAAQACAAIABAABAP//AwADAAAA/v/9//7/AAACAAEAAQABAAMABAADAAIAAQAAAAEA/P/3//v/+//7//3/9//5/////v8BAAAA/P///wAAAAAFAAgABAABAAMAAwAAAAIABQAFAAIAAAAAAAIAAgD//wAA/f/6//3//v/9//v/+//9//3/AAAAAP//AAAAAAIAAwACAAIAAQAAAAIAAwAAAPz//v///wAABAACAAAAAQAAAAIAAAD7//3/AgADAAMAAQAAAAAA/v/+/////f/8//7//f/8/wAA///9//r/+v/9/////f/6//v/+f/3//z////9//z//v/+//3//P8AAAEAAAABAAEA/v8AAAMAAQABAAEAAAACAAMABQADAAIABAAAAAMACQAJAAcABgAEAAYABgADAAQABAADAAEAAAAAAP7//////wEAAgABAAQABgACAAAA///+/wAAAQAEAAIA//8AAP3/AAD9//v/AAAAAAAAAQAAAAEAAQD//////f///wAA/f/+/wAAAAAAAAIAAwABAAEA/v/9//3//P/+////AAACAAIAAAD///7///////7////+/////v/9//z//f/9//7/AQABAAIAAwACAAEAAAAAAAEAAQAAAAIAAgAAAAIABQAEAAIAAAABAAMAAQABAP//AAADAAAAAAABAAEAAQAAAAAAAQAAAAEAAAAAAAEA/v///////v////////////3///////7//f/+/////v/9////AAAAAP3//f8AAAAAAAABAAAAAAD//wAAAQAAAAIAAAD//wIAAgAAAAAAAAAAAAEAAgADAAQABAACAAIAAwACAAQAAwABAAMAAgABAP////8AAP///v/9//7//////wEA/////wAA//8BAAMAAQAAAAEAAAADAAQAAgABAAEAAAABAAEAAAAAAP//AAAAAAIA///8//7//P/9//7//v/9//v//v/9//3//f/+/wAAAAD//wAAAAD//wAA//8BAAIAAAAAAP////8AAAAAAAAAAAEAAgACAAIABAACAAIAAgAAAAIAAwABAAEAAAD//wEAAQABAAEA/////wAAAAAAAAEAAQABAAAAAAABAAAAAAAAAP////8AAAAAAAAAAP//AAABAAAAAAAAAP//AAAAAAAA/////wAAAQABAAAAAAABAAEAAQABAAAAAAABAAEAAAD/////AgABAAEAAAAAAAAAAAABAAAAAQABAP//AAAAAAAAAAD//wAAAAAAAAAAAAD///////////7//v8AAAAAAAD//wAA//8AAAAA/v////7///////7//////wAAAAAAAAIAAAAAAP//AAABAAEAAAABAAMAAgACAAIAAgABAAEAAwABAAMABAACAAEAAQABAAAAAAAAAAAA//8AAAAAAAAAAAAAAAAAAAAA/////wAA/////////v////////8AAAAAAQABAAEAAQABAAAAAAAAAP////8AAAAAAAAAAP//AQAAAAAAAQAAAAAAAAD/////AAAAAP///v8AAAAAAAABAP///v8AAAAA//////////8AAAEAAQAAAAAAAAAAAAEAAgACAAEAAQABAAEAAQABAAAAAAABAAAAAAABAAAAAAABAAAAAAAAAP//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAD+//3//f/9//3//P/9//7//v///wAAAAAAAAEAAQAAAAAAAAABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQD//wEAAAAAAAAAAAAAAAAABAAAAAIAAQABAAMAAQACAAEAAQADAAIAAQAAAAAAAQD/////AAD//wAAAAAAAAAAAAAAAP7/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///v////7/////////AQAAAP///v///wAA//////7//v/+//7//////wAAAAAAAP//AAABAAAAAAAAAAAAAAAAAAAA//8AAP////8AAAAAAAAAAAAAAAAAAAAAAAAAAAEAAQABAAAAAQABAAAAAAAAAAAAAAD//wAAAAD//////v/+//7//f/+//3//v////7//////////v///wAA/v/+//7//v8AAAAAAAAAAAAAAAAAAP//AgABAAEAAwAEAAUABQAFAAQABAAFAAQABQAEAAQABAAEAAQAAgADAAMAAgADAAEAAgABAAAAAQD///////8AAP////////7///////3//f/+//z///////3////8/////v/+//7//f/+//3//v/9//3//v/+//3//v/+//3////+//7/AAAAAAAA/v8AAAAA//8AAP//AAAAAAAAAAD/////AAD///////8AAAAA//8AAP7//v8AAAAAAAAAAAAAAAABAAAAAQABAAEAAAACAAMAAQABAAIAAQACAAEAAAACAAEAAAAAAAAAAQAAAP//AAAAAAAAAAAAAAAAAAAAAP//////////AAD+/////////////f////7///////7/AAD/////AAD//wAAAAAAAAEAAQAAAAEAAAABAAMAAQACAAIAAQADAAIAAQADAAIAAQADAAIAAAABAAEAAAAAAAEAAQABAAAAAAAAAAEAAAAAAAAA//8AAP7/AAAAAP//AAAAAAAAAAAAAP///////wAAAAAAAP////////7///////7/AAAAAAAAAAAAAP//AAAAAAAA//8AAAAAAAAAAP//AAD+////AQAAAAAAAAAAAAAAAAAAAP//////////AAAAAP7////+/wAAAAD+////AAAAAAEAAAAAAAAAAAABAAEAAAAAAAAAAAAAAAAAAAD//wAAAQAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAP//AAABAP/////+//3//v/+//3////+//7///////////8AAAAAAAAAAP//AAAAAAAAAAD//wAAAgABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQABAAAAAAACAAIAAgABAAAAAAACAAIAAQAAAAAAAQAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAP7///////7//P/9//7//f/+//7//v/+//7//v/+//////////7////9//7//////////v/+///////+/wAA/////wAA/v////////8AAAAAAAAAAAAAAAAAAP//AQADAAIAAQADAAQAAQACAAEAAAAAAAAAAAABAAIAAgADAAMAAAABAAIAAAABAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////AAAAAAAAAAAAAAAA//8AAAAA/////wAAAAAAAP////////7//v/+//7//v///wAA//////////8AAP///////////////////////wAAAAAAAP////8AAP//AAAAAAAA//8AAAAAAAABAAEAAAABAAEAAAABAAEAAQABAAEAAgABAAAAAAAAAAAAAAAAAAAAAQAAAAEAAQAAAAEAAQABAAAAAAABAAAAAAAAAAAAAQABAAAAAAABAAAAAAABAAAAAQABAAAAAQAAAAAAAAAAAAAAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AAAAAAAAAAD//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== \ No newline at end of file diff --git a/python/text-to-speech/results/google.txt b/python/text-to-speech/results/google.txt new file mode 100644 index 00000000..19909b25 --- /dev/null +++ b/python/text-to-speech/results/google.txt @@ -0,0 +1 @@ +//NExAASoAEkAUAAAXwGQ/p/gDhGY/4fADM/x/ADAzMfwfAM6/o/AyB+Y/N4A7pkc+A7AyOeH/AMwZD/0+A4P8/4DgDo/4fADo/w/gBgZ4f4/AR63R+AhfA81AUf47wD//NExAgUiyowAZNoAIHidDXtgtAXBH/EmB2CZkr/4WsZZff/+tOHPLw8P/8S8Yc3RN3Hp//+XEEXQNzM3////qNHMzcwLiCac0/////zM3bWncwNC+boKhAsSKDAV90A//NExAgS4cKcAYNQAM+vABFdTDxmRCqimDwaEZaIkBAUgNs/NLnbGEhMNPzrVIFPvJm/ONfYwkE+Ri5l/+Y11WLnAAXV+n6HjBQmc//+tLaagQESJOt3hyTZiQnjMvw6//NExA8TMWKoAc9IAFYEPV3dawoSzDBkasnJWCooQpTjVuz/P9/zz/3/fWQ/j3LQPH1nZe+++0EQcPlKrkoFWGBCMftpbLOTvFRagKXtjORQvVLmEjQcfnk70D7pU44b//NExBUWuTagAMvMcDtwm6jSR/sJNlQTAxWVzG+QdAJrAomaWUlU/yzNX+T3z//9m1medMBsUYkgeGBEYgFAbSFGO0OOAuQiizvfUOD0uoe6fx3u7JlQ3nZYLMs3UOao//NExA0VgWawAMYMlKw4YVjA12JJyT0fBI0uKzxl83MlCXgQieqwO2nyudv6pLP4Um/zt4973h+2fx+9/T03XRCSZPHJp1AIMjsoo0EPtWz/2F3AQwrvLoMXLesIVzrT//NExAoU8W60AMYQlCR77CH4FFF5QIEOLk6ALq4t1JeV0sKALV+zDpq9luT6/3VHl9Bz/sfzfp8L9+8XBrYdg8DUkB5lUJpkSKowQzVc3A1nctiP/xwx39CitfwVQwyz//NExAkSEVq8AMPMlGLWpsVa02Kdgz46LEOgRyRwm07wNAY0rsvMVTqaNSaNaBLqk1fdtfX/////d23VmoGgthZ1zc1CLJHC5Iwd+lXFogOmaGSeL2E8uLOd1kyECQp+//NExBMRIVK8AHvMlQ+zheivslKjEZU+DDLC2nWswY8a943vjWr43vb43b//vrM9MTpIBgqBA2uf6V1A6h+l3xIBQ8u7JU/BYnzwQGKPgGCQmpwHEcgkLWpzrH6vjRP4//NExCESeWa4AHvKlIeQc2jrfKlxfzTyRZtbrjOldd1/qlHFhpzCYsLsGi1GMPMokZxSeu/S3QHENVuJbVvNaxfC+qgvg1lYykKqXQlx/F4LkujlMU41Cca2Jqc4mA8q//NExCoQqS64AHvOcXi00oapjnJtRf/3Z0UdID4lF2jRXMIZNY3dcKxY4X0aFLncnn0n3LFmGErC9H2iDBLYrRNR/k9KxHHyrTdblCaBtgNCwYDxoh5Io7nL6O/+iW9R//NExDoSESasAMPOcKoPBwiJnnR4YDa///9iVbijphYgkTSaw1ltIChmtbtuzeBsrOSMQDMEwSHw4TiQwJKIqCMTAyENltitougYm95rITet3eZC3DO8SlmflZhSkg+c//NExEQSKVqkAMMElP9P//yq0quA2ohqww/Sw/UlkPxyOUTAkojm5Hgotq1QQFREOmA2uC/BAyCQPl0IJhsL6jyfheXFO5y8v7r7J02CGmoQLHQ2FHtO0////JC9kpDN//NExE4SqRakAMPScCRKAx25MZxaO1owbRoARBXA0NBUDMCC2kWrKyUXIVSDBKAoH4FC70CiFfHJt1BiN05fARmQVcPdKkv////91iPR/qXoEElSkhA0OySG4jFK0yWH//NExFYRYPagAMJScMyF0JMIDZKabcwpclYV40vrriFJiUA6fKsmuIzlQulz27tnbT7qFaOoHLmUBUjQKX////zPbSrEypUxIgujlFb0Tuy9/C+lWcqAYWnLdWNI/kta//NExGMRmQqcAMJYcLnXtaUtjdQNLSbQWGdFmSN5azCHw74RtZxtSY4yJlAYIgdH////Q7///0V+DkRhYkxqkghyLSmH5lyn/hxaroW4NooZzz1+HbM/s8/8bV+1eCpd//NExG8RqRKYAMPScOeDQBSwJiwJAqPhVEySSHCAUpszVSn2xcMjCRUBHTVn////pvX29P+UPPDM4HNw/TEZVLxQGpgXdRNjmgcfaU02He+SJPJVIcJhz54mlxax8Lmp//NExHsUQR6QAVhIACczCuCXAKIZYXQTwE/AgKzIyWcF8DOBGi6J6MMLQXygkPUexk7VLMiEamwww9lj2JUpGxK0tfmrLrSqrQuzf/91oqN0klHupa0Eta//1MnP9kTY//NExH0hEqJ4AZpoAdb/bIFpG9ZVgkPqlevVgL5Om9klcaBqFjLNI5RxmxKalnDHdnn/tMvtZtMc5Nevu7ekBeYHYRUIxFqupOqVicgAsbol8L5rXO29ad0wct7aU91A//NExEsbMWoUAdhgAGtcIFUxp9qDKHvUTbKkhEQJE6mXVi1Jo8oDXLJJUKKYp61BEa1KIwLTSqjjMMxqmrTCrDVCpUKoHway4xry8rjHfHNj8lKcSElSCwqRKCoVPQoc//NExDEYwUYAAMJScJaWBIEg0TJiklrylcY+TSjxUioGn15INPLA1iIeCrhMDT+uIpGdeInnmDXM5Iqtj6gaCYhmCuKPnnWWtyq4hGg+XJD7LTKIhLHF2NyeSiqtdXJV//NExCESkbWMAGJElCTXhrOxioh2//oq/KiqjtuUyKn/3KGBgwTMmQEK/xUW/+Kijf6haoWFVUxBTTXYUNNntBQ28i7Tc/GCN14Jo3UyUTcUPONmtTU0xF9lFfBVi5jj//NExCkAAANIAAAAAF5UP5IJoGgaCohx+SxSiw7F3fQXPuixe0kFAeGFnkGIji9wn/yWLDv6Abn5/oiIiIn//T4iJXcO9dw4GLeaF13d3Qrz+Cw8PH+8/4AH8AQnAABH//NExHwAAANIAAAAAIAYe072jOCP8zxCBSiPGaUSFnSoXydOnhkDLKzVx9ejtbWzWtGjkgVKZkCDo6C/j/4q5LBRR++nDZYbjvm2VZFRfG7hdWSy/Oacyi/v//lRJuX+//NExMwH4CQAAPe8AEuqxZZf4+F5V3m/Kb/6/GoAqLBUhFduI/76O27EYo8cL+NGlM7PUnCRQo8y4djSij4dpOEgQEfDtqiL/9nKinb5TBlI7OVP/+YoYGDIfULC4rFR//NExP8Y6f3sAHoGmVEj//rFcVFjVbP4qKcWTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//NExO4V0LIAAHpMTaqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//NExOkUQbmYAMGElKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq \ No newline at end of file diff --git a/python/text-to-speech/test_main.py b/python/text-to-speech/test_main.py new file mode 100644 index 00000000..3006a7e6 --- /dev/null +++ b/python/text-to-speech/test_main.py @@ -0,0 +1,276 @@ +# Standard library +import base64 +import unittest +import pathlib +from unittest.mock import patch + +# Third party +import requests +from parameterized import parameterized +from google.cloud import texttospeech + + +# Local imports +import main +import secret + +RESULT_GOOGLE = ( + pathlib.Path("python/text-to-speech/results/google.txt"). + read_text(encoding="utf-8")) + +# Path to krakenio encoded result (str). +RESULT_AZURE = ( + pathlib.Path("python/text-to-speech/results/azure.txt"). + read_text(encoding="utf-8")) + + +class MyRequest: + """Class for defining My Request structure.""" + def __init__(self, data): + self.payload = data.get("payload", {}) + self.variables = data.get("variables", {}) + + +class MyResponse: + """Class for defining My Response structure.""" + def __init__(self): + self._json = None + + def json(self, data=None): + """Create a response for json.""" + if data is not None: + self._json = data + return self._json + + +class GoogleTest(unittest.TestCase): + """Google API Test Cases""" + def setUp(self): + self.req = MyRequest({ + "payload": { + "provider": "google", + "text": "hi", + "language": "en-US", + }, + "variables": { + "API_KEY": secret.GOOGLE_API_KEY, + "PROJECT_ID": secret.GOOGLE_PROJECT_ID, + } + }) + self.google_instance = main.Google(self.req) + + def tearDown(self): + self.google_instance = None + + @parameterized.expand([ + ({"API_KEY": None, "PROJECT_ID": "123"},), # Missing API KEY + ({"API_KEY": "123", "PROJECT_ID": None},), # Missing PROJECT ID + ({"API_KEY": None, "PROJECT_ID": None},), # Missing Both + ]) + def test_validate_request(self, variables): + self.req.variables["API_KEY"] = variables["API_KEY"] + self.req.variables["PROJECT_ID"] = variables["PROJECT_ID"] + self.assertRaises(ValueError, self.google_instance.validate_request, self.req) + + def test_speech_happy(self): + """Test speech method for successful text-to-speech synthesis.""" + self.google_instance.api_key = "123" + self.google_instance.project_id = "123" + # Set up mock + with patch.object(texttospeech, "TextToSpeechClient") as mock_client: + mock_response = mock_client.return_value + mock_response.synthesize_speech.return_value.audio_content = base64.b64decode(RESULT_GOOGLE) + # Call the speech method + audio_stream = self.google_instance.speech("hi", "en-US") + # Assert the result + self.assertEqual(audio_stream, base64.b64decode(RESULT_GOOGLE)) + + def test_speech_error(self): + """Test speech method for unsuccessful text-to-speech synthesis.""" + self.google_instance.api_key = "123" + self.google_instance.project_id = "123" + # Variables + # Set up mock + with patch.object(texttospeech, "TextToSpeechClient") as mock_client: + mock_response = mock_client.return_value + mock_response.synthesize_speech.return_value.audio_content = b'A12' + # Call the speech method + audio_stream = self.google_instance.speech("hello", "en-US") + # Assert the result + self.assertNotEqual(audio_stream, base64.b64decode(RESULT_GOOGLE)) + + # def test_google_credential(self): + # self.google_instance.api_key = "INCORRECT_CREDENTIAL" + # self.google_instance.project_id = "INCORRECT_CREDENTIAL" + # # Set up mock + # with patch.object(texttospeech, "TextToSpeechClient") as mock_client: + # mock_client.side_effect = Exception + # self.assertRaises(Exception, self.google_instance.speech, "hello", "en-US") # Incorrect credentials raise exception + + # def test_google_language(self): + # with patch.object(texttospeech, "TextToSpeechClient") as mock_client: + # mock_client.side_effect = Exception + # self.assertRaises(Exception, self.google_instance.speech, "hello", "en-EN") # Incorrect language + # self.assertRaises(Exception, self.google_instance.speech, "hello", None) # Empty language code + + # def test_speech_text(self): + # with patch.object(texttospeech, "TextToSpeechClient") as mock_client: + # mock_client.side_effect = Exception + # self.assertRaises(Exception, self.google_instance.speech, None, "en-US") # Empty Text + + # def test_google_credential(self): + # self.google_instance.api_key = "INCORRECT_CREDENTIAL" + # self.google_instance.project_id = "INCORRECT_CREDENTIAL" + # self.assertRaises(Exception, self.google_instance.speech, "hello", "en-US") # Incorrect credentials raise exception + + # def test_google_language(self): + # self.assertRaises(Exception, self.google_instance.speech, "hello", "en-EN") # Incorrect language code + # self.assertRaises(Exception, self.google_instance.speech, "hello", None) # Empty language code + + # def test_speech_text(self): + # self.assertRaises(Exception, self.google_instance.speech, None, "en-US") + # self.assertEqual(b'', self.google_instance.speech("", "en-US")) + + +class AzureTest(unittest.TestCase): + """Azure API Test Cases""" + def setUp(self): + self.req = MyRequest({ + "payload": { + "provider": "azure", + "text": "hi", + "language": "en-US", + }, + "variables": { + "API_KEY": secret.AZURE_API_KEY, + "REGION_KEY": secret.AZURE_REGION_KEY, + } + }) + self.azure_instance = main.Azure(self.req) + + def tearDown(self): + self.azure_instance = None + + # def test_validate_request(self, req): + """Test validate_request method when all required fields are present.""" + # pass + +# def test_validate_request_missing_aws_access_key_id(self, req): +# """Test validate_request methsod when 'AWS_ACCESS_KEY_ID' is missing.""" +# pass + +# def test_validate_request_missing_aws_secret_access_key(self, req): +# """Test validate_request method when 'AWS_SECRET_ACCESS_KEY' is missing.""" +# pass + +# def test_speech(self, text, language): +# """Test speech method for text-to-speech synthesis.""" +# pass + +# def test_speech_key_exception(self, text, language): +# """Test speech method for handling exceptions during text-to-speech synthesis.""" +# pass + + +# class AWSTest(unittest.TestCase): +# """AWS API Test Cases""" +# def test_validate_request(self, req): +# """Test validate_request method when all required fields are present.""" +# pass + +# def test_validate_request_missing_aws_access_key_id(self, req): +# """Test validate_request method when 'AWS_ACCESS_KEY_ID' is missing.""" +# pass + +# def test_validate_request_missing_aws_secret_access_key(self, req): +# """Test validate_request method when 'AWS_SECRET_ACCESS_KEY' is missing.""" +# pass + +# def test_speech(self, text, language): +# """Test speech method for text-to-speech synthesis.""" +# pass + +# def test_speech_key_exception(self, text, language): +# """Test speech method for handling exceptions during text-to-speech synthesis.""" +# pass + + +# class ValidateCommonTest(unittest.TestCase): +# """Test Cases for validate_common function""" +# def test_validate_common(self, req): +# """Test validate_common function with valid input.""" +# pass + +# def test_missing_text(self, req): +# """Test validate_common function when 'text' is missing.""" +# pass + +# def test_missing_language(self, req): +# """Test validate_common function when 'language' is missing.""" +# pass + + +# class MainTest(unittest.TestCase): +# """Test Cases for main function.""" +# @unittest.skipUnless(secret.GOOGLE_API_KEY, "No Google API Key set.") +# def test_main(self): +# """Unittest for main function success json response.""" +# want = { +# "success": True, +# "audio_stream": RESULT_GOOGLE, +# } +# # Create a request +# req = MyRequest({ +# "payload": { +# "provider": "google", +# "text": "hi", +# "language": "en-US", +# }, +# "variables": { +# "API_KEY": secret.API_KEY_TINYPNG, +# "PROJECT_ID": secret.GOOGLE_API_KEY, +# } +# }) +# # Create a response object +# res = MyResponse() +# main.main(req, res) +# # Check the response +# got = res.json() +# self.assertEqual(got, want) + +# def test_main_value_error(self): +# """Unittest for main function when a value error is raised.""" +# want = {"success": False, "error": "Missing payload"} +# # Create a request +# req = MyRequest({"payload": {}, "variables": {}}) +# # Create a response object +# res = MyResponse() +# main.main(req, res) + +# # Check the response +# got = res.json() +# self.assertEqual(got, want) + +# def test_main_exception(self): +# """Unittest case for main function when exception is raised.""" +# # Create a request +# req = MyRequest({ +# "payload": { +# "provider": "tinypng", +# "image": base64.b64encode(IMAGE).decode() +# }, +# "variables": { +# "API_KEY": "wrong_api_key" +# } +# }) +# # Create a response object +# res = MyResponse() +# main.main(req, res) + +# # Check the response +# got = res.json() +# self.assertFalse(got["success"]) +# self.assertIn("AccountError", got["error"]) + +if __name__ == "__main__": + unittest.main() From 252f56c2f87b7c6c2b34ffa02aaaa023a539e0d7 Mon Sep 17 00:00:00 2001 From: rubynguyen1510 <50710444+rubynguyen1510@users.noreply.github.com> Date: Thu, 27 Jul 2023 21:33:43 +0000 Subject: [PATCH 02/11] added unit test aws --- python/text-to-speech/requirements.txt | 3 +- python/text-to-speech/test_main.py | 43 ++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/python/text-to-speech/requirements.txt b/python/text-to-speech/requirements.txt index 2105adb5..f3900617 100644 --- a/python/text-to-speech/requirements.txt +++ b/python/text-to-speech/requirements.txt @@ -1,3 +1,4 @@ boto3==1.28.9 azure-cognitiveservices-speech==1.24.0 -google-cloud-texttospeech==2.14.1 \ No newline at end of file +google-cloud-texttospeech==2.14.1 +parameterized==0.9.0 diff --git a/python/text-to-speech/test_main.py b/python/text-to-speech/test_main.py index 3006a7e6..7627cbde 100644 --- a/python/text-to-speech/test_main.py +++ b/python/text-to-speech/test_main.py @@ -186,9 +186,40 @@ def tearDown(self): # """Test validate_request method when 'AWS_SECRET_ACCESS_KEY' is missing.""" # pass -# def test_speech(self, text, language): -# """Test speech method for text-to-speech synthesis.""" -# pass + def test_speech(self): + """Test speech method for successful text-to-speech synthesis.""" + req = MyRequest({ + "payload": { + "provider": "aws", + "text": "hi", + "language": "en-US", + }, + "variables": { + "API_KEY": "123", + "PROJECT_ID": "123", + } + }) + # Create an instance of Google Class + aws_instance = main.AWS(req) + # Variables + text = "hello" + language = "en-US" + # Set up mock + with patch.object(texttospeech, "TextToSpeechClient") as mock_client: + mock_response = mock_client.return_value + mock_response.synthesize_speech.return_value.audio_content = base64.b64decode(RESULT_AWS) + + # Call the speech method + audio_stream = aws_instance.speech(text, language) + + # Assert that the mock client was called with the correct arguments + mock_client.assert_called_once_with(client_options={"api_key": "123", "secret_api_key": "123"}) + + # Assert that the synthesize_speech method was called with the correct arguments + mock_response.synthesize_speech.assert_called_once_with(VoiceId="Joanna", OutputFormat="mp3", Text=text, LanguageCode=language) + + # Assert the result + self.assertEqual(audio_stream, base64.b64decode(RESULT_AWS)) # def test_speech_key_exception(self, text, language): # """Test speech method for handling exceptions during text-to-speech synthesis.""" @@ -227,13 +258,13 @@ def tearDown(self): # "language": "en-US", # }, # "variables": { -# "API_KEY": secret.API_KEY_TINYPNG, -# "PROJECT_ID": secret.GOOGLE_API_KEY, +# "API_KEY": "123", +# "PROJECT_ID": "123", # } # }) -# # Create a response object # res = MyResponse() # main.main(req, res) + # # Check the response # got = res.json() # self.assertEqual(got, want) From 46c425ec61216c15161cf60ef59b83f93495aba4 Mon Sep 17 00:00:00 2001 From: Noah Jacinto Date: Fri, 28 Jul 2023 12:12:33 -0700 Subject: [PATCH 03/11] Fixed test main --- python/text-to-speech/main.py | 3 - python/text-to-speech/test_main.py | 315 ++++++++++------------------- 2 files changed, 112 insertions(+), 206 deletions(-) diff --git a/python/text-to-speech/main.py b/python/text-to-speech/main.py index 7b72e9df..887e2387 100644 --- a/python/text-to-speech/main.py +++ b/python/text-to-speech/main.py @@ -30,9 +30,6 @@ def speech(self, text: str, language: str) -> bytes: class Google(TextToSpeech): """This class represents the implementation of Google text to speech.""" - api_key = None - project_id = None - def validate_request(self, req: requests) -> None: """ This method validates the request data for Google text to speech. diff --git a/python/text-to-speech/test_main.py b/python/text-to-speech/test_main.py index 3006a7e6..c669f7c8 100644 --- a/python/text-to-speech/test_main.py +++ b/python/text-to-speech/test_main.py @@ -24,6 +24,28 @@ read_text(encoding="utf-8")) +def get_instance(provider, key, project_id): + IMPLEMENTATIONS = { + "google": main.Google, + "azure": main.Azure, + "aws": main.AWS, + } + + req = MyRequest({ + "payload": { + "provider": provider, + "text": "hi", + "language": "en-US", + }, + "variables": { + "API_KEY": key, + "PROJECT_ID": project_id, + } + }) + + return IMPLEMENTATIONS[provider](req) + + class MyRequest: """Class for defining My Request structure.""" def __init__(self, data): @@ -45,232 +67,119 @@ def json(self, data=None): class GoogleTest(unittest.TestCase): """Google API Test Cases""" - def setUp(self): - self.req = MyRequest({ - "payload": { - "provider": "google", - "text": "hi", - "language": "en-US", - }, - "variables": { - "API_KEY": secret.GOOGLE_API_KEY, - "PROJECT_ID": secret.GOOGLE_PROJECT_ID, - } - }) - self.google_instance = main.Google(self.req) - - def tearDown(self): - self.google_instance = None - @parameterized.expand([ - ({"API_KEY": None, "PROJECT_ID": "123"},), # Missing API KEY - ({"API_KEY": "123", "PROJECT_ID": None},), # Missing PROJECT ID - ({"API_KEY": None, "PROJECT_ID": None},), # Missing Both + (None, "123"), # Missing API KEY + ("123", None), # Missing PROJECT ID + (None, None), # Missing Both ]) - def test_validate_request(self, variables): - self.req.variables["API_KEY"] = variables["API_KEY"] - self.req.variables["PROJECT_ID"] = variables["PROJECT_ID"] - self.assertRaises(ValueError, self.google_instance.validate_request, self.req) + def test_validate_request(self, key, project_id): + self.assertRaises(ValueError, get_instance, "google", key, project_id) def test_speech_happy(self): """Test speech method for successful text-to-speech synthesis.""" - self.google_instance.api_key = "123" - self.google_instance.project_id = "123" + instance = get_instance("google", "123", "123") # Set up mock - with patch.object(texttospeech, "TextToSpeechClient") as mock_client: - mock_response = mock_client.return_value - mock_response.synthesize_speech.return_value.audio_content = base64.b64decode(RESULT_GOOGLE) + with patch.object(texttospeech.TextToSpeechClient, "synthesize_speech") as mock_synthesize_speech: + mock_synthesize_speech.return_value.audio_content = base64.b64decode(RESULT_GOOGLE) # Call the speech method - audio_stream = self.google_instance.speech("hi", "en-US") + audio_bytes = instance.speech("hi", "en-US") # Assert the result - self.assertEqual(audio_stream, base64.b64decode(RESULT_GOOGLE)) + self.assertEqual(audio_bytes, base64.b64decode(RESULT_GOOGLE)) def test_speech_error(self): """Test speech method for unsuccessful text-to-speech synthesis.""" - self.google_instance.api_key = "123" - self.google_instance.project_id = "123" - # Variables + instance = get_instance("google", "123", "123") # Set up mock - with patch.object(texttospeech, "TextToSpeechClient") as mock_client: - mock_response = mock_client.return_value - mock_response.synthesize_speech.return_value.audio_content = b'A12' + with patch.object(texttospeech.TextToSpeechClient, "synthesize_speech") as mock_synthesize_speech: + mock_synthesize_speech.return_value.audio_content = b"INCORRECT_VALUE" # Call the speech method - audio_stream = self.google_instance.speech("hello", "en-US") + audio_bytes = instance.speech("hi", "en-US") # Assert the result - self.assertNotEqual(audio_stream, base64.b64decode(RESULT_GOOGLE)) + self.assertNotEqual(audio_bytes, base64.b64decode(RESULT_GOOGLE)) - # def test_google_credential(self): - # self.google_instance.api_key = "INCORRECT_CREDENTIAL" - # self.google_instance.project_id = "INCORRECT_CREDENTIAL" - # # Set up mock - # with patch.object(texttospeech, "TextToSpeechClient") as mock_client: - # mock_client.side_effect = Exception - # self.assertRaises(Exception, self.google_instance.speech, "hello", "en-US") # Incorrect credentials raise exception + def test_google_credential(self): + instance = get_instance("google", "WRONG_API_KEY", "WRONG_PROJECT_ID") + # Set up mock + with patch.object(texttospeech.TextToSpeechClient, "synthesize_speech") as mock_synthesize_speech: + # Raise Exception + mock_synthesize_speech.side_effect = Exception + self.assertRaises(Exception, instance.speech, "hello", "en-US") # Incorrect credentials raise exception + + def test_google_language(self): + instance = get_instance("google", "", "") + with patch.object(texttospeech.TextToSpeechClient, "synthesize_speech") as mock_synthesize_speech: + mock_synthesize_speech.side_effect = Exception + self.assertRaises(Exception, instance.speech, "hello", "en-EN") # Incorrect language + self.assertRaises(Exception, instance.speech, "hello", None) # Empty language code + + def test_speech_text(self): + instance = get_instance("google", "", "") + with patch.object(texttospeech, "TextToSpeechClient") as mock_client: + mock_client.side_effect = Exception + self.assertRaises(Exception, instance.speech, None, "en-US") # Empty Text - # def test_google_language(self): - # with patch.object(texttospeech, "TextToSpeechClient") as mock_client: - # mock_client.side_effect = Exception - # self.assertRaises(Exception, self.google_instance.speech, "hello", "en-EN") # Incorrect language - # self.assertRaises(Exception, self.google_instance.speech, "hello", None) # Empty language code - # def test_speech_text(self): - # with patch.object(texttospeech, "TextToSpeechClient") as mock_client: - # mock_client.side_effect = Exception - # self.assertRaises(Exception, self.google_instance.speech, None, "en-US") # Empty Text +class AzureTest(unittest.TestCase): + """Azure API Test Cases""" - # def test_google_credential(self): - # self.google_instance.api_key = "INCORRECT_CREDENTIAL" - # self.google_instance.project_id = "INCORRECT_CREDENTIAL" - # self.assertRaises(Exception, self.google_instance.speech, "hello", "en-US") # Incorrect credentials raise exception + def test_validate_request(self, req): + """Test validate_request method when all required fields are present.""" + pass - # def test_google_language(self): - # self.assertRaises(Exception, self.google_instance.speech, "hello", "en-EN") # Incorrect language code - # self.assertRaises(Exception, self.google_instance.speech, "hello", None) # Empty language code + def test_validate_request_missing_aws_access_key_id(self, req): + """Test validate_request methsod when 'AWS_ACCESS_KEY_ID' is missing.""" + pass - # def test_speech_text(self): - # self.assertRaises(Exception, self.google_instance.speech, None, "en-US") - # self.assertEqual(b'', self.google_instance.speech("", "en-US")) + def test_validate_request_missing_aws_secret_access_key(self, req): + """Test validate_request method when 'AWS_SECRET_ACCESS_KEY' is missing.""" + pass + def test_speech(self, text, language): + """Test speech method for text-to-speech synthesis.""" + pass -class AzureTest(unittest.TestCase): - """Azure API Test Cases""" - def setUp(self): - self.req = MyRequest({ - "payload": { - "provider": "azure", - "text": "hi", - "language": "en-US", - }, - "variables": { - "API_KEY": secret.AZURE_API_KEY, - "REGION_KEY": secret.AZURE_REGION_KEY, - } - }) - self.azure_instance = main.Azure(self.req) - - def tearDown(self): - self.azure_instance = None - - # def test_validate_request(self, req): + def test_speech_key_exception(self, text, language): + """Test speech method for handling exceptions during text-to-speech synthesis.""" + pass + + +class AWSTest(unittest.TestCase): + """AWS API Test Cases""" + def test_validate_request(self, req): """Test validate_request method when all required fields are present.""" - # pass - -# def test_validate_request_missing_aws_access_key_id(self, req): -# """Test validate_request methsod when 'AWS_ACCESS_KEY_ID' is missing.""" -# pass - -# def test_validate_request_missing_aws_secret_access_key(self, req): -# """Test validate_request method when 'AWS_SECRET_ACCESS_KEY' is missing.""" -# pass - -# def test_speech(self, text, language): -# """Test speech method for text-to-speech synthesis.""" -# pass - -# def test_speech_key_exception(self, text, language): -# """Test speech method for handling exceptions during text-to-speech synthesis.""" -# pass - - -# class AWSTest(unittest.TestCase): -# """AWS API Test Cases""" -# def test_validate_request(self, req): -# """Test validate_request method when all required fields are present.""" -# pass - -# def test_validate_request_missing_aws_access_key_id(self, req): -# """Test validate_request method when 'AWS_ACCESS_KEY_ID' is missing.""" -# pass - -# def test_validate_request_missing_aws_secret_access_key(self, req): -# """Test validate_request method when 'AWS_SECRET_ACCESS_KEY' is missing.""" -# pass - -# def test_speech(self, text, language): -# """Test speech method for text-to-speech synthesis.""" -# pass - -# def test_speech_key_exception(self, text, language): -# """Test speech method for handling exceptions during text-to-speech synthesis.""" -# pass - - -# class ValidateCommonTest(unittest.TestCase): -# """Test Cases for validate_common function""" -# def test_validate_common(self, req): -# """Test validate_common function with valid input.""" -# pass - -# def test_missing_text(self, req): -# """Test validate_common function when 'text' is missing.""" -# pass - -# def test_missing_language(self, req): -# """Test validate_common function when 'language' is missing.""" -# pass - - -# class MainTest(unittest.TestCase): -# """Test Cases for main function.""" -# @unittest.skipUnless(secret.GOOGLE_API_KEY, "No Google API Key set.") -# def test_main(self): -# """Unittest for main function success json response.""" -# want = { -# "success": True, -# "audio_stream": RESULT_GOOGLE, -# } -# # Create a request -# req = MyRequest({ -# "payload": { -# "provider": "google", -# "text": "hi", -# "language": "en-US", -# }, -# "variables": { -# "API_KEY": secret.API_KEY_TINYPNG, -# "PROJECT_ID": secret.GOOGLE_API_KEY, -# } -# }) -# # Create a response object -# res = MyResponse() -# main.main(req, res) -# # Check the response -# got = res.json() -# self.assertEqual(got, want) - -# def test_main_value_error(self): -# """Unittest for main function when a value error is raised.""" -# want = {"success": False, "error": "Missing payload"} -# # Create a request -# req = MyRequest({"payload": {}, "variables": {}}) -# # Create a response object -# res = MyResponse() -# main.main(req, res) - -# # Check the response -# got = res.json() -# self.assertEqual(got, want) - -# def test_main_exception(self): -# """Unittest case for main function when exception is raised.""" -# # Create a request -# req = MyRequest({ -# "payload": { -# "provider": "tinypng", -# "image": base64.b64encode(IMAGE).decode() -# }, -# "variables": { -# "API_KEY": "wrong_api_key" -# } -# }) -# # Create a response object -# res = MyResponse() -# main.main(req, res) - -# # Check the response -# got = res.json() -# self.assertFalse(got["success"]) -# self.assertIn("AccountError", got["error"]) + pass + + def test_validate_request_missing_aws_access_key_id(self, req): + """Test validate_request method when 'AWS_ACCESS_KEY_ID' is missing.""" + pass + + def test_validate_request_missing_aws_secret_access_key(self, req): + """Test validate_request method when 'AWS_SECRET_ACCESS_KEY' is missing.""" + pass + + def test_speech(self, text, language): + """Test speech method for text-to-speech synthesis.""" + pass + + def test_speech_key_exception(self, text, language): + """Test speech method for handling exceptions during text-to-speech synthesis.""" + pass + + +class ValidateCommonTest(unittest.TestCase): + """Test Cases for validate_common function""" + def test_validate_common(self, req): + """Test validate_common function with valid input.""" + pass + + def test_missing_text(self, req): + """Test validate_common function when 'text' is missing.""" + pass + + def test_missing_language(self, req): + """Test validate_common function when 'language' is missing.""" + pass + if __name__ == "__main__": unittest.main() From fb4f90efffc30f6f63efd443bff6761d2c45b694 Mon Sep 17 00:00:00 2001 From: Noah Jacinto <95050103+Mushmou@users.noreply.github.com> Date: Fri, 28 Jul 2023 12:19:42 -0700 Subject: [PATCH 04/11] Fixed test main (#10) Co-authored-by: Noah Jacinto --- python/text-to-speech/main.py | 3 - python/text-to-speech/test_main.py | 339 +++++++++++++---------------- 2 files changed, 151 insertions(+), 191 deletions(-) diff --git a/python/text-to-speech/main.py b/python/text-to-speech/main.py index 7b72e9df..887e2387 100644 --- a/python/text-to-speech/main.py +++ b/python/text-to-speech/main.py @@ -30,9 +30,6 @@ def speech(self, text: str, language: str) -> bytes: class Google(TextToSpeech): """This class represents the implementation of Google text to speech.""" - api_key = None - project_id = None - def validate_request(self, req: requests) -> None: """ This method validates the request data for Google text to speech. diff --git a/python/text-to-speech/test_main.py b/python/text-to-speech/test_main.py index 7627cbde..ee7cbeb7 100644 --- a/python/text-to-speech/test_main.py +++ b/python/text-to-speech/test_main.py @@ -24,6 +24,28 @@ read_text(encoding="utf-8")) +def get_instance(provider, key, project_id): + IMPLEMENTATIONS = { + "google": main.Google, + "azure": main.Azure, + "aws": main.AWS, + } + + req = MyRequest({ + "payload": { + "provider": provider, + "text": "hi", + "language": "en-US", + }, + "variables": { + "API_KEY": key, + "PROJECT_ID": project_id, + } + }) + + return IMPLEMENTATIONS[provider](req) + + class MyRequest: """Class for defining My Request structure.""" def __init__(self, data): @@ -45,146 +67,149 @@ def json(self, data=None): class GoogleTest(unittest.TestCase): """Google API Test Cases""" - def setUp(self): - self.req = MyRequest({ - "payload": { - "provider": "google", - "text": "hi", - "language": "en-US", - }, - "variables": { - "API_KEY": secret.GOOGLE_API_KEY, - "PROJECT_ID": secret.GOOGLE_PROJECT_ID, - } - }) - self.google_instance = main.Google(self.req) - - def tearDown(self): - self.google_instance = None - @parameterized.expand([ - ({"API_KEY": None, "PROJECT_ID": "123"},), # Missing API KEY - ({"API_KEY": "123", "PROJECT_ID": None},), # Missing PROJECT ID - ({"API_KEY": None, "PROJECT_ID": None},), # Missing Both + (None, "123"), # Missing API KEY + ("123", None), # Missing PROJECT ID + (None, None), # Missing Both ]) - def test_validate_request(self, variables): - self.req.variables["API_KEY"] = variables["API_KEY"] - self.req.variables["PROJECT_ID"] = variables["PROJECT_ID"] - self.assertRaises(ValueError, self.google_instance.validate_request, self.req) + def test_validate_request(self, key, project_id): + self.assertRaises(ValueError, get_instance, "google", key, project_id) def test_speech_happy(self): """Test speech method for successful text-to-speech synthesis.""" - self.google_instance.api_key = "123" - self.google_instance.project_id = "123" + instance = get_instance("google", "123", "123") # Set up mock - with patch.object(texttospeech, "TextToSpeechClient") as mock_client: - mock_response = mock_client.return_value - mock_response.synthesize_speech.return_value.audio_content = base64.b64decode(RESULT_GOOGLE) + with patch.object(texttospeech.TextToSpeechClient, "synthesize_speech") as mock_synthesize_speech: + mock_synthesize_speech.return_value.audio_content = base64.b64decode(RESULT_GOOGLE) # Call the speech method - audio_stream = self.google_instance.speech("hi", "en-US") + audio_bytes = instance.speech("hi", "en-US") # Assert the result - self.assertEqual(audio_stream, base64.b64decode(RESULT_GOOGLE)) + self.assertEqual(audio_bytes, base64.b64decode(RESULT_GOOGLE)) def test_speech_error(self): """Test speech method for unsuccessful text-to-speech synthesis.""" - self.google_instance.api_key = "123" - self.google_instance.project_id = "123" - # Variables + instance = get_instance("google", "123", "123") # Set up mock - with patch.object(texttospeech, "TextToSpeechClient") as mock_client: - mock_response = mock_client.return_value - mock_response.synthesize_speech.return_value.audio_content = b'A12' + with patch.object(texttospeech.TextToSpeechClient, "synthesize_speech") as mock_synthesize_speech: + mock_synthesize_speech.return_value.audio_content = b"INCORRECT_VALUE" # Call the speech method - audio_stream = self.google_instance.speech("hello", "en-US") + audio_bytes = instance.speech("hi", "en-US") # Assert the result - self.assertNotEqual(audio_stream, base64.b64decode(RESULT_GOOGLE)) + self.assertNotEqual(audio_bytes, base64.b64decode(RESULT_GOOGLE)) - # def test_google_credential(self): - # self.google_instance.api_key = "INCORRECT_CREDENTIAL" - # self.google_instance.project_id = "INCORRECT_CREDENTIAL" - # # Set up mock - # with patch.object(texttospeech, "TextToSpeechClient") as mock_client: - # mock_client.side_effect = Exception - # self.assertRaises(Exception, self.google_instance.speech, "hello", "en-US") # Incorrect credentials raise exception + def test_google_credential(self): + instance = get_instance("google", "WRONG_API_KEY", "WRONG_PROJECT_ID") + # Set up mock + with patch.object(texttospeech.TextToSpeechClient, "synthesize_speech") as mock_synthesize_speech: + # Raise Exception + mock_synthesize_speech.side_effect = Exception + self.assertRaises(Exception, instance.speech, "hello", "en-US") # Incorrect credentials raise exception + + def test_google_language(self): + instance = get_instance("google", "", "") + with patch.object(texttospeech.TextToSpeechClient, "synthesize_speech") as mock_synthesize_speech: + mock_synthesize_speech.side_effect = Exception + self.assertRaises(Exception, instance.speech, "hello", "en-EN") # Incorrect language + self.assertRaises(Exception, instance.speech, "hello", None) # Empty language code + + def test_speech_text(self): + instance = get_instance("google", "", "") + with patch.object(texttospeech, "TextToSpeechClient") as mock_client: + mock_client.side_effect = Exception + self.assertRaises(Exception, instance.speech, None, "en-US") # Empty Text - # def test_google_language(self): - # with patch.object(texttospeech, "TextToSpeechClient") as mock_client: - # mock_client.side_effect = Exception - # self.assertRaises(Exception, self.google_instance.speech, "hello", "en-EN") # Incorrect language - # self.assertRaises(Exception, self.google_instance.speech, "hello", None) # Empty language code - # def test_speech_text(self): - # with patch.object(texttospeech, "TextToSpeechClient") as mock_client: - # mock_client.side_effect = Exception - # self.assertRaises(Exception, self.google_instance.speech, None, "en-US") # Empty Text +class AzureTest(unittest.TestCase): + """Azure API Test Cases""" - # def test_google_credential(self): - # self.google_instance.api_key = "INCORRECT_CREDENTIAL" - # self.google_instance.project_id = "INCORRECT_CREDENTIAL" - # self.assertRaises(Exception, self.google_instance.speech, "hello", "en-US") # Incorrect credentials raise exception + def test_validate_request(self, req): + """Test validate_request method when all required fields are present.""" + pass - # def test_google_language(self): - # self.assertRaises(Exception, self.google_instance.speech, "hello", "en-EN") # Incorrect language code - # self.assertRaises(Exception, self.google_instance.speech, "hello", None) # Empty language code + def test_validate_request_missing_aws_access_key_id(self, req): + """Test validate_request methsod when 'AWS_ACCESS_KEY_ID' is missing.""" + pass - # def test_speech_text(self): - # self.assertRaises(Exception, self.google_instance.speech, None, "en-US") - # self.assertEqual(b'', self.google_instance.speech("", "en-US")) + def test_validate_request_missing_aws_secret_access_key(self, req): + """Test validate_request method when 'AWS_SECRET_ACCESS_KEY' is missing.""" + pass + def test_speech(self, text, language): + """Test speech method for text-to-speech synthesis.""" + pass -class AzureTest(unittest.TestCase): - """Azure API Test Cases""" - def setUp(self): - self.req = MyRequest({ - "payload": { - "provider": "azure", - "text": "hi", - "language": "en-US", - }, - "variables": { - "API_KEY": secret.AZURE_API_KEY, - "REGION_KEY": secret.AZURE_REGION_KEY, - } - }) - self.azure_instance = main.Azure(self.req) + def test_speech_key_exception(self, text, language): + """Test speech method for handling exceptions during text-to-speech synthesis.""" + pass - def tearDown(self): - self.azure_instance = None - # def test_validate_request(self, req): +class AWSTest(unittest.TestCase): + """AWS API Test Cases""" + def test_validate_request(self, req): """Test validate_request method when all required fields are present.""" - # pass + pass + + def test_validate_request_missing_aws_access_key_id(self, req): + """Test validate_request method when 'AWS_ACCESS_KEY_ID' is missing.""" + pass + + def test_validate_request_missing_aws_secret_access_key(self, req): + """Test validate_request method when 'AWS_SECRET_ACCESS_KEY' is missing.""" + pass + + def test_speech(self, text, language): + """Test speech method for text-to-speech synthesis.""" + pass + + def test_speech_key_exception(self, text, language): + """Test speech method for handling exceptions during text-to-speech synthesis.""" + pass + + +class ValidateCommonTest(unittest.TestCase): + """Test Cases for validate_common function""" + def test_validate_common(self, req): + """Test validate_common function with valid input.""" + pass -# def test_validate_request_missing_aws_access_key_id(self, req): -# """Test validate_request methsod when 'AWS_ACCESS_KEY_ID' is missing.""" -# pass + def test_missing_text(self, req): + """Test validate_common function when 'text' is missing.""" + pass -# def test_validate_request_missing_aws_secret_access_key(self, req): -# """Test validate_request method when 'AWS_SECRET_ACCESS_KEY' is missing.""" -# pass + def test_missing_language(self, req): + """Test validate_common function when 'language' is missing.""" + pass -# def test_speech(self, text, language): -# """Test speech method for text-to-speech synthesis.""" -# pass + def test_validate_request_missing_aws_access_key_id(self, req): + """Test validate_request methsod when 'AWS_ACCESS_KEY_ID' is missing.""" + pass -# def test_speech_key_exception(self, text, language): -# """Test speech method for handling exceptions during text-to-speech synthesis.""" -# pass + def test_validate_request_missing_aws_secret_access_key(self, req): + """Test validate_request method when 'AWS_SECRET_ACCESS_KEY' is missing.""" + pass + def test_speech(self, text, language): + """Test speech method for text-to-speech synthesis.""" + pass -# class AWSTest(unittest.TestCase): -# """AWS API Test Cases""" -# def test_validate_request(self, req): -# """Test validate_request method when all required fields are present.""" -# pass + def test_speech_key_exception(self, text, language): + """Test speech method for handling exceptions during text-to-speech synthesis.""" + pass -# def test_validate_request_missing_aws_access_key_id(self, req): -# """Test validate_request method when 'AWS_ACCESS_KEY_ID' is missing.""" -# pass -# def test_validate_request_missing_aws_secret_access_key(self, req): -# """Test validate_request method when 'AWS_SECRET_ACCESS_KEY' is missing.""" -# pass +class AWSTest(unittest.TestCase): + """AWS API Test Cases""" + def test_validate_request(self, req): + """Test validate_request method when all required fields are present.""" + pass + + def test_validate_request_missing_aws_access_key_id(self, req): + """Test validate_request method when 'AWS_ACCESS_KEY_ID' is missing.""" + pass + + def test_validate_request_missing_aws_secret_access_key(self, req): + """Test validate_request method when 'AWS_SECRET_ACCESS_KEY' is missing.""" + pass def test_speech(self): """Test speech method for successful text-to-speech synthesis.""" @@ -221,87 +246,25 @@ def test_speech(self): # Assert the result self.assertEqual(audio_stream, base64.b64decode(RESULT_AWS)) -# def test_speech_key_exception(self, text, language): -# """Test speech method for handling exceptions during text-to-speech synthesis.""" -# pass - - -# class ValidateCommonTest(unittest.TestCase): -# """Test Cases for validate_common function""" -# def test_validate_common(self, req): -# """Test validate_common function with valid input.""" -# pass - -# def test_missing_text(self, req): -# """Test validate_common function when 'text' is missing.""" -# pass - -# def test_missing_language(self, req): -# """Test validate_common function when 'language' is missing.""" -# pass - - -# class MainTest(unittest.TestCase): -# """Test Cases for main function.""" -# @unittest.skipUnless(secret.GOOGLE_API_KEY, "No Google API Key set.") -# def test_main(self): -# """Unittest for main function success json response.""" -# want = { -# "success": True, -# "audio_stream": RESULT_GOOGLE, -# } -# # Create a request -# req = MyRequest({ -# "payload": { -# "provider": "google", -# "text": "hi", -# "language": "en-US", -# }, -# "variables": { -# "API_KEY": "123", -# "PROJECT_ID": "123", -# } -# }) -# res = MyResponse() -# main.main(req, res) - -# # Check the response -# got = res.json() -# self.assertEqual(got, want) - -# def test_main_value_error(self): -# """Unittest for main function when a value error is raised.""" -# want = {"success": False, "error": "Missing payload"} -# # Create a request -# req = MyRequest({"payload": {}, "variables": {}}) -# # Create a response object -# res = MyResponse() -# main.main(req, res) - -# # Check the response -# got = res.json() -# self.assertEqual(got, want) - -# def test_main_exception(self): -# """Unittest case for main function when exception is raised.""" -# # Create a request -# req = MyRequest({ -# "payload": { -# "provider": "tinypng", -# "image": base64.b64encode(IMAGE).decode() -# }, -# "variables": { -# "API_KEY": "wrong_api_key" -# } -# }) -# # Create a response object -# res = MyResponse() -# main.main(req, res) - -# # Check the response -# got = res.json() -# self.assertFalse(got["success"]) -# self.assertIn("AccountError", got["error"]) + def test_speech_key_exception(self, text, language): + """Test speech method for handling exceptions during text-to-speech synthesis.""" + pass + + +class ValidateCommonTest(unittest.TestCase): + """Test Cases for validate_common function""" + def test_validate_common(self, req): + """Test validate_common function with valid input.""" + pass + + def test_missing_text(self, req): + """Test validate_common function when 'text' is missing.""" + pass + + def test_missing_language(self, req): + """Test validate_common function when 'language' is missing.""" + pass + if __name__ == "__main__": unittest.main() From d3e31aa3875b0b6a02080620608f798330c85997 Mon Sep 17 00:00:00 2001 From: Ruby Nguyen <50710444+rubynguyen1510@users.noreply.github.com> Date: Fri, 28 Jul 2023 12:37:54 -0700 Subject: [PATCH 05/11] New main.py (#11) * finalized main.py --------- Co-authored-by: Noah Jacinto --- python/.gitignore | 154 ----------------------------- python/text-to-speech/main.py | 126 +++++++++++++---------- python/text-to-speech/test_main.py | 1 + 3 files changed, 76 insertions(+), 205 deletions(-) delete mode 100644 python/.gitignore diff --git a/python/.gitignore b/python/.gitignore deleted file mode 100644 index d2457101..00000000 --- a/python/.gitignore +++ /dev/null @@ -1,154 +0,0 @@ -# Python gitignore from: https://github.com/github/gitignore/blob/main/Python.gitignore - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ diff --git a/python/text-to-speech/main.py b/python/text-to-speech/main.py index 887e2387..b593667d 100644 --- a/python/text-to-speech/main.py +++ b/python/text-to-speech/main.py @@ -1,3 +1,4 @@ +"""Synthesize text to speech using Google, Azure and AWS API.""" # Standard library import base64 import abc @@ -9,9 +10,6 @@ import azure.cognitiveservices.speech as speechsdk import boto3 -# Local imports -import secret - class TextToSpeech(): """Base class for Text to Speech.""" @@ -20,7 +18,7 @@ def __init__(self, req: requests) -> None: self.validate_request(req) @abc.abstractmethod - def validate_request(self, req: requests): + def validate_request(self, req: requests) -> None: """Abstract validate request method for providers.""" @abc.abstractmethod @@ -47,7 +45,7 @@ def validate_request(self, req: requests) -> None: self.api_key = req.variables.get("API_KEY") self.project_id = req.variables.get("PROJECT_ID") - def speech(self, text, language) -> bytes: + def speech(self, text: str, language: str) -> bytes: """ Converts the given text into speech with the Google text to speech API. @@ -59,25 +57,33 @@ def speech(self, text, language) -> bytes: bytes: The synthezied speech in bytes. """ # Instantiate a client. - client = texttospeech.TextToSpeechClient(client_options={"api_key": self.api_key, "quota_project_id": self.project_id}) + client = texttospeech.TextToSpeechClient(client_options={ + "api_key": self.api_key, + "quota_project_id": self.project_id, + }) # Set the text input to be synthesized. synthesis_input = texttospeech.SynthesisInput(text=text) - # Build the voice request, select the language code ("en-US") and the ssml voice gender is neutral. - voice = texttospeech.VoiceSelectionParams(language_code=language, ssml_gender=texttospeech.SsmlVoiceGender.NEUTRAL) + # Build the voice request, select the language code ("en-US") + # and the ssml voice gender is neutral. + voice = texttospeech.VoiceSelectionParams( + language_code=language, + ssml_gender=texttospeech.SsmlVoiceGender.NEUTRAL + ) # Select the type of audio file you want returned. - audio_config = texttospeech.AudioConfig(audio_encoding=texttospeech.AudioEncoding.MP3) - # Perform the text-to-speech request on the text input with the selected voice parameters and audio file type. - response = client.synthesize_speech(input=synthesis_input, voice=voice, audio_config=audio_config) + audio_config = texttospeech.AudioConfig( + audio_encoding=texttospeech.AudioEncoding.MP3) + # Perform the text-to-speech request on the text input + # with the selected voice parameters and audio file type. + response = client.synthesize_speech( + input=synthesis_input, + voice=voice, + audio_config=audio_config + ) return response.audio_content class Azure(TextToSpeech): - """ - This class represents the implementation of Azure text to speech. - """ - api_key = None - region_key = None - + """This class represents the implementation of Azure text to speech."" def validate_request(self, req: requests) -> None: """ This method validates the request data for Azure text to speech. @@ -88,13 +94,13 @@ def validate_request(self, req: requests) -> None: ValueError: If any required value is missing or invalid. """ if not req.variables.get("API_KEY"): - raise ValueError("Missing API_KEY") + raise ValueError("Missing API_KEY.") if not req.variables.get("REGION_KEY"): - raise ValueError("Missing region") + raise ValueError("Missing REGION_KEY.") self.api_key = req.variables.get("API_KEY") self.region_key = req.variables.get("REGION_KEY") - def speech(self, text, language) -> bytes: + def speech(self, text: str, language: str) -> bytes: """ Converts the given text into speech with the Google text to speech API. @@ -106,22 +112,25 @@ def speech(self, text, language) -> bytes: bytes: The synthezied speech in bytes. """ # Set the speech configuration to speech key and region key. - speech_config = speechsdk.SpeechConfig(subscription=self.api_key, region=self.region_key) + speech_config = speechsdk.SpeechConfig( + subscription=self.api_key, + region=self.region_key + ) # The language of the voice that speaks. speech_config.speech_synthesis_language = language # Set the speech. - speech_synthesizer = speechsdk.SpeechSynthesizer(speech_config=speech_config, audio_config=None) + speech_synthesizer = speechsdk.SpeechSynthesizer( + speech_config=speech_config, + audio_config=None + ) # Response for the speech synthesizer. response = speech_synthesizer.speak_text_async(text).get().audio_data return response class AWS(TextToSpeech): - """ - This class represents the implementation of AWS text to speech. - """ - api_key = None - secret_api_key = None + """This class represents the implementation of AWS text to speech. """ + voice_id = "Joanna" def validate_request(self, req: requests) -> None: """ @@ -139,7 +148,7 @@ def validate_request(self, req: requests) -> None: self.api_key = req.payload.get("API_KEY") self.secret_api_key = req.payload.get("SECRET_API_KEY") - def speech(self, text, language) -> bytes: + def speech(self, text: str, language: str) -> bytes: """ Converts the given text into speech with the AWS text to speech API. @@ -150,12 +159,27 @@ def speech(self, text, language) -> bytes: Returns: bytes: The synthezied speech in bytes. """ - polly_client = boto3.Session(aws_access_key_id=self.api_key, aws_secret_access_key=self.secret_api_key, region_name="us-west-2").client("polly") - response = polly_client.synthesize_speech(VoiceId="Joanna", OutputFormat="mp3", Text=text, LanguageCode=language) - return response["AudioStream"].read().decode() + # Call polly client using boto3.session + polly_client = boto3.Session( + aws_access_key_id=self.api_key, + aws_secret_access_key=self.secret_api_key, + region_name="us-west-2" + ).client("polly") + # Get response from polly client + response = polly_client.synthesize_speech( + VoiceId=AWS.voice_id, + OutputFormat="mp3", + Text=text, + LanguageCode=language + ) + return response["Audiostream"].read() + + +list_of_providers = ["google", "azure", "aws"] + +def validate_common(req: requests) -> tuple[str]: -def validate_common(req: requests) -> tuple: """ This function validates the common fields in the request data that are independent of the text-to-speech provider. @@ -176,11 +200,15 @@ def validate_common(req: requests) -> tuple: # Check if variables is empty. if not req.variables: - raise ValueError("Missing variables.") + raise ValueError("Missing Variables.") # Check if provider is empty. if not req.payload.get("provider"): - raise ValueError("Missing provider") + raise ValueError("Missing Provider.") + + # Check if provider is in the list + if req.payload.get("provider").lower not in list_of_providers: + raise ValueError("Invalid Provider.") # Check if text is empty. if not req.payload.get("text"): @@ -191,14 +219,8 @@ def validate_common(req: requests) -> tuple: raise ValueError("Missing Language.") # Return the text and langage. - return (req.payload.get("text"), req.payload.get("language")) - - -IMPLEMENTATIONS = { - "google": Google, - "azure": Azure, - "aws": AWS, -} + return (req.payload.get("provider").lower(), + req.payload.get("text"), req.payload.get("language")) def main(req: requests, res: json) -> json: @@ -214,26 +236,28 @@ def main(req: requests, res: json) -> json: containing the synthesized audio in base64 encoded format. """ try: - text, language = validate_common(req) - provider_class = IMPLEMENTATIONS[req.payload.get("provider")](req) + provider, text, language = validate_common(req) + if provider == "google": + provider_class = Google(req) + elif provider == "azure": + provider_class = Azure(req) + else: + provider_class = AWS(req) except (ValueError) as value_error: return res.json({ "success": False, - "error": f"{value_error}", + "error": str(value_error), }) try: - audio_stream = provider_class.speech(text, language) + audio_bytes = provider_class.speech(text, language) except Exception as error: return res.json({ "success": False, "error": f"{type(error).__name__}: {error}", }) - # f = open("python/text-to-speech/results/azure.txt", "w") - # f.write(base64.b64encode(audio_stream).decode()) - return res.json({ "success": True, - "audio_stream": base64.b64encode(audio_stream).decode(), - }) \ No newline at end of file + "audio_bytes": base64.b64encode(audio_bytes).decode(), + }) diff --git a/python/text-to-speech/test_main.py b/python/text-to-speech/test_main.py index ee7cbeb7..62a7ab58 100644 --- a/python/text-to-speech/test_main.py +++ b/python/text-to-speech/test_main.py @@ -24,6 +24,7 @@ read_text(encoding="utf-8")) + def get_instance(provider, key, project_id): IMPLEMENTATIONS = { "google": main.Google, From 21aae456049dde4a60337c1a677148680f138c2e Mon Sep 17 00:00:00 2001 From: Noah Jacinto Date: Fri, 28 Jul 2023 12:53:45 -0700 Subject: [PATCH 06/11] Working version of Text To Speech --- python/text-to-speech/main.py | 5 +-- python/text-to-speech/test_main.py | 60 +----------------------------- 2 files changed, 4 insertions(+), 61 deletions(-) diff --git a/python/text-to-speech/main.py b/python/text-to-speech/main.py index b593667d..f4c8526f 100644 --- a/python/text-to-speech/main.py +++ b/python/text-to-speech/main.py @@ -83,7 +83,7 @@ def speech(self, text: str, language: str) -> bytes: class Azure(TextToSpeech): - """This class represents the implementation of Azure text to speech."" + """This class represents the implementation of Azure text to speech.""" def validate_request(self, req: requests) -> None: """ This method validates the request data for Azure text to speech. @@ -178,8 +178,7 @@ def speech(self, text: str, language: str) -> bytes: list_of_providers = ["google", "azure", "aws"] -def validate_common(req: requests) -> tuple[str]: - +def validate_common(req: requests) -> tuple: """ This function validates the common fields in the request data that are independent of the text-to-speech provider. diff --git a/python/text-to-speech/test_main.py b/python/text-to-speech/test_main.py index b7687593..20cc2f07 100644 --- a/python/text-to-speech/test_main.py +++ b/python/text-to-speech/test_main.py @@ -9,7 +9,6 @@ from parameterized import parameterized from google.cloud import texttospeech - # Local imports import main @@ -23,7 +22,6 @@ read_text(encoding="utf-8")) - def get_instance(provider, key, project_id): IMPLEMENTATIONS = { "google": main.Google, @@ -143,60 +141,6 @@ def test_speech_key_exception(self, text, language): pass -class AWSTest(unittest.TestCase): - """AWS API Test Cases""" - def test_validate_request(self, req): - """Test validate_request method when all required fields are present.""" - pass - - def test_validate_request_missing_aws_access_key_id(self, req): - """Test validate_request method when 'AWS_ACCESS_KEY_ID' is missing.""" - pass - - def test_validate_request_missing_aws_secret_access_key(self, req): - """Test validate_request method when 'AWS_SECRET_ACCESS_KEY' is missing.""" - pass - - def test_speech(self, text, language): - """Test speech method for text-to-speech synthesis.""" - pass - - def test_speech_key_exception(self, text, language): - """Test speech method for handling exceptions during text-to-speech synthesis.""" - pass - - -class ValidateCommonTest(unittest.TestCase): - """Test Cases for validate_common function""" - def test_validate_common(self, req): - """Test validate_common function with valid input.""" - pass - - def test_missing_text(self, req): - """Test validate_common function when 'text' is missing.""" - pass - - def test_missing_language(self, req): - """Test validate_common function when 'language' is missing.""" - pass - - def test_validate_request_missing_aws_access_key_id(self, req): - """Test validate_request methsod when 'AWS_ACCESS_KEY_ID' is missing.""" - pass - - def test_validate_request_missing_aws_secret_access_key(self, req): - """Test validate_request method when 'AWS_SECRET_ACCESS_KEY' is missing.""" - pass - - def test_speech(self, text, language): - """Test speech method for text-to-speech synthesis.""" - pass - - def test_speech_key_exception(self, text, language): - """Test speech method for handling exceptions during text-to-speech synthesis.""" - pass - - class AWSTest(unittest.TestCase): """AWS API Test Cases""" def test_validate_request(self, req): @@ -232,7 +176,7 @@ def test_speech(self): # Set up mock with patch.object(texttospeech, "TextToSpeechClient") as mock_client: mock_response = mock_client.return_value - mock_response.synthesize_speech.return_value.audio_content = base64.b64decode(RESULT_AWS) + mock_response.synthesize_speech.return_value.audio_content = base64.b64decode("RESULT_AWS") # Call the speech method audio_stream = aws_instance.speech(text, language) @@ -244,7 +188,7 @@ def test_speech(self): mock_response.synthesize_speech.assert_called_once_with(VoiceId="Joanna", OutputFormat="mp3", Text=text, LanguageCode=language) # Assert the result - self.assertEqual(audio_stream, base64.b64decode(RESULT_AWS)) + self.assertEqual(audio_stream, base64.b64decode("RESULT_AWS")) def test_speech_key_exception(self, text, language): """Test speech method for handling exceptions during text-to-speech synthesis.""" From e6d2c71a5dd9c90cedab51ac632a2596fc0baa34 Mon Sep 17 00:00:00 2001 From: Noah Jacinto <95050103+Mushmou@users.noreply.github.com> Date: Fri, 28 Jul 2023 12:56:16 -0700 Subject: [PATCH 07/11] Noah (#12) * Fixed test main * Working version of Text To Speech --------- Co-authored-by: Noah Jacinto --- python/text-to-speech/main.py | 5 +-- python/text-to-speech/test_main.py | 61 +----------------------------- 2 files changed, 4 insertions(+), 62 deletions(-) diff --git a/python/text-to-speech/main.py b/python/text-to-speech/main.py index b593667d..f4c8526f 100644 --- a/python/text-to-speech/main.py +++ b/python/text-to-speech/main.py @@ -83,7 +83,7 @@ def speech(self, text: str, language: str) -> bytes: class Azure(TextToSpeech): - """This class represents the implementation of Azure text to speech."" + """This class represents the implementation of Azure text to speech.""" def validate_request(self, req: requests) -> None: """ This method validates the request data for Azure text to speech. @@ -178,8 +178,7 @@ def speech(self, text: str, language: str) -> bytes: list_of_providers = ["google", "azure", "aws"] -def validate_common(req: requests) -> tuple[str]: - +def validate_common(req: requests) -> tuple: """ This function validates the common fields in the request data that are independent of the text-to-speech provider. diff --git a/python/text-to-speech/test_main.py b/python/text-to-speech/test_main.py index 62a7ab58..20cc2f07 100644 --- a/python/text-to-speech/test_main.py +++ b/python/text-to-speech/test_main.py @@ -9,10 +9,8 @@ from parameterized import parameterized from google.cloud import texttospeech - # Local imports import main -import secret RESULT_GOOGLE = ( pathlib.Path("python/text-to-speech/results/google.txt"). @@ -24,7 +22,6 @@ read_text(encoding="utf-8")) - def get_instance(provider, key, project_id): IMPLEMENTATIONS = { "google": main.Google, @@ -144,60 +141,6 @@ def test_speech_key_exception(self, text, language): pass -class AWSTest(unittest.TestCase): - """AWS API Test Cases""" - def test_validate_request(self, req): - """Test validate_request method when all required fields are present.""" - pass - - def test_validate_request_missing_aws_access_key_id(self, req): - """Test validate_request method when 'AWS_ACCESS_KEY_ID' is missing.""" - pass - - def test_validate_request_missing_aws_secret_access_key(self, req): - """Test validate_request method when 'AWS_SECRET_ACCESS_KEY' is missing.""" - pass - - def test_speech(self, text, language): - """Test speech method for text-to-speech synthesis.""" - pass - - def test_speech_key_exception(self, text, language): - """Test speech method for handling exceptions during text-to-speech synthesis.""" - pass - - -class ValidateCommonTest(unittest.TestCase): - """Test Cases for validate_common function""" - def test_validate_common(self, req): - """Test validate_common function with valid input.""" - pass - - def test_missing_text(self, req): - """Test validate_common function when 'text' is missing.""" - pass - - def test_missing_language(self, req): - """Test validate_common function when 'language' is missing.""" - pass - - def test_validate_request_missing_aws_access_key_id(self, req): - """Test validate_request methsod when 'AWS_ACCESS_KEY_ID' is missing.""" - pass - - def test_validate_request_missing_aws_secret_access_key(self, req): - """Test validate_request method when 'AWS_SECRET_ACCESS_KEY' is missing.""" - pass - - def test_speech(self, text, language): - """Test speech method for text-to-speech synthesis.""" - pass - - def test_speech_key_exception(self, text, language): - """Test speech method for handling exceptions during text-to-speech synthesis.""" - pass - - class AWSTest(unittest.TestCase): """AWS API Test Cases""" def test_validate_request(self, req): @@ -233,7 +176,7 @@ def test_speech(self): # Set up mock with patch.object(texttospeech, "TextToSpeechClient") as mock_client: mock_response = mock_client.return_value - mock_response.synthesize_speech.return_value.audio_content = base64.b64decode(RESULT_AWS) + mock_response.synthesize_speech.return_value.audio_content = base64.b64decode("RESULT_AWS") # Call the speech method audio_stream = aws_instance.speech(text, language) @@ -245,7 +188,7 @@ def test_speech(self): mock_response.synthesize_speech.assert_called_once_with(VoiceId="Joanna", OutputFormat="mp3", Text=text, LanguageCode=language) # Assert the result - self.assertEqual(audio_stream, base64.b64decode(RESULT_AWS)) + self.assertEqual(audio_stream, base64.b64decode("RESULT_AWS")) def test_speech_key_exception(self, text, language): """Test speech method for handling exceptions during text-to-speech synthesis.""" From edc867e994de98e898d511e11c315ed8fca0d66d Mon Sep 17 00:00:00 2001 From: Noah Jacinto Date: Fri, 28 Jul 2023 13:07:09 -0700 Subject: [PATCH 08/11] Deleted aws sandbox dir --- python/aws-sandbox/.gitignore | 1 - python/aws-sandbox/polly.py | 20 -------------------- 2 files changed, 21 deletions(-) delete mode 100644 python/aws-sandbox/.gitignore delete mode 100644 python/aws-sandbox/polly.py diff --git a/python/aws-sandbox/.gitignore b/python/aws-sandbox/.gitignore deleted file mode 100644 index b47515cb..00000000 --- a/python/aws-sandbox/.gitignore +++ /dev/null @@ -1 +0,0 @@ -secret.py \ No newline at end of file diff --git a/python/aws-sandbox/polly.py b/python/aws-sandbox/polly.py deleted file mode 100644 index c53cb71b..00000000 --- a/python/aws-sandbox/polly.py +++ /dev/null @@ -1,20 +0,0 @@ -import boto3 -import secret -import base64 - -polly_client = boto3.Session( - aws_access_key_id=secret.aws_access_key_id, - aws_secret_access_key=secret.aws_secret_access_key, - region_name='us-west-2').client('polly') - -t = "Hello, this is a sample text to be converted into speech." - -response = polly_client.synthesize_speech( - VoiceId='Joanna', - OutputFormat='mp3', - SampleRate='8000', - Text=t, - TextType='text', - ) - -print(base64.b64encode(response['AudioStream'].read().decode())) \ No newline at end of file From d049c3c12a1a7daf3c0071dc7919e4a5365a3191 Mon Sep 17 00:00:00 2001 From: Noah Jacinto Date: Mon, 31 Jul 2023 10:53:38 -0700 Subject: [PATCH 09/11] made a working version of google test and fixed run commands in req.txt --- python/text-to-speech/README.md | 4 +- python/text-to-speech/main.py | 2 +- python/text-to-speech/requirements.txt | 3 +- python/text-to-speech/test_main.py | 202 +++++++++++++------------ 4 files changed, 111 insertions(+), 100 deletions(-) diff --git a/python/text-to-speech/README.md b/python/text-to-speech/README.md index fdcaf5f0..1fb8d432 100644 --- a/python/text-to-speech/README.md +++ b/python/text-to-speech/README.md @@ -56,13 +56,13 @@ $ cd python/send-http-request 2. Enter this function folder and build the code: ``` -docker run --rm --interactive --tty --volume $PWD:/usr/code openruntimes/python:3.10 sh /usr/local/src/build.sh +docker run --rm --interactive --tty --volume $PWD:/usr/code openruntimes/python:v2-3.10 sh /usr/local/src/build.sh ``` As a result, a `code.tar.gz` file will be generated. 3. Start the Open Runtime: ``` -docker run -p 3000:3000 -e INTERNAL_RUNTIME_KEY=secret-key -e INTERNAL_RUNTIME_ENTRYPOINT=main.py --rm --interactive --tty --volume $PWD/code.tar.gz:/tmp/code.tar.gz:ro openruntimes/python:3.10 sh /usr/local/src/start.sh +docker run -p 3000:3000 -e INTERNAL_RUNTIME_KEY=secret-key -e INTERNAL_RUNTIME_ENTRYPOINT=main.py --rm --interactive --tty --volume $PWD/code.tar.gz:/tmp/code.tar.gz:ro openruntimes/python:v2-3.10 sh /usr/local/src/start.sh ``` Your function is now listening on port `3000`, and you can execute it by sending `POST` request with appropriate authorization headers. To learn more about runtime, you can visit Python runtime [README](https://github.com/open-runtimes/open-runtimes/tree/main/runtimes/python-3.10). diff --git a/python/text-to-speech/main.py b/python/text-to-speech/main.py index f4c8526f..c5d4df58 100644 --- a/python/text-to-speech/main.py +++ b/python/text-to-speech/main.py @@ -206,7 +206,7 @@ def validate_common(req: requests) -> tuple: raise ValueError("Missing Provider.") # Check if provider is in the list - if req.payload.get("provider").lower not in list_of_providers: + if (req.payload.get("provider").lower() not in list_of_providers): raise ValueError("Invalid Provider.") # Check if text is empty. diff --git a/python/text-to-speech/requirements.txt b/python/text-to-speech/requirements.txt index f3900617..e0920f39 100644 --- a/python/text-to-speech/requirements.txt +++ b/python/text-to-speech/requirements.txt @@ -1,4 +1,5 @@ boto3==1.28.9 -azure-cognitiveservices-speech==1.24.0 google-cloud-texttospeech==2.14.1 +azure-cognitiveservices-speech==1.24.0 parameterized==0.9.0 +requests==2.31.0 \ No newline at end of file diff --git a/python/text-to-speech/test_main.py b/python/text-to-speech/test_main.py index 20cc2f07..c9212f45 100644 --- a/python/text-to-speech/test_main.py +++ b/python/text-to-speech/test_main.py @@ -22,28 +22,6 @@ read_text(encoding="utf-8")) -def get_instance(provider, key, project_id): - IMPLEMENTATIONS = { - "google": main.Google, - "azure": main.Azure, - "aws": main.AWS, - } - - req = MyRequest({ - "payload": { - "provider": provider, - "text": "hi", - "language": "en-US", - }, - "variables": { - "API_KEY": key, - "PROJECT_ID": project_id, - } - }) - - return IMPLEMENTATIONS[provider](req) - - class MyRequest: """Class for defining My Request structure.""" def __init__(self, data): @@ -65,17 +43,31 @@ def json(self, data=None): class GoogleTest(unittest.TestCase): """Google API Test Cases""" + def get_google_instance(self, key, project_id): + req = MyRequest({ + "payload": { + "provider": "google", + "text": "hi", + "language": "en-US", + }, + "variables": { + "API_KEY": key, + "PROJECT_ID": project_id, + } + }) + return main.Google(req) + @parameterized.expand([ (None, "123"), # Missing API KEY ("123", None), # Missing PROJECT ID (None, None), # Missing Both ]) def test_validate_request(self, key, project_id): - self.assertRaises(ValueError, get_instance, "google", key, project_id) + self.assertRaises(ValueError, self.get_google_instance, key, project_id) def test_speech_happy(self): """Test speech method for successful text-to-speech synthesis.""" - instance = get_instance("google", "123", "123") + instance = self.get_google_instance("123", "123") # Set up mock with patch.object(texttospeech.TextToSpeechClient, "synthesize_speech") as mock_synthesize_speech: mock_synthesize_speech.return_value.audio_content = base64.b64decode(RESULT_GOOGLE) @@ -86,7 +78,7 @@ def test_speech_happy(self): def test_speech_error(self): """Test speech method for unsuccessful text-to-speech synthesis.""" - instance = get_instance("google", "123", "123") + instance = self.get_google_instance("123", "123") # Set up mock with patch.object(texttospeech.TextToSpeechClient, "synthesize_speech") as mock_synthesize_speech: mock_synthesize_speech.return_value.audio_content = b"INCORRECT_VALUE" @@ -96,7 +88,7 @@ def test_speech_error(self): self.assertNotEqual(audio_bytes, base64.b64decode(RESULT_GOOGLE)) def test_google_credential(self): - instance = get_instance("google", "WRONG_API_KEY", "WRONG_PROJECT_ID") + instance = self.get_google_instance("WRONG_API_KEY", "WRONG_PROJECT_ID") # Set up mock with patch.object(texttospeech.TextToSpeechClient, "synthesize_speech") as mock_synthesize_speech: # Raise Exception @@ -104,14 +96,14 @@ def test_google_credential(self): self.assertRaises(Exception, instance.speech, "hello", "en-US") # Incorrect credentials raise exception def test_google_language(self): - instance = get_instance("google", "", "") + instance = self.get_google_instance("", "") with patch.object(texttospeech.TextToSpeechClient, "synthesize_speech") as mock_synthesize_speech: mock_synthesize_speech.side_effect = Exception self.assertRaises(Exception, instance.speech, "hello", "en-EN") # Incorrect language self.assertRaises(Exception, instance.speech, "hello", None) # Empty language code def test_speech_text(self): - instance = get_instance("google", "", "") + instance = self.get_google_instance("", "") with patch.object(texttospeech, "TextToSpeechClient") as mock_client: mock_client.side_effect = Exception self.assertRaises(Exception, instance.speech, None, "en-US") # Empty Text @@ -119,7 +111,25 @@ def test_speech_text(self): class AzureTest(unittest.TestCase): """Azure API Test Cases""" - + def get_azure_instance(self, key, project_id): + req = MyRequest({ + "payload": { + "provider": "azure", + "text": "hi", + "language": "en-US", + }, + "variables": { + "API_KEY": key, + "PROJECT_ID": project_id, + } + }) + return main.Azure(req) + + @parameterized.expand([ + (None, "123"), # Missing API KEY + ("123", None), # Missing PROJECT ID + (None, None), # Missing Both + ]) def test_validate_request(self, req): """Test validate_request method when all required fields are present.""" pass @@ -141,73 +151,73 @@ def test_speech_key_exception(self, text, language): pass -class AWSTest(unittest.TestCase): - """AWS API Test Cases""" - def test_validate_request(self, req): - """Test validate_request method when all required fields are present.""" - pass - - def test_validate_request_missing_aws_access_key_id(self, req): - """Test validate_request method when 'AWS_ACCESS_KEY_ID' is missing.""" - pass - - def test_validate_request_missing_aws_secret_access_key(self, req): - """Test validate_request method when 'AWS_SECRET_ACCESS_KEY' is missing.""" - pass - - def test_speech(self): - """Test speech method for successful text-to-speech synthesis.""" - req = MyRequest({ - "payload": { - "provider": "aws", - "text": "hi", - "language": "en-US", - }, - "variables": { - "API_KEY": "123", - "PROJECT_ID": "123", - } - }) - # Create an instance of Google Class - aws_instance = main.AWS(req) - # Variables - text = "hello" - language = "en-US" - # Set up mock - with patch.object(texttospeech, "TextToSpeechClient") as mock_client: - mock_response = mock_client.return_value - mock_response.synthesize_speech.return_value.audio_content = base64.b64decode("RESULT_AWS") - - # Call the speech method - audio_stream = aws_instance.speech(text, language) - - # Assert that the mock client was called with the correct arguments - mock_client.assert_called_once_with(client_options={"api_key": "123", "secret_api_key": "123"}) - - # Assert that the synthesize_speech method was called with the correct arguments - mock_response.synthesize_speech.assert_called_once_with(VoiceId="Joanna", OutputFormat="mp3", Text=text, LanguageCode=language) - - # Assert the result - self.assertEqual(audio_stream, base64.b64decode("RESULT_AWS")) - - def test_speech_key_exception(self, text, language): - """Test speech method for handling exceptions during text-to-speech synthesis.""" - pass - - -class ValidateCommonTest(unittest.TestCase): - """Test Cases for validate_common function""" - def test_validate_common(self, req): - """Test validate_common function with valid input.""" - pass - - def test_missing_text(self, req): - """Test validate_common function when 'text' is missing.""" - pass - - def test_missing_language(self, req): - """Test validate_common function when 'language' is missing.""" - pass +# class AWSTest(unittest.TestCase): +# """AWS API Test Cases""" +# def test_validate_request(self, req): +# """Test validate_request method when all required fields are present.""" +# pass + +# def test_validate_request_missing_aws_access_key_id(self, req): +# """Test validate_request method when 'AWS_ACCESS_KEY_ID' is missing.""" +# pass + +# def test_validate_request_missing_aws_secret_access_key(self, req): +# """Test validate_request method when 'AWS_SECRET_ACCESS_KEY' is missing.""" +# pass + + # def test_speech(self): + # """Test speech method for successful text-to-speech synthesis.""" + # req = MyRequest({ + # "payload": { + # "provider": "aws", + # "text": "hi", + # "language": "en-US", + # }, + # "variables": { + # "API_KEY": "123", + # "PROJECT_ID": "123", + # } + # }) + # # Create an instance of Google Class + # aws_instance = main.AWS(req) + # # Variables + # text = "hello" + # language = "en-US" + # # Set up mock + # with patch.object(texttospeech, "TextToSpeechClient") as mock_client: + # mock_response = mock_client.return_value + # mock_response.synthesize_speech.return_value.audio_content = base64.b64decode("RESULT_AWS") + + # # Call the speech method + # audio_stream = aws_instance.speech(text, language) + + # # Assert that the mock client was called with the correct arguments + # mock_client.assert_called_once_with(client_options={"api_key": "123", "secret_api_key": "123"}) + + # # Assert that the synthesize_speech method was called with the correct arguments + # mock_response.synthesize_speech.assert_called_once_with(VoiceId="Joanna", OutputFormat="mp3", Text=text, LanguageCode=language) + + # # Assert the result + # self.assertEqual(audio_stream, base64.b64decode("RESULT_AWS")) + + # def test_speech_key_exception(self, text, language): + # """Test speech method for handling exceptions during text-to-speech synthesis.""" + # pass + + +# class ValidateCommonTest(unittest.TestCase): +# """Test Cases for validate_common function""" +# def test_validate_common(self, req): +# """Test validate_common function with valid input.""" +# pass + +# def test_missing_text(self, req): +# """Test validate_common function when 'text' is missing.""" +# pass + +# def test_missing_language(self, req): +# """Test validate_common function when 'language' is missing.""" +# pass if __name__ == "__main__": From abb1034ec547cfb8ac68d8c67e05e0bc55baa18b Mon Sep 17 00:00:00 2001 From: Ruby Nguyen <50710444+rubynguyen1510@users.noreply.github.com> Date: Mon, 31 Jul 2023 13:19:23 -0700 Subject: [PATCH 10/11] added unit tests for AWS and validate_common (#13) --- python/aws-sandbox/.gitignore | 1 - python/aws-sandbox/polly.py | 20 ---- .../__pycache__/main.cpython-310.pyc | Bin 0 -> 7230 bytes .../__pycache__/secret.cpython-310.pyc | Bin 0 -> 251 bytes .../__pycache__/test_main.cpython-310.pyc | Bin 0 -> 8658 bytes python/text-to-speech/main.py | 15 +-- python/text-to-speech/requirements.txt | 2 +- python/text-to-speech/results/aws.txt | 1 + python/text-to-speech/test.py | 20 ++++ python/text-to-speech/test_main.py | 106 ++++++++++-------- 10 files changed, 89 insertions(+), 76 deletions(-) delete mode 100644 python/aws-sandbox/.gitignore delete mode 100644 python/aws-sandbox/polly.py create mode 100644 python/text-to-speech/__pycache__/main.cpython-310.pyc create mode 100644 python/text-to-speech/__pycache__/secret.cpython-310.pyc create mode 100644 python/text-to-speech/__pycache__/test_main.cpython-310.pyc create mode 100644 python/text-to-speech/test.py diff --git a/python/aws-sandbox/.gitignore b/python/aws-sandbox/.gitignore deleted file mode 100644 index b47515cb..00000000 --- a/python/aws-sandbox/.gitignore +++ /dev/null @@ -1 +0,0 @@ -secret.py \ No newline at end of file diff --git a/python/aws-sandbox/polly.py b/python/aws-sandbox/polly.py deleted file mode 100644 index c53cb71b..00000000 --- a/python/aws-sandbox/polly.py +++ /dev/null @@ -1,20 +0,0 @@ -import boto3 -import secret -import base64 - -polly_client = boto3.Session( - aws_access_key_id=secret.aws_access_key_id, - aws_secret_access_key=secret.aws_secret_access_key, - region_name='us-west-2').client('polly') - -t = "Hello, this is a sample text to be converted into speech." - -response = polly_client.synthesize_speech( - VoiceId='Joanna', - OutputFormat='mp3', - SampleRate='8000', - Text=t, - TextType='text', - ) - -print(base64.b64encode(response['AudioStream'].read().decode())) \ No newline at end of file diff --git a/python/text-to-speech/__pycache__/main.cpython-310.pyc b/python/text-to-speech/__pycache__/main.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..26a213e7b1f84802ff106d7097cbc074c392b8ba GIT binary patch literal 7230 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 literal 0 HcmV?d00001 diff --git a/python/text-to-speech/__pycache__/secret.cpython-310.pyc b/python/text-to-speech/__pycache__/secret.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..35456c8bb80f2bb9fedaa5bf08c4840d451280e0 GIT binary patch literal 251 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 None: Raises: ValueError: If any required value is missing or invalid. """ - if not req.payload.get("API_KEY"): + if not req.variables.get("API_KEY"): raise ValueError("Missing API_KEY.") - if not req.payload.get("SECRET_API_KEY"): + if not req.variables.get("SECRET_API_KEY"): raise ValueError("Missing SECRET_API_KEY.") self.api_key = req.payload.get("API_KEY") self.secret_api_key = req.payload.get("SECRET_API_KEY") @@ -172,7 +172,7 @@ def speech(self, text: str, language: str) -> bytes: Text=text, LanguageCode=language ) - return response["Audiostream"].read() + return response["Audiostream"] list_of_providers = ["google", "azure", "aws"] @@ -195,7 +195,7 @@ def validate_common(req: requests) -> tuple: """ # Check if the payload is empty. if not req.payload: - raise ValueError("Missing payload") + raise ValueError("Missing Payload.") # Check if variables is empty. if not req.variables: @@ -206,7 +206,7 @@ def validate_common(req: requests) -> tuple: raise ValueError("Missing Provider.") # Check if provider is in the list - if req.payload.get("provider").lower not in list_of_providers: + if (req.payload.get("provider").lower() not in list_of_providers): raise ValueError("Invalid Provider.") # Check if text is empty. @@ -221,8 +221,9 @@ def validate_common(req: requests) -> tuple: return (req.payload.get("provider").lower(), req.payload.get("text"), req.payload.get("language")) - +# print("google".lower() not in list_of_providers) def main(req: requests, res: json) -> json: + """ Main Function for Text to Speech. diff --git a/python/text-to-speech/requirements.txt b/python/text-to-speech/requirements.txt index f3900617..e33582af 100644 --- a/python/text-to-speech/requirements.txt +++ b/python/text-to-speech/requirements.txt @@ -1,4 +1,4 @@ boto3==1.28.9 -azure-cognitiveservices-speech==1.24.0 google-cloud-texttospeech==2.14.1 +# azure-cognitiveservices-speech==1.30.0 parameterized==0.9.0 diff --git a/python/text-to-speech/results/aws.txt b/python/text-to-speech/results/aws.txt index e69de29b..19909b25 100644 --- a/python/text-to-speech/results/aws.txt +++ b/python/text-to-speech/results/aws.txt @@ -0,0 +1 @@ +//NExAASoAEkAUAAAXwGQ/p/gDhGY/4fADM/x/ADAzMfwfAM6/o/AyB+Y/N4A7pkc+A7AyOeH/AMwZD/0+A4P8/4DgDo/4fADo/w/gBgZ4f4/AR63R+AhfA81AUf47wD//NExAgUiyowAZNoAIHidDXtgtAXBH/EmB2CZkr/4WsZZff/+tOHPLw8P/8S8Yc3RN3Hp//+XEEXQNzM3////qNHMzcwLiCac0/////zM3bWncwNC+boKhAsSKDAV90A//NExAgS4cKcAYNQAM+vABFdTDxmRCqimDwaEZaIkBAUgNs/NLnbGEhMNPzrVIFPvJm/ONfYwkE+Ri5l/+Y11WLnAAXV+n6HjBQmc//+tLaagQESJOt3hyTZiQnjMvw6//NExA8TMWKoAc9IAFYEPV3dawoSzDBkasnJWCooQpTjVuz/P9/zz/3/fWQ/j3LQPH1nZe+++0EQcPlKrkoFWGBCMftpbLOTvFRagKXtjORQvVLmEjQcfnk70D7pU44b//NExBUWuTagAMvMcDtwm6jSR/sJNlQTAxWVzG+QdAJrAomaWUlU/yzNX+T3z//9m1medMBsUYkgeGBEYgFAbSFGO0OOAuQiizvfUOD0uoe6fx3u7JlQ3nZYLMs3UOao//NExA0VgWawAMYMlKw4YVjA12JJyT0fBI0uKzxl83MlCXgQieqwO2nyudv6pLP4Um/zt4973h+2fx+9/T03XRCSZPHJp1AIMjsoo0EPtWz/2F3AQwrvLoMXLesIVzrT//NExAoU8W60AMYQlCR77CH4FFF5QIEOLk6ALq4t1JeV0sKALV+zDpq9luT6/3VHl9Bz/sfzfp8L9+8XBrYdg8DUkB5lUJpkSKowQzVc3A1nctiP/xwx39CitfwVQwyz//NExAkSEVq8AMPMlGLWpsVa02Kdgz46LEOgRyRwm07wNAY0rsvMVTqaNSaNaBLqk1fdtfX/////d23VmoGgthZ1zc1CLJHC5Iwd+lXFogOmaGSeL2E8uLOd1kyECQp+//NExBMRIVK8AHvMlQ+zheivslKjEZU+DDLC2nWswY8a943vjWr43vb43b//vrM9MTpIBgqBA2uf6V1A6h+l3xIBQ8u7JU/BYnzwQGKPgGCQmpwHEcgkLWpzrH6vjRP4//NExCESeWa4AHvKlIeQc2jrfKlxfzTyRZtbrjOldd1/qlHFhpzCYsLsGi1GMPMokZxSeu/S3QHENVuJbVvNaxfC+qgvg1lYykKqXQlx/F4LkujlMU41Cca2Jqc4mA8q//NExCoQqS64AHvOcXi00oapjnJtRf/3Z0UdID4lF2jRXMIZNY3dcKxY4X0aFLncnn0n3LFmGErC9H2iDBLYrRNR/k9KxHHyrTdblCaBtgNCwYDxoh5Io7nL6O/+iW9R//NExDoSESasAMPOcKoPBwiJnnR4YDa///9iVbijphYgkTSaw1ltIChmtbtuzeBsrOSMQDMEwSHw4TiQwJKIqCMTAyENltitougYm95rITet3eZC3DO8SlmflZhSkg+c//NExEQSKVqkAMMElP9P//yq0quA2ohqww/Sw/UlkPxyOUTAkojm5Hgotq1QQFREOmA2uC/BAyCQPl0IJhsL6jyfheXFO5y8v7r7J02CGmoQLHQ2FHtO0////JC9kpDN//NExE4SqRakAMPScCRKAx25MZxaO1owbRoARBXA0NBUDMCC2kWrKyUXIVSDBKAoH4FC70CiFfHJt1BiN05fARmQVcPdKkv////91iPR/qXoEElSkhA0OySG4jFK0yWH//NExFYRYPagAMJScMyF0JMIDZKabcwpclYV40vrriFJiUA6fKsmuIzlQulz27tnbT7qFaOoHLmUBUjQKX////zPbSrEypUxIgujlFb0Tuy9/C+lWcqAYWnLdWNI/kta//NExGMRmQqcAMJYcLnXtaUtjdQNLSbQWGdFmSN5azCHw74RtZxtSY4yJlAYIgdH////Q7///0V+DkRhYkxqkghyLSmH5lyn/hxaroW4NooZzz1+HbM/s8/8bV+1eCpd//NExG8RqRKYAMPScOeDQBSwJiwJAqPhVEySSHCAUpszVSn2xcMjCRUBHTVn////pvX29P+UPPDM4HNw/TEZVLxQGpgXdRNjmgcfaU02He+SJPJVIcJhz54mlxax8Lmp//NExHsUQR6QAVhIACczCuCXAKIZYXQTwE/AgKzIyWcF8DOBGi6J6MMLQXygkPUexk7VLMiEamwww9lj2JUpGxK0tfmrLrSqrQuzf/91oqN0klHupa0Eta//1MnP9kTY//NExH0hEqJ4AZpoAdb/bIFpG9ZVgkPqlevVgL5Om9klcaBqFjLNI5RxmxKalnDHdnn/tMvtZtMc5Nevu7ekBeYHYRUIxFqupOqVicgAsbol8L5rXO29ad0wct7aU91A//NExEsbMWoUAdhgAGtcIFUxp9qDKHvUTbKkhEQJE6mXVi1Jo8oDXLJJUKKYp61BEa1KIwLTSqjjMMxqmrTCrDVCpUKoHway4xry8rjHfHNj8lKcSElSCwqRKCoVPQoc//NExDEYwUYAAMJScJaWBIEg0TJiklrylcY+TSjxUioGn15INPLA1iIeCrhMDT+uIpGdeInnmDXM5Iqtj6gaCYhmCuKPnnWWtyq4hGg+XJD7LTKIhLHF2NyeSiqtdXJV//NExCESkbWMAGJElCTXhrOxioh2//oq/KiqjtuUyKn/3KGBgwTMmQEK/xUW/+Kijf6haoWFVUxBTTXYUNNntBQ28i7Tc/GCN14Jo3UyUTcUPONmtTU0xF9lFfBVi5jj//NExCkAAANIAAAAAF5UP5IJoGgaCohx+SxSiw7F3fQXPuixe0kFAeGFnkGIji9wn/yWLDv6Abn5/oiIiIn//T4iJXcO9dw4GLeaF13d3Qrz+Cw8PH+8/4AH8AQnAABH//NExHwAAANIAAAAAIAYe072jOCP8zxCBSiPGaUSFnSoXydOnhkDLKzVx9ejtbWzWtGjkgVKZkCDo6C/j/4q5LBRR++nDZYbjvm2VZFRfG7hdWSy/Oacyi/v//lRJuX+//NExMwH4CQAAPe8AEuqxZZf4+F5V3m/Kb/6/GoAqLBUhFduI/76O27EYo8cL+NGlM7PUnCRQo8y4djSij4dpOEgQEfDtqiL/9nKinb5TBlI7OVP/+YoYGDIfULC4rFR//NExP8Y6f3sAHoGmVEj//rFcVFjVbP4qKcWTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//NExO4V0LIAAHpMTaqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//NExOkUQbmYAMGElKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq \ No newline at end of file diff --git a/python/text-to-speech/test.py b/python/text-to-speech/test.py new file mode 100644 index 00000000..2f110698 --- /dev/null +++ b/python/text-to-speech/test.py @@ -0,0 +1,20 @@ +import main +import test_main +import secret + +req = test_main.MyRequest({ + "payload": { + "provider": "google", + "text": "hi", + "language": "en-US", + }, + "variables": { + "API_KEY": secret.GOOGLE_API_KEY, + "PROJECT_ID": secret.GOOGLE_PROJECT_ID, + } +}) + +res = test_main.MyResponse() + +main.main(req, res) +print(res.json()) \ No newline at end of file diff --git a/python/text-to-speech/test_main.py b/python/text-to-speech/test_main.py index 20cc2f07..012d6444 100644 --- a/python/text-to-speech/test_main.py +++ b/python/text-to-speech/test_main.py @@ -8,19 +8,23 @@ import requests from parameterized import parameterized from google.cloud import texttospeech +import boto3 # Local imports import main RESULT_GOOGLE = ( - pathlib.Path("python/text-to-speech/results/google.txt"). + pathlib.Path("results/google.txt"). read_text(encoding="utf-8")) # Path to krakenio encoded result (str). RESULT_AZURE = ( - pathlib.Path("python/text-to-speech/results/azure.txt"). + pathlib.Path("results/azure.txt"). read_text(encoding="utf-8")) +RESULT_AWS = ( + pathlib.Path("results/aws.txt"). + read_text(encoding="utf-8")) def get_instance(provider, key, project_id): IMPLEMENTATIONS = { @@ -143,20 +147,7 @@ def test_speech_key_exception(self, text, language): class AWSTest(unittest.TestCase): """AWS API Test Cases""" - def test_validate_request(self, req): - """Test validate_request method when all required fields are present.""" - pass - - def test_validate_request_missing_aws_access_key_id(self, req): - """Test validate_request method when 'AWS_ACCESS_KEY_ID' is missing.""" - pass - - def test_validate_request_missing_aws_secret_access_key(self, req): - """Test validate_request method when 'AWS_SECRET_ACCESS_KEY' is missing.""" - pass - - def test_speech(self): - """Test speech method for successful text-to-speech synthesis.""" + def get_instance(self, key, secret_key): req = MyRequest({ "payload": { "provider": "aws", @@ -164,50 +155,71 @@ def test_speech(self): "language": "en-US", }, "variables": { - "API_KEY": "123", - "PROJECT_ID": "123", + "API_KEY": key, + "SECRET_API_KEY": secret_key, } }) - # Create an instance of Google Class - aws_instance = main.AWS(req) - # Variables - text = "hello" - language = "en-US" - # Set up mock - with patch.object(texttospeech, "TextToSpeechClient") as mock_client: - mock_response = mock_client.return_value - mock_response.synthesize_speech.return_value.audio_content = base64.b64decode("RESULT_AWS") - - # Call the speech method - audio_stream = aws_instance.speech(text, language) - - # Assert that the mock client was called with the correct arguments - mock_client.assert_called_once_with(client_options={"api_key": "123", "secret_api_key": "123"}) + return main.AWS(req) - # Assert that the synthesize_speech method was called with the correct arguments - mock_response.synthesize_speech.assert_called_once_with(VoiceId="Joanna", OutputFormat="mp3", Text=text, LanguageCode=language) + @parameterized.expand([ + (None, "123"), # Missing API KEY + ("123", None), # Missing SECRET API KEY + (None, None), # Missing Both + ]) + def test_validate_request(self, key, secret_key): + self.assertRaises(ValueError, self.get_instance, key, secret_key) + def test_speech_happy(self): + """Test speech method for successful text-to-speech synthesis.""" + instance = self.get_instance("123", "123") + # Set up mock + with patch.object(boto3.Session, "client") as mock_client: + mock_response = {"Audiostream": base64.b64decode(RESULT_AWS)} + mock_client.return_value.synthesize_speech.return_value = mock_response + got = instance.speech("hi", "en-US") + want = base64.b64decode(RESULT_AWS) # Assert the result - self.assertEqual(audio_stream, base64.b64decode("RESULT_AWS")) + self.assertEqual(got, want) - def test_speech_key_exception(self, text, language): + def test_speech_key_exception(self): """Test speech method for handling exceptions during text-to-speech synthesis.""" - pass + instance = self.get_instance("123", "123") + self.assertRaises(Exception, instance.speech, "hi", "en-US") class ValidateCommonTest(unittest.TestCase): """Test Cases for validate_common function""" - def test_validate_common(self, req): - """Test validate_common function with valid input.""" - pass + def get_req(self, payload, variables, provider, text, language): + return MyRequest({ + payload: { + "provider": provider, + "text": text, + "language": language, + }, + variables: { + "key": "123", + } + }) - def test_missing_text(self, req): - """Test validate_common function when 'text' is missing.""" - pass + def test_validate_common_happy(self): + """Test validate common method happy path.""" + want = ("google", "hi", "en-US") + req = self.get_req("payload", "variables", "google", "hi", "en-US") + got = main.validate_common(req) + self.assertEquals(got, want) - def test_missing_language(self, req): - """Test validate_common function when 'language' is missing.""" - pass + @parameterized.expand([ + ("", "variables", "aws", "hi", "en-US"), # Missing payload + ("payload", "", "aws", "hi", "en-US"), # Missing variables + ("payload", "variables", "", "hi", "en-US"), # Missing provider + ("payload", "variables", "awss", "hi", "en-US"), # Invalid provider + ("payload", "variables", "aws", "", "en-US"), # Missing text + ("payload", "variables", "aws", "hi", ""), # Missing language + ]) + def test_validate_common_errors(self, payload, variables, provider, text, language): + """Test validate common method when it raises value errors.""" + req = self.get_req(payload, variables, provider, text, language) + self.assertRaises(ValueError, main.validate_common, req) if __name__ == "__main__": From ac3ab30c141ce0863d80ba8d44dc81eb474ce857 Mon Sep 17 00:00:00 2001 From: Noah Jacinto Date: Mon, 31 Jul 2023 13:20:54 -0700 Subject: [PATCH 11/11] Working version of azure and google, made change to readme, removed azure from req --- python/text-to-speech/main.py | 59 ++++---- python/text-to-speech/requirements.txt | 1 - python/text-to-speech/results/aws.txt | 1 + python/text-to-speech/test_main.py | 192 +++++++++++++++---------- 4 files changed, 143 insertions(+), 110 deletions(-) diff --git a/python/text-to-speech/main.py b/python/text-to-speech/main.py index c5d4df58..7f6be4bc 100644 --- a/python/text-to-speech/main.py +++ b/python/text-to-speech/main.py @@ -7,7 +7,6 @@ # Third party import requests from google.cloud import texttospeech -import azure.cognitiveservices.speech as speechsdk import boto3 @@ -100,6 +99,15 @@ def validate_request(self, req: requests) -> None: self.api_key = req.variables.get("API_KEY") self.region_key = req.variables.get("REGION_KEY") + def get_token(self, subscription_key): + fetch_token_url = 'https://westus.api.cognitive.microsoft.com/sts/v1.0/issuetoken' + headers = { + 'Ocp-Apim-Subscription-Key': subscription_key + } + response = requests.post(fetch_token_url, headers=headers) + access_token = str(response.text) + return access_token + def speech(self, text: str, language: str) -> bytes: """ Converts the given text into speech with the Google text to speech API. @@ -111,21 +119,17 @@ def speech(self, text: str, language: str) -> bytes: Returns: bytes: The synthezied speech in bytes. """ - # Set the speech configuration to speech key and region key. - speech_config = speechsdk.SpeechConfig( - subscription=self.api_key, - region=self.region_key - ) - # The language of the voice that speaks. - speech_config.speech_synthesis_language = language - # Set the speech. - speech_synthesizer = speechsdk.SpeechSynthesizer( - speech_config=speech_config, - audio_config=None - ) - # Response for the speech synthesizer. - response = speech_synthesizer.speak_text_async(text).get().audio_data - return response + url = f"https://{self.region_key}.tts.speech.microsoft.com/cognitiveservices/v1" + + headers_azure = { + 'Content-type': 'application/ssml+xml', + # 'Ocp-Apim-Subscription-Key': self.api_key, + 'Authorization': 'Bearer ' + self.get_token(self.api_key), + 'X-Microsoft-OutputFormat': 'audio-16khz-32kbitrate-mono-mp3', + } + data_azure = f"{text}" + response = requests.request("POST", url, headers=headers_azure, data=data_azure) + return response.content class AWS(TextToSpeech): @@ -141,12 +145,12 @@ def validate_request(self, req: requests) -> None: Raises: ValueError: If any required value is missing or invalid. """ - if not req.payload.get("API_KEY"): + if not req.variables.get("API_KEY"): raise ValueError("Missing API_KEY.") - if not req.payload.get("SECRET_API_KEY"): + if not req.variables.get("SECRET_API_KEY"): raise ValueError("Missing SECRET_API_KEY.") - self.api_key = req.payload.get("API_KEY") - self.secret_api_key = req.payload.get("SECRET_API_KEY") + self.api_key = req.variables.get("API_KEY") + self.secret_api_key = req.variables.get("SECRET_API_KEY") def speech(self, text: str, language: str) -> bytes: """ @@ -160,19 +164,10 @@ def speech(self, text: str, language: str) -> bytes: bytes: The synthezied speech in bytes. """ # Call polly client using boto3.session - polly_client = boto3.Session( - aws_access_key_id=self.api_key, - aws_secret_access_key=self.secret_api_key, - region_name="us-west-2" - ).client("polly") + polly_client = boto3.Session(aws_access_key_id=self.api_key, aws_secret_access_key=self.secret_api_key, region_name="us-west-2").client("polly") # Get response from polly client - response = polly_client.synthesize_speech( - VoiceId=AWS.voice_id, - OutputFormat="mp3", - Text=text, - LanguageCode=language - ) - return response["Audiostream"].read() + response = polly_client.synthesize_speech(VoiceId=AWS.voice_id,OutputFormat="mp3",Text=text,LanguageCode=language) + return response["Audiostream"] list_of_providers = ["google", "azure", "aws"] diff --git a/python/text-to-speech/requirements.txt b/python/text-to-speech/requirements.txt index e0920f39..809845e6 100644 --- a/python/text-to-speech/requirements.txt +++ b/python/text-to-speech/requirements.txt @@ -1,5 +1,4 @@ boto3==1.28.9 google-cloud-texttospeech==2.14.1 -azure-cognitiveservices-speech==1.24.0 parameterized==0.9.0 requests==2.31.0 \ No newline at end of file diff --git a/python/text-to-speech/results/aws.txt b/python/text-to-speech/results/aws.txt index e69de29b..19909b25 100644 --- a/python/text-to-speech/results/aws.txt +++ b/python/text-to-speech/results/aws.txt @@ -0,0 +1 @@ +//NExAASoAEkAUAAAXwGQ/p/gDhGY/4fADM/x/ADAzMfwfAM6/o/AyB+Y/N4A7pkc+A7AyOeH/AMwZD/0+A4P8/4DgDo/4fADo/w/gBgZ4f4/AR63R+AhfA81AUf47wD//NExAgUiyowAZNoAIHidDXtgtAXBH/EmB2CZkr/4WsZZff/+tOHPLw8P/8S8Yc3RN3Hp//+XEEXQNzM3////qNHMzcwLiCac0/////zM3bWncwNC+boKhAsSKDAV90A//NExAgS4cKcAYNQAM+vABFdTDxmRCqimDwaEZaIkBAUgNs/NLnbGEhMNPzrVIFPvJm/ONfYwkE+Ri5l/+Y11WLnAAXV+n6HjBQmc//+tLaagQESJOt3hyTZiQnjMvw6//NExA8TMWKoAc9IAFYEPV3dawoSzDBkasnJWCooQpTjVuz/P9/zz/3/fWQ/j3LQPH1nZe+++0EQcPlKrkoFWGBCMftpbLOTvFRagKXtjORQvVLmEjQcfnk70D7pU44b//NExBUWuTagAMvMcDtwm6jSR/sJNlQTAxWVzG+QdAJrAomaWUlU/yzNX+T3z//9m1medMBsUYkgeGBEYgFAbSFGO0OOAuQiizvfUOD0uoe6fx3u7JlQ3nZYLMs3UOao//NExA0VgWawAMYMlKw4YVjA12JJyT0fBI0uKzxl83MlCXgQieqwO2nyudv6pLP4Um/zt4973h+2fx+9/T03XRCSZPHJp1AIMjsoo0EPtWz/2F3AQwrvLoMXLesIVzrT//NExAoU8W60AMYQlCR77CH4FFF5QIEOLk6ALq4t1JeV0sKALV+zDpq9luT6/3VHl9Bz/sfzfp8L9+8XBrYdg8DUkB5lUJpkSKowQzVc3A1nctiP/xwx39CitfwVQwyz//NExAkSEVq8AMPMlGLWpsVa02Kdgz46LEOgRyRwm07wNAY0rsvMVTqaNSaNaBLqk1fdtfX/////d23VmoGgthZ1zc1CLJHC5Iwd+lXFogOmaGSeL2E8uLOd1kyECQp+//NExBMRIVK8AHvMlQ+zheivslKjEZU+DDLC2nWswY8a943vjWr43vb43b//vrM9MTpIBgqBA2uf6V1A6h+l3xIBQ8u7JU/BYnzwQGKPgGCQmpwHEcgkLWpzrH6vjRP4//NExCESeWa4AHvKlIeQc2jrfKlxfzTyRZtbrjOldd1/qlHFhpzCYsLsGi1GMPMokZxSeu/S3QHENVuJbVvNaxfC+qgvg1lYykKqXQlx/F4LkujlMU41Cca2Jqc4mA8q//NExCoQqS64AHvOcXi00oapjnJtRf/3Z0UdID4lF2jRXMIZNY3dcKxY4X0aFLncnn0n3LFmGErC9H2iDBLYrRNR/k9KxHHyrTdblCaBtgNCwYDxoh5Io7nL6O/+iW9R//NExDoSESasAMPOcKoPBwiJnnR4YDa///9iVbijphYgkTSaw1ltIChmtbtuzeBsrOSMQDMEwSHw4TiQwJKIqCMTAyENltitougYm95rITet3eZC3DO8SlmflZhSkg+c//NExEQSKVqkAMMElP9P//yq0quA2ohqww/Sw/UlkPxyOUTAkojm5Hgotq1QQFREOmA2uC/BAyCQPl0IJhsL6jyfheXFO5y8v7r7J02CGmoQLHQ2FHtO0////JC9kpDN//NExE4SqRakAMPScCRKAx25MZxaO1owbRoARBXA0NBUDMCC2kWrKyUXIVSDBKAoH4FC70CiFfHJt1BiN05fARmQVcPdKkv////91iPR/qXoEElSkhA0OySG4jFK0yWH//NExFYRYPagAMJScMyF0JMIDZKabcwpclYV40vrriFJiUA6fKsmuIzlQulz27tnbT7qFaOoHLmUBUjQKX////zPbSrEypUxIgujlFb0Tuy9/C+lWcqAYWnLdWNI/kta//NExGMRmQqcAMJYcLnXtaUtjdQNLSbQWGdFmSN5azCHw74RtZxtSY4yJlAYIgdH////Q7///0V+DkRhYkxqkghyLSmH5lyn/hxaroW4NooZzz1+HbM/s8/8bV+1eCpd//NExG8RqRKYAMPScOeDQBSwJiwJAqPhVEySSHCAUpszVSn2xcMjCRUBHTVn////pvX29P+UPPDM4HNw/TEZVLxQGpgXdRNjmgcfaU02He+SJPJVIcJhz54mlxax8Lmp//NExHsUQR6QAVhIACczCuCXAKIZYXQTwE/AgKzIyWcF8DOBGi6J6MMLQXygkPUexk7VLMiEamwww9lj2JUpGxK0tfmrLrSqrQuzf/91oqN0klHupa0Eta//1MnP9kTY//NExH0hEqJ4AZpoAdb/bIFpG9ZVgkPqlevVgL5Om9klcaBqFjLNI5RxmxKalnDHdnn/tMvtZtMc5Nevu7ekBeYHYRUIxFqupOqVicgAsbol8L5rXO29ad0wct7aU91A//NExEsbMWoUAdhgAGtcIFUxp9qDKHvUTbKkhEQJE6mXVi1Jo8oDXLJJUKKYp61BEa1KIwLTSqjjMMxqmrTCrDVCpUKoHway4xry8rjHfHNj8lKcSElSCwqRKCoVPQoc//NExDEYwUYAAMJScJaWBIEg0TJiklrylcY+TSjxUioGn15INPLA1iIeCrhMDT+uIpGdeInnmDXM5Iqtj6gaCYhmCuKPnnWWtyq4hGg+XJD7LTKIhLHF2NyeSiqtdXJV//NExCESkbWMAGJElCTXhrOxioh2//oq/KiqjtuUyKn/3KGBgwTMmQEK/xUW/+Kijf6haoWFVUxBTTXYUNNntBQ28i7Tc/GCN14Jo3UyUTcUPONmtTU0xF9lFfBVi5jj//NExCkAAANIAAAAAF5UP5IJoGgaCohx+SxSiw7F3fQXPuixe0kFAeGFnkGIji9wn/yWLDv6Abn5/oiIiIn//T4iJXcO9dw4GLeaF13d3Qrz+Cw8PH+8/4AH8AQnAABH//NExHwAAANIAAAAAIAYe072jOCP8zxCBSiPGaUSFnSoXydOnhkDLKzVx9ejtbWzWtGjkgVKZkCDo6C/j/4q5LBRR++nDZYbjvm2VZFRfG7hdWSy/Oacyi/v//lRJuX+//NExMwH4CQAAPe8AEuqxZZf4+F5V3m/Kb/6/GoAqLBUhFduI/76O27EYo8cL+NGlM7PUnCRQo8y4djSij4dpOEgQEfDtqiL/9nKinb5TBlI7OVP/+YoYGDIfULC4rFR//NExP8Y6f3sAHoGmVEj//rFcVFjVbP4qKcWTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//NExO4V0LIAAHpMTaqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//NExOkUQbmYAMGElKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq \ No newline at end of file diff --git a/python/text-to-speech/test_main.py b/python/text-to-speech/test_main.py index c9212f45..89616d08 100644 --- a/python/text-to-speech/test_main.py +++ b/python/text-to-speech/test_main.py @@ -8,6 +8,8 @@ import requests from parameterized import parameterized from google.cloud import texttospeech +import azure.cognitiveservices.speech as speechsdk +import boto3 # Local imports import main @@ -21,6 +23,9 @@ pathlib.Path("python/text-to-speech/results/azure.txt"). read_text(encoding="utf-8")) +RESULT_AWS = ( + pathlib.Path("python/text-to-speech/results/aws.txt"). + read_text(encoding="utf-8")) class MyRequest: """Class for defining My Request structure.""" @@ -93,20 +98,24 @@ def test_google_credential(self): with patch.object(texttospeech.TextToSpeechClient, "synthesize_speech") as mock_synthesize_speech: # Raise Exception mock_synthesize_speech.side_effect = Exception - self.assertRaises(Exception, instance.speech, "hello", "en-US") # Incorrect credentials raise exception + # Incorrect credentials raise exception + self.assertRaises(Exception, instance.speech, "hello", "en-US") def test_google_language(self): instance = self.get_google_instance("", "") with patch.object(texttospeech.TextToSpeechClient, "synthesize_speech") as mock_synthesize_speech: mock_synthesize_speech.side_effect = Exception - self.assertRaises(Exception, instance.speech, "hello", "en-EN") # Incorrect language - self.assertRaises(Exception, instance.speech, "hello", None) # Empty language code + # Incorrect language + self.assertRaises(Exception, instance.speech, "hello", "en-EN") + # Empty language code + self.assertRaises(Exception, instance.speech, "hello", None) def test_speech_text(self): instance = self.get_google_instance("", "") with patch.object(texttospeech, "TextToSpeechClient") as mock_client: mock_client.side_effect = Exception - self.assertRaises(Exception, instance.speech, None, "en-US") # Empty Text + # Set empty text + self.assertRaises(Exception, instance.speech, None, "en-US") class AzureTest(unittest.TestCase): @@ -130,13 +139,20 @@ def get_azure_instance(self, key, project_id): ("123", None), # Missing PROJECT ID (None, None), # Missing Both ]) - def test_validate_request(self, req): - """Test validate_request method when all required fields are present.""" - pass + def test_validate_request(self, key, project_id): + """Test validate method when all required fields are present.""" + self.assertRaises(ValueError, self.get_azure_instance, key, project_id) - def test_validate_request_missing_aws_access_key_id(self, req): - """Test validate_request methsod when 'AWS_ACCESS_KEY_ID' is missing.""" - pass + def test_speech_happy(self): + """Test speech method for successful text-to-speech synthesis.""" + instance = self.get_azure_instance("123", "123") + # Set up mock + with patch.object(speechsdk.SpeechSynthesizer, "speak_text_async") as mock_synthesize_speech: + mock_synthesize_speech.return_value.audio_content = base64.b64decode(RESULT_AZURE) + # Call the speech method + audio_bytes = instance.speech("hi", "en-US") + # Assert the result + self.assertEqual(audio_bytes, base64.b64decode(RESULT_AZURE)) def test_validate_request_missing_aws_secret_access_key(self, req): """Test validate_request method when 'AWS_SECRET_ACCESS_KEY' is missing.""" @@ -151,73 +167,95 @@ def test_speech_key_exception(self, text, language): pass -# class AWSTest(unittest.TestCase): -# """AWS API Test Cases""" -# def test_validate_request(self, req): -# """Test validate_request method when all required fields are present.""" -# pass - -# def test_validate_request_missing_aws_access_key_id(self, req): -# """Test validate_request method when 'AWS_ACCESS_KEY_ID' is missing.""" -# pass - -# def test_validate_request_missing_aws_secret_access_key(self, req): -# """Test validate_request method when 'AWS_SECRET_ACCESS_KEY' is missing.""" -# pass - - # def test_speech(self): - # """Test speech method for successful text-to-speech synthesis.""" - # req = MyRequest({ - # "payload": { - # "provider": "aws", - # "text": "hi", - # "language": "en-US", - # }, - # "variables": { - # "API_KEY": "123", - # "PROJECT_ID": "123", - # } - # }) - # # Create an instance of Google Class - # aws_instance = main.AWS(req) - # # Variables - # text = "hello" - # language = "en-US" - # # Set up mock - # with patch.object(texttospeech, "TextToSpeechClient") as mock_client: - # mock_response = mock_client.return_value - # mock_response.synthesize_speech.return_value.audio_content = base64.b64decode("RESULT_AWS") - - # # Call the speech method - # audio_stream = aws_instance.speech(text, language) - - # # Assert that the mock client was called with the correct arguments - # mock_client.assert_called_once_with(client_options={"api_key": "123", "secret_api_key": "123"}) - - # # Assert that the synthesize_speech method was called with the correct arguments - # mock_response.synthesize_speech.assert_called_once_with(VoiceId="Joanna", OutputFormat="mp3", Text=text, LanguageCode=language) - - # # Assert the result - # self.assertEqual(audio_stream, base64.b64decode("RESULT_AWS")) - - # def test_speech_key_exception(self, text, language): - # """Test speech method for handling exceptions during text-to-speech synthesis.""" - # pass - - -# class ValidateCommonTest(unittest.TestCase): -# """Test Cases for validate_common function""" -# def test_validate_common(self, req): -# """Test validate_common function with valid input.""" -# pass - -# def test_missing_text(self, req): -# """Test validate_common function when 'text' is missing.""" -# pass - -# def test_missing_language(self, req): -# """Test validate_common function when 'language' is missing.""" -# pass +class AWSTest(unittest.TestCase): + """AWS API Test Cases""" + def get_aws_instance(self, key, secret_key): + req = MyRequest({ + "payload": { + "provider": "azure", + "text": "hi", + "language": "en-US", + }, + "variables": { + "API_KEY": key, + "SECRET_API_KEY": secret_key, + } + }) + return main.AWS(req) + + def test_validate_request(self, req): + """Test validate_request method when all required fields are present.""" + pass + + def test_speech_happy(self): + """Test speech method for successful text-to-speech synthesis.""" + instance = self.get_aws_instance("123", "123") + # Set up mock + with patch.object(boto3.Session, "client") as mock_client: + mock_response = {"Audiostream": base64.b64decode(RESULT_AWS)} + mock_client.return_value.synthesize_speech.return_value = mock_response + # Call the speech method + got = instance.speech("hi", "en-US") + # Assert the result + self.assertEqual(got, base64.b64decode(RESULT_AWS)) + + def test_validate_request_missing_aws_secret_access_key(self, req): + """Test validate_request method when 'AWS_SECRET_ACCESS_KEY' is missing.""" + pass + + def test_speech(self): + """Test speech method for successful text-to-speech synthesis.""" + req = MyRequest({ + "payload": { + "provider": "aws", + "text": "hi", + "language": "en-US", + }, + "variables": { + "API_KEY": "123", + "PROJECT_ID": "123", + } + }) + # Create an instance of Google Class + aws_instance = main.AWS(req) + # Variables + text = "hello" + language = "en-US" + # Set up mock + with patch.object(texttospeech, "TextToSpeechClient") as mock_client: + mock_response = mock_client.return_value + mock_response.synthesize_speech.return_value.audio_content = base64.b64decode("RESULT_AWS") + + # Call the speech method + audio_stream = aws_instance.speech(text, language) + + # Assert that the mock client was called with the correct arguments + mock_client.assert_called_once_with(client_options={"api_key": "123", "secret_api_key": "123"}) + + # Assert that the synthesize_speech method was called with the correct arguments + mock_response.synthesize_speech.assert_called_once_with(VoiceId="Joanna", OutputFormat="mp3", Text=text, LanguageCode=language) + + # Assert the result + self.assertEqual(audio_stream, base64.b64decode("RESULT_AWS")) + + def test_speech_key_exception(self, text, language): + """Test speech method for handling exceptions during text-to-speech synthesis.""" + pass + + +class ValidateCommonTest(unittest.TestCase): + """Test Cases for validate_common function""" + def test_validate_common(self, req): + """Test validate_common function with valid input.""" + pass + + def test_missing_text(self, req): + """Test validate_common function when 'text' is missing.""" + pass + + def test_missing_language(self, req): + """Test validate_common function when 'language' is missing.""" + pass if __name__ == "__main__":