From 86a978fadfed6f16d018418fde2adb81139fe2db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Wed, 27 May 2020 21:28:26 +0200 Subject: [PATCH] Fix audio service startup without Internet If started up without Network connection the audio service would crash importing the google_tts module as it tried to fetch language list from google. This restructures it to be a just-in-time check. It also adds a cached list of supported languages to use if an error occurs fetching language codes. A small fix allowing "en-US" to be matched to "en-us" instead of the generic "en" is also included. --- mycroft/tts/google_tts.py | 83 ++++++++++++++++++++++++--- test/unittests/tts/test_google_tts.py | 13 ++++- 2 files changed, 86 insertions(+), 10 deletions(-) diff --git a/mycroft/tts/google_tts.py b/mycroft/tts/google_tts.py index 7f8aef028fea..d81cebeef722 100755 --- a/mycroft/tts/google_tts.py +++ b/mycroft/tts/google_tts.py @@ -17,18 +17,84 @@ from .tts import TTS, TTSValidator -supported_langs = tts_langs() +from mycroft.util.log import LOG + +# Live list of languages +# Cached list of supported languages (2020-05-27) +_default_langs = {'af': 'Afrikaans', 'sq': 'Albanian', 'ar': 'Arabic', + 'hy': 'Armenian', 'bn': 'Bengali', 'bs': 'Bosnian', + 'ca': 'Catalan', 'hr': 'Croatian', 'cs': 'Czech', + 'da': 'Danish', 'nl': 'Dutch', 'en': 'English', + 'eo': 'Esperanto', 'et': 'Estonian', 'tl': 'Filipino', + 'fi': 'Finnish', 'fr': 'French', 'de': 'German', + 'el': 'Greek', 'gu': 'Gujarati', 'hi': 'Hindi', + 'hu': 'Hungarian', 'is': 'Icelandic', 'id': 'Indonesian', + 'it': 'Italian', 'ja': 'Japanese', 'jw': 'Javanese', + 'kn': 'Kannada', 'km': 'Khmer', 'ko': 'Korean', + 'la': 'Latin', 'lv': 'Latvian', 'mk': 'Macedonian', + 'ml': 'Malayalam', 'mr': 'Marathi', + 'my': 'Myanmar (Burmese)', 'ne': 'Nepali', + 'no': 'Norwegian', 'pl': 'Polish', 'pt': 'Portuguese', + 'ro': 'Romanian', 'ru': 'Russian', 'sr': 'Serbian', + 'si': 'Sinhala', 'sk': 'Slovak', 'es': 'Spanish', + 'su': 'Sundanese', 'sw': 'Swahili', 'sv': 'Swedish', + 'ta': 'Tamil', 'te': 'Telugu', 'th': 'Thai', 'tr': 'Turkish', + 'uk': 'Ukrainian', 'ur': 'Urdu', 'vi': 'Vietnamese', + 'cy': 'Welsh', 'zh-cn': 'Chinese (Mandarin/China)', + 'zh-tw': 'Chinese (Mandarin/Taiwan)', + 'en-us': 'English (US)', 'en-ca': 'English (Canada)', + 'en-uk': 'English (UK)', 'en-gb': 'English (UK)', + 'en-au': 'English (Australia)', 'en-gh': 'English (Ghana)', + 'en-in': 'English (India)', 'en-ie': 'English (Ireland)', + 'en-nz': 'English (New Zealand)', + 'en-ng': 'English (Nigeria)', + 'en-ph': 'English (Philippines)', + 'en-za': 'English (South Africa)', + 'en-tz': 'English (Tanzania)', 'fr-ca': 'French (Canada)', + 'fr-fr': 'French (France)', 'pt-br': 'Portuguese (Brazil)', + 'pt-pt': 'Portuguese (Portugal)', 'es-es': 'Spanish (Spain)', + 'es-us': 'Spanish (United States)' + } + + +_supported_langs = None + + +def get_supported_langs(): + """Get dict of supported languages. + + Tries to fetch remote list, if that fails a local cache will be used. + + Returns: + (dict): Lang code to lang name map. + """ + global _supported_langs + if not _supported_langs: + try: + _supported_langs = tts_langs() + except Exception: + LOG.warning('Couldn\'t fetch upto date language codes') + return _supported_langs or _default_langs class GoogleTTS(TTS): """Interface to google TTS.""" def __init__(self, lang, config): - if lang.lower() not in supported_langs and \ - lang[:2].lower() in supported_langs: - lang = lang[:2] + self._google_lang = None super(GoogleTTS, self).__init__(lang, config, GoogleTTSValidator( self), 'mp3') + @property + def google_lang(self): + """Property containing a converted language code suitable for gTTS.""" + supported_langs = get_supported_langs() + if not self._google_lang: + if self.lang.lower() in supported_langs: + self._google_lang = self.lang.lower() + elif self.lang[:2].lower() in supported_langs: + self._google_lang = self.lang[:2] + return self._google_lang or self.lang.lower() + def get_tts(self, sentence, wav_file): """Fetch tts audio using gTTS. @@ -38,7 +104,7 @@ def get_tts(self, sentence, wav_file): Returns: Tuple ((str) written file, None) """ - tts = gTTS(text=sentence, lang=self.lang) + tts = gTTS(text=sentence, lang=self.google_lang) tts.save(wav_file) return (wav_file, None) # No phonemes @@ -48,10 +114,9 @@ def __init__(self, tts): super(GoogleTTSValidator, self).__init__(tts) def validate_lang(self): - lang = self.tts.lang - if lang.lower() not in supported_langs: - raise ValueError("Language not supported by gTTS: {}" - .format(lang)) + lang = self.tts.google_lang + if lang.lower() not in get_supported_langs(): + raise ValueError("Language not supported by gTTS: {}".format(lang)) def validate_connection(self): try: diff --git a/test/unittests/tts/test_google_tts.py b/test/unittests/tts/test_google_tts.py index 7f4d1269e3c8..68e018f3e619 100644 --- a/test/unittests/tts/test_google_tts.py +++ b/test/unittests/tts/test_google_tts.py @@ -2,6 +2,7 @@ from unittest import mock from mycroft.tts.google_tts import GoogleTTS, GoogleTTSValidator +import mycroft.tts.google_tts as google_tts_mod @mock.patch('mycroft.tts.google_tts.gTTS') @@ -13,7 +14,7 @@ def test_get_tts(self, _, gtts_mock): tts = GoogleTTS('en-US', {}) sentence = 'help me Obi-Wan Kenobi, you are my only hope' mp3_file, vis = tts.get_tts(sentence, 'output.mp3') - gtts_mock.assert_called_with(text=sentence, lang='en-US') + gtts_mock.assert_called_with(text=sentence, lang='en-us') gtts_response.save.assert_called_with('output.mp3') def test_validator(self, _, gtts_mock): @@ -24,3 +25,13 @@ def sideeffect(**kwargs): raise Exception gtts_mock.side_effect = sideeffect validator.validate_connection() + + @mock.patch('mycroft.tts.google_tts.tts_langs') + def test_lang_connection_error(self, mock_get_langs, _, gtts_mock): + google_tts_mod._supported_langs = None + + def sideeffect(**kwargs): + raise Exception + mock_get_langs.side_effect = sideeffect + tts = GoogleTTS('en-US', {}) + self.assertEqual(tts.google_lang, 'en-us')