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",
"name": "MaryTTS",
"documentation": "https://www.home-assistant.io/integrations/marytts",
"requirements": [],
"requirements": [
"speak2mary==1.4.0"
],
"dependencies": [],
"codeowners": []
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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))