mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
Deprecate cloud tts gender (#112256)
* Deprecate cloud tts gender option * Update http api and prefs * Test migration of prefs to minor version 4 * Adjust breaking date * Add test for bad voice in http api * Flatten tts info * Fix comments * Fix comment date Co-authored-by: Erik Montnemery <erik@montnemery.com> * Clarify voice validator --------- Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
d31124d5d4
commit
ac008a4c6d
@ -33,7 +33,7 @@ PREF_GOOGLE_SETTINGS_VERSION = "google_settings_version"
|
||||
PREF_TTS_DEFAULT_VOICE = "tts_default_voice"
|
||||
PREF_GOOGLE_CONNECTED = "google_connected"
|
||||
PREF_REMOTE_ALLOW_REMOTE_ENABLE = "remote_allow_remote_enable"
|
||||
DEFAULT_TTS_DEFAULT_VOICE = ("en-US", "female")
|
||||
DEFAULT_TTS_DEFAULT_VOICE = ("en-US", "JennyNeural")
|
||||
DEFAULT_DISABLE_2FA = False
|
||||
DEFAULT_ALEXA_REPORT_STATE = True
|
||||
DEFAULT_GOOGLE_REPORT_STATE = True
|
||||
|
@ -16,7 +16,7 @@ from aiohttp import web
|
||||
import attr
|
||||
from hass_nabucasa import Cloud, auth, thingtalk
|
||||
from hass_nabucasa.const import STATE_DISCONNECTED
|
||||
from hass_nabucasa.voice import MAP_VOICE
|
||||
from hass_nabucasa.voice import TTS_VOICES
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
@ -426,6 +426,16 @@ async def websocket_subscription(
|
||||
async_manage_legacy_subscription_issue(hass, data)
|
||||
|
||||
|
||||
def validate_language_voice(value: tuple[str, str]) -> tuple[str, str]:
|
||||
"""Validate language and voice."""
|
||||
language, voice = value
|
||||
if language not in TTS_VOICES:
|
||||
raise vol.Invalid(f"Invalid language {language}")
|
||||
if voice not in TTS_VOICES[language]:
|
||||
raise vol.Invalid(f"Invalid voice {voice} for language {language}")
|
||||
return value
|
||||
|
||||
|
||||
@_require_cloud_login
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
@ -436,7 +446,7 @@ async def websocket_subscription(
|
||||
vol.Optional(PREF_GOOGLE_REPORT_STATE): bool,
|
||||
vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str),
|
||||
vol.Optional(PREF_TTS_DEFAULT_VOICE): vol.All(
|
||||
vol.Coerce(tuple), vol.In(MAP_VOICE)
|
||||
vol.Coerce(tuple), validate_language_voice
|
||||
),
|
||||
vol.Optional(PREF_REMOTE_ALLOW_REMOTE_ENABLE): bool,
|
||||
}
|
||||
@ -840,5 +850,12 @@ def tts_info(
|
||||
) -> None:
|
||||
"""Fetch available tts info."""
|
||||
connection.send_result(
|
||||
msg["id"], {"languages": [(lang, gender.value) for lang, gender in MAP_VOICE]}
|
||||
msg["id"],
|
||||
{
|
||||
"languages": [
|
||||
(language, voice)
|
||||
for language, voices in TTS_VOICES.items()
|
||||
for voice in voices
|
||||
]
|
||||
},
|
||||
)
|
||||
|
@ -6,6 +6,8 @@ from collections.abc import Callable, Coroutine
|
||||
from typing import Any
|
||||
import uuid
|
||||
|
||||
from hass_nabucasa.voice import MAP_VOICE
|
||||
|
||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||
from homeassistant.auth.models import User
|
||||
from homeassistant.components import webhook
|
||||
@ -48,7 +50,7 @@ from .const import (
|
||||
|
||||
STORAGE_KEY = DOMAIN
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_VERSION_MINOR = 3
|
||||
STORAGE_VERSION_MINOR = 4
|
||||
|
||||
ALEXA_SETTINGS_VERSION = 3
|
||||
GOOGLE_SETTINGS_VERSION = 3
|
||||
@ -82,6 +84,24 @@ class CloudPreferencesStore(Store):
|
||||
# In HA Core 2024.9, remove the import and also remove the Google
|
||||
# assistant store if it's not been migrated by manual Google assistant
|
||||
old_data.setdefault(PREF_GOOGLE_CONNECTED, await google_connected())
|
||||
if old_minor_version < 4:
|
||||
# Update the default TTS voice to the new default.
|
||||
# The default tts voice is a tuple.
|
||||
# The first item is the language, the second item used to be gender.
|
||||
# The new second item is the voice name.
|
||||
default_tts_voice = old_data.get(PREF_TTS_DEFAULT_VOICE)
|
||||
if default_tts_voice and (voice_item_two := default_tts_voice[1]) in (
|
||||
"female",
|
||||
"male",
|
||||
):
|
||||
language: str = default_tts_voice[0]
|
||||
if voice := MAP_VOICE.get((language, voice_item_two)):
|
||||
old_data[PREF_TTS_DEFAULT_VOICE] = (
|
||||
language,
|
||||
voice,
|
||||
)
|
||||
else:
|
||||
old_data[PREF_TTS_DEFAULT_VOICE] = DEFAULT_TTS_DEFAULT_VOICE
|
||||
|
||||
return old_data
|
||||
|
||||
@ -332,7 +352,10 @@ class CloudPreferences:
|
||||
|
||||
@property
|
||||
def tts_default_voice(self) -> tuple[str, str]:
|
||||
"""Return the default TTS voice."""
|
||||
"""Return the default TTS voice.
|
||||
|
||||
The return value is a tuple of language and voice.
|
||||
"""
|
||||
return self._prefs.get(PREF_TTS_DEFAULT_VOICE, DEFAULT_TTS_DEFAULT_VOICE) # type: ignore[no-any-return]
|
||||
|
||||
async def get_cloud_user(self) -> str:
|
||||
|
@ -24,6 +24,17 @@
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_gender": {
|
||||
"title": "The '{deprecated_option}' text-to-speech option is deprecated",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "[%key:component::cloud::issues::deprecated_voice::title%]",
|
||||
"description": "The '{deprecated_option}' option for text-to-speech in the {integration_name} integration is deprecated and will be removed.\nPlease update your automations and scripts to replace the '{deprecated_option}' option with an option for a supported '{replacement_option}' instead."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated_tts_platform_config": {
|
||||
"title": "The Cloud text-to-speech platform configuration is deprecated",
|
||||
"description": "The whole `platform: cloud` entry under the `tts:` section in configuration.yaml is deprecated and should be removed. You can use the UI to change settings for the Cloud text-to-speech platform. Please adjust your configuration.yaml and restart Home Assistant to fix this issue."
|
||||
|
@ -7,7 +7,7 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from hass_nabucasa import Cloud
|
||||
from hass_nabucasa.voice import MAP_VOICE, TTS_VOICES, AudioOutput, VoiceError
|
||||
from hass_nabucasa.voice import MAP_VOICE, TTS_VOICES, AudioOutput, Gender, VoiceError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.tts import (
|
||||
@ -97,17 +97,7 @@ async def async_get_engine(
|
||||
) -> CloudProvider:
|
||||
"""Set up Cloud speech component."""
|
||||
cloud: Cloud[CloudClient] = hass.data[DOMAIN]
|
||||
|
||||
language: str | None
|
||||
gender: str | None
|
||||
if discovery_info is not None:
|
||||
language = None
|
||||
gender = None
|
||||
else:
|
||||
language = config[CONF_LANG]
|
||||
gender = config[ATTR_GENDER]
|
||||
|
||||
cloud_provider = CloudProvider(cloud, language, gender)
|
||||
cloud_provider = CloudProvider(cloud)
|
||||
if discovery_info is not None:
|
||||
discovery_info["platform_loaded"].set()
|
||||
return cloud_provider
|
||||
@ -134,11 +124,11 @@ class CloudTTSEntity(TextToSpeechEntity):
|
||||
def __init__(self, cloud: Cloud[CloudClient]) -> None:
|
||||
"""Initialize cloud text-to-speech entity."""
|
||||
self.cloud = cloud
|
||||
self._language, self._gender = cloud.client.prefs.tts_default_voice
|
||||
self._language, self._voice = cloud.client.prefs.tts_default_voice
|
||||
|
||||
async def _sync_prefs(self, prefs: CloudPreferences) -> None:
|
||||
"""Sync preferences."""
|
||||
self._language, self._gender = prefs.tts_default_voice
|
||||
self._language, self._voice = prefs.tts_default_voice
|
||||
|
||||
@property
|
||||
def default_language(self) -> str:
|
||||
@ -149,8 +139,8 @@ class CloudTTSEntity(TextToSpeechEntity):
|
||||
def default_options(self) -> dict[str, Any]:
|
||||
"""Return a dict include default options."""
|
||||
return {
|
||||
ATTR_GENDER: self._gender,
|
||||
ATTR_AUDIO_OUTPUT: AudioOutput.MP3,
|
||||
ATTR_VOICE: self._voice,
|
||||
}
|
||||
|
||||
@property
|
||||
@ -161,6 +151,7 @@ class CloudTTSEntity(TextToSpeechEntity):
|
||||
@property
|
||||
def supported_options(self) -> list[str]:
|
||||
"""Return list of supported options like voice, emotion."""
|
||||
# The gender option is deprecated and will be removed in 2024.10.0.
|
||||
return [ATTR_GENDER, ATTR_VOICE, ATTR_AUDIO_OUTPUT]
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
@ -184,6 +175,8 @@ class CloudTTSEntity(TextToSpeechEntity):
|
||||
self, message: str, language: str, options: dict[str, Any]
|
||||
) -> TtsAudioType:
|
||||
"""Load TTS from Home Assistant Cloud."""
|
||||
gender: Gender | str | None = options.get(ATTR_GENDER)
|
||||
gender = handle_deprecated_gender(self.hass, gender)
|
||||
original_voice: str | None = options.get(ATTR_VOICE)
|
||||
voice = handle_deprecated_voice(self.hass, original_voice)
|
||||
# Process TTS
|
||||
@ -191,7 +184,7 @@ class CloudTTSEntity(TextToSpeechEntity):
|
||||
data = await self.cloud.voice.process_tts(
|
||||
text=message,
|
||||
language=language,
|
||||
gender=options.get(ATTR_GENDER),
|
||||
gender=gender,
|
||||
voice=voice,
|
||||
output=options[ATTR_AUDIO_OUTPUT],
|
||||
)
|
||||
@ -205,24 +198,16 @@ class CloudTTSEntity(TextToSpeechEntity):
|
||||
class CloudProvider(Provider):
|
||||
"""Home Assistant Cloud speech API provider."""
|
||||
|
||||
def __init__(
|
||||
self, cloud: Cloud[CloudClient], language: str | None, gender: str | None
|
||||
) -> None:
|
||||
def __init__(self, cloud: Cloud[CloudClient]) -> None:
|
||||
"""Initialize cloud provider."""
|
||||
self.cloud = cloud
|
||||
self.name = "Cloud"
|
||||
self._language = language
|
||||
self._gender = gender
|
||||
|
||||
if self._language is not None:
|
||||
return
|
||||
|
||||
self._language, self._gender = cloud.client.prefs.tts_default_voice
|
||||
self._language, self._voice = cloud.client.prefs.tts_default_voice
|
||||
cloud.client.prefs.async_listen_updates(self._sync_prefs)
|
||||
|
||||
async def _sync_prefs(self, prefs: CloudPreferences) -> None:
|
||||
"""Sync preferences."""
|
||||
self._language, self._gender = prefs.tts_default_voice
|
||||
self._language, self._voice = prefs.tts_default_voice
|
||||
|
||||
@property
|
||||
def default_language(self) -> str | None:
|
||||
@ -237,6 +222,7 @@ class CloudProvider(Provider):
|
||||
@property
|
||||
def supported_options(self) -> list[str]:
|
||||
"""Return list of supported options like voice, emotion."""
|
||||
# The gender option is deprecated and will be removed in 2024.10.0.
|
||||
return [ATTR_GENDER, ATTR_VOICE, ATTR_AUDIO_OUTPUT]
|
||||
|
||||
@callback
|
||||
@ -250,23 +236,25 @@ class CloudProvider(Provider):
|
||||
def default_options(self) -> dict[str, Any]:
|
||||
"""Return a dict include default options."""
|
||||
return {
|
||||
ATTR_GENDER: self._gender,
|
||||
ATTR_AUDIO_OUTPUT: AudioOutput.MP3,
|
||||
ATTR_VOICE: self._voice,
|
||||
}
|
||||
|
||||
async def async_get_tts_audio(
|
||||
self, message: str, language: str, options: dict[str, Any]
|
||||
) -> TtsAudioType:
|
||||
"""Load TTS from Home Assistant Cloud."""
|
||||
original_voice: str | None = options.get(ATTR_VOICE)
|
||||
assert self.hass is not None
|
||||
gender: Gender | str | None = options.get(ATTR_GENDER)
|
||||
gender = handle_deprecated_gender(self.hass, gender)
|
||||
original_voice: str | None = options.get(ATTR_VOICE)
|
||||
voice = handle_deprecated_voice(self.hass, original_voice)
|
||||
# Process TTS
|
||||
try:
|
||||
data = await self.cloud.voice.process_tts(
|
||||
text=message,
|
||||
language=language,
|
||||
gender=options.get(ATTR_GENDER),
|
||||
gender=gender,
|
||||
voice=voice,
|
||||
output=options[ATTR_AUDIO_OUTPUT],
|
||||
)
|
||||
@ -277,6 +265,32 @@ class CloudProvider(Provider):
|
||||
return (str(options[ATTR_AUDIO_OUTPUT].value), data)
|
||||
|
||||
|
||||
@callback
|
||||
def handle_deprecated_gender(
|
||||
hass: HomeAssistant,
|
||||
gender: Gender | str | None,
|
||||
) -> Gender | None:
|
||||
"""Handle deprecated gender."""
|
||||
if gender is None:
|
||||
return None
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_gender",
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
severity=IssueSeverity.WARNING,
|
||||
breaks_in_ha_version="2024.10.0",
|
||||
translation_key="deprecated_gender",
|
||||
translation_placeholders={
|
||||
"integration_name": "Home Assistant Cloud",
|
||||
"deprecated_option": "gender",
|
||||
"replacement_option": "voice",
|
||||
},
|
||||
)
|
||||
return Gender(gender)
|
||||
|
||||
|
||||
@callback
|
||||
def handle_deprecated_voice(
|
||||
hass: HomeAssistant,
|
||||
|
@ -2,13 +2,15 @@
|
||||
|
||||
from copy import deepcopy
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
|
||||
import aiohttp
|
||||
from hass_nabucasa import thingtalk, voice
|
||||
from hass_nabucasa import thingtalk
|
||||
from hass_nabucasa.auth import Unauthenticated, UnknownError
|
||||
from hass_nabucasa.const import STATE_CONNECTED
|
||||
from hass_nabucasa.voice import TTS_VOICES
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.alexa import errors as alexa_errors
|
||||
@ -17,6 +19,7 @@ from homeassistant.components.assist_pipeline.pipeline import STORAGE_KEY
|
||||
from homeassistant.components.cloud.const import DEFAULT_EXPOSED_DOMAINS, DOMAIN
|
||||
from homeassistant.components.google_assistant.helpers import GoogleEntity
|
||||
from homeassistant.components.homeassistant import exposed_entities
|
||||
from homeassistant.components.websocket_api import ERR_INVALID_FORMAT
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
@ -774,7 +777,7 @@ async def test_websocket_status(
|
||||
"google_report_state": True,
|
||||
"remote_allow_remote_enable": True,
|
||||
"remote_enabled": False,
|
||||
"tts_default_voice": ["en-US", "female"],
|
||||
"tts_default_voice": ["en-US", "JennyNeural"],
|
||||
},
|
||||
"alexa_entities": {
|
||||
"include_domains": [],
|
||||
@ -896,14 +899,13 @@ async def test_websocket_update_preferences(
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json(
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"id": 5,
|
||||
"type": "cloud/update_prefs",
|
||||
"alexa_enabled": False,
|
||||
"google_enabled": False,
|
||||
"google_secure_devices_pin": "1234",
|
||||
"tts_default_voice": ["en-GB", "male"],
|
||||
"tts_default_voice": ["en-GB", "RyanNeural"],
|
||||
"remote_allow_remote_enable": False,
|
||||
}
|
||||
)
|
||||
@ -914,7 +916,34 @@ async def test_websocket_update_preferences(
|
||||
assert not cloud.client.prefs.alexa_enabled
|
||||
assert cloud.client.prefs.google_secure_devices_pin == "1234"
|
||||
assert cloud.client.prefs.remote_allow_remote_enable is False
|
||||
assert cloud.client.prefs.tts_default_voice == ("en-GB", "male")
|
||||
assert cloud.client.prefs.tts_default_voice == ("en-GB", "RyanNeural")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("language", "voice"), [("en-GB", "bad_voice"), ("bad_language", "RyanNeural")]
|
||||
)
|
||||
async def test_websocket_update_preferences_bad_voice(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
cloud: MagicMock,
|
||||
setup_cloud: None,
|
||||
language: str,
|
||||
voice: str,
|
||||
) -> None:
|
||||
"""Test updating preference."""
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "cloud/update_prefs",
|
||||
"tts_default_voice": [language, voice],
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
|
||||
assert not response["success"]
|
||||
assert response["error"]["code"] == ERR_INVALID_FORMAT
|
||||
assert cloud.client.prefs.tts_default_voice == ("en-US", "JennyNeural")
|
||||
|
||||
|
||||
async def test_websocket_update_preferences_alexa_report_state(
|
||||
@ -1596,24 +1625,23 @@ async def test_tts_info(
|
||||
setup_cloud: None,
|
||||
) -> None:
|
||||
"""Test that we can get TTS info."""
|
||||
# Verify the format is as expected
|
||||
assert voice.MAP_VOICE[("en-US", voice.Gender.FEMALE)] == "JennyNeural"
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
with patch.dict(
|
||||
"homeassistant.components.cloud.http_api.MAP_VOICE",
|
||||
{
|
||||
("en-US", voice.Gender.MALE): "GuyNeural",
|
||||
("en-US", voice.Gender.FEMALE): "JennyNeural",
|
||||
},
|
||||
clear=True,
|
||||
):
|
||||
await client.send_json({"id": 5, "type": "cloud/tts/info"})
|
||||
response = await client.receive_json()
|
||||
await client.send_json_auto_id({"type": "cloud/tts/info"})
|
||||
response = await client.receive_json()
|
||||
|
||||
assert response["success"]
|
||||
assert response["result"] == {"languages": [["en-US", "male"], ["en-US", "female"]]}
|
||||
assert response["result"] == {
|
||||
"languages": json.loads(
|
||||
json.dumps(
|
||||
[
|
||||
(language, voice)
|
||||
for language, voices in TTS_VOICES.items()
|
||||
for voice in voices
|
||||
]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -1,13 +1,15 @@
|
||||
"""Test Cloud preferences."""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import ANY, patch
|
||||
from unittest.mock import ANY, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||
from homeassistant.components.cloud.const import DOMAIN, PREF_TTS_DEFAULT_VOICE
|
||||
from homeassistant.components.cloud.prefs import STORAGE_KEY, CloudPreferences
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
|
||||
async def test_set_username(hass: HomeAssistant) -> None:
|
||||
@ -149,3 +151,26 @@ async def test_import_google_assistant_settings(
|
||||
prefs = CloudPreferences(hass)
|
||||
await prefs.async_initialize()
|
||||
assert prefs.google_connected == google_connected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("stored_language", "expected_language", "voice"),
|
||||
[("en-US", "en-US", "GuyNeural"), ("missing_language", "en-US", "JennyNeural")],
|
||||
)
|
||||
async def test_tts_default_voice_legacy_gender(
|
||||
hass: HomeAssistant,
|
||||
cloud: MagicMock,
|
||||
hass_storage: dict[str, Any],
|
||||
stored_language: str,
|
||||
expected_language: str,
|
||||
voice: str,
|
||||
) -> None:
|
||||
"""Test tts with legacy gender as default tts voice setting in storage."""
|
||||
hass_storage[STORAGE_KEY] = {
|
||||
"version": 1,
|
||||
"data": {PREF_TTS_DEFAULT_VOICE: [stored_language, "male"]},
|
||||
}
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert cloud.client.prefs.tts_default_voice == (expected_language, voice)
|
||||
|
@ -6,7 +6,7 @@ from http import HTTPStatus
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from hass_nabucasa.voice import MAP_VOICE, VoiceError, VoiceTokenError
|
||||
from hass_nabucasa.voice import TTS_VOICES, VoiceError, VoiceTokenError
|
||||
import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
@ -44,7 +44,11 @@ async def internal_url_mock(hass: HomeAssistant) -> None:
|
||||
|
||||
def test_default_exists() -> None:
|
||||
"""Test our default language exists."""
|
||||
assert const.DEFAULT_TTS_DEFAULT_VOICE in MAP_VOICE
|
||||
assert const.DEFAULT_TTS_DEFAULT_VOICE[0] in TTS_VOICES
|
||||
assert (
|
||||
const.DEFAULT_TTS_DEFAULT_VOICE[1]
|
||||
in TTS_VOICES[const.DEFAULT_TTS_DEFAULT_VOICE[0]]
|
||||
)
|
||||
|
||||
|
||||
def test_schema() -> None:
|
||||
@ -105,7 +109,7 @@ async def test_prefs_default_voice(
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert cloud.client.prefs.tts_default_voice == ("en-US", "female")
|
||||
assert cloud.client.prefs.tts_default_voice == ("en-US", "JennyNeural")
|
||||
|
||||
on_start_callback = cloud.register_on_start.call_args[0][0]
|
||||
await on_start_callback()
|
||||
@ -116,13 +120,13 @@ async def test_prefs_default_voice(
|
||||
assert engine is not None
|
||||
# The platform config provider will be overridden by the discovery info provider.
|
||||
assert engine.default_language == "en-US"
|
||||
assert engine.default_options == {"gender": "female", "audio_output": "mp3"}
|
||||
assert engine.default_options == {"audio_output": "mp3", "voice": "JennyNeural"}
|
||||
|
||||
await set_cloud_prefs({"tts_default_voice": ("nl-NL", "male")})
|
||||
await set_cloud_prefs({"tts_default_voice": ("nl-NL", "MaartenNeural")})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert engine.default_language == "nl-NL"
|
||||
assert engine.default_options == {"gender": "male", "audio_output": "mp3"}
|
||||
assert engine.default_options == {"audio_output": "mp3", "voice": "MaartenNeural"}
|
||||
|
||||
|
||||
async def test_deprecated_platform_config(
|
||||
@ -224,11 +228,11 @@ async def test_get_tts_audio(
|
||||
"url": (
|
||||
"http://example.local:8123/api/tts_proxy/"
|
||||
"42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||
f"_en-us_e09b5a0968_{expected_url_suffix}.mp3"
|
||||
f"_en-us_5c97d21c48_{expected_url_suffix}.mp3"
|
||||
),
|
||||
"path": (
|
||||
"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||
f"_en-us_e09b5a0968_{expected_url_suffix}.mp3"
|
||||
f"_en-us_5c97d21c48_{expected_url_suffix}.mp3"
|
||||
),
|
||||
}
|
||||
await hass.async_block_till_done()
|
||||
@ -237,7 +241,7 @@ async def test_get_tts_audio(
|
||||
assert mock_process_tts.call_args is not None
|
||||
assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door."
|
||||
assert mock_process_tts.call_args.kwargs["language"] == "en-US"
|
||||
assert mock_process_tts.call_args.kwargs["gender"] == "female"
|
||||
assert mock_process_tts.call_args.kwargs["gender"] is None
|
||||
assert mock_process_tts.call_args.kwargs["output"] == "mp3"
|
||||
|
||||
|
||||
@ -276,11 +280,11 @@ async def test_get_tts_audio_logged_out(
|
||||
"url": (
|
||||
"http://example.local:8123/api/tts_proxy/"
|
||||
"42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||
f"_en-us_e09b5a0968_{expected_url_suffix}.mp3"
|
||||
f"_en-us_5c97d21c48_{expected_url_suffix}.mp3"
|
||||
),
|
||||
"path": (
|
||||
"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||
f"_en-us_e09b5a0968_{expected_url_suffix}.mp3"
|
||||
f"_en-us_5c97d21c48_{expected_url_suffix}.mp3"
|
||||
),
|
||||
}
|
||||
await hass.async_block_till_done()
|
||||
@ -289,7 +293,7 @@ async def test_get_tts_audio_logged_out(
|
||||
assert mock_process_tts.call_args is not None
|
||||
assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door."
|
||||
assert mock_process_tts.call_args.kwargs["language"] == "en-US"
|
||||
assert mock_process_tts.call_args.kwargs["gender"] == "female"
|
||||
assert mock_process_tts.call_args.kwargs["gender"] is None
|
||||
assert mock_process_tts.call_args.kwargs["output"] == "mp3"
|
||||
|
||||
|
||||
@ -340,11 +344,11 @@ async def test_tts_entity(
|
||||
"url": (
|
||||
"http://example.local:8123/api/tts_proxy/"
|
||||
"42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||
f"_en-us_e09b5a0968_{entity_id}.mp3"
|
||||
f"_en-us_5c97d21c48_{entity_id}.mp3"
|
||||
),
|
||||
"path": (
|
||||
"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||
f"_en-us_e09b5a0968_{entity_id}.mp3"
|
||||
f"_en-us_5c97d21c48_{entity_id}.mp3"
|
||||
),
|
||||
}
|
||||
await hass.async_block_till_done()
|
||||
@ -353,7 +357,7 @@ async def test_tts_entity(
|
||||
assert mock_process_tts.call_args is not None
|
||||
assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door."
|
||||
assert mock_process_tts.call_args.kwargs["language"] == "en-US"
|
||||
assert mock_process_tts.call_args.kwargs["gender"] == "female"
|
||||
assert mock_process_tts.call_args.kwargs["gender"] is None
|
||||
assert mock_process_tts.call_args.kwargs["output"] == "mp3"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
@ -480,11 +484,11 @@ async def test_deprecated_voice(
|
||||
"url": (
|
||||
"http://example.local:8123/api/tts_proxy/"
|
||||
"42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||
f"_{language.lower()}_1c4ec2f170_{expected_url_suffix}.mp3"
|
||||
f"_{language.lower()}_87567e3e29_{expected_url_suffix}.mp3"
|
||||
),
|
||||
"path": (
|
||||
"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||
f"_{language.lower()}_1c4ec2f170_{expected_url_suffix}.mp3"
|
||||
f"_{language.lower()}_87567e3e29_{expected_url_suffix}.mp3"
|
||||
),
|
||||
}
|
||||
await hass.async_block_till_done()
|
||||
@ -493,7 +497,7 @@ async def test_deprecated_voice(
|
||||
assert mock_process_tts.call_args is not None
|
||||
assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door."
|
||||
assert mock_process_tts.call_args.kwargs["language"] == language
|
||||
assert mock_process_tts.call_args.kwargs["gender"] == "female"
|
||||
assert mock_process_tts.call_args.kwargs["gender"] is None
|
||||
assert mock_process_tts.call_args.kwargs["voice"] == replacement_voice
|
||||
assert mock_process_tts.call_args.kwargs["output"] == "mp3"
|
||||
issue = issue_registry.async_get_issue(
|
||||
@ -513,11 +517,11 @@ async def test_deprecated_voice(
|
||||
"url": (
|
||||
"http://example.local:8123/api/tts_proxy/"
|
||||
"42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||
f"_{language.lower()}_a1c3b0ac0e_{expected_url_suffix}.mp3"
|
||||
f"_{language.lower()}_13646b7d32_{expected_url_suffix}.mp3"
|
||||
),
|
||||
"path": (
|
||||
"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||
f"_{language.lower()}_a1c3b0ac0e_{expected_url_suffix}.mp3"
|
||||
f"_{language.lower()}_13646b7d32_{expected_url_suffix}.mp3"
|
||||
),
|
||||
}
|
||||
await hass.async_block_till_done()
|
||||
@ -526,7 +530,7 @@ async def test_deprecated_voice(
|
||||
assert mock_process_tts.call_args is not None
|
||||
assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door."
|
||||
assert mock_process_tts.call_args.kwargs["language"] == language
|
||||
assert mock_process_tts.call_args.kwargs["gender"] == "female"
|
||||
assert mock_process_tts.call_args.kwargs["gender"] is None
|
||||
assert mock_process_tts.call_args.kwargs["voice"] == replacement_voice
|
||||
assert mock_process_tts.call_args.kwargs["output"] == "mp3"
|
||||
issue = issue_registry.async_get_issue(
|
||||
@ -542,3 +546,107 @@ async def test_deprecated_voice(
|
||||
"deprecated_voice": deprecated_voice,
|
||||
"replacement_voice": replacement_voice,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("data", "expected_url_suffix"),
|
||||
[
|
||||
({"platform": DOMAIN}, DOMAIN),
|
||||
({"engine_id": DOMAIN}, DOMAIN),
|
||||
({"engine_id": "tts.home_assistant_cloud"}, "tts.home_assistant_cloud"),
|
||||
],
|
||||
)
|
||||
async def test_deprecated_gender(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: IssueRegistry,
|
||||
cloud: MagicMock,
|
||||
hass_client: ClientSessionGenerator,
|
||||
data: dict[str, Any],
|
||||
expected_url_suffix: str,
|
||||
) -> None:
|
||||
"""Test we create an issue when a deprecated gender is used for text-to-speech."""
|
||||
language = "zh-CN"
|
||||
gender_option = "male"
|
||||
mock_process_tts = AsyncMock(
|
||||
return_value=b"",
|
||||
)
|
||||
cloud.voice.process_tts = mock_process_tts
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
||||
await hass.async_block_till_done()
|
||||
await cloud.login("test-user", "test-pass")
|
||||
client = await hass_client()
|
||||
|
||||
# Test without deprecated gender option.
|
||||
url = "/api/tts_get_url"
|
||||
data |= {
|
||||
"message": "There is someone at the door.",
|
||||
"language": language,
|
||||
}
|
||||
|
||||
req = await client.post(url, json=data)
|
||||
assert req.status == HTTPStatus.OK
|
||||
response = await req.json()
|
||||
|
||||
assert response == {
|
||||
"url": (
|
||||
"http://example.local:8123/api/tts_proxy/"
|
||||
"42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||
f"_{language.lower()}_5c97d21c48_{expected_url_suffix}.mp3"
|
||||
),
|
||||
"path": (
|
||||
"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||
f"_{language.lower()}_5c97d21c48_{expected_url_suffix}.mp3"
|
||||
),
|
||||
}
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_process_tts.call_count == 1
|
||||
assert mock_process_tts.call_args is not None
|
||||
assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door."
|
||||
assert mock_process_tts.call_args.kwargs["language"] == language
|
||||
assert mock_process_tts.call_args.kwargs["voice"] == "JennyNeural"
|
||||
assert mock_process_tts.call_args.kwargs["output"] == "mp3"
|
||||
issue = issue_registry.async_get_issue("cloud", "deprecated_gender")
|
||||
assert issue is None
|
||||
mock_process_tts.reset_mock()
|
||||
|
||||
# Test with deprecated gender option.
|
||||
data["options"] = {"gender": gender_option}
|
||||
|
||||
req = await client.post(url, json=data)
|
||||
assert req.status == HTTPStatus.OK
|
||||
response = await req.json()
|
||||
|
||||
assert response == {
|
||||
"url": (
|
||||
"http://example.local:8123/api/tts_proxy/"
|
||||
"42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||
f"_{language.lower()}_5dded72256_{expected_url_suffix}.mp3"
|
||||
),
|
||||
"path": (
|
||||
"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491"
|
||||
f"_{language.lower()}_5dded72256_{expected_url_suffix}.mp3"
|
||||
),
|
||||
}
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_process_tts.call_count == 1
|
||||
assert mock_process_tts.call_args is not None
|
||||
assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door."
|
||||
assert mock_process_tts.call_args.kwargs["language"] == language
|
||||
assert mock_process_tts.call_args.kwargs["gender"] == gender_option
|
||||
assert mock_process_tts.call_args.kwargs["voice"] == "JennyNeural"
|
||||
assert mock_process_tts.call_args.kwargs["output"] == "mp3"
|
||||
issue = issue_registry.async_get_issue("cloud", "deprecated_gender")
|
||||
assert issue is not None
|
||||
assert issue.breaks_in_ha_version == "2024.10.0"
|
||||
assert issue.is_fixable is True
|
||||
assert issue.is_persistent is True
|
||||
assert issue.severity == IssueSeverity.WARNING
|
||||
assert issue.translation_key == "deprecated_gender"
|
||||
assert issue.translation_placeholders == {
|
||||
"integration_name": "Home Assistant Cloud",
|
||||
"deprecated_option": "gender",
|
||||
"replacement_option": "voice",
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user