mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 18:27:09 +00:00
Add additional support over NC (#28527)
* Add voice support over NC * Add disocery support for TTS / STT * fix cloud TTS discovery * Fix dev config * Fix discovery * Bump hass-nabucasa 0.25 * Add channel support * Fix lint * Update homeassistant/components/cloud/__init__.py Co-Authored-By: Paulus Schoutsen <balloob@gmail.com> * Update homeassistant/components/cloud/tts.py Co-Authored-By: Paulus Schoutsen <balloob@gmail.com> * Update homeassistant/components/cloud/tts.py Co-Authored-By: Paulus Schoutsen <balloob@gmail.com> * Update homeassistant/components/cloud/tts.py Co-Authored-By: Paulus Schoutsen <balloob@gmail.com> * bump hass-nabucasa * Update tts.py * fix lint
This commit is contained in:
parent
925e26b061
commit
005a1b2713
@ -145,7 +145,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_engine(hass, config):
|
def get_engine(hass, config, discovery_info=None):
|
||||||
"""Set up Amazon Polly speech component."""
|
"""Set up Amazon Polly speech component."""
|
||||||
output_format = config.get(CONF_OUTPUT_FORMAT)
|
output_format = config.get(CONF_OUTPUT_FORMAT)
|
||||||
sample_rate = config.get(CONF_SAMPLE_RATE, DEFAULT_SAMPLE_RATES[output_format])
|
sample_rate = config.get(CONF_SAMPLE_RATE, DEFAULT_SAMPLE_RATES[output_format])
|
||||||
|
@ -52,7 +52,7 @@ _OPTIONS = {
|
|||||||
SUPPORTED_OPTIONS = [CONF_PERSON, CONF_PITCH, CONF_SPEED, CONF_VOLUME]
|
SUPPORTED_OPTIONS = [CONF_PERSON, CONF_PITCH, CONF_SPEED, CONF_VOLUME]
|
||||||
|
|
||||||
|
|
||||||
def get_engine(hass, config):
|
def get_engine(hass, config, discovery_info=None):
|
||||||
"""Set up Baidu TTS component."""
|
"""Set up Baidu TTS component."""
|
||||||
return BaiduTTSProvider(hass, config)
|
return BaiduTTSProvider(hass, config)
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ from homeassistant.util.aiohttp import MockRequest
|
|||||||
from . import account_link, http_api
|
from . import account_link, http_api
|
||||||
from .client import CloudClient
|
from .client import CloudClient
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CONF_ACCOUNT_LINK_URL,
|
||||||
CONF_ACME_DIRECTORY_SERVER,
|
CONF_ACME_DIRECTORY_SERVER,
|
||||||
CONF_ALEXA,
|
CONF_ALEXA,
|
||||||
CONF_ALEXA_ACCESS_TOKEN_URL,
|
CONF_ALEXA_ACCESS_TOKEN_URL,
|
||||||
@ -38,7 +39,7 @@ from .const import (
|
|||||||
CONF_REMOTE_API_URL,
|
CONF_REMOTE_API_URL,
|
||||||
CONF_SUBSCRIPTION_INFO_URL,
|
CONF_SUBSCRIPTION_INFO_URL,
|
||||||
CONF_USER_POOL_ID,
|
CONF_USER_POOL_ID,
|
||||||
CONF_ACCOUNT_LINK_URL,
|
CONF_VOICE_API_URL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
MODE_DEV,
|
MODE_DEV,
|
||||||
MODE_PROD,
|
MODE_PROD,
|
||||||
@ -103,6 +104,7 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
vol.Optional(CONF_ALEXA_ACCESS_TOKEN_URL): vol.Url(),
|
vol.Optional(CONF_ALEXA_ACCESS_TOKEN_URL): vol.Url(),
|
||||||
vol.Optional(CONF_GOOGLE_ACTIONS_REPORT_STATE_URL): vol.Url(),
|
vol.Optional(CONF_GOOGLE_ACTIONS_REPORT_STATE_URL): vol.Url(),
|
||||||
vol.Optional(CONF_ACCOUNT_LINK_URL): vol.Url(),
|
vol.Optional(CONF_ACCOUNT_LINK_URL): vol.Url(),
|
||||||
|
vol.Optional(CONF_VOICE_API_URL): vol.Url(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -230,21 +232,28 @@ async def async_setup(hass, config):
|
|||||||
DOMAIN, SERVICE_REMOTE_DISCONNECT, _service_handler
|
DOMAIN, SERVICE_REMOTE_DISCONNECT, _service_handler
|
||||||
)
|
)
|
||||||
|
|
||||||
loaded_binary_sensor = False
|
loaded = False
|
||||||
|
|
||||||
async def _on_connect():
|
async def _on_connect():
|
||||||
"""Discover RemoteUI binary sensor."""
|
"""Discover RemoteUI binary sensor."""
|
||||||
nonlocal loaded_binary_sensor
|
nonlocal loaded
|
||||||
|
|
||||||
if loaded_binary_sensor:
|
# Prevent multiple discovery
|
||||||
|
if loaded:
|
||||||
return
|
return
|
||||||
|
loaded = True
|
||||||
|
|
||||||
loaded_binary_sensor = True
|
|
||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
hass.helpers.discovery.async_load_platform(
|
hass.helpers.discovery.async_load_platform(
|
||||||
"binary_sensor", DOMAIN, {}, config
|
"binary_sensor", DOMAIN, {}, config
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.helpers.discovery.async_load_platform("stt", DOMAIN, {}, config)
|
||||||
|
)
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.helpers.discovery.async_load_platform("tts", DOMAIN, {}, config)
|
||||||
|
)
|
||||||
|
|
||||||
cloud.iot.register_on_connect(_on_connect)
|
cloud.iot.register_on_connect(_on_connect)
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ CONF_ACME_DIRECTORY_SERVER = "acme_directory_server"
|
|||||||
CONF_ALEXA_ACCESS_TOKEN_URL = "alexa_access_token_url"
|
CONF_ALEXA_ACCESS_TOKEN_URL = "alexa_access_token_url"
|
||||||
CONF_GOOGLE_ACTIONS_REPORT_STATE_URL = "google_actions_report_state_url"
|
CONF_GOOGLE_ACTIONS_REPORT_STATE_URL = "google_actions_report_state_url"
|
||||||
CONF_ACCOUNT_LINK_URL = "account_link_url"
|
CONF_ACCOUNT_LINK_URL = "account_link_url"
|
||||||
|
CONF_VOICE_API_URL = "voice_api_url"
|
||||||
|
|
||||||
MODE_DEV = "development"
|
MODE_DEV = "development"
|
||||||
MODE_PROD = "production"
|
MODE_PROD = "production"
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "cloud",
|
"domain": "cloud",
|
||||||
"name": "Cloud",
|
"name": "Cloud",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/cloud",
|
"documentation": "https://www.home-assistant.io/integrations/cloud",
|
||||||
"requirements": ["hass-nabucasa==0.23"],
|
"requirements": ["hass-nabucasa==0.26"],
|
||||||
"dependencies": ["http", "webhook"],
|
"dependencies": ["http", "webhook"],
|
||||||
"codeowners": ["@home-assistant/cloud"]
|
"codeowners": ["@home-assistant/cloud"]
|
||||||
}
|
}
|
||||||
|
106
homeassistant/components/cloud/stt.py
Normal file
106
homeassistant/components/cloud/stt.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
"""Support for the cloud for speech to text service."""
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from aiohttp import StreamReader
|
||||||
|
from hass_nabucasa import Cloud
|
||||||
|
from hass_nabucasa.voice import VoiceError
|
||||||
|
|
||||||
|
from homeassistant.components.stt import Provider, SpeechMetadata, SpeechResult
|
||||||
|
from homeassistant.components.stt.const import (
|
||||||
|
AudioBitRates,
|
||||||
|
AudioChannels,
|
||||||
|
AudioCodecs,
|
||||||
|
AudioFormats,
|
||||||
|
AudioSampleRates,
|
||||||
|
SpeechResultState,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
SUPPORT_LANGUAGES = [
|
||||||
|
"da-DK",
|
||||||
|
"de-DE",
|
||||||
|
"en-AU",
|
||||||
|
"en-CA",
|
||||||
|
"en-GB",
|
||||||
|
"en-US",
|
||||||
|
"es-ES",
|
||||||
|
"fi-FI",
|
||||||
|
"fr-CA",
|
||||||
|
"fr-FR",
|
||||||
|
"it-IT",
|
||||||
|
"ja-JP",
|
||||||
|
"nl-NL",
|
||||||
|
"pl-PL",
|
||||||
|
"pt-PT",
|
||||||
|
"ru-RU",
|
||||||
|
"sv-SE",
|
||||||
|
"th-TH",
|
||||||
|
"zh-CN",
|
||||||
|
"zh-HK",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_engine(hass, config, discovery_info=None):
|
||||||
|
"""Set up Cloud speech component."""
|
||||||
|
cloud: Cloud = hass.data[DOMAIN]
|
||||||
|
|
||||||
|
return CloudProvider(cloud)
|
||||||
|
|
||||||
|
|
||||||
|
class CloudProvider(Provider):
|
||||||
|
"""NabuCasa speech API provider."""
|
||||||
|
|
||||||
|
def __init__(self, cloud: Cloud) -> None:
|
||||||
|
"""Hass NabuCasa Speech to text."""
|
||||||
|
self.cloud = cloud
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_languages(self) -> List[str]:
|
||||||
|
"""Return a list of supported languages."""
|
||||||
|
return SUPPORT_LANGUAGES
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_formats(self) -> List[AudioFormats]:
|
||||||
|
"""Return a list of supported formats."""
|
||||||
|
return [AudioFormats.WAV, AudioFormats.OGG]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_codecs(self) -> List[AudioCodecs]:
|
||||||
|
"""Return a list of supported codecs."""
|
||||||
|
return [AudioCodecs.PCM, AudioCodecs.OPUS]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_bit_rates(self) -> List[AudioBitRates]:
|
||||||
|
"""Return a list of supported bitrates."""
|
||||||
|
return [AudioBitRates.BITRATE_16]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_sample_rates(self) -> List[AudioSampleRates]:
|
||||||
|
"""Return a list of supported samplerates."""
|
||||||
|
return [AudioSampleRates.SAMPLERATE_16000]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_channels(self) -> List[AudioChannels]:
|
||||||
|
"""Return a list of supported channels."""
|
||||||
|
return [AudioChannels.CHANNEL_MONO]
|
||||||
|
|
||||||
|
async def async_process_audio_stream(
|
||||||
|
self, metadata: SpeechMetadata, stream: StreamReader
|
||||||
|
) -> SpeechResult:
|
||||||
|
"""Process an audio stream to STT service."""
|
||||||
|
content = f"audio/{metadata.format!s}; codecs=audio/{metadata.codec!s}; samplerate=16000"
|
||||||
|
|
||||||
|
# Process STT
|
||||||
|
try:
|
||||||
|
result = await self.cloud.voice.process_stt(
|
||||||
|
stream, content, metadata.language
|
||||||
|
)
|
||||||
|
except VoiceError:
|
||||||
|
return SpeechResult(None, SpeechResultState.ERROR)
|
||||||
|
|
||||||
|
# Return Speech as Text
|
||||||
|
return SpeechResult(
|
||||||
|
result.text,
|
||||||
|
SpeechResultState.SUCCESS if result.success else SpeechResultState.ERROR,
|
||||||
|
)
|
81
homeassistant/components/cloud/tts.py
Normal file
81
homeassistant/components/cloud/tts.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
"""Support for the cloud for text to speech service."""
|
||||||
|
|
||||||
|
from hass_nabucasa.voice import VoiceError
|
||||||
|
from hass_nabucasa import Cloud
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
CONF_GENDER = "gender"
|
||||||
|
|
||||||
|
SUPPORT_LANGUAGES = ["en-US", "de-DE", "es-ES"]
|
||||||
|
SUPPORT_GENDER = ["male", "female"]
|
||||||
|
|
||||||
|
DEFAULT_LANG = "en-US"
|
||||||
|
DEFAULT_GENDER = "female"
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES),
|
||||||
|
vol.Optional(CONF_GENDER, default=DEFAULT_GENDER): vol.In(SUPPORT_GENDER),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_engine(hass, config, discovery_info=None):
|
||||||
|
"""Set up Cloud speech component."""
|
||||||
|
cloud: Cloud = hass.data[DOMAIN]
|
||||||
|
|
||||||
|
if discovery_info is not None:
|
||||||
|
language = DEFAULT_LANG
|
||||||
|
gender = DEFAULT_GENDER
|
||||||
|
else:
|
||||||
|
language = config[CONF_LANG]
|
||||||
|
gender = config[CONF_GENDER]
|
||||||
|
|
||||||
|
return CloudProvider(cloud, language, gender)
|
||||||
|
|
||||||
|
|
||||||
|
class CloudProvider(Provider):
|
||||||
|
"""NabuCasa Cloud speech API provider."""
|
||||||
|
|
||||||
|
def __init__(self, cloud: Cloud, language: str, gender: str):
|
||||||
|
"""Initialize cloud provider."""
|
||||||
|
self.cloud = cloud
|
||||||
|
self.name = "Cloud"
|
||||||
|
self._language = language
|
||||||
|
self._gender = gender
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_language(self):
|
||||||
|
"""Return the default language."""
|
||||||
|
return self._language
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_languages(self):
|
||||||
|
"""Return list of supported languages."""
|
||||||
|
return SUPPORT_LANGUAGES
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_options(self):
|
||||||
|
"""Return list of supported options like voice, emotion."""
|
||||||
|
return [CONF_GENDER]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_options(self):
|
||||||
|
"""Return a dict include default options."""
|
||||||
|
return {CONF_GENDER: self._gender}
|
||||||
|
|
||||||
|
async def async_get_tts_audio(self, message, language, options=None):
|
||||||
|
"""Load TTS from NabuCasa Cloud."""
|
||||||
|
# Process TTS
|
||||||
|
try:
|
||||||
|
data = await self.cloud.voice.process_tts(
|
||||||
|
message, language, gender=options[CONF_GENDER]
|
||||||
|
)
|
||||||
|
except VoiceError:
|
||||||
|
return (None, None)
|
||||||
|
|
||||||
|
return ("mp3", data)
|
@ -6,16 +6,17 @@ from aiohttp import StreamReader
|
|||||||
from homeassistant.components.stt import Provider, SpeechMetadata, SpeechResult
|
from homeassistant.components.stt import Provider, SpeechMetadata, SpeechResult
|
||||||
from homeassistant.components.stt.const import (
|
from homeassistant.components.stt.const import (
|
||||||
AudioBitRates,
|
AudioBitRates,
|
||||||
|
AudioChannels,
|
||||||
|
AudioCodecs,
|
||||||
AudioFormats,
|
AudioFormats,
|
||||||
AudioSampleRates,
|
AudioSampleRates,
|
||||||
AudioCodecs,
|
|
||||||
SpeechResultState,
|
SpeechResultState,
|
||||||
)
|
)
|
||||||
|
|
||||||
SUPPORT_LANGUAGES = ["en", "de"]
|
SUPPORT_LANGUAGES = ["en", "de"]
|
||||||
|
|
||||||
|
|
||||||
async def async_get_engine(hass, config):
|
async def async_get_engine(hass, config, discovery_info=None):
|
||||||
"""Set up Demo speech component."""
|
"""Set up Demo speech component."""
|
||||||
return DemoProvider()
|
return DemoProvider()
|
||||||
|
|
||||||
@ -48,6 +49,11 @@ class DemoProvider(Provider):
|
|||||||
"""Return a list of supported sample rates."""
|
"""Return a list of supported sample rates."""
|
||||||
return [AudioSampleRates.SAMPLERATE_16000, AudioSampleRates.SAMPLERATE_44100]
|
return [AudioSampleRates.SAMPLERATE_16000, AudioSampleRates.SAMPLERATE_44100]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_channels(self) -> List[AudioChannels]:
|
||||||
|
"""Return a list of supported channels."""
|
||||||
|
return [AudioChannels.CHANNEL_STEREO]
|
||||||
|
|
||||||
async def async_process_audio_stream(
|
async def async_process_audio_stream(
|
||||||
self, metadata: SpeechMetadata, stream: StreamReader
|
self, metadata: SpeechMetadata, stream: StreamReader
|
||||||
) -> SpeechResult:
|
) -> SpeechResult:
|
||||||
|
@ -14,7 +14,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_engine(hass, config):
|
def get_engine(hass, config, discovery_info=None):
|
||||||
"""Set up Demo speech component."""
|
"""Set up Demo speech component."""
|
||||||
return DemoProvider(config[CONF_LANG])
|
return DemoProvider(config[CONF_LANG])
|
||||||
|
|
||||||
|
@ -122,7 +122,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_get_engine(hass, config):
|
async def async_get_engine(hass, config, discovery_info=None):
|
||||||
"""Set up Google Cloud TTS component."""
|
"""Set up Google Cloud TTS component."""
|
||||||
key_file = config.get(CONF_KEY_FILE)
|
key_file = config.get(CONF_KEY_FILE)
|
||||||
if key_file:
|
if key_file:
|
||||||
|
@ -81,7 +81,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_get_engine(hass, config):
|
async def async_get_engine(hass, config, discovery_info=None):
|
||||||
"""Set up Google speech component."""
|
"""Set up Google speech component."""
|
||||||
return GoogleProvider(hass, config[CONF_LANG])
|
return GoogleProvider(hass, config[CONF_LANG])
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_get_engine(hass, config):
|
async def async_get_engine(hass, config, discovery_info=None):
|
||||||
"""Set up MaryTTS speech component."""
|
"""Set up MaryTTS speech component."""
|
||||||
return MaryTTSProvider(hass, config)
|
return MaryTTSProvider(hass, config)
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_engine(hass, config):
|
def get_engine(hass, config, discovery_info=None):
|
||||||
"""Set up Microsoft speech component."""
|
"""Set up Microsoft speech component."""
|
||||||
return MicrosoftProvider(
|
return MicrosoftProvider(
|
||||||
config[CONF_API_KEY],
|
config[CONF_API_KEY],
|
||||||
|
@ -20,7 +20,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_engine(hass, config):
|
def get_engine(hass, config, discovery_info=None):
|
||||||
"""Set up Pico speech component."""
|
"""Set up Pico speech component."""
|
||||||
if shutil.which("pico2wave") is None:
|
if shutil.which("pico2wave") is None:
|
||||||
_LOGGER.error("'pico2wave' was not found")
|
_LOGGER.error("'pico2wave' was not found")
|
||||||
|
@ -7,21 +7,22 @@ from typing import Dict, List, Optional
|
|||||||
from aiohttp import StreamReader, web
|
from aiohttp import StreamReader, web
|
||||||
from aiohttp.hdrs import istr
|
from aiohttp.hdrs import istr
|
||||||
from aiohttp.web_exceptions import (
|
from aiohttp.web_exceptions import (
|
||||||
|
HTTPBadRequest,
|
||||||
HTTPNotFound,
|
HTTPNotFound,
|
||||||
HTTPUnsupportedMediaType,
|
HTTPUnsupportedMediaType,
|
||||||
HTTPBadRequest,
|
|
||||||
)
|
)
|
||||||
import attr
|
import attr
|
||||||
|
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers import config_per_platform
|
from homeassistant.helpers import config_per_platform, discovery
|
||||||
from homeassistant.helpers.typing import HomeAssistantType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
from homeassistant.setup import async_prepare_setup_platform
|
from homeassistant.setup import async_prepare_setup_platform
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
AudioBitRates,
|
AudioBitRates,
|
||||||
|
AudioChannels,
|
||||||
AudioCodecs,
|
AudioCodecs,
|
||||||
AudioFormats,
|
AudioFormats,
|
||||||
AudioSampleRates,
|
AudioSampleRates,
|
||||||
@ -37,14 +38,17 @@ async def async_setup(hass: HomeAssistantType, config):
|
|||||||
"""Set up STT."""
|
"""Set up STT."""
|
||||||
providers = {}
|
providers = {}
|
||||||
|
|
||||||
async def async_setup_platform(p_type, p_config, disc_info=None):
|
async def async_setup_platform(p_type, p_config=None, discovery_info=None):
|
||||||
"""Set up a TTS platform."""
|
"""Set up a TTS platform."""
|
||||||
|
if p_config is None:
|
||||||
|
p_config = {}
|
||||||
|
|
||||||
platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type)
|
platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type)
|
||||||
if platform is None:
|
if platform is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
provider = await platform.async_get_engine(hass, p_config)
|
provider = await platform.async_get_engine(hass, p_config, discovery_info)
|
||||||
if provider is None:
|
if provider is None:
|
||||||
_LOGGER.error("Error setting up platform %s", p_type)
|
_LOGGER.error("Error setting up platform %s", p_type)
|
||||||
return
|
return
|
||||||
@ -65,6 +69,13 @@ async def async_setup(hass: HomeAssistantType, config):
|
|||||||
if setup_tasks:
|
if setup_tasks:
|
||||||
await asyncio.wait(setup_tasks)
|
await asyncio.wait(setup_tasks)
|
||||||
|
|
||||||
|
# Add discovery support
|
||||||
|
async def async_platform_discovered(platform, info):
|
||||||
|
"""Handle for discovered platform."""
|
||||||
|
await async_setup_platform(platform, discovery_info=info)
|
||||||
|
|
||||||
|
discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
|
||||||
|
|
||||||
hass.http.register_view(SpeechToTextView(providers))
|
hass.http.register_view(SpeechToTextView(providers))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -78,13 +89,14 @@ class SpeechMetadata:
|
|||||||
codec: AudioCodecs = attr.ib()
|
codec: AudioCodecs = attr.ib()
|
||||||
bit_rate: AudioBitRates = attr.ib(converter=int)
|
bit_rate: AudioBitRates = attr.ib(converter=int)
|
||||||
sample_rate: AudioSampleRates = attr.ib(converter=int)
|
sample_rate: AudioSampleRates = attr.ib(converter=int)
|
||||||
|
channel: AudioChannels = attr.ib(converter=int)
|
||||||
|
|
||||||
|
|
||||||
@attr.s
|
@attr.s
|
||||||
class SpeechResult:
|
class SpeechResult:
|
||||||
"""Result of audio Speech."""
|
"""Result of audio Speech."""
|
||||||
|
|
||||||
text: str = attr.ib()
|
text: Optional[str] = attr.ib()
|
||||||
result: SpeechResultState = attr.ib()
|
result: SpeechResultState = attr.ib()
|
||||||
|
|
||||||
|
|
||||||
@ -112,12 +124,17 @@ class Provider(ABC):
|
|||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def supported_bit_rates(self) -> List[AudioBitRates]:
|
def supported_bit_rates(self) -> List[AudioBitRates]:
|
||||||
"""Return a list of supported bit_rates."""
|
"""Return a list of supported bit rates."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def supported_sample_rates(self) -> List[AudioSampleRates]:
|
def supported_sample_rates(self) -> List[AudioSampleRates]:
|
||||||
"""Return a list of supported sample_rates."""
|
"""Return a list of supported sample rates."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def supported_channels(self) -> List[AudioChannels]:
|
||||||
|
"""Return a list of supported channels."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def async_process_audio_stream(
|
async def async_process_audio_stream(
|
||||||
@ -137,6 +154,7 @@ class Provider(ABC):
|
|||||||
or metadata.codec not in self.supported_codecs
|
or metadata.codec not in self.supported_codecs
|
||||||
or metadata.bit_rate not in self.supported_bit_rates
|
or metadata.bit_rate not in self.supported_bit_rates
|
||||||
or metadata.sample_rate not in self.supported_sample_rates
|
or metadata.sample_rate not in self.supported_sample_rates
|
||||||
|
or metadata.channel not in self.supported_channels
|
||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
@ -157,7 +175,7 @@ class SpeechToTextView(HomeAssistantView):
|
|||||||
def _metadata_from_header(request: web.Request) -> Optional[SpeechMetadata]:
|
def _metadata_from_header(request: web.Request) -> Optional[SpeechMetadata]:
|
||||||
"""Extract metadata from header.
|
"""Extract metadata from header.
|
||||||
|
|
||||||
X-Speech-Content: format=wav; codec=pcm; samplerate=16000; bitrate=16; language=de_de
|
X-Speech-Content: format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=1; language=de_de
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = request.headers[istr("X-Speech-Content")].split(";")
|
data = request.headers[istr("X-Speech-Content")].split(";")
|
||||||
@ -213,5 +231,6 @@ class SpeechToTextView(HomeAssistantView):
|
|||||||
"codecs": stt_provider.supported_codecs,
|
"codecs": stt_provider.supported_codecs,
|
||||||
"sample_rates": stt_provider.supported_sample_rates,
|
"sample_rates": stt_provider.supported_sample_rates,
|
||||||
"bit_rates": stt_provider.supported_bit_rates,
|
"bit_rates": stt_provider.supported_bit_rates,
|
||||||
|
"channels": stt_provider.supported_channels,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -19,7 +19,7 @@ class AudioFormats(str, Enum):
|
|||||||
|
|
||||||
|
|
||||||
class AudioBitRates(int, Enum):
|
class AudioBitRates(int, Enum):
|
||||||
"""Supported Audio bit_rates."""
|
"""Supported Audio bit rates."""
|
||||||
|
|
||||||
BITRATE_8 = 8
|
BITRATE_8 = 8
|
||||||
BITRATE_16 = 16
|
BITRATE_16 = 16
|
||||||
@ -28,7 +28,7 @@ class AudioBitRates(int, Enum):
|
|||||||
|
|
||||||
|
|
||||||
class AudioSampleRates(int, Enum):
|
class AudioSampleRates(int, Enum):
|
||||||
"""Supported Audio sample_rates."""
|
"""Supported Audio sample rates."""
|
||||||
|
|
||||||
SAMPLERATE_8000 = 8000
|
SAMPLERATE_8000 = 8000
|
||||||
SAMPLERATE_11000 = 11000
|
SAMPLERATE_11000 = 11000
|
||||||
@ -41,6 +41,13 @@ class AudioSampleRates(int, Enum):
|
|||||||
SAMPLERATE_48000 = 48000
|
SAMPLERATE_48000 = 48000
|
||||||
|
|
||||||
|
|
||||||
|
class AudioChannels(int, Enum):
|
||||||
|
"""Supported Audio channel."""
|
||||||
|
|
||||||
|
CHANNEL_MONO = 1
|
||||||
|
CHANNEL_STEREO = 2
|
||||||
|
|
||||||
|
|
||||||
class SpeechResultState(str, Enum):
|
class SpeechResultState(str, Enum):
|
||||||
"""Result state of speech."""
|
"""Result state of speech."""
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ from homeassistant.components.media_player.const import (
|
|||||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, ENTITY_MATCH_ALL
|
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, ENTITY_MATCH_ALL
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import config_per_platform
|
from homeassistant.helpers import config_per_platform, discovery
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.typing import HomeAssistantType
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
from homeassistant.setup import async_prepare_setup_platform
|
from homeassistant.setup import async_prepare_setup_platform
|
||||||
@ -118,17 +118,24 @@ async def async_setup(hass, config):
|
|||||||
hass.http.register_view(TextToSpeechView(tts))
|
hass.http.register_view(TextToSpeechView(tts))
|
||||||
hass.http.register_view(TextToSpeechUrlView(tts))
|
hass.http.register_view(TextToSpeechUrlView(tts))
|
||||||
|
|
||||||
async def async_setup_platform(p_type, p_config, disc_info=None):
|
async def async_setup_platform(p_type, p_config=None, discovery_info=None):
|
||||||
"""Set up a TTS platform."""
|
"""Set up a TTS platform."""
|
||||||
|
if p_config is None:
|
||||||
|
p_config = {}
|
||||||
|
|
||||||
platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type)
|
platform = await async_prepare_setup_platform(hass, config, DOMAIN, p_type)
|
||||||
if platform is None:
|
if platform is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if hasattr(platform, "async_get_engine"):
|
if hasattr(platform, "async_get_engine"):
|
||||||
provider = await platform.async_get_engine(hass, p_config)
|
provider = await platform.async_get_engine(
|
||||||
|
hass, p_config, discovery_info
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
provider = await hass.async_add_job(platform.get_engine, hass, p_config)
|
provider = await hass.async_add_job(
|
||||||
|
platform.get_engine, hass, p_config, discovery_info
|
||||||
|
)
|
||||||
|
|
||||||
if provider is None:
|
if provider is None:
|
||||||
_LOGGER.error("Error setting up platform %s", p_type)
|
_LOGGER.error("Error setting up platform %s", p_type)
|
||||||
@ -178,6 +185,12 @@ async def async_setup(hass, config):
|
|||||||
if setup_tasks:
|
if setup_tasks:
|
||||||
await asyncio.wait(setup_tasks)
|
await asyncio.wait(setup_tasks)
|
||||||
|
|
||||||
|
async def async_platform_discovered(platform, info):
|
||||||
|
"""Handle for discovered platform."""
|
||||||
|
await async_setup_platform(platform, discovery_info=info)
|
||||||
|
|
||||||
|
discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
|
||||||
|
|
||||||
async def async_clear_cache_handle(service):
|
async def async_clear_cache_handle(service):
|
||||||
"""Handle clear cache service call."""
|
"""Handle clear cache service call."""
|
||||||
await tts.async_clear_cache()
|
await tts.async_clear_cache()
|
||||||
|
@ -131,7 +131,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_get_engine(hass, config):
|
async def async_get_engine(hass, config, discovery_info=None):
|
||||||
"""Set up VoiceRSS TTS component."""
|
"""Set up VoiceRSS TTS component."""
|
||||||
return VoiceRSSProvider(hass, config)
|
return VoiceRSSProvider(hass, config)
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_engine(hass, config):
|
def get_engine(hass, config, discovery_info=None):
|
||||||
"""Set up IBM Watson TTS component."""
|
"""Set up IBM Watson TTS component."""
|
||||||
from ibm_watson import TextToSpeechV1
|
from ibm_watson import TextToSpeechV1
|
||||||
from ibm_cloud_sdk_core.authenticators import IAMAuthenticator
|
from ibm_cloud_sdk_core.authenticators import IAMAuthenticator
|
||||||
|
@ -79,7 +79,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
|||||||
SUPPORTED_OPTIONS = [CONF_CODEC, CONF_VOICE, CONF_EMOTION, CONF_SPEED]
|
SUPPORTED_OPTIONS = [CONF_CODEC, CONF_VOICE, CONF_EMOTION, CONF_SPEED]
|
||||||
|
|
||||||
|
|
||||||
async def async_get_engine(hass, config):
|
async def async_get_engine(hass, config, discovery_info=None):
|
||||||
"""Set up VoiceRSS speech component."""
|
"""Set up VoiceRSS speech component."""
|
||||||
return YandexSpeechKitProvider(hass, config)
|
return YandexSpeechKitProvider(hass, config)
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ certifi>=2019.9.11
|
|||||||
contextvars==2.4;python_version<"3.7"
|
contextvars==2.4;python_version<"3.7"
|
||||||
cryptography==2.8
|
cryptography==2.8
|
||||||
distro==1.4.0
|
distro==1.4.0
|
||||||
hass-nabucasa==0.23
|
hass-nabucasa==0.26
|
||||||
home-assistant-frontend==20191025.1
|
home-assistant-frontend==20191025.1
|
||||||
importlib-metadata==0.23
|
importlib-metadata==0.23
|
||||||
jinja2>=2.10.3
|
jinja2>=2.10.3
|
||||||
|
1
pylintrc
1
pylintrc
@ -44,6 +44,7 @@ disable=
|
|||||||
too-many-public-methods,
|
too-many-public-methods,
|
||||||
too-many-return-statements,
|
too-many-return-statements,
|
||||||
too-many-statements,
|
too-many-statements,
|
||||||
|
too-many-boolean-expressions,
|
||||||
unnecessary-pass,
|
unnecessary-pass,
|
||||||
unused-argument
|
unused-argument
|
||||||
|
|
||||||
|
@ -622,7 +622,7 @@ habitipy==0.2.0
|
|||||||
hangups==0.4.9
|
hangups==0.4.9
|
||||||
|
|
||||||
# homeassistant.components.cloud
|
# homeassistant.components.cloud
|
||||||
hass-nabucasa==0.23
|
hass-nabucasa==0.26
|
||||||
|
|
||||||
# homeassistant.components.mqtt
|
# homeassistant.components.mqtt
|
||||||
hbmqtt==0.9.5
|
hbmqtt==0.9.5
|
||||||
|
@ -204,7 +204,7 @@ ha-ffmpeg==2.0
|
|||||||
hangups==0.4.9
|
hangups==0.4.9
|
||||||
|
|
||||||
# homeassistant.components.cloud
|
# homeassistant.components.cloud
|
||||||
hass-nabucasa==0.23
|
hass-nabucasa==0.26
|
||||||
|
|
||||||
# homeassistant.components.mqtt
|
# homeassistant.components.mqtt
|
||||||
hbmqtt==0.9.5
|
hbmqtt==0.9.5
|
||||||
|
@ -27,6 +27,7 @@ async def test_demo_settings(hass_client):
|
|||||||
"sample_rates": [16000, 44100],
|
"sample_rates": [16000, 44100],
|
||||||
"formats": ["wav"],
|
"formats": ["wav"],
|
||||||
"codecs": ["pcm"],
|
"codecs": ["pcm"],
|
||||||
|
"channels": [2],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -45,7 +46,7 @@ async def test_demo_speech_wrong_metadata(hass_client):
|
|||||||
response = await client.post(
|
response = await client.post(
|
||||||
"/api/stt/demo",
|
"/api/stt/demo",
|
||||||
headers={
|
headers={
|
||||||
"X-Speech-Content": "format=wav; codec=pcm; sample_rate=8000; bit_rate=16; language=de"
|
"X-Speech-Content": "format=wav; codec=pcm; sample_rate=8000; bit_rate=16; channel=1; language=de"
|
||||||
},
|
},
|
||||||
data=b"Test",
|
data=b"Test",
|
||||||
)
|
)
|
||||||
@ -59,7 +60,7 @@ async def test_demo_speech(hass_client):
|
|||||||
response = await client.post(
|
response = await client.post(
|
||||||
"/api/stt/demo",
|
"/api/stt/demo",
|
||||||
headers={
|
headers={
|
||||||
"X-Speech-Content": "format=wav; codec=pcm; sample_rate=16000; bit_rate=16; language=de"
|
"X-Speech-Content": "format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=2; language=de"
|
||||||
},
|
},
|
||||||
data=b"Test",
|
data=b"Test",
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user