From 85a84549eb73207ecb7cfc069cdd42ffaccb4cc0 Mon Sep 17 00:00:00 2001 From: Lupin Demid Date: Sun, 15 Jan 2017 18:43:10 +0400 Subject: [PATCH] Yandex tts component (#5342) * Added Yandex SpeechKit TTS * Added test stub * Added two test and added property for yandex tts * Copy all test from voice rss * Added test vith different speaker and code style changes * Added new line to end of file * Url format replaced with url_params --- homeassistant/components/tts/yandextts.py | 114 ++++++++++++ tests/components/tts/test_yandextts.py | 215 ++++++++++++++++++++++ 2 files changed, 329 insertions(+) create mode 100644 homeassistant/components/tts/yandextts.py create mode 100644 tests/components/tts/test_yandextts.py diff --git a/homeassistant/components/tts/yandextts.py b/homeassistant/components/tts/yandextts.py new file mode 100644 index 00000000000..d5825ce297f --- /dev/null +++ b/homeassistant/components/tts/yandextts.py @@ -0,0 +1,114 @@ +""" +Support for the yandex speechkit tts service. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/tts/yandextts/ +""" +import asyncio +import logging + +import aiohttp +import async_timeout +import voluptuous as vol + +from homeassistant.const import CONF_API_KEY +from homeassistant.components.tts import Provider, PLATFORM_SCHEMA, CONF_LANG +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + + +_LOGGER = logging.getLogger(__name__) + +YANDEX_API_URL = "https://tts.voicetech.yandex.net/generate?" + +SUPPORT_LANGUAGES = [ + 'ru-RU', 'en-US', 'tr-TR', 'uk-UK' +] + +SUPPORT_CODECS = [ + 'mp3', 'wav', 'opus', +] + +SUPPORT_VOICES = [ + 'jane', 'oksana', 'alyss', 'omazh', + 'zahar', 'ermil' +] +CONF_CODEC = 'codec' +CONF_VOICE = 'voice' + +DEFAULT_LANG = 'en-US' +DEFAULT_CODEC = 'mp3' +DEFAULT_VOICE = 'zahar' + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES), + vol.Optional(CONF_CODEC, default=DEFAULT_CODEC): vol.In(SUPPORT_CODECS), + vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): vol.In(SUPPORT_VOICES), +}) + + +@asyncio.coroutine +def async_get_engine(hass, config): + """Setup VoiceRSS speech component.""" + return YandexSpeechKitProvider(hass, config) + + +class YandexSpeechKitProvider(Provider): + """VoiceRSS speech api provider.""" + + def __init__(self, hass, conf): + """Init VoiceRSS TTS service.""" + self.hass = hass + self._codec = conf.get(CONF_CODEC) + self._key = conf.get(CONF_API_KEY) + self._speaker = conf.get(CONF_VOICE) + self._language = conf.get(CONF_LANG) + + @property + def default_language(self): + """Default language.""" + return self._language + + @property + def supported_languages(self): + """List of supported languages.""" + return SUPPORT_LANGUAGES + + @asyncio.coroutine + def async_get_tts_audio(self, message, language): + """Load TTS from yandex.""" + websession = async_get_clientsession(self.hass) + + actual_language = language + + request = None + try: + with async_timeout.timeout(10, loop=self.hass.loop): + url_param = { + 'text': message, + 'lang': actual_language, + 'key': self._key, + 'speaker': self._speaker, + 'format': self._codec, + } + + request = yield from websession.get(YANDEX_API_URL, + params=url_param) + + if request.status != 200: + _LOGGER.error("Error %d on load url %s.", + request.status, request.url) + return (None, None) + data = yield from request.read() + + except (asyncio.TimeoutError, aiohttp.errors.ClientError): + _LOGGER.error("Timeout for yandex speech kit api.") + return (None, None) + + finally: + if request is not None: + yield from request.release() + + return (self._codec, data) diff --git a/tests/components/tts/test_yandextts.py b/tests/components/tts/test_yandextts.py new file mode 100644 index 00000000000..f0c6eb85200 --- /dev/null +++ b/tests/components/tts/test_yandextts.py @@ -0,0 +1,215 @@ +"""The tests for the Yandex SpeechKit speech platform.""" +import asyncio +import os +import shutil + +import homeassistant.components.tts as tts +from homeassistant.bootstrap import setup_component +from homeassistant.components.media_player import ( + SERVICE_PLAY_MEDIA, DOMAIN as DOMAIN_MP) +from tests.common import ( + get_test_home_assistant, assert_setup_component, mock_service) + + +class TestTTSYandexPlatform(object): + """Test the speech component.""" + + def setup_method(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self._base_url = "https://tts.voicetech.yandex.net/generate?" + + def teardown_method(self): + """Stop everything that was started.""" + default_tts = self.hass.config.path(tts.DEFAULT_CACHE_DIR) + if os.path.isdir(default_tts): + shutil.rmtree(default_tts) + + self.hass.stop() + + def test_setup_component(self): + """Test setup component.""" + config = { + tts.DOMAIN: { + 'platform': 'yandextts', + 'api_key': '1234567xx' + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + def test_setup_component_without_api_key(self): + """Test setup component without api key.""" + config = { + tts.DOMAIN: { + 'platform': 'yandextts', + } + } + + with assert_setup_component(0, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + def test_service_say(self, aioclient_mock): + """Test service call say.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + url = "https://tts.voicetech.yandex.net/generate?format=mp3" \ + "&speaker=zahar&key=1234567xx&text=HomeAssistant&lang=en-US" + aioclient_mock.get( + url, status=200, content=b'test') + + config = { + tts.DOMAIN: { + 'platform': 'yandextts', + 'api_key': '1234567xx' + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'yandextts_say', { + tts.ATTR_MESSAGE: "HomeAssistant", + }) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 1 + assert len(calls) == 1 + + def test_service_say_russian_config(self, aioclient_mock): + """Test service call say.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + url = "https://tts.voicetech.yandex.net/generate?format=mp3" \ + "&speaker=zahar&key=1234567xx&text=HomeAssistant&lang=ru-RU" + aioclient_mock.get( + url, status=200, content=b'test') + + config = { + tts.DOMAIN: { + 'platform': 'yandextts', + 'api_key': '1234567xx', + 'language': 'ru-RU', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'yandextts_say', { + tts.ATTR_MESSAGE: "HomeAssistant", + }) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 1 + assert len(calls) == 1 + + def test_service_say_russian_service(self, aioclient_mock): + """Test service call say.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + url = "https://tts.voicetech.yandex.net/generate?format=mp3" \ + "&speaker=zahar&key=1234567xx&text=HomeAssistant&lang=ru-RU" + aioclient_mock.get( + url, status=200, content=b'test') + + config = { + tts.DOMAIN: { + 'platform': 'yandextts', + 'api_key': '1234567xx', + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'yandextts_say', { + tts.ATTR_MESSAGE: "HomeAssistant", + tts.ATTR_LANGUAGE: "ru-RU" + }) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 1 + assert len(calls) == 1 + + def test_service_say_timeout(self, aioclient_mock): + """Test service call say.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + url = "https://tts.voicetech.yandex.net/generate?format=mp3" \ + "&speaker=zahar&key=1234567xx&text=HomeAssistant&lang=en-US" + aioclient_mock.get( + url, status=200, exc=asyncio.TimeoutError()) + + config = { + tts.DOMAIN: { + 'platform': 'yandextts', + 'api_key': '1234567xx' + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'yandextts_say', { + tts.ATTR_MESSAGE: "HomeAssistant", + }) + self.hass.block_till_done() + + assert len(calls) == 0 + assert len(aioclient_mock.mock_calls) == 1 + + def test_service_say_http_error(self, aioclient_mock): + """Test service call say.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + url = "https://tts.voicetech.yandex.net/generate?format=mp3" \ + "&speaker=zahar&key=1234567xx&text=HomeAssistant&lang=en-US" + aioclient_mock.get( + url, status=403, content=b'test') + + config = { + tts.DOMAIN: { + 'platform': 'yandextts', + 'api_key': '1234567xx' + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'yandextts_say', { + tts.ATTR_MESSAGE: "HomeAssistant", + }) + self.hass.block_till_done() + + assert len(calls) == 0 + + def test_service_say_specifed_speaker(self, aioclient_mock): + """Test service call say.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + url = "https://tts.voicetech.yandex.net/generate?format=mp3" \ + "&speaker=alyss&key=1234567xx&text=HomeAssistant&lang=en-US" + aioclient_mock.get( + url, status=200, content=b'test') + + config = { + tts.DOMAIN: { + 'platform': 'yandextts', + 'api_key': '1234567xx', + 'voice': 'alyss' + } + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + self.hass.services.call(tts.DOMAIN, 'yandextts_say', { + tts.ATTR_MESSAGE: "HomeAssistant", + }) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 1 + assert len(calls) == 1