Skip to content
This repository was archived by the owner on Jan 25, 2022. It is now read-only.

Commit d7383e8

Browse files
authored
Implementación del servicio Text To Speech (#63)
* Initial project layout * Matame camion * Minor modifications, readme and docker-compose for tts * Estoy flipando * Toma esa git :)
1 parent 515f4cf commit d7383e8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+462
-0
lines changed

release/docker-compose.yml

+12
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,18 @@ services:
7171
networks:
7272
- postgres
7373

74+
texttospeech:
75+
build: ../src/texttospeechservice/
76+
container_name: texttospeechservice
77+
ports:
78+
- "5003:5000"
79+
depends_on:
80+
- kafka
81+
networks:
82+
- event_bus
83+
environment:
84+
KAFKA_ENDPOINT: "kafka:9092"
85+
7486
authenticationservice:
7587
build: ../src/authenticationservice/
7688
container_name: authservice
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

src/texttospeechservice/Dockerfile

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM python:3.8.7-slim
2+
3+
EXPOSE 5000
4+
5+
RUN mkdir app
6+
COPY . app
7+
WORKDIR app
8+
RUN pip install -r requirements.txt
9+
ENV FLASK_CONFIG=production
10+
ENV GOOGLE_APPLICATION_CREDENTIALS=keys.json
11+
ENTRYPOINT ["python", "wsgi.py"]

src/texttospeechservice/config.py

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
""" Configuration module for the flask app.
2+
3+
Each class represents a configuration to use by the flask app
4+
created in src/__init__.py.
5+
"""
6+
7+
import os
8+
9+
def _try_get_config_from_env(config_key, default):
10+
if config_key not in os.environ:
11+
return default
12+
return os.environ[config_key]
13+
14+
class BaseConfig():
15+
DEBUG = False
16+
TESTING = False
17+
# these are our environment variables...
18+
# if they are not found, a default value is provided for local tests
19+
KAFKA_ENDPOINT = _try_get_config_from_env('KAFKA_ENDPOINT', 'localhost:9092')
20+
KAFKA_LOGGING_TOPIC = _try_get_config_from_env('KAFKA_LOGGING_TOPIC', 'service_logs')
21+
SERVICE_KEY = _try_get_config_from_env('SERVICE_KEY', 'tts_service')
22+
LOCALE_MAPPINGS_FILE = _try_get_config_from_env('MAPPINGS_FILE', 'lang2locale.csv')
23+
DEPLOY_PORT = int(_try_get_config_from_env('DEPLOY_PORT', '5000'))
24+
25+
class DevelopmentConfig(BaseConfig):
26+
DEBUG = True
27+
TESTING = True
28+
ENV = 'development'
29+
30+
class ProductionConfig(BaseConfig):
31+
ENV = 'production'
32+
33+
class TestingConfig(BaseConfig):
34+
TESTING = True
35+
ENV = 'testing'

