Home Assistant Unofficial Reference 2024.12.1
tts.py
Go to the documentation of this file.
1 """Support for the cloud for text-to-speech service."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any
7 
8 from hass_nabucasa import Cloud
9 from hass_nabucasa.voice import MAP_VOICE, TTS_VOICES, AudioOutput, Gender, VoiceError
10 import voluptuous as vol
11 
12 from homeassistant.components.tts import (
13  ATTR_AUDIO_OUTPUT,
14  ATTR_VOICE,
15  CONF_LANG,
16  PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA,
17  Provider,
18  TextToSpeechEntity,
19  TtsAudioType,
20  Voice,
21 )
22 from homeassistant.config_entries import ConfigEntry
23 from homeassistant.const import CONF_PLATFORM, Platform
24 from homeassistant.core import HomeAssistant, async_get_hass, callback
26 from homeassistant.helpers.entity_platform import AddEntitiesCallback
27 from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
28 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
29 from homeassistant.setup import async_when_setup
30 
31 from .assist_pipeline import async_migrate_cloud_pipeline_engine
32 from .client import CloudClient
33 from .const import DATA_CLOUD, DATA_PLATFORMS_SETUP, DOMAIN, TTS_ENTITY_UNIQUE_ID
34 from .prefs import CloudPreferences
35 
36 ATTR_GENDER = "gender"
37 
38 DEPRECATED_VOICES = {"XiaoxuanNeural": "XiaozhenNeural"}
39 SUPPORT_LANGUAGES = list(TTS_VOICES)
40 
41 _LOGGER = logging.getLogger(__name__)
42 
43 
44 def _deprecated_platform(value: str) -> str:
45  """Validate if platform is deprecated."""
46  if value == DOMAIN:
47  _LOGGER.warning(
48  "The cloud tts platform configuration is deprecated, "
49  "please remove it from your configuration "
50  "and use the UI to change settings instead"
51  )
52  hass = async_get_hass()
54  hass,
55  DOMAIN,
56  "deprecated_tts_platform_config",
57  breaks_in_ha_version="2024.9.0",
58  is_fixable=False,
59  severity=IssueSeverity.WARNING,
60  translation_key="deprecated_tts_platform_config",
61  )
62  return value
63 
64 
65 def validate_lang(value: dict[str, Any]) -> dict[str, Any]:
66  """Validate chosen gender or language."""
67  if (lang := value.get(CONF_LANG)) is None:
68  return value
69 
70  if (gender := value.get(ATTR_GENDER)) is None:
71  gender = value[ATTR_GENDER] = next(
72  (chk_gender for chk_lang, chk_gender in MAP_VOICE if chk_lang == lang), None
73  )
74 
75  if (lang, gender) not in MAP_VOICE:
76  raise vol.Invalid("Unsupported language and gender specified.")
77 
78  return value
79 
80 
81 PLATFORM_SCHEMA = vol.All(
82  TTS_PLATFORM_SCHEMA.extend(
83  {
84  vol.Required(CONF_PLATFORM): vol.All(cv.string, _deprecated_platform),
85  vol.Optional(CONF_LANG): str,
86  vol.Optional(ATTR_GENDER): str,
87  }
88  ),
89  validate_lang,
90 )
91 
92 
93 async def async_get_engine(
94  hass: HomeAssistant,
95  config: ConfigType,
96  discovery_info: DiscoveryInfoType | None = None,
97 ) -> CloudProvider:
98  """Set up Cloud speech component."""
99  cloud = hass.data[DATA_CLOUD]
100  cloud_provider = CloudProvider(cloud)
101  if discovery_info is not None:
102  discovery_info["platform_loaded"].set()
103  return cloud_provider
104 
105 
107  hass: HomeAssistant,
108  config_entry: ConfigEntry,
109  async_add_entities: AddEntitiesCallback,
110 ) -> None:
111  """Set up Home Assistant Cloud text-to-speech platform."""
112  tts_platform_loaded = hass.data[DATA_PLATFORMS_SETUP][Platform.TTS]
113  tts_platform_loaded.set()
114  cloud = hass.data[DATA_CLOUD]
116 
117 
119  """Home Assistant Cloud text-to-speech entity."""
120 
121  _attr_name = "Home Assistant Cloud"
122  _attr_unique_id = TTS_ENTITY_UNIQUE_ID
123 
124  def __init__(self, cloud: Cloud[CloudClient]) -> None:
125  """Initialize cloud text-to-speech entity."""
126  self.cloudcloud = cloud
127  self._language, self._voice_voice = cloud.client.prefs.tts_default_voice
128 
129  async def _sync_prefs(self, prefs: CloudPreferences) -> None:
130  """Sync preferences."""
131  self._language, self._voice_voice = prefs.tts_default_voice
132 
133  @property
134  def default_language(self) -> str:
135  """Return the default language."""
136  return self._language
137 
138  @property
139  def default_options(self) -> dict[str, Any]:
140  """Return a dict include default options."""
141  return {
142  ATTR_AUDIO_OUTPUT: AudioOutput.MP3,
143  }
144 
145  @property
146  def supported_languages(self) -> list[str]:
147  """Return list of supported languages."""
148  return SUPPORT_LANGUAGES
149 
150  @property
151  def supported_options(self) -> list[str]:
152  """Return list of supported options like voice, emotion."""
153  # The gender option is deprecated and will be removed in 2024.10.0.
154  return [ATTR_GENDER, ATTR_VOICE, ATTR_AUDIO_OUTPUT]
155 
156  async def async_added_to_hass(self) -> None:
157  """Handle entity which will be added."""
158  await super().async_added_to_hass()
159 
160  async def pipeline_setup(hass: HomeAssistant, _comp: str) -> None:
161  """When assist_pipeline is set up."""
162  assert self.platformplatform.config_entry
163  self.platformplatform.config_entry.async_create_task(
164  hass,
166  self.hasshass, platform=Platform.TTS, engine_id=self.entity_identity_id
167  ),
168  )
169 
170  async_when_setup(self.hasshass, "assist_pipeline", pipeline_setup)
171 
172  self.async_on_removeasync_on_remove(
173  self.cloudcloud.client.prefs.async_listen_updates(self._sync_prefs_sync_prefs)
174  )
175 
176  @callback
177  def async_get_supported_voices(self, language: str) -> list[Voice] | None:
178  """Return a list of supported voices for a language."""
179  if not (voices := TTS_VOICES.get(language)):
180  return None
181  return [Voice(voice, voice) for voice in voices]
182 
184  self, message: str, language: str, options: dict[str, Any]
185  ) -> TtsAudioType:
186  """Load TTS from Home Assistant Cloud."""
187  gender: Gender | str | None = options.get(ATTR_GENDER)
188  gender = handle_deprecated_gender(self.hasshass, gender)
189  original_voice: str | None = options.get(ATTR_VOICE)
190  if original_voice is None and language == self._language:
191  original_voice = self._voice_voice
192  voice = handle_deprecated_voice(self.hasshass, original_voice)
193  if voice not in TTS_VOICES[language]:
194  default_voice = TTS_VOICES[language][0]
195  _LOGGER.debug(
196  "Unsupported voice %s detected, falling back to default %s for %s",
197  voice,
198  default_voice,
199  language,
200  )
201  voice = default_voice
202  # Process TTS
203  try:
204  data = await self.cloudcloud.voice.process_tts(
205  text=message,
206  language=language,
207  gender=gender,
208  voice=voice,
209  output=options[ATTR_AUDIO_OUTPUT],
210  )
211  except VoiceError as err:
212  _LOGGER.error("Voice error: %s", err)
213  return (None, None)
214 
215  return (str(options[ATTR_AUDIO_OUTPUT].value), data)
216 
217 
218 class CloudProvider(Provider):
219  """Home Assistant Cloud speech API provider."""
220 
221  def __init__(self, cloud: Cloud[CloudClient]) -> None:
222  """Initialize cloud provider."""
223  self.cloudcloud = cloud
224  self.namename = "Home Assistant Cloud"
225  self._language, self._voice_voice = cloud.client.prefs.tts_default_voice
226  cloud.client.prefs.async_listen_updates(self._sync_prefs_sync_prefs)
227 
228  async def _sync_prefs(self, prefs: CloudPreferences) -> None:
229  """Sync preferences."""
230  self._language, self._voice_voice = prefs.tts_default_voice
231 
232  @property
233  def default_language(self) -> str | None:
234  """Return the default language."""
235  return self._language
236 
237  @property
238  def supported_languages(self) -> list[str]:
239  """Return list of supported languages."""
240  return SUPPORT_LANGUAGES
241 
242  @property
243  def supported_options(self) -> list[str]:
244  """Return list of supported options like voice, emotion."""
245  # The gender option is deprecated and will be removed in 2024.10.0.
246  return [ATTR_GENDER, ATTR_VOICE, ATTR_AUDIO_OUTPUT]
247 
248  @callback
249  def async_get_supported_voices(self, language: str) -> list[Voice] | None:
250  """Return a list of supported voices for a language."""
251  if not (voices := TTS_VOICES.get(language)):
252  return None
253  return [Voice(voice, voice) for voice in voices]
254 
255  @property
256  def default_options(self) -> dict[str, Any]:
257  """Return a dict include default options."""
258  return {
259  ATTR_AUDIO_OUTPUT: AudioOutput.MP3,
260  }
261 
263  self, message: str, language: str, options: dict[str, Any]
264  ) -> TtsAudioType:
265  """Load TTS from Home Assistant Cloud."""
266  assert self.hass is not None
267  gender: Gender | str | None = options.get(ATTR_GENDER)
268  gender = handle_deprecated_gender(self.hass, gender)
269  original_voice: str | None = options.get(ATTR_VOICE)
270  if original_voice is None and language == self._language:
271  original_voice = self._voice_voice
272  voice = handle_deprecated_voice(self.hass, original_voice)
273  if voice not in TTS_VOICES[language]:
274  default_voice = TTS_VOICES[language][0]
275  _LOGGER.debug(
276  "Unsupported voice %s detected, falling back to default %s for %s",
277  voice,
278  default_voice,
279  language,
280  )
281  voice = default_voice
282  # Process TTS
283  try:
284  data = await self.cloudcloud.voice.process_tts(
285  text=message,
286  language=language,
287  gender=gender,
288  voice=voice,
289  output=options[ATTR_AUDIO_OUTPUT],
290  )
291  except VoiceError as err:
292  _LOGGER.error("Voice error: %s", err)
293  return (None, None)
294 
295  return (str(options[ATTR_AUDIO_OUTPUT].value), data)
296 
297 
298 @callback
300  hass: HomeAssistant,
301  gender: Gender | str | None,
302 ) -> Gender | None:
303  """Handle deprecated gender."""
304  if gender is None:
305  return None
307  hass,
308  DOMAIN,
309  "deprecated_gender",
310  is_fixable=True,
311  is_persistent=True,
312  severity=IssueSeverity.WARNING,
313  breaks_in_ha_version="2024.10.0",
314  translation_key="deprecated_gender",
315  translation_placeholders={
316  "integration_name": "Home Assistant Cloud",
317  "deprecated_option": "gender",
318  "replacement_option": "voice",
319  },
320  )
321  return Gender(gender)
322 
323 
324 @callback
326  hass: HomeAssistant,
327  original_voice: str | None,
328 ) -> str | None:
329  """Handle deprecated voice."""
330  voice = original_voice
331  if (
332  original_voice
333  and voice
334  and (voice := DEPRECATED_VOICES.get(original_voice, original_voice))
335  != original_voice
336  ):
338  hass,
339  DOMAIN,
340  f"deprecated_voice_{original_voice}",
341  is_fixable=True,
342  is_persistent=True,
343  severity=IssueSeverity.WARNING,
344  breaks_in_ha_version="2024.8.0",
345  translation_key="deprecated_voice",
346  translation_placeholders={
347  "deprecated_voice": original_voice,
348  "replacement_voice": voice,
349  },
350  )
351  return voice
None _sync_prefs(self, CloudPreferences prefs)
Definition: tts.py:228
None __init__(self, Cloud[CloudClient] cloud)
Definition: tts.py:221
list[Voice]|None async_get_supported_voices(self, str language)
Definition: tts.py:249
TtsAudioType async_get_tts_audio(self, str message, str language, dict[str, Any] options)
Definition: tts.py:264
None _sync_prefs(self, CloudPreferences prefs)
Definition: tts.py:129
TtsAudioType async_get_tts_audio(self, str message, str language, dict[str, Any] options)
Definition: tts.py:185
list[Voice]|None async_get_supported_voices(self, str language)
Definition: tts.py:177
None __init__(self, Cloud[CloudClient] cloud)
Definition: tts.py:124
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
None async_migrate_cloud_pipeline_engine(HomeAssistant hass, Platform platform, str engine_id)
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: tts.py:110
CloudProvider async_get_engine(HomeAssistant hass, ConfigType config, DiscoveryInfoType|None discovery_info=None)
Definition: tts.py:97
str|None handle_deprecated_voice(HomeAssistant hass, str|None original_voice)
Definition: tts.py:328
str _deprecated_platform(str value)
Definition: tts.py:44
dict[str, Any] validate_lang(dict[str, Any] value)
Definition: tts.py:65
Gender|None handle_deprecated_gender(HomeAssistant hass, Gender|str|None gender)
Definition: tts.py:302
None async_create_issue(HomeAssistant hass, str entry_id)
Definition: repairs.py:69
HomeAssistant async_get_hass()
Definition: core.py:286
None async_when_setup(core.HomeAssistant hass, str component, Callable[[core.HomeAssistant, str], Awaitable[None]] when_setup_cb)
Definition: setup.py:587