Skip to content

Commit

Permalink
Implement Azure stt and tts support
Browse files Browse the repository at this point in the history
  • Loading branch information
Aculeasis committed Dec 17, 2018
1 parent 01393e5 commit 3a98a92
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 19 deletions.
10 changes: 6 additions & 4 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import utils
from languages import CONFIG as LNG, LANG_CODE
from lib import volume
from lib import yandex_utils
from lib import keys_utils
from lib.proxy import proxies
from owner import Owner

Expand All @@ -27,7 +27,7 @@ def __init__(self, cfg: dict, path: dict, owner: Owner):
self._to_tts = [] # Пока player нет храним фразы тут.
self._to_log = [] # А тут принты в лог
self._config_init()
self._yandex_keys = yandex_utils.Keystore()
self._keystore = keys_utils.Keystore()

def __print(self, msg, lvl):
self._to_log.append((msg, lvl))
Expand All @@ -36,12 +36,14 @@ def key(self, prov, api_key):
if prov == 'aws':
return self._aws_credentials()
key_ = self.gt(prov, api_key)
if prov == 'azure':
return self._keystore.azure(key_, self.gt('azure', 'region'))
api = self.yandex_api(prov)
if api == 2 or (prov == 'yandex' and not key_):
# Будем брать ключ у транслита для старой версии
# и (folderId, aim) для новой через oauth
try:
key_ = self._yandex_keys.get(key_, api)
key_ = self._keystore.yandex(key_, api)
except RuntimeError as e:
raise RuntimeError(LNG['err_ya_key'].format(e))
return key_
Expand Down Expand Up @@ -389,7 +391,7 @@ class ConfigUpdater:
# Не переводим значение ключей в нижний регистр даже если они от сервера
NOT_LOWER = {
'apikeytts', 'apikeystt',
'speaker',
'speaker', 'gender',
'access_key_id', 'secret_access_key',
'object_name', 'object_method', 'terminal', 'username', 'password'
}
Expand Down
50 changes: 48 additions & 2 deletions src/lib/STT.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from utils import REQUEST_ERRORS
from .proxy import proxies
from .sr_wrapper import google_reply_parser, UnknownValueError, Recognizer, AudioData, StreamRecognition, RequestError
from .yandex_utils import requests_post, xml_yandex
from .keys_utils import requests_post, xml_yandex

__all__ = ['support', 'GetSTT', 'RequestError']

Expand Down Expand Up @@ -176,6 +176,51 @@ def _parse_response(self):
raise UnknownValueError(e)


class Azure(BaseSTT):
# https://docs.microsoft.com/en-us/azure/cognitive-services/Speech-Service/rest-apis
URL = 'https://{}.stt.speech.microsoft.com/speech/recognition/conversation/cognitiveservices/v1'

def __init__(self, audio_data, key, lang, **_):
if len(key) != 2:
raise RuntimeError('Wrong key')

ext = 'wav'
rate = 16000
width = 2
url = self.URL.format(key[1])
headers = {
'Authorization': 'Bearer {}'.format(key[0]),
'Content-type': 'audio/{}; codec=audio/pcm; samplerate={}'.format(ext, rate),
'Accept': 'application/json'
}
kwargs = {
'language': lang,
'format': 'simple',
'profanity': 'raw',
}
super().__init__(url, audio_data, ext, headers, rate, width, 'stt_azure', **kwargs)

def _parse_response(self):
try:
result = json.loads(self._rq.text)
if not isinstance(result, dict):
result = {}
except (json.JSONDecodeError, ValueError) as e:
raise RuntimeError(e)

status = result.get('RecognitionStatus')
text = result.get('DisplayText')
if status is None:
raise RuntimeError('Wrong reply - \'RecognitionStatus\' missing')
if status == 'NoMatch':
raise UnknownValueError()
if status != 'Success':
raise RuntimeError('Recognition error: {}'.format(status))
if text is None:
raise RuntimeError('Wrong reply - \'DisplayText\' missing')
self._text = text


