From ad2d794fd68137491ccc81f7d4281c7fd266c34a Mon Sep 17 00:00:00 2001 From: tronikos Date: Fri, 5 Jul 2024 11:03:06 -0700 Subject: [PATCH] Get languages and voices from the API in Google Cloud TTS (#120852) --- .../components/google_cloud/helpers.py | 19 +++ homeassistant/components/google_cloud/tts.py | 108 ++++++------------ 2 files changed, 53 insertions(+), 74 deletions(-) create mode 100644 homeassistant/components/google_cloud/helpers.py diff --git a/homeassistant/components/google_cloud/helpers.py b/homeassistant/components/google_cloud/helpers.py new file mode 100644 index 00000000000..6a890f90cc7 --- /dev/null +++ b/homeassistant/components/google_cloud/helpers.py @@ -0,0 +1,19 @@ +"""Helper classes for Google Cloud integration.""" + +from __future__ import annotations + +from google.cloud import texttospeech + + +async def async_tts_voices( + client: texttospeech.TextToSpeechAsyncClient, +) -> dict[str, list[str]]: + """Get TTS voice model names keyed by language.""" + voices: dict[str, list[str]] = {} + list_voices_response = await client.list_voices() + for voice in list_voices_response.voices: + language_code = voice.language_codes[0] + if language_code not in voices: + voices[language_code] = [] + voices[language_code].append(voice.name) + return voices diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index f9b01c9b870..92a8a6cdf5e 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -11,9 +11,13 @@ from homeassistant.components.tts import ( CONF_LANG, PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA, Provider, + Voice, ) +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv +from .helpers import async_tts_voices + _LOGGER = logging.getLogger(__name__) CONF_KEY_FILE = "key_file" @@ -26,70 +30,12 @@ CONF_GAIN = "gain" CONF_PROFILES = "profiles" CONF_TEXT_TYPE = "text_type" -SUPPORTED_LANGUAGES = [ - "af-ZA", - "ar-XA", - "bg-BG", - "bn-IN", - "ca-ES", - "cmn-CN", - "cmn-TW", - "cs-CZ", - "da-DK", - "de-DE", - "el-GR", - "en-AU", - "en-GB", - "en-IN", - "en-US", - "es-ES", - "es-US", - "eu-ES", - "fi-FI", - "fil-PH", - "fr-CA", - "fr-FR", - "gl-ES", - "gu-IN", - "he-IL", - "hi-IN", - "hu-HU", - "id-ID", - "is-IS", - "it-IT", - "ja-JP", - "kn-IN", - "ko-KR", - "lv-LV", - "lt-LT", - "ml-IN", - "mr-IN", - "ms-MY", - "nb-NO", - "nl-BE", - "nl-NL", - "pa-IN", - "pl-PL", - "pt-BR", - "pt-PT", - "ro-RO", - "ru-RU", - "sk-SK", - "sr-RS", - "sv-SE", - "ta-IN", - "te-IN", - "th-TH", - "tr-TR", - "uk-UA", - "vi-VN", - "yue-HK", -] DEFAULT_LANG = "en-US" DEFAULT_GENDER = "NEUTRAL" -VOICE_REGEX = r"[a-z]{2,3}-[A-Z]{2}-(Standard|Wavenet)-[A-Z]|" +LANG_REGEX = r"[a-z]{2,3}-[A-Z]{2}|" +VOICE_REGEX = r"[a-z]{2,3}-[A-Z]{2}-.*-[A-Z]|" DEFAULT_VOICE = "" DEFAULT_ENCODING = "MP3" @@ -143,7 +89,7 @@ TEXT_TYPE_SCHEMA = vol.All(vol.Lower, vol.In(SUPPORTED_TEXT_TYPES)) PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_KEY_FILE): cv.string, - vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORTED_LANGUAGES), + vol.Optional(CONF_LANG, default=DEFAULT_LANG): cv.matches_regex(LANG_REGEX), vol.Optional(CONF_GENDER, default=DEFAULT_GENDER): GENDER_SCHEMA, vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): VOICE_SCHEMA, vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): SCHEMA_ENCODING, @@ -163,10 +109,21 @@ async def async_get_engine(hass, config, discovery_info=None): if not os.path.isfile(key_file): _LOGGER.error("File %s doesn't exist", key_file) return None - + if key_file: + client = texttospeech.TextToSpeechAsyncClient.from_service_account_json( + key_file + ) + else: + client = texttospeech.TextToSpeechAsyncClient() + try: + voices = await async_tts_voices(client) + except GoogleAPIError as err: + _LOGGER.error("Error from calling list_voices: %s", err) + return None return GoogleCloudTTSProvider( hass, - key_file, + client, + voices, config[CONF_LANG], config[CONF_GENDER], config[CONF_VOICE], @@ -184,8 +141,9 @@ class GoogleCloudTTSProvider(Provider): def __init__( self, - hass, - key_file=None, + hass: HomeAssistant, + client: texttospeech.TextToSpeechAsyncClient, + voices: dict[str, list[str]], language=DEFAULT_LANG, gender=DEFAULT_GENDER, voice=DEFAULT_VOICE, @@ -195,10 +153,12 @@ class GoogleCloudTTSProvider(Provider): gain=0, profiles=None, text_type=DEFAULT_TEXT_TYPE, - ): + ) -> None: """Init Google Cloud TTS service.""" self.hass = hass self.name = "Google Cloud TTS" + self._client = client + self._voices = voices self._language = language self._gender = gender self._voice = voice @@ -209,17 +169,10 @@ class GoogleCloudTTSProvider(Provider): self._profiles = profiles self._text_type = text_type - if key_file: - self._client = ( - texttospeech.TextToSpeechAsyncClient.from_service_account_json(key_file) - ) - else: - self._client = texttospeech.TextToSpeechAsyncClient() - @property def supported_languages(self): """Return list of supported languages.""" - return SUPPORTED_LANGUAGES + return list(self._voices) @property def default_language(self): @@ -245,6 +198,13 @@ class GoogleCloudTTSProvider(Provider): CONF_TEXT_TYPE: self._text_type, } + @callback + def async_get_supported_voices(self, language: str) -> list[Voice] | None: + """Return a list of supported voices for a language.""" + if not (voices := self._voices.get(language)): + return None + return [Voice(voice, voice) for voice in voices] + async def async_get_tts_audio(self, message, language, options): """Load TTS from google.""" options_schema = vol.Schema(