mirror of https://github.com/home-assistant/core
181 lines
6.0 KiB
Python
181 lines
6.0 KiB
Python
"""Helper classes for Google Cloud integration."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
import functools
|
|
import operator
|
|
from typing import Any
|
|
|
|
from google.cloud import texttospeech
|
|
from google.oauth2.service_account import Credentials
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.components.tts import CONF_LANG
|
|
import homeassistant.helpers.config_validation as cv
|
|
from homeassistant.helpers.selector import (
|
|
NumberSelector,
|
|
NumberSelectorConfig,
|
|
SelectSelector,
|
|
SelectSelectorConfig,
|
|
SelectSelectorMode,
|
|
)
|
|
|
|
from .const import (
|
|
CONF_ENCODING,
|
|
CONF_GAIN,
|
|
CONF_GENDER,
|
|
CONF_KEY_FILE,
|
|
CONF_PITCH,
|
|
CONF_PROFILES,
|
|
CONF_SPEED,
|
|
CONF_TEXT_TYPE,
|
|
CONF_VOICE,
|
|
DEFAULT_LANG,
|
|
)
|
|
|
|
DEFAULT_VOICE = ""
|
|
|
|
|
|
async def async_tts_voices(
|
|
client: texttospeech.TextToSpeechAsyncClient,
|
|
) -> dict[str, list[str]]:
|
|
"""Get TTS voice model names keyed by language."""
|
|
voices: dict[str, list[str]] = {}
|
|
list_voices_response = await client.list_voices()
|
|
for voice in list_voices_response.voices:
|
|
language_code = voice.language_codes[0]
|
|
if language_code not in voices:
|
|
voices[language_code] = []
|
|
voices[language_code].append(voice.name)
|
|
return voices
|
|
|
|
|
|
def tts_options_schema(
|
|
config_options: Mapping[str, Any],
|
|
voices: dict[str, list[str]],
|
|
from_config_flow: bool = False,
|
|
) -> vol.Schema:
|
|
"""Return schema for TTS options with default values from config or constants."""
|
|
# If we are called from the config flow we want the defaults to be from constants
|
|
# to allow clearing the current value (passed as suggested_value) in the UI.
|
|
# If we aren't called from the config flow we want the defaults to be from the config.
|
|
defaults = {} if from_config_flow else config_options
|
|
return vol.Schema(
|
|
{
|
|
vol.Optional(
|
|
CONF_GENDER,
|
|
default=defaults.get(
|
|
CONF_GENDER,
|
|
texttospeech.SsmlVoiceGender.NEUTRAL.name, # type: ignore[attr-defined]
|
|
),
|
|
): vol.All(
|
|
vol.Upper,
|
|
SelectSelector(
|
|
SelectSelectorConfig(
|
|
mode=SelectSelectorMode.DROPDOWN,
|
|
options=list(texttospeech.SsmlVoiceGender.__members__),
|
|
)
|
|
),
|
|
),
|
|
vol.Optional(
|
|
CONF_VOICE,
|
|
default=defaults.get(CONF_VOICE, DEFAULT_VOICE),
|
|
): SelectSelector(
|
|
SelectSelectorConfig(
|
|
mode=SelectSelectorMode.DROPDOWN,
|
|
options=["", *functools.reduce(operator.iadd, voices.values(), [])],
|
|
)
|
|
),
|
|
vol.Optional(
|
|
CONF_ENCODING,
|
|
default=defaults.get(
|
|
CONF_ENCODING,
|
|
texttospeech.AudioEncoding.MP3.name, # type: ignore[attr-defined]
|
|
),
|
|
): vol.All(
|
|
vol.Upper,
|
|
SelectSelector(
|
|
SelectSelectorConfig(
|
|
mode=SelectSelectorMode.DROPDOWN,
|
|
options=list(texttospeech.AudioEncoding.__members__),
|
|
)
|
|
),
|
|
),
|
|
vol.Optional(
|
|
CONF_SPEED,
|
|
default=defaults.get(CONF_SPEED, 1.0),
|
|
): NumberSelector(NumberSelectorConfig(min=0.25, max=4.0, step=0.01)),
|
|
vol.Optional(
|
|
CONF_PITCH,
|
|
default=defaults.get(CONF_PITCH, 0),
|
|
): NumberSelector(NumberSelectorConfig(min=-20.0, max=20.0, step=0.1)),
|
|
vol.Optional(
|
|
CONF_GAIN,
|
|
default=defaults.get(CONF_GAIN, 0),
|
|
): NumberSelector(NumberSelectorConfig(min=-96.0, max=16.0, step=0.1)),
|
|
vol.Optional(
|
|
CONF_PROFILES,
|
|
default=defaults.get(CONF_PROFILES, []),
|
|
): SelectSelector(
|
|
SelectSelectorConfig(
|
|
mode=SelectSelectorMode.DROPDOWN,
|
|
options=[
|
|
# https://cloud.google.com/text-to-speech/docs/audio-profiles
|
|
"wearable-class-device",
|
|
"handset-class-device",
|
|
"headphone-class-device",
|
|
"small-bluetooth-speaker-class-device",
|
|
"medium-bluetooth-speaker-class-device",
|
|
"large-home-entertainment-class-device",
|
|
"large-automotive-class-device",
|
|
"telephony-class-application",
|
|
],
|
|
multiple=True,
|
|
sort=False,
|
|
)
|
|
),
|
|
vol.Optional(
|
|
CONF_TEXT_TYPE,
|
|
default=defaults.get(CONF_TEXT_TYPE, "text"),
|
|
): vol.All(
|
|
vol.Lower,
|
|
SelectSelector(
|
|
SelectSelectorConfig(
|
|
mode=SelectSelectorMode.DROPDOWN,
|
|
options=["text", "ssml"],
|
|
)
|
|
),
|
|
),
|
|
}
|
|
)
|
|
|
|
|
|
def tts_platform_schema() -> vol.Schema:
|
|
"""Return schema for TTS platform."""
|
|
return vol.Schema(
|
|
{
|
|
vol.Optional(CONF_KEY_FILE): cv.string,
|
|
vol.Optional(CONF_LANG, default=DEFAULT_LANG): cv.matches_regex(
|
|
r"[a-z]{2,3}-[A-Z]{2}|"
|
|
),
|
|
**tts_options_schema({}, {}).schema,
|
|
vol.Optional(CONF_VOICE, default=DEFAULT_VOICE): cv.matches_regex(
|
|
r"[a-z]{2,3}-[A-Z]{2}-.*-[A-Z]|"
|
|
),
|
|
}
|
|
)
|
|
|
|
|
|
def validate_service_account_info(info: Mapping[str, str]) -> None:
|
|
"""Validate service account info.
|
|
|
|
Args:
|
|
info: The service account info in Google format.
|
|
|
|
Raises:
|
|
ValueError: If the info is not in the expected format.
|
|
|
|
"""
|
|
Credentials.from_service_account_info(info) # type:ignore[no-untyped-call]
|