160 lines
5.2 KiB
Python
160 lines
5.2 KiB
Python
|
"""Entity for Text-to-Speech."""
|
||
|
|
||
|
from collections.abc import Mapping
|
||
|
from functools import partial
|
||
|
from typing import Any, final
|
||
|
|
||
|
from propcache.api import cached_property
|
||
|
|
||
|
from homeassistant.components.media_player import (
|
||
|
ATTR_MEDIA_ANNOUNCE,
|
||
|
ATTR_MEDIA_CONTENT_ID,
|
||
|
ATTR_MEDIA_CONTENT_TYPE,
|
||
|
DOMAIN as DOMAIN_MP,
|
||
|
SERVICE_PLAY_MEDIA,
|
||
|
MediaType,
|
||
|
)
|
||
|
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN
|
||
|
from homeassistant.core import callback
|
||
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||
|
from homeassistant.util import dt as dt_util
|
||
|
|
||
|
from .const import TtsAudioType
|
||
|
from .media_source import generate_media_source_id
|
||
|
from .models import Voice
|
||
|
|
||
|
CACHED_PROPERTIES_WITH_ATTR_ = {
|
||
|
"default_language",
|
||
|
"default_options",
|
||
|
"supported_languages",
|
||
|
"supported_options",
|
||
|
}
|
||
|
|
||
|
|
||
|
class TextToSpeechEntity(RestoreEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||
|
"""Represent a single TTS engine."""
|
||
|
|
||
|
_attr_should_poll = False
|
||
|
__last_tts_loaded: str | None = None
|
||
|
|
||
|
_attr_default_language: str
|
||
|
_attr_default_options: Mapping[str, Any] | None = None
|
||
|
_attr_supported_languages: list[str]
|
||
|
_attr_supported_options: list[str] | None = None
|
||
|
|
||
|
@property
|
||
|
@final
|
||
|
def state(self) -> str | None:
|
||
|
"""Return the state of the entity."""
|
||
|
if self.__last_tts_loaded is None:
|
||
|
return None
|
||
|
return self.__last_tts_loaded
|
||
|
|
||
|
@cached_property
|
||
|
def supported_languages(self) -> list[str]:
|
||
|
"""Return a list of supported languages."""
|
||
|
return self._attr_supported_languages
|
||
|
|
||
|
@cached_property
|
||
|
def default_language(self) -> str:
|
||
|
"""Return the default language."""
|
||
|
return self._attr_default_language
|
||
|
|
||
|
@cached_property
|
||
|
def supported_options(self) -> list[str] | None:
|
||
|
"""Return a list of supported options like voice, emotions."""
|
||
|
return self._attr_supported_options
|
||
|
|
||
|
@cached_property
|
||
|
def default_options(self) -> Mapping[str, Any] | None:
|
||
|
"""Return a mapping with the default options."""
|
||
|
return self._attr_default_options
|
||
|
|
||
|
@callback
|
||
|
def async_get_supported_voices(self, language: str) -> list[Voice] | None:
|
||
|
"""Return a list of supported voices for a language."""
|
||
|
return None
|
||
|
|
||
|
async def async_internal_added_to_hass(self) -> None:
|
||
|
"""Call when the entity is added to hass."""
|
||
|
await super().async_internal_added_to_hass()
|
||
|
try:
|
||
|
_ = self.default_language
|
||
|
except AttributeError as err:
|
||
|
raise AttributeError(
|
||
|
"TTS entities must either set the '_attr_default_language' attribute or override the 'default_language' property"
|
||
|
) from err
|
||
|
try:
|
||
|
_ = self.supported_languages
|
||
|
except AttributeError as err:
|
||
|
raise AttributeError(
|
||
|
"TTS entities must either set the '_attr_supported_languages' attribute or override the 'supported_languages' property"
|
||
|
) from err
|
||
|
state = await self.async_get_last_state()
|
||
|
if (
|
||
|
state is not None
|
||
|
and state.state is not None
|
||
|
and state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN)
|
||
|
):
|
||
|
self.__last_tts_loaded = state.state
|
||
|
|
||
|
async def async_speak(
|
||
|
self,
|
||
|
media_player_entity_id: list[str],
|
||
|
message: str,
|
||
|
cache: bool,
|
||
|
language: str | None = None,
|
||
|
options: dict | None = None,
|
||
|
) -> None:
|
||
|
"""Speak via a Media Player."""
|
||
|
await self.hass.services.async_call(
|
||
|
DOMAIN_MP,
|
||
|
SERVICE_PLAY_MEDIA,
|
||
|
{
|
||
|
ATTR_ENTITY_ID: media_player_entity_id,
|
||
|
ATTR_MEDIA_CONTENT_ID: generate_media_source_id(
|
||
|
self.hass,
|
||
|
message=message,
|
||
|
engine=self.entity_id,
|
||
|
language=language,
|
||
|
options=options,
|
||
|
cache=cache,
|
||
|
),
|
||
|
ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
|
||
|
ATTR_MEDIA_ANNOUNCE: True,
|
||
|
},
|
||
|
blocking=True,
|
||
|
context=self._context,
|
||
|
)
|
||
|
|
||
|
@final
|
||
|
async def internal_async_get_tts_audio(
|
||
|
self, message: str, language: str, options: dict[str, Any]
|
||
|
) -> TtsAudioType:
|
||
|
"""Process an audio stream to TTS service.
|
||
|
|
||
|
Only streaming content is allowed!
|
||
|
"""
|
||
|
self.__last_tts_loaded = dt_util.utcnow().isoformat()
|
||
|
self.async_write_ha_state()
|
||
|
return await self.async_get_tts_audio(
|
||
|
message=message, language=language, options=options
|
||
|
)
|
||
|
|
||
|
def get_tts_audio(
|
||
|
self, message: str, language: str, options: dict[str, Any]
|
||
|
) -> TtsAudioType:
|
||
|
"""Load tts audio file from the engine."""
|
||
|
raise NotImplementedError
|
||
|
|
||
|
async def async_get_tts_audio(
|
||
|
self, message: str, language: str, options: dict[str, Any]
|
||
|
) -> TtsAudioType:
|
||
|
"""Load tts audio file from the engine.
|
||
|
|
||
|
Return a tuple of file extension and data as bytes.
|
||
|
"""
|
||
|
return await self.hass.async_add_executor_job(
|
||
|
partial(self.get_tts_audio, message, language, options=options)
|
||
|
)
|