mirror of https://github.com/home-assistant/core
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:
parent
fc95744bb7
commit
7fed328e1c
|
@ -2,7 +2,9 @@
|
|||
"domain": "marytts",
|
||||
"name": "MaryTTS",
|
||||
"documentation": "https://www.home-assistant.io/integrations/marytts",
|
||||
"requirements": [],
|
||||
"requirements": [
|
||||
"speak2mary==1.4.0"
|
||||
],
|
||||
"dependencies": [],
|
||||
"codeowners": []
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Reference in New Issue