diff --git a/README.md b/README.md index 1293b07..a4e3d69 100644 --- a/README.md +++ b/README.md @@ -42,12 +42,14 @@ Using websockets for communication allows for two way communication where the se Link for example: https://stackoverflow.com/questions/53331127/python-websockets-send-to-client-and-keep-connection-alive More examples (includes jwt authentication, though this is in node.js, still useful for figuring out how to do this stuff): https://www.linode.com/docs/guides/authenticating-over-websockets-with-jwt/ + ## Ideas * Dashboard with api call counts (would require linking into all active skills, callbacks with class inheritance maybe?) * Phone calls from Jarvis speaker * JARVIS, initiate the House Party Protocol (takeover screen and show retro style text interface, possibly showing data from dashboard) + ## Wants, but limitations prevent * *tumble weed bounces by* Oh, dear. \ No newline at end of file diff --git a/backend/skills/alarms.py b/backend/skills/alarms.py index d48df12..5eba1d9 100644 --- a/backend/skills/alarms.py +++ b/backend/skills/alarms.py @@ -10,11 +10,11 @@ import requests if __name__ == "__main__": # Handle running this script directly vs as a project from config import ntfy_url - from utility import parsetime2 + from utility import parsetime from skill import Skill else: from skills.config import ntfy_url - from skills.utility import parsetime2 + from skills.utility import parsetime from skills.skill import Skill import threading @@ -96,7 +96,7 @@ class Alarms(Skill): def run(self, query="", duration_string="", name=""): if "add" in query: # duration = time.mktime(parsetime2(duration_string).timetuple()) - duration = parsetime2(duration_string) + duration = parsetime(duration_string) self._add_alarm(duration, name) return True # Return true to indicate success if "remove" in query: @@ -110,6 +110,6 @@ class Alarms(Skill): if __name__ == "__main__": dur = Alarms() - dur.run("add", "15 seconds", "test alarm") + dur.run("add", "1 47 in the afternoon", "test alarm") # dur._add_alarm(123, "123") # dur._trigger_alarm("123") \ No newline at end of file diff --git a/backend/skills/config.py.example b/backend/skills/config.py.example index f729990..a6c09f5 100644 --- a/backend/skills/config.py.example +++ b/backend/skills/config.py.example @@ -1,4 +1,7 @@ # Copy & Rename this file to config.py and fill in data ntfy_url="" # Obtained from ntfy.sh app (choose a random string of numbers/letters for better security) deepl_api_key="" # Obtained from https://www.deepl.com/en/docs-api -google_api_key="" #Obtained from https://cloud.google.com/translate/pricing \ No newline at end of file +google_api_key="" #Obtained from https://cloud.google.com/translate/pricing +default_morning_time = "08:00" #8am +default_afternoon_time = "13:00" #1pm +default_evening_time = "18:00" #6pm \ No newline at end of file diff --git a/backend/skills/regexTimeParser.py b/backend/skills/regexTimeParser.py new file mode 100644 index 0000000..fcbae9f --- /dev/null +++ b/backend/skills/regexTimeParser.py @@ -0,0 +1,219 @@ +""" +Test Cases: + +1 in the afternoon +147 in the afternoon +1223 in the morning +1 pm +132 pm +1426 +half past 1 in the afternoon +quarter past 12 in the afternoon +tomorrow at 3 in the afternoon +tomorrow morning +yesterday morning +this afternoon +this morning +monday at 3 in the afternoon +wednesday at 930 +1030 +10 after 10 in the evening +10 before 10 in the evening +""" + +""" +Expected Test Case Outputs (run on monday sept 11 at 7:37pm): + +['11-09-2023 13:00', [['1'], [], ['afternoon'], [], [], []]] +['11-09-2023 13:47', [['147'], [], ['afternoon'], [], [], []]] +['11-09-2023 00:23', [['1223'], [], ['morning'], [], [], []]] +['11-09-2023 13:00', [['1'], [], ['pm'], [], [], []]] +['11-09-2023 13:32', [['132'], [], ['pm'], [], [], []]] +['11-09-2023 14:26', [['1426'], [], [], [], [], []]] +['11-09-2023 13:30', [['1'], [], ['afternoon'], ['half past'], [], []]] +['11-09-2023 12:15', [['12'], [], ['afternoon'], ['quarter past'], [], []]] +['12-09-2023 15:00', [['3'], [], ['afternoon'], [], ['tomorrow'], []]] +['12-09-2023 08:00', [[], [], ['morning'], [], ['tomorrow'], []]] +['10-09-2023 08:00', [[], [], ['morning'], [], ['yesterday'], []]] +['11-09-2023 13:00', [[], [], ['afternoon'], [], ['this'], []]] +['11-09-2023 08:00', [[], [], ['morning'], [], ['this'], []]] +['11-09-2023 15:00', [['3'], [], ['afternoon'], [], [], ['monday']]] +['13-09-2023 09:30', [['930'], [], [], [], [], ['wednesday']]] +['11-09-2023 10:30', [['1030'], [], [], [], [], []]] +['11-09-2023 10:10', [['10', '10'], ['after'], ['evening'], [], [], []]] +['11-09-2023 10:50', [['10', '10'], ['before'], ['evening'], [], [], []]] +""" + +""" +Regex Breakdown: + + +([0-9]+) +(before|after) +(afternoon|morning|pm|am|evening) +(half past|quarter past|quarter to) +(today|yesterday|tomorrow|tonight|this) +(monday|tuesday|wednesday|thursday|friday|saturday|sunday) + +after each word, add \b + +""" + +import re +from datetime import datetime, timedelta +import time + +if __name__ == '__main__': + from config import default_morning_time, default_afternoon_time, default_evening_time +else: + from skills.config import default_morning_time, default_afternoon_time, default_evening_time + + +morning_datetime = datetime.strptime(default_morning_time, "%H:%M") +afternoon_datetime = datetime.strptime(default_afternoon_time, "%H:%M") +evening_datetime = datetime.strptime(default_evening_time, "%H:%M") + + +dayofweek_to_number = { + "monday": 0, + "tuesday": 1, + "wednesday": 2, + "thursday": 3, + "friday": 4, + "saturday": 5, + "sunday": 6 +} + +class RegexTimeParser: + def __init__(self): + self.regex_string = r"([0-9]+)\b|(before\b|after\b)|(afternoon\b|morning\b|pm\b|am\b|evening\b)|(half past\b|quarter past\b||quarter to\b)|(today\b|yesterday\b|tomorrow\b|tonight\b|this\b)|(monday\b|tuesday\b|wednesday\b|thursday\b|friday\b|saturday\b|sunday\b)" + # self.regex_string = "([0-9]+)|(before|after)|(afternoon|morning|pm|am|evening)|(half past|quarter past)|(today|yesterday|tomorrow|tonight)|(monday|tuesday|wednesday|thursday|friday|saturday|sunday)" + self.regex_exp = re.compile(self.regex_string, re.IGNORECASE) + + def _merge_findall(self, list_of_lists): + out = [[], [], [], [], [], []] + for pos in range(6): + for group in list_of_lists: + if group[pos] != '': + out[pos].append(group[pos]) + return out + + def _parse_time_string(self, phrase): + """Takes time string and parses hours/minutes into dict""" + hours = 0 + minutes = 0 + match len(phrase): + case 1: + #hour + hours = int(phrase) + case 2: + #hour + hours = int(phrase) + case 3: + #hour first digit, last two digits minutes + hours = int(phrase[:1]) + minutes = int(phrase[1:3]) + case 4: + #hour first two digits, last two digits minutes + hours = int(phrase[:2]) + minutes = int(phrase[2:4]) + + return {"hours": hours, "minutes": minutes} + + def parse_time(self, phrase): + + matches = self._merge_findall(self.regex_exp.findall(phrase)) + date = datetime.now() + + if matches[2]: + if "afternoon" in matches[2]: + #default afternoon time from config + date = date.replace(hour=afternoon_datetime.hour, minute=afternoon_datetime.minute) + elif "evening" in matches[2]: + #default evening time from config + date = date.replace(hour=evening_datetime.hour, minute=evening_datetime.minute) + elif "pm" in matches[2]: + #default afternoon time from config + date = date.replace(hour=afternoon_datetime.hour, minute=afternoon_datetime.minute) + elif "morning" in matches[2]: + #default morning time from config + date = date.replace(hour=morning_datetime.hour, minute=morning_datetime.minute) + elif "am" in matches[2]: + #default morning time from config + date = date.replace(hour=morning_datetime.hour, minute=morning_datetime.minute) + + + if len(matches[0]) > 1: #uses only first two number groups ([0-9]+) + t = self._parse_time_string(matches[0][0]) + t2 = self._parse_time_string(matches[0][1]) + if "after" in matches[1]: + date = date.replace(hour = t2['hours'], minute = int(matches[0][1])) + elif "before" in matches[1]: + date = date.replace(hour = t['hours'], minute = 60-int(matches[0][0])) + + elif len(matches[0]) > 0: #([0-9]+) + t = self._parse_time_string(matches[0][0]) + if "afternoon" in matches[2] or "evening" in matches[2] or "pm" in matches[2]: + if t["hours"] < 12: + t["hours"] = t["hours"]+12 + elif "morning" in matches[2] or "am" in matches[2]: + if t["hours"] >= 12: + t["hours"] = t["hours"]-12 + date = date.replace(hour = t["hours"], minute = t["minutes"]) + + + if matches[3]: #(half past|quarter past|quarter to) + match matches[3][0]: + case "quarter past": + date = date.replace(minute = 15) + case "half past": + date = date.replace(minute = 30) + case "quarter to": + date = date.replace(minute = 45) + + + if matches[4]: #(today|yesterday|tomorrow|tonight|this) + match matches[4][0]: + case "yesterday": + yesterday_date = datetime.now() - timedelta(days = 1) + # print(yesterday_date) + # print(timedelta(1)) + # print(datetime.now()) + date = date.replace(day=yesterday_date.day, month=yesterday_date.month, year=yesterday_date.year) + case "tomorrow": + tomorrow_date = datetime.now() + timedelta(1) + date = date.replace(day=tomorrow_date.day, month=tomorrow_date.month, year=tomorrow_date.year) + + if matches[5]: #(monday|tuesday|wednesday|thursday|friday|saturday|sunday) + date = date + timedelta(days = (dayofweek_to_number[matches[5][0]] - date.weekday() + 7) % 7) + + return [date.strftime("%d-%m-%Y %H:%M"), matches] + + +if __name__ == '__main__': + test = RegexTimeParser() + + test_phrases = [ + "1 in the afternoon", #done + "147 in the afternoon", #done + "1223 in the morning", #done + "1 pm", #done + "132 pm", #done + "1426", #done + "half past 1 in the afternoon", #done + "quarter past 12 in the afternoon", #done + "tomorrow at 3 in the afternoon", #done + "tomorrow morning", #done + "yesterday morning", #done + "this afternoon", #done + "this morning", #done + "monday at 3 in the afternoon", #done + "wednesday at 930", #done + "1030", #done + "10 after 10 in the evening", #done + "10 before 10 in the evening" #done + ] + + + for ph in test_phrases: + print(test.parse_time(ph)) diff --git a/backend/skills/translations.py b/backend/skills/translations.py index bc56980..7c6f196 100644 --- a/backend/skills/translations.py +++ b/backend/skills/translations.py @@ -8,6 +8,12 @@ Reading material for this: https://www.deepl.com/en/docs-api https://cloud.google.com/translate/docs/overview + +offline local: +https://github.com/argosopentech/argos-translate + +use deep-translator to access both deepl and google translate and more +https://github.com/nidhaloff/deep-translator """ class Translations(Skill): diff --git a/backend/skills/utility.py b/backend/skills/utility.py index 6b83e78..5ae2bbc 100644 --- a/backend/skills/utility.py +++ b/backend/skills/utility.py @@ -1,9 +1,23 @@ from ctparse import ctparse #Used for parsing time (parsetime), https://github.com/comtravo/ctparse import parsedatetime #Used for parsing time (parsetime2), https://github.com/bear/parsedatetime +import HumanTime +import arrow +import natural_time from datetime import datetime import time +""" +Reading Material: + +https://github.com/nltk/nltk_contrib/blob/95d1806e2f4e89e960b76a685b1fba2eaa7d5142/nltk_contrib/timex.py#L29 +This has a bunch of regex expressions for NLP of time, might be able to modify this + parsedatetime + ctparse into my +own custom implementation that does everything I want. Maybe if it works well enough I can post it to pypi as its own +module and github for contributions. + +""" + + def parsetime(phrase): """ Takes in natrual language time phrase, outputs datetime object @@ -28,12 +42,40 @@ def parsetime2(phrase): """ time_struct, parse_status = parsedatetime.Calendar().parse(phrase) - print(time_struct, parse_status) + # print(time_struct, parse_status) return datetime(*time_struct[:6]) + +def parsetime3(phrase): + return HumanTime.parseTime(phrase) + +def parsetime4(phrase): + return arrow.utcnow().dehumanize(phrase) + +def parsetime5(phrase): + return natural_time.natural_time(phrase) + + if __name__ == "__main__": - t = parsetime('May 5th in the afternoon') - print(t) + # t = parsetime('May 5th in the afternoon') + # print(t) + + # t55 = parsetime('set an alarm for 2:30 in the afternoon') + # print(t55) + + # t66 = parsetime('147 in the afternoon') + # print(t66) + # t67 = parsetime2('147 in the afternoon') + # print(t67) + # t68 = parsetime2('147 after noon') + # print(t68) + t88 = parsetime('one forty seven in the afternoon') + print(t88) + # t89 = parsetime5('147 in the afternoon') + # print(t89) + # t90 = parsetime5('147 after noon') + # print(t90) + # print(time.mktime(t67.timetuple())) # t5 = parsetime('in 5 minutes 30 seconds') # print(t5) @@ -45,15 +87,15 @@ if __name__ == "__main__": # print(t2.resolution) - t2 = parsetime2('now') - print(time.mktime(t2.timetuple())) + # t2 = parsetime2('now') + # print(time.mktime(t2.timetuple())) - t3 = parsetime2('in 5 minutes 30 seconds') - print(time.mktime(t3.timetuple())) + # t3 = parsetime2('in 5 minutes 30 seconds') + # print(time.mktime(t3.timetuple())) - t4 = parsetime2('4 in the afternoon') - print(time.mktime(t4.timetuple())) - print(t4) + # t4 = parsetime2('4 in the afternoon') + # print(time.mktime(t4.timetuple())) + # print(t4) # print(time.strftime("%H:%M:%S", t3.timetuple())) # for x in t: