1
mirror of https://github.com/home-assistant/core synced 2024-08-28 03:36:46 +02:00
ha-core/homeassistant/components/cloud/tts.py
Michael Hansen ae516ffbb5
Automatically convert TTS audio to MP3 on demand (#102814)
* Add ATTR_PREFERRED_FORMAT to TTS for auto-converting audio

* Move conversion into SpeechManager

* Handle None case for expected_extension

* Only use ATTR_AUDIO_OUTPUT

* Prefer MP3 in pipelines

* Automatically convert to mp3 on demand

* Add preferred audio format

* Break out preferred format

* Add ATTR_BLOCKING to allow async fetching

* Make a copy of supported options

* Fix MaryTTS tests

* Update ESPHome to use "wav" instead of "raw"

* Clean up tests, remove blocking

* Clean up rest of TTS tests

* Fix ESPHome tests

* More test coverage
2023-11-06 15:26:00 -05:00

154 lines
4.3 KiB
Python

"""Support for the cloud for text-to-speech service."""
from __future__ import annotations
import logging
from typing import Any
from hass_nabucasa import Cloud
from hass_nabucasa.voice import MAP_VOICE, TTS_VOICES, AudioOutput, VoiceError
import voluptuous as vol
from homeassistant.components.tts import (
ATTR_AUDIO_OUTPUT,
ATTR_VOICE,
CONF_LANG,
PLATFORM_SCHEMA,
Provider,
TtsAudioType,
Voice,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .client import CloudClient
from .const import DOMAIN
from .prefs import CloudPreferences
ATTR_GENDER = "gender"
SUPPORT_LANGUAGES = list(TTS_VOICES)
_LOGGER = logging.getLogger(__name__)
def validate_lang(value: dict[str, Any]) -> dict[str, Any]:
"""Validate chosen gender or language."""
if (lang := value.get(CONF_LANG)) is None:
return value
if (gender := value.get(ATTR_GENDER)) is None:
gender = value[ATTR_GENDER] = next(
(chk_gender for chk_lang, chk_gender in MAP_VOICE if chk_lang == lang), None
)
if (lang, gender) not in MAP_VOICE:
raise vol.Invalid("Unsupported language and gender specified.")
return value
PLATFORM_SCHEMA = vol.All(
PLATFORM_SCHEMA.extend(
{
vol.Optional(CONF_LANG): str,
vol.Optional(ATTR_GENDER): str,
}
),
validate_lang,
)
async def async_get_engine(
hass: HomeAssistant,
config: ConfigType,
discovery_info: DiscoveryInfoType | None = None,
) -> CloudProvider:
"""Set up Cloud speech component."""
cloud: Cloud[CloudClient] = hass.data[DOMAIN]
language: str | None
gender: str | None
if discovery_info is not None:
language = None
gender = None
else:
language = config[CONF_LANG]
gender = config[ATTR_GENDER]
cloud_provider = CloudProvider(cloud, language, gender)
if discovery_info is not None:
discovery_info["platform_loaded"].set()
return cloud_provider
class CloudProvider(Provider):
"""NabuCasa Cloud speech API provider."""
def __init__(
self, cloud: Cloud[CloudClient], language: str | None, gender: str | None
) -> None:
"""Initialize cloud provider."""
self.cloud = cloud
self.name = "Cloud"
self._language = language
self._gender = gender
if self._language is not None:
return
self._language, self._gender = cloud.client.prefs.tts_default_voice
cloud.client.prefs.async_listen_updates(self._sync_prefs)
async def _sync_prefs(self, prefs: CloudPreferences) -> None:
"""Sync preferences."""
self._language, self._gender = prefs.tts_default_voice
@property
def default_language(self) -> str | None:
"""Return the default language."""
return self._language
@property
def supported_languages(self) -> list[str]:
"""Return list of supported languages."""
return SUPPORT_LANGUAGES
@property
def supported_options(self) -> list[str]:
"""Return list of supported options like voice, emotion."""
return [ATTR_GENDER, ATTR_VOICE, ATTR_AUDIO_OUTPUT]
@callback
def async_get_supported_voices(self, language: str) -> list[Voice] | None:
"""Return a list of supported voices for a language."""
if not (voices := TTS_VOICES.get(language)):
return None
return [Voice(voice, voice) for voice in voices]
@property
def default_options(self) -> dict[str, Any]:
"""Return a dict include default options."""
return {
ATTR_GENDER: self._gender,
ATTR_AUDIO_OUTPUT: AudioOutput.MP3,
}
async def async_get_tts_audio(
self, message: str, language: str, options: dict[str, Any]
) -> TtsAudioType:
"""Load TTS from NabuCasa Cloud."""
# Process TTS
try:
data = await self.cloud.voice.process_tts(
text=message,
language=language,
gender=options.get(ATTR_GENDER),
voice=options.get(ATTR_VOICE),
output=options[ATTR_AUDIO_OUTPUT],
)
except VoiceError as err:
_LOGGER.error("Voice error: %s", err)
return (None, None)
return (str(options[ATTR_AUDIO_OUTPUT].value), data)