Home Assistant Unofficial Reference 2024.12.1
tts.py
Go to the documentation of this file.
1 """Support for the Google Cloud TTS service."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from pathlib import Path
7 from typing import Any, cast
8 
9 from google.api_core.exceptions import GoogleAPIError, Unauthenticated
10 from google.cloud import texttospeech
11 import voluptuous as vol
12 
13 from homeassistant.components.tts import (
14  CONF_LANG,
15  PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA,
16  Provider,
17  TextToSpeechEntity,
18  TtsAudioType,
19  Voice,
20 )
21 from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
22 from homeassistant.core import HomeAssistant, callback
23 from homeassistant.helpers import device_registry as dr
24 from homeassistant.helpers.entity_platform import AddEntitiesCallback
25 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
26 
27 from .const import (
28  CONF_ENCODING,
29  CONF_GAIN,
30  CONF_GENDER,
31  CONF_KEY_FILE,
32  CONF_PITCH,
33  CONF_PROFILES,
34  CONF_SERVICE_ACCOUNT_INFO,
35  CONF_SPEED,
36  CONF_TEXT_TYPE,
37  CONF_VOICE,
38  DEFAULT_LANG,
39  DOMAIN,
40 )
41 from .helpers import async_tts_voices, tts_options_schema, tts_platform_schema
42 
43 _LOGGER = logging.getLogger(__name__)
44 
45 PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend(tts_platform_schema().schema)
46 
47 
48 async def async_get_engine(
49  hass: HomeAssistant,
50  config: ConfigType,
51  discovery_info: DiscoveryInfoType | None = None,
52 ) -> Provider | None:
53  """Set up Google Cloud TTS component."""
54  if key_file := config.get(CONF_KEY_FILE):
55  key_file = hass.config.path(key_file)
56  if not Path(key_file).is_file():
57  _LOGGER.error("File %s doesn't exist", key_file)
58  return None
59  if key_file:
60  client = texttospeech.TextToSpeechAsyncClient.from_service_account_file(
61  key_file
62  )
63  if not hass.config_entries.async_entries(DOMAIN):
64  _LOGGER.debug("Creating config entry by importing: %s", config)
65  hass.async_create_task(
66  hass.config_entries.flow.async_init(
67  DOMAIN, context={"source": SOURCE_IMPORT}, data=config
68  )
69  )
70  else:
71  client = texttospeech.TextToSpeechAsyncClient()
72  try:
73  voices = await async_tts_voices(client)
74  except GoogleAPIError as err:
75  _LOGGER.error("Error from calling list_voices: %s", err)
76  return None
78  client,
79  voices,
80  config.get(CONF_LANG, DEFAULT_LANG),
81  tts_options_schema(config, voices),
82  )
83 
84 
86  hass: HomeAssistant,
87  config_entry: ConfigEntry,
88  async_add_entities: AddEntitiesCallback,
89 ) -> None:
90  """Set up Google Cloud text-to-speech."""
91  service_account_info = config_entry.data[CONF_SERVICE_ACCOUNT_INFO]
92  client: texttospeech.TextToSpeechAsyncClient = (
93  texttospeech.TextToSpeechAsyncClient.from_service_account_info(
94  service_account_info
95  )
96  )
97  try:
98  voices = await async_tts_voices(client)
99  except GoogleAPIError as err:
100  _LOGGER.error("Error from calling list_voices: %s", err)
101  if isinstance(err, Unauthenticated):
102  config_entry.async_start_reauth(hass)
103  return
104  options_schema = tts_options_schema(dict(config_entry.options), voices)
105  language = config_entry.options.get(CONF_LANG, DEFAULT_LANG)
107  [
109  config_entry,
110  client,
111  voices,
112  language,
113  options_schema,
114  )
115  ]
116  )
117 
118 
120  """The Google Cloud TTS base provider."""
121 
122  def __init__(
123  self,
124  client: texttospeech.TextToSpeechAsyncClient,
125  voices: dict[str, list[str]],
126  language: str,
127  options_schema: vol.Schema,
128  ) -> None:
129  """Init Google Cloud TTS base provider."""
130  self._client_client = client
131  self._voices_voices = voices
132  self._language_language = language
133  self._options_schema_options_schema = options_schema
134 
135  @property
136  def supported_languages(self) -> list[str]:
137  """Return a list of supported languages."""
138  return list(self._voices_voices)
139 
140  @property
141  def default_language(self) -> str:
142  """Return the default language."""
143  return self._language_language
144 
145  @property
146  def supported_options(self) -> list[str]:
147  """Return a list of supported options."""
148  return [option.schema for option in self._options_schema_options_schema.schema]
149 
150  @property
151  def default_options(self) -> dict[str, Any]:
152  """Return a dict including default options."""
153  return cast(dict[str, Any], self._options_schema_options_schema({}))
154 
155  @callback
156  def async_get_supported_voices(self, language: str) -> list[Voice] | None:
157  """Return a list of supported voices for a language."""
158  if not (voices := self._voices_voices.get(language)):
159  return None
160  return [Voice(voice, voice) for voice in voices]
161 
163  self,
164  message: str,
165  language: str,
166  options: dict[str, Any],
167  ) -> TtsAudioType:
168  """Load TTS from Google Cloud."""
169  try:
170  options = self._options_schema_options_schema(options)
171  except vol.Invalid as err:
172  _LOGGER.error("Error: %s when validating options: %s", err, options)
173  return None, None
174 
175  encoding: texttospeech.AudioEncoding = texttospeech.AudioEncoding[
176  options[CONF_ENCODING]
177  ] # type: ignore[misc]
178  gender: texttospeech.SsmlVoiceGender | None = texttospeech.SsmlVoiceGender[
179  options[CONF_GENDER]
180  ] # type: ignore[misc]
181  voice = options[CONF_VOICE]
182  if voice:
183  gender = None
184  if not voice.startswith(language):
185  language = voice[:5]
186 
187  request = texttospeech.SynthesizeSpeechRequest(
188  input=texttospeech.SynthesisInput(**{options[CONF_TEXT_TYPE]: message}),
189  voice=texttospeech.VoiceSelectionParams(
190  language_code=language,
191  ssml_gender=gender,
192  name=voice,
193  ),
194  audio_config=texttospeech.AudioConfig(
195  audio_encoding=encoding,
196  speaking_rate=options[CONF_SPEED],
197  pitch=options[CONF_PITCH],
198  volume_gain_db=options[CONF_GAIN],
199  effects_profile_id=options[CONF_PROFILES],
200  ),
201  )
202 
203  response = await self._client_client.synthesize_speech(request, timeout=10)
204 
205  if encoding == texttospeech.AudioEncoding.MP3:
206  extension = "mp3"
207  elif encoding == texttospeech.AudioEncoding.OGG_OPUS:
208  extension = "ogg"
209  else:
210  extension = "wav"
211 
212  return extension, response.audio_content
213 
214 
216  """The Google Cloud TTS entity."""
217 
218  def __init__(
219  self,
220  entry: ConfigEntry,
221  client: texttospeech.TextToSpeechAsyncClient,
222  voices: dict[str, list[str]],
223  language: str,
224  options_schema: vol.Schema,
225  ) -> None:
226  """Init Google Cloud TTS entity."""
227  super().__init__(client, voices, language, options_schema)
228  self._attr_unique_id_attr_unique_id = f"{entry.entry_id}"
229  self._attr_name_attr_name = entry.title
230  self._attr_device_info_attr_device_info = dr.DeviceInfo(
231  identifiers={(DOMAIN, entry.entry_id)},
232  manufacturer="Google",
233  model="Cloud",
234  entry_type=dr.DeviceEntryType.SERVICE,
235  )
236  self._entry_entry = entry
237 
239  self, message: str, language: str, options: dict[str, Any]
240  ) -> TtsAudioType:
241  """Load TTS from Google Cloud."""
242  try:
243  return await self._async_get_tts_audio_async_get_tts_audio(message, language, options)
244  except GoogleAPIError as err:
245  _LOGGER.error("Error occurred during Google Cloud TTS call: %s", err)
246  if isinstance(err, Unauthenticated):
247  self._entry_entry.async_start_reauth(self.hasshass)
248  return None, None
249 
250 
252  """The Google Cloud TTS API provider."""
253 
254  def __init__(
255  self,
256  client: texttospeech.TextToSpeechAsyncClient,
257  voices: dict[str, list[str]],
258  language: str,
259  options_schema: vol.Schema,
260  ) -> None:
261  """Init Google Cloud TTS service."""
262  super().__init__(client, voices, language, options_schema)
263  self.namename = "Google Cloud TTS"
264 
266  self, message: str, language: str, options: dict[str, Any]
267  ) -> TtsAudioType:
268  """Load TTS from Google Cloud."""
269  try:
270  return await self._async_get_tts_audio_async_get_tts_audio(message, language, options)
271  except GoogleAPIError as err:
272  _LOGGER.error("Error occurred during Google Cloud TTS call: %s", err)
273  return None, None
list[Voice]|None async_get_supported_voices(self, str language)
Definition: tts.py:156
TtsAudioType _async_get_tts_audio(self, str message, str language, dict[str, Any] options)
Definition: tts.py:167
None __init__(self, texttospeech.TextToSpeechAsyncClient client, dict[str, list[str]] voices, str language, vol.Schema options_schema)
Definition: tts.py:128
TtsAudioType async_get_tts_audio(self, str message, str language, dict[str, Any] options)
Definition: tts.py:240
None __init__(self, ConfigEntry entry, texttospeech.TextToSpeechAsyncClient client, dict[str, list[str]] voices, str language, vol.Schema options_schema)
Definition: tts.py:225
TtsAudioType async_get_tts_audio(self, str message, str language, dict[str, Any] options)
Definition: tts.py:267
None __init__(self, texttospeech.TextToSpeechAsyncClient client, dict[str, list[str]] voices, str language, vol.Schema options_schema)
Definition: tts.py:260
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
dict[str, list[str]] async_tts_voices(texttospeech.TextToSpeechAsyncClient client)
Definition: helpers.py:42
vol.Schema tts_options_schema(Mapping[str, Any] config_options, dict[str, list[str]] voices, bool from_config_flow=False)
Definition: helpers.py:58
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: tts.py:89
Provider|None async_get_engine(HomeAssistant hass, ConfigType config, DiscoveryInfoType|None discovery_info=None)
Definition: tts.py:52