src/texttospeechservice/keys.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"type": "service_account",
3+
"project_id": "translate-3e8bb",
4+
"private_key_id": "fedd58a3dd62b7453f7d7917d23ecdd66a11bb1c",
5+
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC3eiMaoxj9x9T/\noMvEew/7omPK1BYmss2pbBFWi3q4RTJC1JUY2ae+YLZKByyqwdyMc8/GW2JOfzZc\nYb4B6eZd2/v4hToCFB6qNN1e0BAEZbhO2ZlJoZkOr4yse8cuQ/IohX7YxFrCOmdh\nQFSdTxpWEFmTZYUri2QBS7TrbiVObVzC3AoHEaPg0qdlVIRjlKkkS53BFgPdCl+9\nbG3RkDKex5JHpUGUc6g6MlrV4/JEB5vsu+SqLHFR37LcQ9M6zmWkJFimNFUx6OPx\n/QM3Zcvii+tPvwFzS/yhyNMIHCssbVbWis1xHRyV45kC/cxABMWl6zxQ746odCiX\nNAeGnTU3AgMBAAECggEAAcuLuGD/VGXpUZVZj2yQAkUOd0rBhLnPHujUKpkAWsX8\nJE2lv8tK9tpFx+ei3XDUHy1Efr5TvrND7UqpHz+rzCAXRDeZeUrus3k2iUAj4ZPu\nzIBLtz3oT3QSpKvGJ4LtEmefKOPGHfLDiPhy9RVz2ByE1wpXos6hH3O4+DmCdLsp\nucwMRjZIpTnFWbFnOc6yQdJWNJ759ZAnvooiII5d6WlgMvSG00Vqh6li8ZV3vFhu\nkfc83+HF7J23psdES0DO5YmQsPRwGqL6ikmcs44QeTyhpaueil1Mbx9PRxycZWKk\nYWATPURtgZq3HDCpjahvIJcCnqXwGjMWS1AOq+v9kQKBgQDvQsxW0zoico/sPK8i\ni2yCtbPA32c4dJtah8jBPxvhIBVf+AoX5DWniQze4BA+YO5vGCoYFUqsimo5f8g/\noi9znRJDsMxKycTcQ0QK526qANjABkZh3YJn6Mb+kRp7rcHYi0NcoFjzaCPy2qIg\nn5y0NlpS8cZQ0sVEzzvGadZBfQKBgQDEUD3TMtINhjWQu5bNUY9zXE9ZEM05tJCs\nniHkCEad65OwgIEPQkWWijZ9fT+czhxSriiKVn/zGLrWRMzBYeH1uPPOpHdtoYUj\nH+OxvOUTJhbyys1XFmkfVZ2ymYvdAs6PscjfRXsHTzKCBfQgjynzUpr4CnT1mFzE\nKRZIyU8PwwKBgQCQmKcTpg5ROTk/xSD22JoYmKVs39bq6JXm7X7nQzOfJ5ujZyz6\naWH/wTT1ESbf/Aa9PzZZXazGf9RYsaAczPCuh3O9UwD0BeKiV0is6lcYCPD1hBVP\nGeaw055HxPvjWQx4yRlJxmJboElxgK5Q1wWGZ/7Id/OpbufngPKYI+hnpQKBgAdw\nDteYnlK62f6wzMbcpzW0sqDqPQxJpg2UNC6CjcJf8YmHZNxiI2bPt77LZwSW3oXX\naVvMaS6ZqkKB+sv83GXF6x7SJmA19WheRe8u+/Lcx5PNUThSdgsk4EOrA5yNBax2\nDOlApaeiPYSlmxk8s1cvswVgyAuiCSm5cMmzLtanAoGBANjC/fbqaslQFn/ufsM/\nR+2toel1toDRObBN3EDP/1/3ct4tNpJYO1KtoZIS0n+ZVM4BXTzRWpb+xy4zb7vh\niXJCVGozL8fSpuSUoJQUUtQAx0Swia3+2Caqi55OVAj39AAmD5mkh9hYbmjXRFIt\nMF1xw6QkhGS64am7ZdQAMn9k\n-----END PRIVATE KEY-----\n",
6+
"client_email": "[email protected]",
7+
"client_id": "115250184755501138688",
8+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
9+
"token_uri": "https://oauth2.googleapis.com/token",
10+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
11+
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/servicios-web%40translate-3e8bb.iam.gserviceaccount.com"
12+
}
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
language,locale
2+
ar,ar-XA
3+
bn,bn-IN
4+
cmn,cmn-CN
5+
cs,cs-CZ
6+
da,da-DK
7+
de,de-DE
8+
el,el-GR
9+
en,en-US
10+
es,es-ES
11+
fi,fi-FI
12+
fil,fil-PH
13+
fr,fr-FR
14+
gu,gu-IN
15+
hi,hi-IN
16+
hu,hu-HU
17+
id, id-ID
18+
it,it-IT
19+
ja,ja-JP
20+
kn,kn-IN
21+
ko,ko-KR
22+
ml,ml-IN
23+
nb,nb-NO
24+
nl,nl-NL
25+
pl,pl-PL
26+
pt,pt-PT
27+
ro,ro-RO
28+
ru,ru-RU
29+
sk,sk-SK
30+
sv,sv-SE
31+
ta,ta-IN
32+
te,te-IN
33+
th,th-TH
34+
tr,tr-TR
35+
uk,uk-UA
36+
vi,vi-VN
37+
yue,yue-HK

src/texttospeechservice/readme.md

