1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-24 21:06:19 +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:
Martin Hjelmare
2024-03-20 09:42:40 +01:00
committed by GitHub
parent d31124d5d4
commit ac008a4c6d
8 changed files with 304 additions and 78 deletions

View File

@@ -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",
}