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
This commit is contained in:
Joakim Sørensen 2025-02-04 13:32:33 +01:00 committed by GitHub
parent 0a32a9d6db
commit dd1def3c5d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 179 additions and 9 deletions

View File

@ -38,6 +38,156 @@ ATTR_GENDER = "gender"
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 @@ class CloudTTSEntity(TextToSpeechEntity):
"""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 @@ class CloudProvider(Provider):
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,

View File

@ -12,7 +12,12 @@ import voluptuous as vol
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