def handle_read(self, message): data = {'list_name': message.data.get('list_name')} # If the user specified a list, read the items on that list if data['list_name']: # Check that the list exists if not self.db.list_exists(data['list_name']): self.speak_dialog('list.not.found', data) # Check that the list is not empty elif self.db.list_empty(data['list_name']): self.speak_dialog('no.items', data) else: data['items'] = join_list(self.db.read_items( data['list_name']), self.translate('and'), lang=self.lang) self.speak_dialog('read.items', data) # Alternatively, simply read lists names else: # Check if there are lists at all if self.db.no_lists(): self.speak_dialog('no.lists') else: lists = self.db.read_lists() data['lists_names'] = join_list(lists, self.translate('and'), lang=self.lang) data['list'] = self.plural_singular_form(lists) self.speak_dialog('read.lists', data)
def test_join_list_de(self): self.assertEqual( join_list(['Hallo', 'Auf wieder Sehen'], 'and', lang='de-de'), 'Hallo und Auf wieder Sehen') self.assertEqual(join_list(['A', 'B', 'C'], 'or', lang='de-de'), 'A, B oder C')
def test_join(self): self.assertEqual(join_list(None, "and"), "") self.assertEqual(join_list([], "and"), "") self.assertEqual(join_list(["a"], "and"), "a") self.assertEqual(join_list(["a", "b"], "and"), "a and b") self.assertEqual(join_list(["a", "b"], "or"), "a or b") self.assertEqual(join_list(["a", "b", "c"], "and"), "a, b and c") self.assertEqual(join_list(["a", "b", "c"], "or"), "a, b or c") self.assertEqual(join_list(["a", "b", "c"], "or", ";"), "a; b or c") self.assertEqual(join_list(["a", "b", "c", "d"], "or"), "a, b, c or d") self.assertEqual(join_list([1, "b", 3, "d"], "or"), "1, b, 3 or d")
def ask_selection(self, options, dialog='', data=None, min_conf=0.65, numeric=False): """Read options, ask dialog question and wait for an answer. This automatically deals with fuzzy matching and selection by number e.g. "first option" "last option" "second option" "option number four" Arguments: options (list): list of options to present user dialog (str): a dialog id or string to read AFTER all options data (dict): Data used to render the dialog min_conf (float): minimum confidence for fuzzy match, if not reached return None numeric (bool): speak options as a numeric menu Returns: string: list element selected by user, or None """ assert isinstance(options, list) if not len(options): return None elif len(options) == 1: return options[0] if numeric: for idx, opt in enumerate(options): opt_str = "{number}, {option_text}".format( number=pronounce_number(idx + 1, self.lang), option_text=opt) self.speak(opt_str, wait=True) else: opt_str = join_list(options, "or", lang=self.lang) + "?" self.speak(opt_str, wait=True) resp = self.get_response(dialog=dialog, data=data) if resp: match, score = match_one(resp, options) if score < min_conf: if self.voc_match(resp, 'last'): resp = options[-1] else: num = extract_number(resp, self.lang, ordinals=True) resp = None if num and num < len(options): resp = options[num - 1] else: resp = match return resp
def handle_status(self, message): utt = message.data.get("utterance") if not len(self.settings["alarm"]): self.speak_dialog("alarms.list.empty") return status, alarms = self._get_alarm_matches(utt, alarm=self.settings["alarm"], max_results=3, dialog='ask.which.alarm', is_response=False) total = None desc = [] if alarms: total = len(alarms) for alarm in alarms: desc.append(self._describe(alarm)) items_string = '' if desc: items_string = join_list(desc, self.translate('and')) if status == 'No Match Found': self.speak_dialog('alarm.not.found') elif status == 'User Cancelled': return elif status == 'Next': reltime = nice_relative_time( self.get_alarm_local(alarms[0]), lang=self.lang ) self.speak_dialog("next.alarm", data={"when": self._describe( alarms[0] ), "duration": reltime}) else: if total == 1: reltime = nice_relative_time( self.get_alarm_local(alarms[0]), lang=self.lang ) self.speak_dialog('alarms.list.single', data={'item': desc[0], 'duration': reltime}) else: self.speak_dialog('alarms.list.multi', data={'count': total, 'items': items_string})
def _get_speakable_timer_list(self, timer_list): """ Get timer list as speakable string """ speakable_timer_list = [] for timer in timer_list: dialog = 'timer.details' if timer['name'] is not None: dialog += '.named' ordinal = (None if timer['ordinal'] <= 1 else self._get_speakable_ordinal(timer)) if ordinal is not None: dialog += '.with.ordinal' data = {'ordinal': ordinal, 'duration': nice_duration(timer["duration"]), 'name': timer['name']} speakable_timer_list.append(self.translate(dialog, data)) names = join_list(speakable_timer_list, self.translate("and")) return names
def _recur_desc(self, recur): # Create a textual description of the recur set day_list = list(recur) day_list.sort() days = " ".join(day_list) for r in self.recurrence_dict: if self.recurrence_dict[r] == days: return r # accept the first perfect match # Assemble a long desc, e.g. "Monday and Wednesday" day_names = [] for day in days.split(" "): for r in self.recurrence_dict: if self.recurrence_dict[r] == day: day_names.append(r) break return join_list(day_names, self.translate('and'))
def test_join_list_de(self): self.assertEqual(join_list(['Hallo', 'Auf wieder Sehen'], 'and'), 'Hallo und Auf wieder Sehen') self.assertEqual(join_list(['A', 'B', 'C'], 'or'), 'A, B oder C')
def _get_alarm_matches(self, utt, alarm=None, max_results=1, dialog='ask.which.alarm', is_response=False): """Get list of alarms that match based on a user utterance. Arguments: utt (str): string spoken by the user alarm (list): list of alarm to match against max_results (int): max number of results desired dialog (str): name of dialog file used for disambiguation is_response (bool): is this being called by get_response Returns: (str): ["All", "Matched", "No Match Found", or "User Cancelled"] (list): list of matched alarm """ alarms = alarm or self.settings['alarm'] all_words = self.translate_list('all') next_words = self.translate_list('next') status = ["All", "Matched", "No Match Found", "User Cancelled", "Next"] # No alarms if alarms is None or len(alarms) == 0: self.log.error("Cannot get match. No active alarms.") return (status[2], None) # Extract Alarm Time when, utt_no_datetime = extract_datetime(utt, lang=self.lang) or (None, None) # Will return dt of unmatched string today = extract_datetime(self.texts.get('today'), lang=self.lang) today = today[0] # Check the time if it's midnight. This is to check if the user # said a recurring alarm with only the Day or if the user did # specify to set an alarm on midnight. If it's confirmed that # it's for a day only, then get another response from the user # to clarify what time on that day the recurring alarm is. is_midnight = self._check_if_utt_has_midnight(utt, when, self.threshold) if when == today and not is_midnight: when = None time_matches = None time_alarm = None if when: time_alarm = to_utc(when).timestamp() if is_midnight: time_alarm = time_alarm + 86400.0 time_matches = [a for a in alarms if abs(a["timestamp"] - time_alarm) <= 60] # Extract Recurrence recur = None recurrence_matches = None for word in self.recurrence_dict: is_match = self._fuzzy_match(word, utt.lower(), self.threshold) if is_match: recur = self._create_day_set(utt) alarm_recur = self._create_recurring_alarm(when, recur) recurrence_matches = [a for a in alarms if a["repeat_rule"] == alarm_recur["repeat_rule"] ] break utt = utt_no_datetime or utt # Extract Ordinal/Cardinal Numbers number = extract_number(utt, ordinals=True, lang=self.lang) if number and number > 0: number = int(number) else: number = None # Extract Name name_matches = [a for a in alarms if a["name"] and self._fuzzy_match(a["name"], utt, self.threshold)] # Match Everything alarm_to_match = None if when: if recur: alarm_to_match = alarm_recur else: alarm_to_match = {"timestamp": time_alarm, "repeat_rule": ""} # Find the Intersection of the Alarms list and all the matched alarms orig_count = len(alarms) if when and time_matches: alarms = [a for a in alarms if a in time_matches] if recur and recurrence_matches: alarms = [a for a in alarms if a in recurrence_matches] if name_matches: alarms = [a for a in alarms if a in name_matches] # Utterance refers to all alarms if utt and any(self._fuzzy_match(i, utt, 1) for i in all_words): return (status[0], alarms) # Utterance refers to the next alarm to go off elif utt and any(self._fuzzy_match(i, utt, 1) for i in next_words): return (status[4], [alarms[0]]) # Given something to match but no match found if ((number and number > len(alarms)) or (recur and not recurrence_matches) or (when and not time_matches)): return (status[2], None) # If number of alarms filtered were the same, assume user asked for # All alarms if (len(alarms) == orig_count and max_results > 1 and not number and not when and not recur): return (status[0], alarms) # Return immediately if there is ordinal if number and number <= len(alarms): return (status[1], [alarms[number - 1]]) # Return immediately if within maximum results elif alarms and len(alarms) <= max_results: return (status[1], alarms) # Ask for reply from user and iterate the function elif alarms and len(alarms) > max_results: desc = [] for alarm in alarms: desc.append(self._describe(alarm)) items_string = '' if desc: items_string = join_list(desc, self.translate('and')) reply = self.get_response(dialog, data={ 'number': len(alarms), 'list': items_string, }, num_retries=1) if reply: return self._get_alarm_matches(reply, alarm=alarms, max_results=max_results, dialog=dialog, is_response=True) else: return (status[3], None) # No matches found return (status[2], None)