Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Google Cloud Speech-to-Text (STT) #120854

Merged
merged 23 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,7 @@ omit =
homeassistant/components/goodwe/number.py
homeassistant/components/goodwe/select.py
homeassistant/components/goodwe/sensor.py
homeassistant/components/google_cloud/stt.py
homeassistant/components/google_cloud/tts.py
homeassistant/components/google_maps/device_tracker.py
homeassistant/components/google_pubsub/__init__.py
Expand Down
2 changes: 1 addition & 1 deletion CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ build.json @home-assistant/supervisor
/tests/components/google_assistant/ @home-assistant/cloud
/homeassistant/components/google_assistant_sdk/ @tronikos
/tests/components/google_assistant_sdk/ @tronikos
/homeassistant/components/google_cloud/ @lufton
/homeassistant/components/google_cloud/ @lufton @tronikos
/homeassistant/components/google_generative_ai_conversation/ @tronikos
/tests/components/google_generative_ai_conversation/ @tronikos
/homeassistant/components/google_mail/ @tkdrob
Expand Down
34 changes: 34 additions & 0 deletions homeassistant/components/google_cloud/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,35 @@
"""The google_cloud component."""

from __future__ import annotations

from pathlib import Path

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed

from .const import CONF_KEY_FILE

PLATFORMS = [Platform.STT, Platform.TTS]


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
if not Path(
hass.config.path(hass.config.path(entry.data[CONF_KEY_FILE]))
).is_file():
raise ConfigEntryAuthFailed("Missing key file")
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(async_update_options))
return True


async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
156 changes: 156 additions & 0 deletions homeassistant/components/google_cloud/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
"""Config flow for the Gogole Cloud integration."""

from __future__ import annotations

from collections.abc import Mapping
import logging
from pathlib import Path
from types import MappingProxyType
from typing import Any

from google.cloud import texttospeech
import voluptuous as vol

from homeassistant.components.tts import CONF_LANG
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlowWithConfigEntry,
)
from homeassistant.const import CONF_NAME
from homeassistant.core import callback
from homeassistant.helpers.selector import (
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
)

from .const import (
CONF_KEY_FILE,
CONF_STT_MODEL,
DEFAULT_LANG,
DEFAULT_STT_MODEL,
DOMAIN,
SUPPORTED_STT_MODELS,
)
from .helpers import async_tts_voices, tts_options_schema

_LOGGER = logging.getLogger(__name__)

STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_KEY_FILE): str,
}
)


class GoogleCloudConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Google Cloud integration."""

VERSION = 1

_name: str | None = None

def __init__(self) -> None:
"""Initialize a new GoogleCloudConfigFlow."""
self.reauth_entry: ConfigEntry | None = None

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, Any] = {}
if user_input is not None:
if Path(self.hass.config.path(user_input[CONF_KEY_FILE])).is_file():
if self.reauth_entry:
return self.async_update_reload_and_abort(
self.reauth_entry,
data=user_input,
)
return self.async_create_entry(
title="Google Cloud",
data=user_input,
)
errors["base"] = "file_not_found"
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)

async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle configuration by re-auth."""
self.reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
return await self.async_step_reauth_confirm()

async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
if user_input is not None:
return await self.async_step_user()
assert self.reauth_entry
return self.async_show_form(
step_id="reauth_confirm",
description_placeholders={
CONF_NAME: self.reauth_entry.title,
CONF_KEY_FILE: self.reauth_entry.data.get(CONF_KEY_FILE, ""),
},
)

@staticmethod
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
) -> GoogleCloudOptionsFlowHandler:
"""Create the options flow."""
return GoogleCloudOptionsFlowHandler(config_entry)


class GoogleCloudOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Google Cloud options flow."""

async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)

key_file = self.hass.config.path(self.config_entry.data[CONF_KEY_FILE])
client: texttospeech.TextToSpeechAsyncClient = (
texttospeech.TextToSpeechAsyncClient.from_service_account_file(key_file)
)
voices = await async_tts_voices(client)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Optional(
CONF_LANG,
description={"suggested_value": self.options.get(CONF_LANG)},
default=DEFAULT_LANG,
): SelectSelector(
SelectSelectorConfig(
mode=SelectSelectorMode.DROPDOWN, options=list(voices)
)
),
**tts_options_schema(MappingProxyType(self.options), voices).schema,
vol.Optional(
CONF_STT_MODEL,
description={
"suggested_value": self.options.get(CONF_STT_MODEL)
},
default=DEFAULT_STT_MODEL,
): SelectSelector(
SelectSelectorConfig(
mode=SelectSelectorMode.DROPDOWN,
options=SUPPORTED_STT_MODELS,
)
),
}
),
)
178 changes: 178 additions & 0 deletions homeassistant/components/google_cloud/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"""Constants for the Google Cloud component."""

from __future__ import annotations

DOMAIN = "google_cloud"

CONF_KEY_FILE = "key_file"

DEFAULT_LANG = "en-US"

# TTS constants
CONF_GENDER = "gender"
CONF_VOICE = "voice"
CONF_ENCODING = "encoding"
CONF_SPEED = "speed"
CONF_PITCH = "pitch"
CONF_GAIN = "gain"
CONF_PROFILES = "profiles"
CONF_TEXT_TYPE = "text_type"

# STT constants
CONF_STT_MODEL = "stt_model"

DEFAULT_STT_MODEL = "command_and_search"

SUPPORTED_STT_MODELS = [
"default",
"command_and_search",
"latest_short",
"latest_long",
"phone_call",
"video",
]

# https://cloud.google.com/speech-to-text/docs/speech-to-text-supported-languages
# Filtered on Model=command_and_search
STT_LANGUAGES = [
"af-ZA",
"am-ET",
"ar-AE",
"ar-BH",
"ar-DZ",
"ar-EG",
"ar-IL",
"ar-IQ",
"ar-JO",
"ar-KW",
"ar-LB",
"ar-MA",
"ar-MR",
"ar-OM",
"ar-PS",
"ar-QA",
"ar-SA",
"ar-SY",
"ar-TN",
"ar-YE",
"az-AZ",
"bg-BG",
"bn-BD",
"bn-IN",
"bs-BA",
"ca-ES",
"cmn-Hans-CN",
"cmn-Hans-HK",
"cmn-Hant-TW",
"cs-CZ",
"da-DK",
"de-AT",
"de-CH",
"de-DE",
"el-GR",
"en-AU",
"en-CA",
"en-GB",
"en-GH",
"en-HK",
"en-IE",
"en-IN",
"en-KE",
"en-NG",
"en-NZ",
"en-PH",
"en-PK",
"en-SG",
"en-TZ",
"en-US",
"en-ZA",
"es-AR",
"es-BO",
"es-CL",
"es-CO",
"es-CR",
"es-DO",
"es-EC",
"es-ES",
"es-GT",
"es-HN",
"es-MX",
"es-NI",
"es-PA",
"es-PE",
"es-PR",
"es-PY",
"es-SV",
"es-US",
"es-UY",
"es-VE",
"et-EE",
"eu-ES",
"fa-IR",
"fi-FI",
"fil-PH",
"fr-BE",
"fr-CA",
"fr-CH",
"fr-FR",
"gl-ES",
"gu-IN",
"hi-IN",
"hr-HR",
"hu-HU",
"hy-AM",
"id-ID",
"is-IS",
"it-CH",
"it-IT",
"iw-IL",
"ja-JP",
"jv-ID",
"ka-GE",
"kk-KZ",
"km-KH",
"kn-IN",
"ko-KR",
"lo-LA",
"lt-LT",
"lv-LV",
"mk-MK",
"ml-IN",
"mn-MN",
"mr-IN",
"ms-MY",
"my-MM",
"ne-NP",
"nl-BE",
"nl-NL",
"no-NO",
"pa-Guru-IN",
"pl-PL",
"pt-BR",
"pt-PT",
"ro-RO",
"ru-RU",
"si-LK",
"sk-SK",
"sl-SI",
"sq-AL",
"sr-RS",
"su-ID",
"sv-SE",
"sw-KE",
"sw-TZ",
"ta-IN",
"ta-LK",
"ta-MY",
"ta-SG",
"te-IN",
"th-TH",
"tr-TR",
"uk-UA",
"ur-IN",
"ur-PK",
"uz-UZ",
"vi-VN",
"yue-Hant-HK",
"zu-ZA",
]
Loading
Loading