class PocketSphinxREST(BaseSTT):
# https://github.com/Aculeasis/pocketsphinx-rest
def __init__(self, audio_data: AudioData, url='http://127.0.0.1:8085', **_):
Expand Down Expand Up @@ -208,7 +253,8 @@ def yandex(yandex_api, **kwargs):


PROVIDERS = {
'google': Google, 'yandex': yandex, 'pocketsphinx-rest': PocketSphinxREST, 'wit.ai': WitAI, 'microsoft': microsoft
'google': Google, 'yandex': yandex, 'pocketsphinx-rest': PocketSphinxREST, 'wit.ai': WitAI, 'microsoft': microsoft,
'azure': Azure
}


Expand Down
41 changes: 40 additions & 1 deletion src/lib/TTS.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@

import hashlib
import subprocess
import time
from shlex import quote

import requests
Expand Down Expand Up @@ -159,6 +161,41 @@ def _request(self, proxy_key):
self._data = self._rq.iter_content


class Azure(AWS):
# https://docs.microsoft.com/en-us/azure/cognitive-services/Speech-Service/rest-apis
URL = 'https://{}.tts.speech.microsoft.com/cognitiveservices/v1'
MAX_CHARS = 1000

# noinspection PyMissingConstructor
def __init__(self, text, buff_size, speaker, key, lang, *_, **__):
if len(key) != 2:
raise RuntimeError('Wrong key')

self._data = None
self._rq = None
self._body = [
"<speak version='1.0' xml:lang='{lang}'><voice xml:lang='{lang}'"
" name='Microsoft Server Speech Text to Speech Voice ({lang}, {speaker})'>",
" {text}",
"</voice></speak>"
]
self._body = '\n'.join(self._body).format(lang=lang, speaker=speaker, text=text).encode()
self._headers = {
'X-Microsoft-OutputFormat': 'audio-24khz-160kbitrate-mono-mp3',
'Content-Type': 'application/ssml+xml',
'Authorization': 'Bearer {}'.format(key[0]),
'User-Agent': hashlib.sha1(str(time.time()).encode()).hexdigest()[:32]
}
self._url = self.URL.format(key[1])
self._buff_size = buff_size

if len(self._body) >= self.MAX_CHARS:
raise RuntimeError('Number of characters must be less than {}'.format(self.MAX_CHARS))

self._request('tts_azure')
self._reply_check()


