From 7fed328e1c3993f4e0757c2b543ef581f2ec4c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20P=C3=B6schl?= Date: Thu, 23 Jan 2020 22:45:06 +0100 Subject: [PATCH] Use speak2mary for MaryTTS integration and enable sound effects (#30805) * Use speak2mary for MaryTTS integration and enable sound effects * Replace static defaults for effects with user configured ones --- .../components/marytts/manifest.json | 4 +- homeassistant/components/marytts/tts.py | 86 ++++++--------- requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/marytts/test_tts.py | 102 +++++++++++------- 5 files changed, 108 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/marytts/manifest.json b/homeassistant/components/marytts/manifest.json index 59517e4f1bb..74f027fd076 100644 --- a/homeassistant/components/marytts/manifest.json +++ b/homeassistant/components/marytts/manifest.json @@ -2,7 +2,9 @@ "domain": "marytts", "name": "MaryTTS", "documentation": "https://www.home-assistant.io/integrations/marytts", - "requirements": [], + "requirements": [ + "speak2mary==1.4.0" + ], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/marytts/tts.py b/homeassistant/components/marytts/tts.py index 742b5e87661..da8208e1883 100644 --- a/homeassistant/components/marytts/tts.py +++ b/homeassistant/components/marytts/tts.py @@ -1,31 +1,29 @@ """Support for the MaryTTS service.""" -import asyncio import logging -import re -import aiohttp -import async_timeout +from speak2mary import MaryTTS import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider -from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.const import CONF_EFFECT, CONF_HOST, CONF_PORT import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -SUPPORT_LANGUAGES = ["de", "en-GB", "en-US", "fr", "it", "lb", "ru", "sv", "te", "tr"] - -SUPPORT_CODEC = ["aiff", "au", "wav"] - CONF_VOICE = "voice" CONF_CODEC = "codec" +SUPPORT_LANGUAGES = MaryTTS.supported_locales() +SUPPORT_CODEC = MaryTTS.supported_codecs() +SUPPORT_OPTIONS = [CONF_EFFECT] +SUPPORT_EFFECTS = MaryTTS.supported_effects().keys() + DEFAULT_HOST = "localhost" DEFAULT_PORT = 59125 -DEFAULT_LANG = "en-US" +DEFAULT_LANG = "en_US" DEFAULT_VOICE = "cmu-slt-hsmm" -DEFAULT_CODEC = "wav" +DEFAULT_CODEC = "WAVE_FILE" +DEFAULT_EFFECTS = {} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -34,6 +32,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES), vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): cv.string, vol.Optional(CONF_CODEC, default=DEFAULT_CODEC): vol.In(SUPPORT_CODEC), + vol.Optional(CONF_EFFECT, default=DEFAULT_EFFECTS): { + vol.All(cv.string, vol.In(SUPPORT_EFFECTS)): cv.string + }, } ) @@ -49,57 +50,40 @@ class MaryTTSProvider(Provider): def __init__(self, hass, conf): """Init MaryTTS TTS service.""" self.hass = hass - self._host = conf.get(CONF_HOST) - self._port = conf.get(CONF_PORT) - self._codec = conf.get(CONF_CODEC) - self._voice = conf.get(CONF_VOICE) - self._language = conf.get(CONF_LANG) + self._mary = MaryTTS( + conf.get(CONF_HOST), + conf.get(CONF_PORT), + conf.get(CONF_CODEC), + conf.get(CONF_LANG), + conf.get(CONF_VOICE), + ) + self._effects = conf.get(CONF_EFFECT) self.name = "MaryTTS" @property def default_language(self): """Return the default language.""" - return self._language + return self._mary.locale @property def supported_languages(self): """Return list of supported languages.""" return SUPPORT_LANGUAGES + @property + def default_options(self): + """Return dict include default options.""" + return {CONF_EFFECT: self._effects} + + @property + def supported_options(self): + """Return a list of supported options.""" + return SUPPORT_OPTIONS + async def async_get_tts_audio(self, message, language, options=None): """Load TTS from MaryTTS.""" - websession = async_get_clientsession(self.hass) + effects = options[CONF_EFFECT] - actual_language = re.sub("-", "_", language) + data = self._mary.speak(message, effects) - try: - with async_timeout.timeout(10): - url = f"http://{self._host}:{self._port}/process?" - - audio = self._codec.upper() - if audio == "WAV": - audio = "WAVE" - - url_param = { - "INPUT_TEXT": message, - "INPUT_TYPE": "TEXT", - "AUDIO": audio, - "VOICE": self._voice, - "OUTPUT_TYPE": "AUDIO", - "LOCALE": actual_language, - } - - request = await websession.get(url, params=url_param) - - if request.status != 200: - _LOGGER.error( - "Error %d on load url %s", request.status, request.url - ) - return (None, None) - data = await request.read() - - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Timeout for MaryTTS API") - return (None, None) - - return (self._codec, data) + return self._mary.codec, data diff --git a/requirements_all.txt b/requirements_all.txt index 37f6356d441..d4acfc264da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1871,6 +1871,9 @@ somecomfort==0.5.2 # homeassistant.components.somfy_mylink somfy-mylink-synergy==1.0.6 +# homeassistant.components.marytts +speak2mary==1.4.0 + # homeassistant.components.speedtestdotnet speedtest-cli==2.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ef6fe126c64..5ea72741fd9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -602,6 +602,9 @@ solaredge==0.0.2 # homeassistant.components.honeywell somecomfort==0.5.2 +# homeassistant.components.marytts +speak2mary==1.4.0 + # homeassistant.components.recorder # homeassistant.components.sql sqlalchemy==1.3.13 diff --git a/tests/components/marytts/test_tts.py b/tests/components/marytts/test_tts.py index 65d4ab7e39c..810998ec0b8 100644 --- a/tests/components/marytts/test_tts.py +++ b/tests/components/marytts/test_tts.py @@ -1,9 +1,12 @@ """The tests for the MaryTTS speech platform.""" -import asyncio import os import shutil +from urllib.parse import urlencode + +from mock import Mock, patch from homeassistant.components.media_player.const import ( + ATTR_MEDIA_CONTENT_ID, DOMAIN as DOMAIN_MP, SERVICE_PLAY_MEDIA, ) @@ -11,7 +14,6 @@ import homeassistant.components.tts as tts from homeassistant.setup import setup_component from tests.common import assert_setup_component, get_test_home_assistant, mock_service -from tests.components.tts.test_init import mutagen_mock # noqa: F401 class TestTTSMaryTTSPlatform: @@ -21,14 +23,15 @@ class TestTTSMaryTTSPlatform: """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() - self.url = "http://localhost:59125/process?" - self.url_param = { + self.host = "localhost" + self.port = 59125 + self.params = { "INPUT_TEXT": "HomeAssistant", "INPUT_TYPE": "TEXT", - "AUDIO": "WAVE", - "VOICE": "cmu-slt-hsmm", "OUTPUT_TYPE": "AUDIO", "LOCALE": "en_US", + "AUDIO": "WAVE_FILE", + "VOICE": "cmu-slt-hsmm", } def teardown_method(self): @@ -46,60 +49,83 @@ class TestTTSMaryTTSPlatform: with assert_setup_component(1, tts.DOMAIN): setup_component(self.hass, tts.DOMAIN, config) - def test_service_say(self, aioclient_mock): + def test_service_say(self): """Test service call say.""" calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - aioclient_mock.get(self.url, params=self.url_param, status=200, content=b"test") + conn = Mock() + response = Mock() + conn.getresponse.return_value = response + response.status = 200 + response.read.return_value = b"audio" config = {tts.DOMAIN: {"platform": "marytts"}} with assert_setup_component(1, tts.DOMAIN): setup_component(self.hass, tts.DOMAIN, config) - self.hass.services.call( - tts.DOMAIN, "marytts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} - ) + with patch("http.client.HTTPConnection", return_value=conn): + self.hass.services.call( + tts.DOMAIN, "marytts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + ) self.hass.block_till_done() - assert len(aioclient_mock.mock_calls) == 1 assert len(calls) == 1 + assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find(".wav") != -1 + conn.request.assert_called_with("POST", "/process", urlencode(self.params)) - def test_service_say_timeout(self, aioclient_mock): + def test_service_say_with_effect(self): + """Test service call say with effects.""" + calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + + conn = Mock() + response = Mock() + conn.getresponse.return_value = response + response.status = 200 + response.read.return_value = b"audio" + + config = { + tts.DOMAIN: {"platform": "marytts", "effect": {"Volume": "amount:2.0;"}} + } + + with assert_setup_component(1, tts.DOMAIN): + setup_component(self.hass, tts.DOMAIN, config) + + with patch("http.client.HTTPConnection", return_value=conn): + self.hass.services.call( + tts.DOMAIN, "marytts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + ) + self.hass.block_till_done() + + assert len(calls) == 1 + assert calls[0].data[ATTR_MEDIA_CONTENT_ID].find(".wav") != -1 + + self.params.update( + {"effect_Volume_selected": "on", "effect_Volume_parameters": "amount:2.0;"} + ) + conn.request.assert_called_with("POST", "/process", urlencode(self.params)) + + def test_service_say_http_error(self): """Test service call say.""" calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - aioclient_mock.get( - self.url, params=self.url_param, status=200, exc=asyncio.TimeoutError() - ) + conn = Mock() + response = Mock() + conn.getresponse.return_value = response + response.status = 500 + response.reason = "test" + response.readline.return_value = "content" config = {tts.DOMAIN: {"platform": "marytts"}} with assert_setup_component(1, tts.DOMAIN): setup_component(self.hass, tts.DOMAIN, config) - self.hass.services.call( - tts.DOMAIN, "marytts_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) - - aioclient_mock.get(self.url, params=self.url_param, status=403, content=b"test") - - config = {tts.DOMAIN: {"platform": "marytts"}} - - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) - - self.hass.services.call( - tts.DOMAIN, "marytts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} - ) + with patch("http.client.HTTPConnection", return_value=conn): + self.hass.services.call( + tts.DOMAIN, "marytts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + ) self.hass.block_till_done() assert len(calls) == 0 + conn.request.assert_called_with("POST", "/process", urlencode(self.params))