diff --git a/__init__.py b/__init__.py index 0765f04..a6a79df 100644 --- a/__init__.py +++ b/__init__.py @@ -37,7 +37,7 @@ from ovos_bus_client import Message from neon_utils.location_utils import get_timezone from neon_utils.skills.neon_skill import NeonSkill -from neon_utils.user_utils import get_user_prefs +from neon_utils.user_utils import get_user_prefs, update_user_profile from neon_utils.language_utils import get_supported_languages from neon_utils.parse_utils import validate_email from lingua_franca.parse import extract_langcode, get_full_lang_code @@ -116,12 +116,12 @@ def _handle_location_ipgeo_update(self, message): if not all(user_coords) or user_coords == default_coords: LOG.info(f'Updating default user config from ip geolocation') new_loc = { - 'lat': str(updated_location['coordinate']['latitude']), - 'lon': str(updated_location['coordinate']['longitude']), - 'city': updated_location['city']['name'], - 'state': updated_location['city']['state']['name'], - 'country': updated_location['city']['state']['country']['name'], - } + 'lat': str(updated_location['coordinate']['latitude']), + 'lon': str(updated_location['coordinate']['longitude']), + 'city': updated_location['city']['name'], + 'state': updated_location['city']['state']['name'], + 'country': updated_location['city']['state']['country']['name'], + } name, offset = self._get_timezone_from_location(new_loc) new_loc['lng'] = new_loc.pop('lon') new_loc['tz'] = name @@ -182,9 +182,9 @@ def handle_unit_change(self, message: Message): private=True) else: updated_prefs = {"units": {"measure": new_unit}} - self.update_profile(updated_prefs, message) + update_user_profile(updated_prefs, message, self.bus) self.speak_dialog("units_changed", - {"unit": self.translate(f"word_{new_unit}")}, + {"unit": self.resources.render_dialog(f"word_{new_unit}")}, private=True) self._emit_weather_update(message) @@ -206,7 +206,7 @@ def handle_time_format_change(self, message: Message): {"scale": str(new_setting)}, private=True) else: updated_prefs = {"units": {"time": new_setting}} - self.update_profile(updated_prefs, message) + update_user_profile(updated_prefs, message, self.bus) self.speak_dialog("time_format_changed", {"scale": str(new_setting)}, private=True) @@ -219,7 +219,7 @@ def handle_date_format_change(self, message: Message): """ new_setting = "YMD" if message.data.get("ymd") else \ "MDY" if message.data.get("mdy") else \ - "DMY" if message.data.get("dmy") else None + "DMY" if message.data.get("dmy") else None if not new_setting: raise RuntimeError("Missing required date format vocab") @@ -230,7 +230,7 @@ def handle_date_format_change(self, message: Message): private=True) else: updated_prefs = {"units": {"date": new_setting}} - self.update_profile(updated_prefs, message) + update_user_profile(updated_prefs, message, self.bus) self.speak_dialog("date_format_changed", {"format": message.data.get(new_setting.lower())}, private=True) @@ -244,7 +244,8 @@ def handle_speak_hesitation(self, message: Message): :param message: Message associated with request """ enabled = True if message.data.get("permit") else False - self.update_profile({"response_mode": {"hesitation": enabled}}) + update_user_profile({"response_mode": {"hesitation": enabled}}, + message, self.bus) if enabled: self.speak_dialog("hesitation_enabled", private=True) else: @@ -271,15 +272,15 @@ def handle_transcription_retention(self, message: Message): current_setting = get_user_prefs(message)["privacy"][kind] if current_setting == allow: self.speak_dialog("transcription_already_set", - {"transcription": self.translate(transcription), - "enabled": self.translate(enabled)}, + {"transcription": self.resources.render_dialog(transcription), + "enabled": self.resources.render_dialog(enabled)}, private=True) else: updated_prefs = {"privacy": {kind: allow}} - self.update_profile(updated_prefs, message) + update_user_profile(updated_prefs, message, self.bus) self.speak_dialog("transcription_changed", - {"transcription": self.translate(transcription), - "enabled": self.translate(enabled)}, + {"transcription": self.resources.render_dialog(transcription), + "enabled": self.resources.render_dialog(enabled)}, private=True) @intent_handler(IntentBuilder("SpeakSpeed").require("speak_to_me") @@ -306,15 +307,16 @@ def handle_speech_speed(self, message: Message): speed = self.MAX_SPEECH_SPEED speed = round(speed, 1) - self.update_profile({"speech": {"speed_multiplier": speed}}) + update_user_profile({"speech": {"speed_multiplier": speed}}, + message, self.bus) if speed == current_speed == self.MAX_SPEECH_SPEED: self.speak_dialog("speech_speed_limit", - {"limit": self.translate("word_faster")}, + {"limit": self.resources.render_dialog("word_faster")}, private=True) elif speed == current_speed == self.MIN_SPEECH_SPEED: self.speak_dialog("speech_speed_limit", - {"limit": self.translate("word_slower")}, + {"limit": self.resources.render_dialog("word_slower")}, private=True) elif speed == 1.0: self.speak_dialog("speech_speed_normal", private=True) @@ -350,35 +352,37 @@ def handle_change_location_timezone(self, message: Message): do_timezone = True do_location = self.ask_yesno( "also_change_location_tz", - {"type": self.translate("word_location"), + {"type": self.resources.render_dialog("word_location"), "new": requested_place}) == "yes" elif message.data.get("location"): do_location = True do_timezone = self.ask_yesno( "also_change_location_tz", - {"type": self.translate("word_timezone"), + {"type": self.resources.render_dialog("word_timezone"), "new": requested_place}) == "yes" else: do_location = False do_timezone = False if do_timezone: - self.update_profile({"location": {"tz": tz_name, - "utc": utc_offset}}) + LOG.info(f"Update timezone: {tz_name}|{utc_offset}") + update_user_profile({"location": {"tz": tz_name, + "utc": utc_offset}}, + message, self.bus) self.speak_dialog("change_location_tz", - {"type": self.translate("word_timezone"), + {"type": self.resources.render_dialog("word_timezone"), "location": f"UTC {utc_offset}"}, private=True) if do_location: LOG.info(f"Update location: {resolved_place}") - self.update_profile({'location': { + update_user_profile({'location': { 'city': resolved_place['address']['city'], 'state': resolved_place['address'].get('state'), 'country': resolved_place['address']['country'], 'lat': float(resolved_place['lat']), - 'lng': float(resolved_place['lon'])}}) + 'lng': float(resolved_place['lon'])}}, message, self.bus) self.speak_dialog("change_location_tz", - {"type": self.translate("word_location"), + {"type": self.resources.render_dialog("word_location"), "location": resolved_place['address']['city']}, private=True) self._emit_weather_update(message) @@ -401,14 +405,15 @@ def handle_change_dialog_mode(self, message: Message): if new_limit_dialog == current_limit_dialog: self.speak_dialog("dialog_mode_already_set", - {"response": self.translate(new_dialog)}, + {"response": self.resources.render_dialog(new_dialog)}, private=True) return - self.update_profile( - {"response_mode": {"limit_dialog": new_limit_dialog}}) + update_user_profile( + {"response_mode": {"limit_dialog": new_limit_dialog}}, + message, self.bus) self.speak_dialog("dialog_mode_changed", - {"response": self.translate(new_dialog)}, + {"response": self.resources.render_dialog(new_dialog)}, private=True) @intent_handler(IntentBuilder("SayMyName").require("tell_me_my") @@ -433,7 +438,7 @@ def handle_say_my_name(self, message: Message): # TODO: Use get_response to ask for the user's name self.speak_dialog( "name_not_known", - {"name_position": self.translate("word_name")}, + {"name_position": self.resources.render_dialog("word_name")}, private=True) return if self.voc_match(utterance, "first_name"): @@ -466,11 +471,11 @@ def handle_say_my_name(self, message: Message): else: # TODO: Use get_response to ask for the user's name self.speak_dialog("name_not_known", - {"name_position": self.translate(request)}, + {"name_position": self.resources.render_dialog(request)}, private=True) else: self.speak_dialog("name_is", - {"name_position": self.translate(request), + {"name_position": self.resources.render_dialog(request), "name": name}, private=True) @intent_handler(IntentBuilder("SayMyEmail").require("tell_me_my") @@ -570,7 +575,8 @@ def handle_set_my_birthday(self, message: Message): # speakable_birthday = nice_date(birth_date, now=anchor_date) speakable_birthday = birth_date.strftime("%B %-d") - self.update_profile({"user": {"dob": formatted_birthday}}, message) + update_user_profile({"user": {"dob": formatted_birthday}}, + message, self.bus) self.speak_dialog("birthday_confirmed", {"birthday": speakable_birthday}, private=True) @@ -590,7 +596,7 @@ def handle_set_my_email(self, message: Message): extracted = message.data.get("rx_setting") LOG.debug(extracted) email_addr: str = extracted.split()[0] + \ - message.data.get("utterance").rsplit(extracted.split()[0])[1] + message.data.get("utterance").rsplit(extracted.split()[0])[1] dot = read_vocab_file(self.find_resource("dot.voc", 'vocab'))[0][0] at = read_vocab_file(self.find_resource("at.voc", 'vocab'))[0][0] email_words = email_addr.split() @@ -603,7 +609,7 @@ def handle_set_my_email(self, message: Message): if not validate_email(email_addr): self.speak_dialog("email_set_error", private=True) - email_addr = self.get_gui_input(self.translate("word_email_title"), + email_addr = self.get_gui_input(self.resources.render_dialog("word_email_title"), "test@neon.ai") if not email_addr or not validate_email(email_addr): LOG.warning(f"Invalid email_addr entered: {email_addr}") @@ -619,7 +625,8 @@ def handle_set_my_email(self, message: Message): if self.ask_yesno("email_overwrite", {"old": self._spoken_email(current_email), "new": self._spoken_email(email_addr)}) == "yes": - self.update_profile({"user": {"email": email_addr}}) + update_user_profile({"user": {"email": email_addr}}, + message, self.bus) self.speak_dialog("email_set", {"email": self._spoken_email(email_addr)}, private=True) @@ -630,16 +637,18 @@ def handle_set_my_email(self, message: Message): return if self.ask_yesno("email_confirmation", {"email": self._spoken_email(email_addr)}) == "yes": - self.update_profile({"user": {"email": email_addr}}) + update_user_profile({"user": {"email": email_addr}}, + message, self.bus) self.speak_dialog("email_set", {"email": self._spoken_email(email_addr)}, private=True) else: self.speak_dialog("email_not_confirmed", private=True) - email_addr = self.get_gui_input(self.translate("word_email_title"), + email_addr = self.get_gui_input(self.resources.render_dialog("word_email_title"), "test@neon.ai") if email_addr: - self.update_profile({"user": {"email": email_addr}}) + update_user_profile({"user": {"email": email_addr}}, + message, self.bus) self.speak_dialog("email_set", {"email": self._spoken_email(email_addr)}, private=True) @@ -715,9 +724,9 @@ def handle_set_my_name(self, message: Message): if (request and len(name.split()) > 3) or len(name.split()) > 4: LOG.warning(f"'{name}' does not look like a {request} name.") confirmed = self.ask_yesno("name_confirm_change", - {"position": self.translate( + {"position": self.resources.render_dialog( f"word_{request or 'name'}"), - "name": name}) + "name": name}) if not confirmed: self.speak_dialog("name_not_confirmed", private=True) @@ -725,19 +734,19 @@ def handle_set_my_name(self, message: Message): if name == user_profile[request]: self.speak_dialog( "name_not_changed", - {"position": self.translate(f"word_{request}"), + {"position": self.resources.render_dialog(f"word_{request}"), "name": name}, private=True) elif confirmed: name_parts = (name if request == n else user_profile.get(n) for n in ("first_name", "middle_name", "last_name")) full_name = " ".join((n for n in name_parts if n)) - self.update_profile({"user": {request: name, + update_user_profile({"user": {request: name, "full_name": full_name}}, - message) + message, self.bus) self.speak_dialog( "name_set_part", - {"position": self.translate(f"word_{request}"), + {"position": self.resources.render_dialog(f"word_{request}"), "name": name}, private=True) else: preferred_name = user_profile["preferred_name"] or name @@ -750,10 +759,12 @@ def handle_set_my_name(self, message: Message): if all((user_profile[n] == updated_user_profile.get(n) for n in ("first_name", "middle_name", "last_name"))): self.speak_dialog("name_not_changed", - {"position": self.translate(f"word_name"), - "name": name}) + {"position": self.resources.render_dialog( + f"word_name"), + "name": name}) else: - self.update_profile({"user": updated_user_profile}, message) + update_user_profile({"user": updated_user_profile}, + message, self.bus) self.speak_dialog("name_set_full", {"nick": preferred_name, "name": name_parts["full_name"]}, @@ -773,20 +784,21 @@ def handle_say_my_language_settings(self, message: Message): primary_lang = pronounce_lang(language_settings["tts_language"]) second_lang = pronounce_lang( language_settings["secondary_tts_language"]) - self.speak_dialog("language_setting", - {"primary": self.translate("word_primary"), - "language": primary_lang, - "gender": self.translate( - f'word_{language_settings["tts_gender"]}')}, - private=True) + self.speak_dialog( + "language_setting", + {"primary": self.resources.render_dialog("word_primary"), + "language": primary_lang, + "gender": self.resources.render_dialog( + f'word_{language_settings["tts_gender"]}')}, + private=True) if second_lang and (second_lang != primary_lang or language_settings["tts_gender"] != language_settings["secondary_tts_gender"]): self.speak_dialog( "language_setting", - {"primary": self.translate("word_secondary"), + {"primary": self.resources.render_dialog("word_secondary"), "language": second_lang, - "gender": self.translate( + "gender": self.resources.render_dialog( f'word_{language_settings["secondary_tts_gender"]}')}, private=True) @@ -800,9 +812,9 @@ def handle_set_stt_language(self, message: Message): :param message: Message associated with request """ requested_lang = message.data.get('rx_language') or \ - message.data.get('request_language') + message.data.get('request_language') lang = self._parse_languages(message.data.get("utterance"))[0] or \ - requested_lang.split()[-1] + requested_lang.split()[-1] try: code, spoken_lang = self._get_lang_code_and_name(lang) except UnsupportedLanguageError as e: @@ -816,10 +828,11 @@ def handle_set_stt_language(self, message: Message): LOG.warning(f"{code} not found in: {self.stt_languages}") self.speak_dialog("language_not_supported", {"lang": spoken_lang, - "io": self.translate('word_understand')}, + "io": self.resources.render_dialog( + 'word_understand')}, private=True) return - dialog_data = {"io": self.translate("word_stt"), + dialog_data = {"io": self.resources.render_dialog("word_stt"), "lang": spoken_lang} if code == get_user_prefs(message)["speech"]["stt_language"]: self.speak_dialog("language_not_changed", dialog_data, @@ -828,7 +841,8 @@ def handle_set_stt_language(self, message: Message): if self.ask_yesno("language_change_confirmation", dialog_data) == "yes": - self.update_profile({"speech": {"stt_language": code}}) + update_user_profile({"speech": {"stt_language": code}}, + message, self.bus) self.speak_dialog("language_set", dialog_data, private=True) else: @@ -844,7 +858,7 @@ def handle_set_tts_language(self, message: Message): :param message: Message associated with request """ language = message.data.get("rx_language") or \ - message.data.get("request_language") + message.data.get("request_language") primary, secondary = \ self._parse_languages(message.data.get("utterance")) LOG.info(f"primary={primary} | secondary={secondary} | " @@ -861,17 +875,19 @@ def handle_set_tts_language(self, message: Message): f" {self.tts_languages}") self.speak_dialog("language_not_supported", {"lang": primary_spoken, - "io": self.translate('word_speak')}, + "io": self.resources.render_dialog( + 'word_speak')}, private=True) return gender = self._get_gender(primary) or \ - user_settings["speech"]["tts_gender"] - self.update_profile({"speech": {"tts_gender": gender, + user_settings["speech"]["tts_gender"] + update_user_profile({"speech": {"tts_gender": gender, "tts_language": primary_code}}, - message) + message, self.bus) self.speak_dialog("language_set", - {"io": self.translate("word_primary"), - "lang": primary_spoken}, private=True) + {"io": self.resources.render_dialog( + "word_primary"), + "lang": primary_spoken}, private=True) except UnsupportedLanguageError: LOG.warning(f"No language for primary request: {primary}") self.speak_dialog("language_not_recognized", {"lang": primary}, @@ -887,18 +903,20 @@ def handle_set_tts_language(self, message: Message): f" {self.tts_languages}") self.speak_dialog("language_not_supported", {"lang": secondary_spoken, - "io": self.translate('word_speak')}, + "io": self.resources.render_dialog( + 'word_speak')}, private=True) return gender = self._get_gender(secondary) or \ - user_settings["speech"]["secondary_tts_gender"] - self.update_profile( + user_settings["speech"]["secondary_tts_gender"] + update_user_profile( {"speech": {"secondary_tts_gender": gender, "secondary_tts_language": secondary_code}}, - message) + message, self.bus) self.speak_dialog("language_set", - {"io": self.translate("word_secondary"), - "lang": secondary_spoken}, private=True) + {"io": self.resources.render_dialog( + "word_secondary"), + "lang": secondary_spoken}, private=True) except UnsupportedLanguageError: LOG.warning(f"No language for secondary request: {secondary}") self.speak_dialog("language_not_recognized", @@ -915,17 +933,19 @@ def handle_set_tts_language(self, message: Message): LOG.warning(f"{code} not found in: {self.tts_languages}") self.speak_dialog("language_not_supported", {"lang": spoken, - "io": self.translate('word_speak')}, + "io": self.resources.render_dialog( + 'word_speak')}, private=True) return gender = self._get_gender(language) or \ - user_settings["speech"]["tts_gender"] - self.update_profile({"speech": {"tts_gender": gender, + user_settings["speech"]["tts_gender"] + update_user_profile({"speech": {"tts_gender": gender, "tts_language": code}}, - message) + message, self.bus) self.speak_dialog("language_set", - {"io": self.translate("word_primary"), - "lang": spoken}, private=True) + {"io": self.resources.render_dialog( + "word_primary"), + "lang": spoken}, private=True) except UnsupportedLanguageError: LOG.warning(f"No language for secondary request: {language}") self.speak_dialog("language_not_recognized", @@ -976,9 +996,9 @@ def handle_no_secondary_language(self, message: Message): Handle a user request to only hear responses in one language :param message: Message associated with request """ - self.update_profile({"speech": {"secondary_tts_language": "", + update_user_profile({"speech": {"secondary_tts_language": "", "secondary_neon_voice": ""}}, - message) + message, self.bus) self.speak_dialog("only_one_language", private=True) def _emit_weather_update(self, message: Message): @@ -995,6 +1015,7 @@ def _parse_languages(self, utterance: str) -> \ :param utterance: raw utterance spoken by the user :returns: spoken primary, secondary languages requested """ + def _get_rx_patterns(rx_file: str, utt: str): with open(rx_file) as f: for pat in f.read().splitlines(): @@ -1004,6 +1025,7 @@ def _get_rx_patterns(rx_file: str, utt: str): res = re.search(pat, utt) if res: return res + utterance = f"{utterance}\n" primary_tts = self.find_resource('primary_tts.rx', 'regex') secondary_tts = self.find_resource('secondary_tts.rx', 'regex') @@ -1019,7 +1041,7 @@ def _get_rx_patterns(rx_file: str, utt: str): if secondary_tts: try: secondary = _get_rx_patterns(secondary_tts, - utterance).group("rx_secondary")\ + utterance).group("rx_secondary") \ .strip() except (IndexError, AttributeError): secondary = None @@ -1075,8 +1097,9 @@ def _spoken_email(self, email_addr: str): """ Get a pronounceable email address string """ - return email_addr.replace('.', f' {self.translate("word_dot")} ')\ - .replace('@', f' {self.translate("word_at")} ') + return email_addr.replace( + '.', f' {self.resources.render_dialog("word_dot")} ') \ + .replace('@', f' {self.resources.render_dialog("word_at")} ') @staticmethod def _get_name_parts(name: str, user_profile: dict) -> dict: @@ -1138,8 +1161,12 @@ def _get_location_from_spoken_location(location: str, LOG.warning(f"Could not locate: {location}") return None if not place['address'].get("city"): - place["address"]["city"] = place["address"].get("town") or \ - place["address"].get("village") + # TODO: Delegate this to `get_full_location` + LOG.warning(f"No city specified in {place['address']}") + place['address']['city'] = place["address"].get("town") or \ + place["address"].get("village") or \ + place["address"].get("hamlet") or \ + place['address'].get('county') except AttributeError: LOG.warning(f"Could not locate: {location}") return None diff --git a/test/test_skill.py b/test/test_skill.py index d145941..5013d03 100644 --- a/test/test_skill.py +++ b/test/test_skill.py @@ -432,13 +432,14 @@ def _init_test_message(voc, location): # Change location and tz _init_test_message("location", "honolulu") + new_city = "Honolulu County" # TODO: This is semi-variable as the map API changes sleep(1) self.skill.handle_change_location_timezone(test_message) self.skill.ask_yesno.assert_called_once_with( "also_change_location_tz", {"type": "timezone", "new": "honolulu"}) self.skill.speak_dialog.assert_has_calls(( call("change_location_tz", - {"type": "location", "location": "Honolulu"}, private=True), + {"type": "location", "location": new_city}, private=True), call("change_location_tz", {"type": "timezone", "location": "UTC -10.0"}, private=True)), True) @@ -448,7 +449,7 @@ def _init_test_message(voc, location): for setting in unchanged: self.assertEqual(profile["location"][setting], test_profile["location"][setting]) - self.assertEqual(profile["location"]["city"], "Honolulu") + self.assertEqual(profile["location"]["city"], new_city) self.assertEqual(profile["location"]["state"], "Hawaii") self.assertAlmostEqual(profile["location"]["lat"], 21.2890997, 0) self.assertAlmostEqual(profile["location"]["lng"], -157.717299, 0)