Home Assistant Unofficial Reference 2024.12.1
tts.py
Go to the documentation of this file.
1 """Support for the Amazon Polly text to speech service."""
2 
3 from __future__ import annotations
4 
5 from collections import defaultdict
6 import logging
7 from typing import Any, Final
8 
9 import boto3
10 import botocore
11 import voluptuous as vol
12 
13 from homeassistant.components.tts import (
14  PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA,
15  Provider,
16  TtsAudioType,
17 )
18 from homeassistant.const import ATTR_CREDENTIALS, CONF_PROFILE_NAME
19 from homeassistant.core import HomeAssistant
21  SUPPORTED_ENGINES,
22  SUPPORTED_REGIONS,
23  SUPPORTED_VOICES,
24 )
26 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
27 
28 from .const import (
29  AWS_CONF_CONNECT_TIMEOUT,
30  AWS_CONF_MAX_POOL_CONNECTIONS,
31  AWS_CONF_READ_TIMEOUT,
32  CONF_ACCESS_KEY_ID,
33  CONF_ENGINE,
34  CONF_OUTPUT_FORMAT,
35  CONF_REGION,
36  CONF_SAMPLE_RATE,
37  CONF_SECRET_ACCESS_KEY,
38  CONF_TEXT_TYPE,
39  CONF_VOICE,
40  CONTENT_TYPE_EXTENSIONS,
41  DEFAULT_ENGINE,
42  DEFAULT_OUTPUT_FORMAT,
43  DEFAULT_REGION,
44  DEFAULT_SAMPLE_RATES,
45  DEFAULT_TEXT_TYPE,
46  DEFAULT_VOICE,
47  SUPPORTED_OUTPUT_FORMATS,
48  SUPPORTED_SAMPLE_RATES,
49  SUPPORTED_SAMPLE_RATES_MAP,
50  SUPPORTED_TEXT_TYPES,
51 )
52 
53 _LOGGER: Final = logging.getLogger(__name__)
54 
55 PLATFORM_SCHEMA: Final = TTS_PLATFORM_SCHEMA.extend(
56  {
57  vol.Optional(CONF_REGION, default=DEFAULT_REGION): vol.In(SUPPORTED_REGIONS),
58  vol.Inclusive(CONF_ACCESS_KEY_ID, ATTR_CREDENTIALS): cv.string,
59  vol.Inclusive(CONF_SECRET_ACCESS_KEY, ATTR_CREDENTIALS): cv.string,
60  vol.Exclusive(CONF_PROFILE_NAME, ATTR_CREDENTIALS): cv.string,
61  vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): vol.In(SUPPORTED_VOICES),
62  vol.Optional(CONF_ENGINE, default=DEFAULT_ENGINE): vol.In(SUPPORTED_ENGINES),
63  vol.Optional(CONF_OUTPUT_FORMAT, default=DEFAULT_OUTPUT_FORMAT): vol.In(
64  SUPPORTED_OUTPUT_FORMATS
65  ),
66  vol.Optional(CONF_SAMPLE_RATE): vol.All(
67  cv.string, vol.In(SUPPORTED_SAMPLE_RATES)
68  ),
69  vol.Optional(CONF_TEXT_TYPE, default=DEFAULT_TEXT_TYPE): vol.In(
70  SUPPORTED_TEXT_TYPES
71  ),
72  }
73 )
74 
75 
77  hass: HomeAssistant,
78  config: ConfigType,
79  discovery_info: DiscoveryInfoType | None = None,
80 ) -> Provider | None:
81  """Set up Amazon Polly speech component."""
82  output_format = config[CONF_OUTPUT_FORMAT]
83  sample_rate = config.get(CONF_SAMPLE_RATE, DEFAULT_SAMPLE_RATES[output_format])
84  if sample_rate not in SUPPORTED_SAMPLE_RATES_MAP[output_format]:
85  _LOGGER.error(
86  "%s is not a valid sample rate for %s", sample_rate, output_format
87  )
88  return None
89 
90  config[CONF_SAMPLE_RATE] = sample_rate
91 
92  profile: str | None = config.get(CONF_PROFILE_NAME)
93 
94  if profile is not None:
95  boto3.setup_default_session(profile_name=profile)
96 
97  aws_config = {
98  CONF_REGION: config[CONF_REGION],
99  CONF_ACCESS_KEY_ID: config.get(CONF_ACCESS_KEY_ID),
100  CONF_SECRET_ACCESS_KEY: config.get(CONF_SECRET_ACCESS_KEY),
101  "config": botocore.config.Config(
102  connect_timeout=AWS_CONF_CONNECT_TIMEOUT,
103  read_timeout=AWS_CONF_READ_TIMEOUT,
104  max_pool_connections=AWS_CONF_MAX_POOL_CONNECTIONS,
105  ),
106  }
107 
108  del config[CONF_REGION]
109  del config[CONF_ACCESS_KEY_ID]
110  del config[CONF_SECRET_ACCESS_KEY]
111 
112  polly_client = boto3.client("polly", **aws_config)
113 
114  supported_languages: list[str] = []
115 
116  all_voices: dict[str, dict[str, str]] = {}
117 
118  all_engines: dict[str, set[str]] = defaultdict(set)
119 
120  all_voices_req = polly_client.describe_voices()
121 
122  for voice in all_voices_req.get("Voices", []):
123  voice_id: str | None = voice.get("Id")
124  if voice_id is None:
125  continue
126  all_voices[voice_id] = voice
127  language_code: str | None = voice.get("LanguageCode")
128  if language_code is not None and language_code not in supported_languages:
129  supported_languages.append(language_code)
130  for engine in voice.get("SupportedEngines"):
131  all_engines[engine].add(voice_id)
132 
133  return AmazonPollyProvider(
134  polly_client, config, supported_languages, all_voices, all_engines
135  )
136 
137 
138 class AmazonPollyProvider(Provider):
139  """Amazon Polly speech api provider."""
140 
141  def __init__(
142  self,
143  polly_client: boto3.client,
144  config: ConfigType,
145  supported_languages: list[str],
146  all_voices: dict[str, dict[str, str]],
147  all_engines: dict[str, set[str]],
148  ) -> None:
149  """Initialize Amazon Polly provider for TTS."""
150  self.clientclient = polly_client
151  self.configconfig = config
152  self.supported_langssupported_langs = supported_languages
153  self.all_voicesall_voices = all_voices
154  self.all_enginesall_engines = all_engines
155  self.default_voice: str = self.configconfig[CONF_VOICE]
156  self.default_engine: str = self.configconfig[CONF_ENGINE]
157  self.namename = "Amazon Polly"
158 
159  @property
160  def supported_languages(self) -> list[str]:
161  """Return a list of supported languages."""
162  return self.supported_langssupported_langs
163 
164  @property
165  def default_language(self) -> str | None:
166  """Return the default language."""
167  return self.all_voicesall_voices.get(self.default_voice, {}).get("LanguageCode")
168 
169  @property
170  def default_options(self) -> dict[str, str]:
171  """Return dict include default options."""
172  return {CONF_VOICE: self.default_voice, CONF_ENGINE: self.default_engine}
173 
174  @property
175  def supported_options(self) -> list[str]:
176  """Return a list of supported options."""
177  return [CONF_VOICE, CONF_ENGINE]
178 
180  self,
181  message: str,
182  language: str,
183  options: dict[str, Any],
184  ) -> TtsAudioType:
185  """Request TTS file from Polly."""
186  voice_id = options.get(CONF_VOICE, self.default_voice)
187  voice_in_dict = self.all_voicesall_voices[voice_id]
188  if language != voice_in_dict.get("LanguageCode"):
189  _LOGGER.error("%s does not support the %s language", voice_id, language)
190  return None, None
191 
192  engine = options.get(CONF_ENGINE, self.default_engine)
193  if voice_id not in self.all_enginesall_engines[engine]:
194  _LOGGER.error("%s does not support the %s engine", voice_id, engine)
195  return None, None
196 
197  _LOGGER.debug("Requesting TTS file for text: %s", message)
198  resp = self.clientclient.synthesize_speech(
199  Engine=engine,
200  OutputFormat=self.configconfig[CONF_OUTPUT_FORMAT],
201  SampleRate=self.configconfig[CONF_SAMPLE_RATE],
202  Text=message,
203  TextType=self.configconfig[CONF_TEXT_TYPE],
204  VoiceId=voice_id,
205  )
206 
207  _LOGGER.debug("Reply received for TTS: %s", message)
208  return (
209  CONTENT_TYPE_EXTENSIONS[resp.get("ContentType")],
210  resp.get("AudioStream").read(),
211  )
TtsAudioType get_tts_audio(self, str message, str language, dict[str, Any] options)
Definition: tts.py:184
None __init__(self, boto3.client polly_client, ConfigType config, list[str] supported_languages, dict[str, dict[str, str]] all_voices, dict[str, set[str]] all_engines)
Definition: tts.py:148
Provider|None get_engine(HomeAssistant hass, ConfigType config, DiscoveryInfoType|None discovery_info=None)
Definition: tts.py:80
bool add(self, _T matcher)
Definition: match.py:185
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88