+44
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Flask==1.1.2
2+
google-cloud-texttospeech
3+
kafka-python==1.4.6
4+
waitress
5+
zeep==4.0.0
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
""" src package __init__ file
2+
"""
3+
4+
import logging
5+
import os
6+
7+
from flask import Flask
8+
9+
from .util.logging_handler import KafkaLoggingHandler
10+
11+
12+
CONFIG = {
13+
"base": "config.BaseConfig",
14+
"development": "config.DevelopmentConfig",
15+
"production": "config.ProductionConfig",
16+
"testing": "config.TestingConfig"
17+
}
18+
19+
def create_app():
20+
""" Factory to create the flask application and load the config.
21+
By default config.BaseConfig is used. In order to change this config
22+
the FLASK_CONFIG environment variable must be changed to match one of
23+
the keys in the CONFIG dict.
24+
25+
Returns
26+
-------
27+
app: Flask.app object
28+
"""
29+
app = Flask(__name__, instance_relative_config=True)
30+
config_name = os.getenv('FLASK_CONFIG', 'base')
31+
app.config.from_object(CONFIG[config_name])
32+
_setup_logging(app.config['KAFKA_ENDPOINT'], app.config['KAFKA_LOGGING_TOPIC'], app.config['SERVICE_KEY'])
33+
with app.app_context():
34+
from .controller import tts
35+
return app
36+
37+
def _setup_logging(kafka_endpoint, topic, service_key):
38+
""" Setup of logging to a kafka endpoint """
39+
logger = logging.getLogger(service_key)
40+
logger.setLevel(logging.DEBUG)
41+
42+
if len(logger.handlers) == 0:
43+
logger.addHandler(logging.StreamHandler())
44+
45+
logger.debug(f"Starting kafka logging with endpoint '{kafka_endpoint}' and topic '{topic}'")
46+
kh = KafkaLoggingHandler(kafka_endpoint, key="text_to_speech", topic=topic)
47+
logger.addHandler(kh)
48+
logger.debug("Logging system started")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .gcloud_client import GCloudTTSClient
2+
from .soap_client import SoapClient
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import logging
2+
3+
from flask import current_app as app
4+
from google.cloud import texttospeech
5+
6+
7+
logger = logging.getLogger(app.config['SERVICE_KEY'])
8+
logger.setLevel(logging.DEBUG)
9+
10+
11+
class GCloudTTSClient():
12+
""" Wrapper around the Google Cloud Text-To-Speech Service.
13+
"""
14+
15+
def __init__(self):
16+
logger.debug("Initializing google cloud client")
17+
self.client = texttospeech.TextToSpeechClient()
18+
self.audio_config = texttospeech.AudioConfig(
19+
audio_encoding=texttospeech.AudioEncoding.MP3
20+
)
21+
22+
def tts(self, text, locale):
23+
""" Transforms text to audio spoken in the given locale.
24+
"""
25+
logger.debug(f"Calling GCloud tts with params[text='{text}', locale={locale}]")
26+
synthesis_input = texttospeech.SynthesisInput(text=text)
27+
voice = texttospeech.VoiceSelectionParams(language_code=locale,
28+
ssml_gender=texttospeech.SsmlVoiceGender.NEUTRAL)
29+
response = self.client.synthesize_speech(input=synthesis_input, voice=voice,
30+
audio_config=self.audio_config)
31+
return response.audio_content
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import logging
2+
import zeep
3+
4+
from flask import current_app as app
5+
6+
7+
logger = logging.getLogger(app.config['SERVICE_KEY'])
8+
logger.setLevel(logging.DEBUG)
9+
10+
11+
class SoapResult:
12+
def __init__(self, successful, result=None):
13+
self.successful = successful
14+
self.result = result
15+
16+
17+
class SoapClient:
18+
""" Allows making request to a given SOAP endpoint.
19+
20+
This class uses the ZEEP Python library (https://docs.python-zeep.org/en/master/index.html#)
21+
to allow users making requests to a SOAP endpoint.
22+
23+
When the zeep client is created, it parses the WSDL given as an argument and creates the
24+
appropiate functions to call the service in the client class. This wrapper allows users
25+
to access those functions with introspection.
26+
27+
Examples
28+
--------
29+
# this sample service provides a see_cars method that returns a list of cars
30+
>>> client = SoapClient('http://localhost:8080/mysoapws/cars.wsdl')
31+
>>> cars = client.see_cars() # cars will be a list of dicts (car 'objects')
32+
"""
33+
34+
def __init__(self, wsdl_endpoint):
35+
settings = zeep.Settings(strict=False, xml_huge_tree=True)
36+
self.client = zeep.Client(wsdl=wsdl_endpoint, settings=settings)
37+
38+
def __getattribute__(self, attr_name, *args, **kwargs):
39+
logger.debug(f"Calling method of SOAP Client: {attr_name}")
40+
logger.debug(f"Args: {args} - Kwargs: {kwargs}")
41+
try:
42+
res = getattr(client.service, attr_name)(*args, **kwargs)
43+
logger.info(f"SOAP request was successful. Result: {res}")
44+
return SoapResult(True, res)
45+
except Exception as e:
46+
logger.error(f"There was an error calling the SOAP client: {e}")
47+
logger.debug("Returning unsuccessful SOAP result")
48+
return SoapResult(False)
+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
""" Controllers of the text to speech service.
2+
"""
3+
4+
import base64
5+
import logging
6+
7+
from flask import current_app as app
8+
from flask import jsonify, request, send_file
9+
10+
from .clients import GCloudTTSClient, SoapClient
11+
from .util.error import TooManyRequestsError
12+
from .util.locale_manager import LocaleManager
13+
14+
15+
logger = logging.getLogger(app.config['SERVICE_KEY'])
16+
logger.setLevel(logging.DEBUG)
17+
18+
gcloud_tts_client = GCloudTTSClient()
19+
#statistics_client = SoapClient()
20+
locale_manager = LocaleManager(app.config['LOCALE_MAPPINGS_FILE'])
21+
22+
23+
@app.errorhandler(404)
24+
def page_not_found(e):
25+
# override default html response
26+
return jsonify({
27+
'response': 'Page not found'
28+
}), 404
29+
30+
@app.route('/api/TextsToSpeechs', methods=['POST'])
31+
def tts():
32+
""" Transforms the given text to an audio.
33+
34+
The returned audio is encoded as a base64 string.
35+
"""
36+
logger.debug("POST to TextsToSpeechs received")
37+
body_data = request.get_json()
38+
logger.debug(f"Data: {body_data}")
39+
40+
if 'language' not in body_data or 'text' not in body_data:
41+
logger.debug("Some of the required information is not present, returning bad request.")
42+
return jsonify({
43+
'response': 'Not all required parameters are present in body'
44+
}), 400 # BadRequest
45+
46+
try:
47+
language = body_data['language']
48+
logger.debug(f"Transforming language '{language}' to valid locale")
49+
locale = language if locale_manager.is_locale(language) \
50+
else locale_manager.try_get_locale(language, 'en-US')
51+
logger.debug(f"Locale after transformation: '{locale}'")
52+
53+
logger.debug("Calling Google Cloud client to convert text to speech")
54+
audio = gcloud_tts_client.tts(body_data['text'], locale)
55+
logger.debug("Audio file retrieved from client")
56+
57+
# Ok, this took a while to figure out... GCloud returns the audio in a base64 encoded string,
58+
# but the python library auto decodes it for us and we get the audio bytes as a result.
59+
# In order to send it again we have to encode the bytes as base64 and decode it
60+
# so it can be sent in json (no bytes allowed there)
61+
# A user can then use it with base64.b64decode(audio_str)
62+
logger.debug("Encoding audio file to base64 and decoding to string...")
63+
audio_str = base64.b64encode(audio).decode()
64+
65+
# TODO: call statistics service to update number of tts of user
66+
#statistics_client.update_
67+
68+
return jsonify({
69+
'locale': locale,
70+
'result': audio_str
71+
}), 201 # Created
72+
except TooManyRequestsError as e:
73+
# Either we have sent too many requests to gcloud...
74+
# ...or we run out of money! :(
75+
logger.error(f"There was an error calling google cloud: {e}")
76+
return jsonify({
77+
'response': 'Too many requests'
78+
}), 429
79+
except Exception as e:
80+
# Internal server error
81+
logger.error(f"An error was raised: {e}")
82+
return jsonify({
83+
'response': 'Internal server error'
84+
}), 500

src/texttospeechservice/src/util/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)