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
This commit is contained in:
Markus Pöschl 2020-01-23 22:45:06 +01:00 committed by Pascal Vizeli
parent fc95744bb7
commit 7fed328e1c
5 changed files with 108 additions and 90 deletions

View File

@ -2,7 +2,9 @@
"domain": "marytts", "domain": "marytts",
"name": "MaryTTS", "name": "MaryTTS",
"documentation": "https://www.home-assistant.io/integrations/marytts", "documentation": "https://www.home-assistant.io/integrations/marytts",
"requirements": [], "requirements": [
"speak2mary==1.4.0"
],
"dependencies": [], "dependencies": [],
"codeowners": [] "codeowners": []
} }

View File

@ -1,31 +1,29 @@
"""Support for the MaryTTS service.""" """Support for the MaryTTS service."""
import asyncio
import logging import logging
import re
import aiohttp from speak2mary import MaryTTS
import async_timeout
import voluptuous as vol import voluptuous as vol
from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider
from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.const import CONF_EFFECT, CONF_HOST, CONF_PORT
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) _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_VOICE = "voice"
CONF_CODEC = "codec" 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_HOST = "localhost"
DEFAULT_PORT = 59125 DEFAULT_PORT = 59125
DEFAULT_LANG = "en-US" DEFAULT_LANG = "en_US"
DEFAULT_VOICE = "cmu-slt-hsmm" DEFAULT_VOICE = "cmu-slt-hsmm"
DEFAULT_CODEC = "wav" DEFAULT_CODEC = "WAVE_FILE"
DEFAULT_EFFECTS = {}
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( 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_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES),
vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): cv.string, vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): cv.string,
vol.Optional(CONF_CODEC, default=DEFAULT_CODEC): vol.In(SUPPORT_CODEC), 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): def __init__(self, hass, conf):
"""Init MaryTTS TTS service.""" """Init MaryTTS TTS service."""
self.hass = hass self.hass = hass
self._host = conf.get(CONF_HOST) self._mary = MaryTTS(
self._port = conf.get(CONF_PORT) conf.get(CONF_HOST),
self._codec = conf.get(CONF_CODEC) conf.get(CONF_PORT),
self._voice = conf.get(CONF_VOICE) conf.get(CONF_CODEC),
self._language = conf.get(CONF_LANG) conf.get(CONF_LANG),
conf.get(CONF_VOICE),
)
self._effects = conf.get(CONF_EFFECT)
self.name = "MaryTTS" self.name = "MaryTTS"
@property @property
def default_language(self): def default_language(self):
"""Return the default language.""" """Return the default language."""
return self._language return self._mary.locale
@property @property
def supported_languages(self): def supported_languages(self):
"""Return list of supported languages.""" """Return list of supported languages."""
return SUPPORT_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): async def async_get_tts_audio(self, message, language, options=None):
"""Load TTS from MaryTTS.""" """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: return self._mary.codec, data
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)

View File

@ -1871,6 +1871,9 @@ somecomfort==0.5.2
# homeassistant.components.somfy_mylink # homeassistant.components.somfy_mylink
somfy-mylink-synergy==1.0.6 somfy-mylink-synergy==1.0.6
# homeassistant.components.marytts
speak2mary==1.4.0
# homeassistant.components.speedtestdotnet # homeassistant.components.speedtestdotnet
speedtest-cli==2.1.2 speedtest-cli==2.1.2

View File

@ -602,6 +602,9 @@ solaredge==0.0.2
# homeassistant.components.honeywell # homeassistant.components.honeywell
somecomfort==0.5.2 somecomfort==0.5.2
# homeassistant.components.marytts
speak2mary==1.4.0
# homeassistant.components.recorder # homeassistant.components.recorder
# homeassistant.components.sql # homeassistant.components.sql
sqlalchemy==1.3.13 sqlalchemy==1.3.13

View File

@ -1,9 +1,12 @@
"""The tests for the MaryTTS speech platform.""" """The tests for the MaryTTS speech platform."""
import asyncio
import os import os
import shutil import shutil
from urllib.parse import urlencode
from mock import Mock, patch
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
ATTR_MEDIA_CONTENT_ID,
DOMAIN as DOMAIN_MP, DOMAIN as DOMAIN_MP,
SERVICE_PLAY_MEDIA, SERVICE_PLAY_MEDIA,
) )
@ -11,7 +14,6 @@ import homeassistant.components.tts as tts
from homeassistant.setup import setup_component from homeassistant.setup import setup_component
from tests.common import assert_setup_component, get_test_home_assistant, mock_service 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: class TestTTSMaryTTSPlatform:
@ -21,14 +23,15 @@ class TestTTSMaryTTSPlatform:
"""Set up things to be run when tests are started.""" """Set up things to be run when tests are started."""
self.hass = get_test_home_assistant() self.hass = get_test_home_assistant()
self.url = "http://localhost:59125/process?" self.host = "localhost"
self.url_param = { self.port = 59125
self.params = {
"INPUT_TEXT": "HomeAssistant", "INPUT_TEXT": "HomeAssistant",
"INPUT_TYPE": "TEXT", "INPUT_TYPE": "TEXT",
"AUDIO": "WAVE",
"VOICE": "cmu-slt-hsmm",
"OUTPUT_TYPE": "AUDIO", "OUTPUT_TYPE": "AUDIO",
"LOCALE": "en_US", "LOCALE": "en_US",
"AUDIO": "WAVE_FILE",
"VOICE": "cmu-slt-hsmm",
} }
def teardown_method(self): def teardown_method(self):
@ -46,60 +49,83 @@ class TestTTSMaryTTSPlatform:
with assert_setup_component(1, tts.DOMAIN): with assert_setup_component(1, tts.DOMAIN):
setup_component(self.hass, tts.DOMAIN, config) setup_component(self.hass, tts.DOMAIN, config)
def test_service_say(self, aioclient_mock): def test_service_say(self):
"""Test service call say.""" """Test service call say."""
calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) 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"}} config = {tts.DOMAIN: {"platform": "marytts"}}
with assert_setup_component(1, tts.DOMAIN): with assert_setup_component(1, tts.DOMAIN):
setup_component(self.hass, tts.DOMAIN, config) setup_component(self.hass, tts.DOMAIN, config)
self.hass.services.call( with patch("http.client.HTTPConnection", return_value=conn):
tts.DOMAIN, "marytts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} self.hass.services.call(
) tts.DOMAIN, "marytts_say", {tts.ATTR_MESSAGE: "HomeAssistant"}
)
self.hass.block_till_done() self.hass.block_till_done()
assert len(aioclient_mock.mock_calls) == 1
assert len(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.""" """Test service call say."""
calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA)
aioclient_mock.get( conn = Mock()
self.url, params=self.url_param, status=200, exc=asyncio.TimeoutError() response = Mock()
) conn.getresponse.return_value = response
response.status = 500
response.reason = "test"
response.readline.return_value = "content"
config = {tts.DOMAIN: {"platform": "marytts"}} config = {tts.DOMAIN: {"platform": "marytts"}}
with assert_setup_component(1, tts.DOMAIN): with assert_setup_component(1, tts.DOMAIN):
setup_component(self.hass, tts.DOMAIN, config) setup_component(self.hass, tts.DOMAIN, config)
self.hass.services.call( with patch("http.client.HTTPConnection", return_value=conn):
tts.DOMAIN, "marytts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} 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"}
)
self.hass.block_till_done() self.hass.block_till_done()
assert len(calls) == 0 assert len(calls) == 0
conn.request.assert_called_with("POST", "/process", urlencode(self.params))