class RHVoiceREST(BaseTTS):
def __init__(self, text, buff_size, speaker, audio_format, url, sets, *_, **__):
super().__init__('{}/say'.format(url or 'http://127.0.0.1:8080'), 'tts_rhvoice-rest', buff_size=buff_size,
Expand Down Expand Up @@ -214,7 +251,9 @@ def yandex(yandex_api, **kwargs):
return Yandex(**kwargs)


PROVIDERS = {'google': Google, 'yandex': yandex, 'aws': aws, 'rhvoice-rest': RHVoiceREST, 'rhvoice': RHVoice}
PROVIDERS = {
'google': Google, 'yandex': yandex, 'aws': aws, 'azure': Azure, 'rhvoice-rest': RHVoiceREST, 'rhvoice': RHVoice
}


def support(name):
Expand Down
53 changes: 42 additions & 11 deletions src/lib/yandex_utils.py → src/lib/keys_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,39 @@
from .proxy import proxies
from .sr_wrapper import UnknownValueError

AZURE_ACCESS_ENDPOINT = 'https://{}.api.cognitive.microsoft.com/sts/v1.0/issueToken'


class Keystore:
# Кэширует старые халявные ключи и новые aim на 11 часов
KEY_LIFETIME = 11 * 3600
YANDEX_LIFETIME = 11 * 3600
# ключи azure живут 10 минут
AZURE_LIFETIME = 595

def __init__(self):
self._cache = {}
self._lock = threading.Lock()

def get(self, key, api: int=1):
def azure(self, key, region):
key = (key, region)
with self._lock:
if key not in self._cache or self._cache[key][1] < time.time():
self._cache[key] = (_azure_token_from_oauth(*key), time.time() + self.AZURE_LIFETIME)
return self._cache[key][0], region

def yandex(self, key, api: int=1):
key = key or ''
if api != 2:
return self._storage(key, 1)
return self._yandex_storage(key, 1)
key = key.split(':', 1)
if len(key) != 2:
raise RuntimeError('Wrong key for Yandex APIv2, must be \'<folderId>:<OAuth>\'')
return key[0], self._storage(key[1], 2)
return key[0], self._yandex_storage(key[1], 2)

def _storage(self, key, api):
def _yandex_storage(self, key, api):
with self._lock:
if key not in self._cache or self._cache[key][1] < time.time():
self._cache[key] = (_get_key(key, api), time.time() + self.KEY_LIFETIME)
self._cache[key] = (_yandex_get_key(key, api), time.time() + self.YANDEX_LIFETIME)
return self._cache[key][0]

def clear(self):
Expand Down Expand Up @@ -92,14 +103,14 @@ def xml_yandex(data):
return text


def _get_key(key, api):
def _yandex_get_key(key, api):
if api != 2:
return _get_api_key_v1()
return _yandex_get_api_key_v1()
else:
return _aim_from_oauth(key)
return _yandex_aim_from_oauth(key)


def _aim_from_oauth(oauth):
def _yandex_aim_from_oauth(oauth):
# https://cloud.yandex.ru/docs/iam/operations/iam-token/create
# Получаем токен по токену, токен живет 12 часов.
url = 'https://iam.api.cloud.yandex.net/iam/v1/tokens'
Expand All @@ -108,7 +119,27 @@ def _aim_from_oauth(oauth):
return requests_post(url, key, json=params, proxies=proxies('key_yandex'))


def _get_api_key_v1():
def _azure_token_from_oauth(key, region):
# https://docs.microsoft.com/en-us/azure/cognitive-services/Speech-Service/rest-apis#authentication
url = AZURE_ACCESS_ENDPOINT.format(region)
headers = {
'Ocp-Apim-Subscription-Key': key,
'Content-type': 'application/x-www-form-urlencoded',
'Content-Length': '0'
}
try:
response = requests.post(url, headers=headers, proxies=proxies('token_azure'))
except REQUEST_ERRORS as e:
raise RuntimeError(str(e))
if not response.ok:
raise RuntimeError('{}: {}'.format(response.status_code, response.reason))
token = response.text
if not token:
raise RuntimeError('Azure send empty token')
return token


def _yandex_get_api_key_v1():
url = 'https://translate.yandex.com'
target = 'SPEECHKIT_KEY:'

Expand Down
3 changes: 3 additions & 0 deletions src/lib/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@
'tts_yandex': ('yandex_tts', 'yandex'),
'tts_aws': ('aws',),
'tts_rhvoice-rest': ('rhvoice-rest',),
'tts_azure': ('azure_tts', 'azure'),
'stt_google': ('google_stt', 'google'),
'stt_yandex': ('yandex_stt', 'yandex'),
'stt_pocketsphinx-rest': ('pocketsphinx-rest',),
'stt_wit.ai': ('wit.ai',),
'stt_microsoft': ('microsoft',),
'stt_azure': ('azure_stt', 'azure'),
'token_google': ('google_token', 'google_tts', 'google'),
'key_yandex': ('yandex_token', 'yandex'),
'token_azure': ('azure_token', 'azure'),
'snowboy_training': ('snowboy',),
}
PARAMS = frozenset(['enable'] + [_val_ for _key_ in PROXIES for _val_ in PROXIES[_key_]])
Expand Down
6 changes: 5 additions & 1 deletion src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@
'pocketsphinx-rest': {
'server': 'http://127.0.0.1:8085',
},
'azure': {
'speaker': 'EkaterinaRUS',
'region': 'westus',
},
'cache': {
'tts_priority': 'yandex',
'tts_size': 100,
Expand Down Expand Up @@ -121,7 +125,7 @@
'line_out': '',
},
'system': {
'ini_version': 13,
'ini_version': 14,
}
}

Expand Down

0 comments on commit 3a98a92

Please sign in to comment.