From dd1def3c5dbe12044e5865ee9076ee356dedc797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Tue, 4 Feb 2025 13:32:33 +0100 Subject: [PATCH] Add default voice for languages in cloud TTS (#137300) * Add default voice for languages in cloud TTS * Add test * use defined voice * Add test to ensure all default voices are valid --- homeassistant/components/cloud/tts.py | 168 ++++++++++++++++++++++++-- tests/components/cloud/test_tts.py | 20 ++- 2 files changed, 179 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/cloud/tts.py b/homeassistant/components/cloud/tts.py index 645ff4f9e75c38..63f36554c654b1 100644 --- a/homeassistant/components/cloud/tts.py +++ b/homeassistant/components/cloud/tts.py @@ -38,6 +38,156 @@ DEPRECATED_VOICES = {"XiaoxuanNeural": "XiaozhenNeural"} SUPPORT_LANGUAGES = list(TTS_VOICES) +DEFAULT_VOICES = { + "af-ZA": "AdriNeural", + "am-ET": "MekdesNeural", + "ar-AE": "FatimaNeural", + "ar-BH": "LailaNeural", + "ar-DZ": "AminaNeural", + "ar-EG": "SalmaNeural", + "ar-IQ": "RanaNeural", + "ar-JO": "SanaNeural", + "ar-KW": "NouraNeural", + "ar-LB": "LaylaNeural", + "ar-LY": "ImanNeural", + "ar-MA": "MounaNeural", + "ar-OM": "AbdullahNeural", + "ar-QA": "AmalNeural", + "ar-SA": "ZariyahNeural", + "ar-SY": "AmanyNeural", + "ar-TN": "ReemNeural", + "ar-YE": "MaryamNeural", + "az-AZ": "BabekNeural", + "bg-BG": "KalinaNeural", + "bn-BD": "NabanitaNeural", + "bn-IN": "TanishaaNeural", + "bs-BA": "GoranNeural", + "ca-ES": "JoanaNeural", + "cs-CZ": "VlastaNeural", + "cy-GB": "NiaNeural", + "da-DK": "ChristelNeural", + "de-AT": "IngridNeural", + "de-CH": "LeniNeural", + "de-DE": "KatjaNeural", + "el-GR": "AthinaNeural", + "en-AU": "NatashaNeural", + "en-CA": "ClaraNeural", + "en-GB": "LibbyNeural", + "en-HK": "YanNeural", + "en-IE": "EmilyNeural", + "en-IN": "NeerjaNeural", + "en-KE": "AsiliaNeural", + "en-NG": "EzinneNeural", + "en-NZ": "MollyNeural", + "en-PH": "RosaNeural", + "en-SG": "LunaNeural", + "en-TZ": "ImaniNeural", + "en-US": "JennyNeural", + "en-ZA": "LeahNeural", + "es-AR": "ElenaNeural", + "es-BO": "SofiaNeural", + "es-CL": "CatalinaNeural", + "es-CO": "SalomeNeural", + "es-CR": "MariaNeural", + "es-CU": "BelkysNeural", + "es-DO": "RamonaNeural", + "es-EC": "AndreaNeural", + "es-ES": "ElviraNeural", + "es-GQ": "TeresaNeural", + "es-GT": "MartaNeural", + "es-HN": "KarlaNeural", + "es-MX": "DaliaNeural", + "es-NI": "YolandaNeural", + "es-PA": "MargaritaNeural", + "es-PE": "CamilaNeural", + "es-PR": "KarinaNeural", + "es-PY": "TaniaNeural", + "es-SV": "LorenaNeural", + "es-US": "PalomaNeural", + "es-UY": "ValentinaNeural", + "es-VE": "PaolaNeural", + "et-EE": "AnuNeural", + "eu-ES": "AinhoaNeural", + "fa-IR": "DilaraNeural", + "fi-FI": "SelmaNeural", + "fil-PH": "BlessicaNeural", + "fr-BE": "CharlineNeural", + "fr-CA": "SylvieNeural", + "fr-CH": "ArianeNeural", + "fr-FR": "DeniseNeural", + "ga-IE": "OrlaNeural", + "gl-ES": "SabelaNeural", + "gu-IN": "DhwaniNeural", + "he-IL": "HilaNeural", + "hi-IN": "SwaraNeural", + "hr-HR": "GabrijelaNeural", + "hu-HU": "NoemiNeural", + "hy-AM": "AnahitNeural", + "id-ID": "GadisNeural", + "is-IS": "GudrunNeural", + "it-IT": "ElsaNeural", + "ja-JP": "NanamiNeural", + "jv-ID": "SitiNeural", + "ka-GE": "EkaNeural", + "kk-KZ": "AigulNeural", + "km-KH": "SreymomNeural", + "kn-IN": "SapnaNeural", + "ko-KR": "SunHiNeural", + "lo-LA": "KeomanyNeural", + "lt-LT": "OnaNeural", + "lv-LV": "EveritaNeural", + "mk-MK": "MarijaNeural", + "ml-IN": "SobhanaNeural", + "mn-MN": "BataaNeural", + "mr-IN": "AarohiNeural", + "ms-MY": "YasminNeural", + "mt-MT": "GraceNeural", + "my-MM": "NilarNeural", + "nb-NO": "IselinNeural", + "ne-NP": "HemkalaNeural", + "nl-BE": "DenaNeural", + "nl-NL": "ColetteNeural", + "pl-PL": "AgnieszkaNeural", + "ps-AF": "LatifaNeural", + "pt-BR": "FranciscaNeural", + "pt-PT": "RaquelNeural", + "ro-RO": "AlinaNeural", + "ru-RU": "SvetlanaNeural", + "si-LK": "ThiliniNeural", + "sk-SK": "ViktoriaNeural", + "sl-SI": "PetraNeural", + "so-SO": "UbaxNeural", + "sq-AL": "AnilaNeural", + "sr-RS": "SophieNeural", + "su-ID": "TutiNeural", + "sv-SE": "SofieNeural", + "sw-KE": "ZuriNeural", + "sw-TZ": "RehemaNeural", + "ta-IN": "PallaviNeural", + "ta-LK": "SaranyaNeural", + "ta-MY": "KaniNeural", + "ta-SG": "VenbaNeural", + "te-IN": "ShrutiNeural", + "th-TH": "AcharaNeural", + "tr-TR": "EmelNeural", + "uk-UA": "PolinaNeural", + "ur-IN": "GulNeural", + "ur-PK": "UzmaNeural", + "uz-UZ": "MadinaNeural", + "vi-VN": "HoaiMyNeural", + "wuu-CN": "XiaotongNeural", + "yue-CN": "XiaoMinNeural", + "zh-CN": "XiaoxiaoNeural", + "zh-CN-henan": "YundengNeural", + "zh-CN-liaoning": "XiaobeiNeural", + "zh-CN-shaanxi": "XiaoniNeural", + "zh-CN-shandong": "YunxiangNeural", + "zh-CN-sichuan": "YunxiNeural", + "zh-HK": "HiuMaanNeural", + "zh-TW": "HsiaoChenNeural", + "zu-ZA": "ThandoNeural", +} + _LOGGER = logging.getLogger(__name__) @@ -186,12 +336,13 @@ async def async_get_tts_audio( """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) - if original_voice is None and language == self._language: - original_voice = self._voice + original_voice: str = options.get( + ATTR_VOICE, + self._voice if language == self._language else DEFAULT_VOICES[language], + ) voice = handle_deprecated_voice(self.hass, original_voice) if voice not in TTS_VOICES[language]: - default_voice = TTS_VOICES[language][0] + default_voice = DEFAULT_VOICES[language] _LOGGER.debug( "Unsupported voice %s detected, falling back to default %s for %s", voice, @@ -266,12 +417,13 @@ async def async_get_tts_audio( 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) - if original_voice is None and language == self._language: - original_voice = self._voice + original_voice: str = options.get( + ATTR_VOICE, + self._voice if language == self._language else DEFAULT_VOICES[language], + ) voice = handle_deprecated_voice(self.hass, original_voice) if voice not in TTS_VOICES[language]: - default_voice = TTS_VOICES[language][0] + default_voice = DEFAULT_VOICES[language] _LOGGER.debug( "Unsupported voice %s detected, falling back to default %s for %s", voice, diff --git a/tests/components/cloud/test_tts.py b/tests/components/cloud/test_tts.py index bf9fd7302ae777..81b10866dff15d 100644 --- a/tests/components/cloud/test_tts.py +++ b/tests/components/cloud/test_tts.py @@ -12,7 +12,12 @@ from homeassistant.components.assist_pipeline.pipeline import STORAGE_KEY from homeassistant.components.cloud.const import DEFAULT_TTS_DEFAULT_VOICE, DOMAIN -from homeassistant.components.cloud.tts import PLATFORM_SCHEMA, SUPPORT_LANGUAGES, Voice +from homeassistant.components.cloud.tts import ( + DEFAULT_VOICES, + PLATFORM_SCHEMA, + SUPPORT_LANGUAGES, + Voice, +) from homeassistant.components.media_player import ( ATTR_MEDIA_CONTENT_ID, DOMAIN as DOMAIN_MP, @@ -61,6 +66,19 @@ def test_default_exists() -> None: assert DEFAULT_TTS_DEFAULT_VOICE[1] in TTS_VOICES[DEFAULT_TTS_DEFAULT_VOICE[0]] +def test_all_languages_have_default() -> None: + """Test all languages have a default voice.""" + assert set(SUPPORT_LANGUAGES).difference(DEFAULT_VOICES) == set() + assert set(DEFAULT_VOICES).difference(SUPPORT_LANGUAGES) == set() + + +@pytest.mark.parametrize(("language", "voice"), DEFAULT_VOICES.items()) +def test_default_voice_is_valid(language: str, voice: str) -> None: + """Test that the default voice is valid.""" + assert language in TTS_VOICES + assert voice in TTS_VOICES[language] + + def test_schema() -> None: """Test schema.""" assert "nl-NL" in SUPPORT_LANGUAGES