Home Assistant Unofficial Reference 2024.12.1
tts.py
Go to the documentation of this file.
1 """Support for Wyoming text-to-speech services."""
2 
3 from collections import defaultdict
4 import io
5 import logging
6 import wave
7 
8 from wyoming.audio import AudioChunk, AudioStop
9 from wyoming.client import AsyncTcpClient
10 from wyoming.tts import Synthesize, SynthesizeVoice
11 
12 from homeassistant.components import tts
13 from homeassistant.config_entries import ConfigEntry
14 from homeassistant.core import HomeAssistant, callback
15 from homeassistant.helpers.entity_platform import AddEntitiesCallback
16 
17 from .const import ATTR_SPEAKER, DOMAIN
18 from .data import WyomingService
19 from .error import WyomingError
20 from .models import DomainDataItem
21 
22 _LOGGER = logging.getLogger(__name__)
23 
24 
26  hass: HomeAssistant,
27  config_entry: ConfigEntry,
28  async_add_entities: AddEntitiesCallback,
29 ) -> None:
30  """Set up Wyoming speech-to-text."""
31  item: DomainDataItem = hass.data[DOMAIN][config_entry.entry_id]
33  [
34  WyomingTtsProvider(config_entry, item.service),
35  ]
36  )
37 
38 
40  """Wyoming text-to-speech provider."""
41 
42  def __init__(
43  self,
44  config_entry: ConfigEntry,
45  service: WyomingService,
46  ) -> None:
47  """Set up provider."""
48  self.serviceservice = service
49  self._tts_service_tts_service = next(tts for tts in service.info.tts if tts.installed)
50 
51  voice_languages: set[str] = set()
52  self._voices: dict[str, list[tts.Voice]] = defaultdict(list)
53  for voice in self._tts_service_tts_service.voices:
54  if not voice.installed:
55  continue
56 
57  voice_languages.update(voice.languages)
58  for language in voice.languages:
59  self._voices[language].append(
60  tts.Voice(
61  voice_id=voice.name,
62  name=voice.description or voice.name,
63  )
64  )
65 
66  # Sort voices by name
67  for language in self._voices:
68  self._voices[language] = sorted(
69  self._voices[language], key=lambda v: v.name
70  )
71 
72  self._supported_languages: list[str] = list(voice_languages)
73 
74  self._attr_name_attr_name = self._tts_service_tts_service.name
75  self._attr_unique_id_attr_unique_id = f"{config_entry.entry_id}-tts"
76 
77  @property
78  def default_language(self):
79  """Return default language."""
80  if not self._supported_languages:
81  return None
82 
83  return self._supported_languages[0]
84 
85  @property
87  """Return list of supported languages."""
88  return self._supported_languages
89 
90  @property
91  def supported_options(self):
92  """Return list of supported options like voice, emotion."""
93  return [
94  tts.ATTR_AUDIO_OUTPUT,
95  tts.ATTR_VOICE,
96  ATTR_SPEAKER,
97  ]
98 
99  @property
100  def default_options(self):
101  """Return a dict include default options."""
102  return {}
103 
104  @callback
105  def async_get_supported_voices(self, language: str) -> list[tts.Voice] | None:
106  """Return a list of supported voices for a language."""
107  return self._voices.get(language)
108 
109  async def async_get_tts_audio(self, message, language, options):
110  """Load TTS from TCP socket."""
111  voice_name: str | None = options.get(tts.ATTR_VOICE)
112  voice_speaker: str | None = options.get(ATTR_SPEAKER)
113 
114  try:
115  async with AsyncTcpClient(self.serviceservice.host, self.serviceservice.port) as client:
116  voice: SynthesizeVoice | None = None
117  if voice_name is not None:
118  voice = SynthesizeVoice(name=voice_name, speaker=voice_speaker)
119 
120  synthesize = Synthesize(text=message, voice=voice)
121  await client.write_event(synthesize.event())
122 
123  with io.BytesIO() as wav_io:
124  wav_writer: wave.Wave_write | None = None
125  while True:
126  event = await client.read_event()
127  if event is None:
128  _LOGGER.debug("Connection lost")
129  return (None, None)
130 
131  if AudioStop.is_type(event.type):
132  break
133 
134  if AudioChunk.is_type(event.type):
135  chunk = AudioChunk.from_event(event)
136  if wav_writer is None:
137  wav_writer = wave.open(wav_io, "wb")
138  wav_writer.setframerate(chunk.rate)
139  wav_writer.setsampwidth(chunk.width)
140  wav_writer.setnchannels(chunk.channels)
141 
142  wav_writer.writeframes(chunk.audio)
143 
144  if wav_writer is not None:
145  wav_writer.close()
146 
147  data = wav_io.getvalue()
148 
149  except (OSError, WyomingError):
150  return (None, None)
151 
152  return ("wav", data)
list[tts.Voice]|None async_get_supported_voices(self, str language)
Definition: tts.py:105
None __init__(self, ConfigEntry config_entry, WyomingService service)
Definition: tts.py:46
def async_get_tts_audio(self, message, language, options)
Definition: tts.py:109
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: tts.py:29