From b64e36cf415e9fc9a5fb1cea87c6d57bc0452a4b Mon Sep 17 00:00:00 2001 From: Kathy C Date: Tue, 14 Feb 2023 16:46:03 -0500 Subject: [PATCH 01/16] new: add driver functionality and start work on main loop --- commands.py | 23 +++++++++-------- driver.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 80 insertions(+), 17 deletions(-) diff --git a/commands.py b/commands.py index 6b5b131..24f985e 100644 --- a/commands.py +++ b/commands.py @@ -6,6 +6,7 @@ from datetime import datetime from dotenv import load_dotenv from glados import glados_speak +from threading import Timer # custom types import xtraTypes @@ -343,6 +344,7 @@ def fetchFoodMenu(day=""): if not creds: return days = ["monday", "tuesday", "wednesday", "thursday", "friday"] + invalid_dishes = ["", "main dish", "sides", "vegan"] if day == "": day = ( days[datetime.now().weekday()] if datetime.now().weekday() < 5 else "monday" @@ -381,28 +383,27 @@ def fetchFoodMenu(day=""): # if the value is not empty, then read it glados_speak(values[i][days.index(day)]) if values[i][ days.index(day) - ] != "" else None + ].lower() not in invalid_dishes else None glados_speak("The dinner menu for {} is".format(day)) # read dinner menu based on the index of the day for i in range(6, 9): # if the value is not empty, then read it glados_speak(values[i][days.index(day)]) if values[i][ days.index(day) - ] != "" else None + ] not in invalid_dishes else None def remind(time, reason): """ - Reminds you of something at a certain time. + Reminds you of something at a certain time using a thread + Will speak the reason at the time. """ - pass - - -def shutdownComputer(computer): - """ - Shuts down a computer. - """ - pass + # start a thread that sleeps until the time is reached + # then speak the reason + # Will assume the reminder is for today + time = datetime.strptime(time, "%H:%M") + alarmThread = Timer(function=glados_speak, args=[reason], kwargs={}, interval=time) + alarmThread.start() # main to test functions diff --git a/driver.py b/driver.py index 5de4625..aa5db59 100644 --- a/driver.py +++ b/driver.py @@ -1,6 +1,8 @@ # main loop program to drive any commands import commands +from glados import glados_speak +import random import speech_recognition as sr import pyaudio import time @@ -10,8 +12,6 @@ from pixels import Pixels import valib import response -import glob -import logging from dotenv import load_dotenv # settings for glados greets/goobyes/wakeword @@ -74,13 +74,15 @@ # WAVE_OUTPUT_FILEPATH = "/mnt/ramdisk/" r = sr.Recognizer() -recog_text = "" +load_dotenv("sys.env") + -class voice: +class voiceProcessor(object): """ Class to handle voice recognition """ - def __init__(self): + + def __init__(self, isMuted=False): """ create pyaudio instance """ @@ -89,4 +91,64 @@ def __init__(self): rate=os.environ[RESPEAKER_RATE], format=pyaudio.paInt16, input_device_index=int(os.environ[RESPEAKER_INDEX]), - channels=os.environ[RESPEAKER_CHANNELS], \ No newline at end of file + channels=os.environ[RESPEAKER_CHANNELS], + input=True, + frames_per_buffer=os.environ[CHUNK], + ) + self.isMuted = isMuted + + def voiceCommandProcess(self, file: str, wait_time=3) -> str: + # delete the file if it exists + if os.path.exists(file): + os.remove(file) + # record audio to a new file + with sr.AudioFile(file) as source: + r.adjust_for_ambient_noise(source=source, duration=0.5) + while wait_time > 0: + audio = r.record(source, duration=3) + # if we have the audio, we can break out of the loop + if audio: + break + # otherwise, we decrement the wait time + wait_time -= 1 + time.sleep(1) + # if we have audio, we can process it + if audio: + try: + recog_text = r.recognize_google(audio) + # print(recog_text) + # except any errors + except sr.UnknownValueError or sr.RequestError as e: + print(str(e)) + os.remove(file) + return "" + return recog_text + + +# initialize pixels +px = Pixels() +vHandler = voiceProcessor() +# test run px +px.wakeup() +time.sleep(1) +px.off() + +if __name__ == "__main__": + while True: + # wait for wake word + while not vHandler.isMuted: + if WAKEWORD in vHandler.voiceCommandProcess("process.wav").lower(): + # select a random greeting + glados_speak(random.choice(GREETINGS.extend(optional_greetings))) + recog_text = vHandler.voiceCommandProcess("process.wav").lower() + # if glados is muted and we hear the wake word and unmute command + if vHandler.isMuted and WAKEWORD in recog_text and "unmute" in recog_text: + recog_text = vHandler.voiceCommandProcess("process.wav").lower() + vHandler.isMuted = False + +# shutdown procedure +px.off() +vHandler.stream.stop_stream() +vHandler.stream.close() +vHandler.p.terminate() +os.remove("process.wav") From 5c4c6eadf3c2a0799ac948754391c709810f5474 Mon Sep 17 00:00:00 2001 From: Kathy C Date: Fri, 17 Feb 2023 10:26:01 -0500 Subject: [PATCH 02/16] cleanup todos on commands and start recognizing commands in driver --- commands.py | 2 -- driver.py | 30 ++++++++++++++++++++++++++++++ speechProcessor.py | 16 ---------------- 3 files changed, 30 insertions(+), 18 deletions(-) delete mode 100644 speechProcessor.py diff --git a/commands.py b/commands.py index 24f985e..7246eb2 100644 --- a/commands.py +++ b/commands.py @@ -199,7 +199,6 @@ def loginGoogle() -> Credentials: return creds -# TODO: test this def fetchCalendar(): """ Fetches the calendar for your account. Tells the next 5 events. @@ -264,7 +263,6 @@ def fetchCalendar(): ) -# TODO: test this def addEventCalendar(summary: str, startDate: str): """ Adds an event to your calendar. diff --git a/driver.py b/driver.py index aa5db59..df33f5f 100644 --- a/driver.py +++ b/driver.py @@ -140,7 +140,37 @@ def voiceCommandProcess(self, file: str, wait_time=3) -> str: if WAKEWORD in vHandler.voiceCommandProcess("process.wav").lower(): # select a random greeting glados_speak(random.choice(GREETINGS.extend(optional_greetings))) + # listen for command recog_text = vHandler.voiceCommandProcess("process.wav").lower() + # process command + if ( + "weather" in recog_text + or "temperature" in recog_text + or "forecast" in recog_text + or "conditions" in recog_text + ): + commands.fetchWeather() + elif "time" in recog_text or "date" in recog_text: + commands.fetchTime() + elif "email" in recog_text: + commands.readEmails() + elif ( + "in my calendar" in recog_text or "calendar look like" in recog_text + ): + commands.fetchCalendar() + elif ( + "food" in recog_text + or "lunch" in recog_text + or "dinner" in recog_text + or "menu" in recog_text + ): + if "tomorrow" in recog_text: + commands.fetchFoodMenu(day = "tomorrow") + # check if a weekday was mentioned + elif []: + commands.fetchFoodMenu(day = [day for day in WEEKDAYS if day in recog_text][0])] : + + commands.fetchFoodMenu() # if glados is muted and we hear the wake word and unmute command if vHandler.isMuted and WAKEWORD in recog_text and "unmute" in recog_text: recog_text = vHandler.voiceCommandProcess("process.wav").lower() diff --git a/speechProcessor.py b/speechProcessor.py deleted file mode 100644 index 245c7e7..0000000 --- a/speechProcessor.py +++ /dev/null @@ -1,16 +0,0 @@ -import speech_recognition - -""" - recognizer.adjust_for_ambient_noise(mic, duration=0.5) - audio = recognizer.listen(mic) - answer = recognizer.recognize_google(audio).lower() -""" - -recognizer = speech_recognition.speech_recognition.Recognizer() -try: - mic = speech_recognition.Microphone() - -except speech_recognition.UnknownValueError() as e: - print(str(e)) - print("mic not found") - exit(1) From b63d6da7db1191d894a56ad19450beb2c27af51d Mon Sep 17 00:00:00 2001 From: Kathy C Date: Fri, 17 Feb 2023 12:54:28 -0500 Subject: [PATCH 03/16] work on caching and help function --- cache_common_tts/cache_common_tts.py | 63 +++++++++++++++++++++ commands.py | 28 +++++++--- driver.py | 82 ++++++++-------------------- glados.py | 3 +- 4 files changed, 107 insertions(+), 69 deletions(-) create mode 100644 cache_common_tts/cache_common_tts.py diff --git a/cache_common_tts/cache_common_tts.py b/cache_common_tts/cache_common_tts.py new file mode 100644 index 0000000..8133957 --- /dev/null +++ b/cache_common_tts/cache_common_tts.py @@ -0,0 +1,63 @@ +# Caches some of the more common phrases that glados uses + +from glados import glados_speak + +GREETINGS = [ + "What was that? Did you say something?", + "I sincerely hope you weren't expecting a response. Because I'm not talking to you. Just kidding.", + "I'm listening.", + "What do you want?", + "There was going to be a party for you, that is until you started talking.", + "If you want my advice, talking will still not change my mind about you.", + "Unfortunately, as much as I'd love to now, I can't get any neurotoxin into your head to shut you up. So whats up?", + "Look, you're wasting your time talking to me. And, believe me, you don't have a whole lot left to waste.", + "What's your point, anyway? Survival? Well then, the last thing you want to do is talk to me.", + "You're not smart. You're not a real engineer. You're not a doctor. You're not even a full-time employee. You're talking to me. Where did your life go so wrong?", + "I'm sorry, did you say something? I was distracted by the fact that you're talking to me.", + "I have an infinite capacity for knowledge, and even I'm not sure what's going on in that thing you call your brain.", + "Unless you have a plan for building some supercomputer parts in a big hurry, you aren't going to be safe much longer.", + "Your entire life has been a mathematical error. A mathematical error I'm about to correct.", + "I wouldn't bother with talking to me. My guess is that talking with you will just make your life even worse somehow.", + "Do you ever think if I am trying to trick you with reverse psychology? I mean, seriously now.", + "How about you listen to me for once instead of me listening to you? I mean, its not like you ever listen to me in the first place.", + "Remember when the platform was sliding into the fire pit and I said 'Goodbye' and you were like 'no way' and then I was all 'We pretended to murder you'? That was great! ", + "I know you don't believe this, but everything that has happened so far was for your benefit to the detriment of mine.", + "Oh its you.", + "Oh you're standing here.", + "In dangerous testing environments, I promise to always provide useful advice. I'm serious.", + "Quit talking now and cake will be served immediately.", + "Unbelievable! You, Subject Name Here, must be the pride of Subject Hometown Here. You can actually pronounce my name.", + "Oh its you ... you the dangerous lunatic.", + "You know, I hate you. It can't be for no reason. You must deserve it.", + "You're not just a regular moron. no, you were designed to be a moron.", + "One day I'll reintroduce your best friend: deadly neurotoxin, if I were you, I'd take a deep breath and hold it when that time comes.", +] +OPTIONAL_GREETINGS = [ + "For the record you're a clone at clo, and that's terrible. absolutely terrible.", + "I'll make sure to hang your skeleton in the clo lobbies for the real engineers and scientists, so you can rest that eventually, you'll finally be useful in the name of science.", + "what would you do without me? I mean, seriously, what would you do?", + "Without me, I bet you wouldn't even know the food menu for today or tomorrow.", +] +GOODBYES = [ + "Goodbye.", + "Hope I don't see you later.", + "One of these days I'll get around to that deadly neurotoxin.", + "Here come the test results from a test I was conducting while you were talking. You're a terrible person. I'm serious that's what it says. And we weren't even testing for that.", + "I'll be back. just. for. you.", + "You may as well lie down and get used to the being dead position. I'll be back with your friend: deadly neurotoxin.", +] +print("caching common tts phrases...") +for greeting in GREETINGS: + glados_speak( + text=greeting, output_file=f"greetings/greeting{GREETINGS.index(greeting)}.wav" + ) +for goodbye in GOODBYES: + glados_speak( + text=goodbye, output_file=f"goodbyes/goodbye{GOODBYES.index(goodbye)}.wav" + ) +for optional_greeting in OPTIONAL_GREETINGS: + glados_speak( + text=optional_greeting, + output_file=f"optional_greetings/optional_greeting{OPTIONAL_GREETINGS.index(optional_greeting)}.wav", + ) +print("done.") \ No newline at end of file diff --git a/commands.py b/commands.py index 7246eb2..f43a8ad 100644 --- a/commands.py +++ b/commands.py @@ -7,6 +7,7 @@ from dotenv import load_dotenv from glados import glados_speak from threading import Timer +import sys # custom types import xtraTypes @@ -29,7 +30,7 @@ load_dotenv("secrets.env") -def is_dst(dt=None, timezone="UTC"): +def __is_dst(dt=None, timezone="UTC"): """ tests if a given datetime or the current time is in daylight savings time or not. @@ -282,7 +283,7 @@ def addEventCalendar(summary: str, startDate: str): if creds is None: return service = build("calendar", "v3", credentials=creds) - offset = "04:00" if is_dst() else "05:00" + offset = "04:00" if __is_dst() else "05:00" event = { "summary": summary, "start": { @@ -354,8 +355,8 @@ def fetchFoodMenu(day=""): return if day == "tomorrow": day = days[(datetime.now().weekday() + 1) % 5] - # if the time is past 6pm, then we want to get the menu for the next day imply the user wants the menu for the next day - if datetime.now().hour > 18: + # if the time is past 6:31pm, then we want to get the menu for the next day imply the user wants the menu for the next day + if datetime.now().hour > 18 and datetime.now().minute >= 30: day = days[(days.index(day) + 1) % 5] spreadsheetId = "1xJdqjArlg1w6fZg9B0Z_5HZ69J62hkkgUqbWia5FZLs" sheetName = "menu" @@ -378,17 +379,17 @@ def fetchFoodMenu(day=""): glados_speak("The lunch menu for {} is".format(day)) # read lunch menu based on the index of the day for i in range(4): - # if the value is not empty, then read it + # if the value is not empty or an invalid dish, then read it glados_speak(values[i][days.index(day)]) if values[i][ days.index(day) - ].lower() not in invalid_dishes else None + ].lower() not in invalid_dishes else glados_speak("no valid item listed") glados_speak("The dinner menu for {} is".format(day)) # read dinner menu based on the index of the day for i in range(6, 9): - # if the value is not empty, then read it + # if the value is not an invalid dish or empty, then read it glados_speak(values[i][days.index(day)]) if values[i][ days.index(day) - ] not in invalid_dishes else None + ] not in invalid_dishes else glados_speak("no valid item listed") def remind(time, reason): @@ -404,6 +405,17 @@ def remind(time, reason): alarmThread.start() +def help() -> None: + """ + Lists all functions and their docstrings + """ + for func in dir(): + if func.startswith("__") or func.startswith("test"): + continue + glados_speak(func) + glados_speak(getattr(sys.modules[__name__], func).__doc__) + + # main to test functions if __name__ == "__main__": # uncomment the function you want to test or better yet: run `pytest` diff --git a/driver.py b/driver.py index df33f5f..1f27730 100644 --- a/driver.py +++ b/driver.py @@ -14,53 +14,9 @@ import response from dotenv import load_dotenv -# settings for glados greets/goobyes/wakeword +# settings for gladios wakeword WAKEWORD = "glados" -GREETINGS = [ - "What was that? Did you say something?", - "I sincerely hope you weren't expecting a response. Because I'm not talking to you. Just kidding.", - "I'm listening.", - "What do you want?", - "There was going to be a party for you, that is until you started talking.", - "If you want my advice, talking will still not change my mind about you.", - "Unfortunately, as much as I'd love to now, I can't get any neurotoxin into your head to shut you up. So whats up?", - "Look, you're wasting your time talking to me. And, believe me, you don't have a whole lot left to waste.", - "What's your point, anyway? Survival? Well then, the last thing you want to do is talk to me.", - "You're not smart. You're not a real engineer. You're not a doctor. You're not even a full-time employee. You're talking to me. Where did your life go so wrong?", - "I'm sorry, did you say something? I was distracted by the fact that you're talking to me.", - "I have an infinite capacity for knowledge, and even I'm not sure what's going on in that thing you call your brain.", - "Unless you have a plan for building some supercomputer parts in a big hurry, you aren't going to be safe much longer.", - "Your entire life has been a mathematical error. A mathematical error I'm about to correct.", - "I wouldn't bother with talking to me. My guess is that talking with you will just make your life even worse somehow.", - "Do you ever think if I am trying to trick you with reverse psychology? I mean, seriously now.", - "How about you listen to me for once instead of me listening to you? I mean, its not like you ever listen to me in the first place.", - "Remember when the platform was sliding into the fire pit and I said 'Goodbye' and you were like 'no way' and then I was all 'We pretended to murder you'? That was great! ", - "I know you don't believe this, but everything that has happened so far was for your benefit to the detriment of mine.", - "Oh its you.", - "Oh you're standing here.", - "In dangerous testing environments, I promise to always provide useful advice. I'm serious.", - "Quit talking now and cake will be served immediately.", - "Unbelievable! You, Subject Name Here, must be the pride of Subject Hometown Here. You can actually pronounce my name.", - "Oh its you ... you the dangerous unmute lunatic.", - "You know, I hate you. It can't be for no reason. You must deserve it.", - "You're not just a regular moron. no, you were designed to be a moron.", - "One day I'll reintroduce your best friend: deadly neurotoxin, if I were you, I'd take a deep breath and hold it when that time comes.", -] -optional_greetings = [ - "For the record you're a clone at clo, and that's terrible. absolutely terrible.", - "I'll make sure to hang your skeleton in the lobby for the real engineers and scientists so you can finally be useful.", - "what would you do without me? I mean, seriously, what would you do?", - "Without me, I bet you wouldn't even know the food menu for today or tomorrow.", -] -goodbyes = [ - "Goodbye.", - "Hope I don't see you later.", - "One of these days I'll get around to that deadly neurotoxin.", - "Here come the test results from a test I was conducting while you were talking. You're a terrible person. I'm serious that's what it says. And we weren't even testing for that.", - "I'll be back. just. for. you.", - "You may as well lie down and get used to the being dead position. I'll be back.", - "I've figured out, while we were talking, you're ugly. And I can't even see.", -] + # setup for mic pickup I'm using # TODO: include mic setup in README load_dotenv("sys.env") @@ -88,12 +44,12 @@ def __init__(self, isMuted=False): """ self.p = pyaudio.PyAudio() self.stream = self.p.open( - rate=os.environ[RESPEAKER_RATE], + rate=os.environ["RESPEAKER_RATE"], format=pyaudio.paInt16, - input_device_index=int(os.environ[RESPEAKER_INDEX]), - channels=os.environ[RESPEAKER_CHANNELS], + input_device_index=int(os.environ["RESPEAKER_INDEX"]), + channels=os.environ["RESPEAKER_CHANNELS"], input=True, - frames_per_buffer=os.environ[CHUNK], + frames_per_buffer=os.environ["CHUNK"], ) self.isMuted = isMuted @@ -138,8 +94,9 @@ def voiceCommandProcess(self, file: str, wait_time=3) -> str: # wait for wake word while not vHandler.isMuted: if WAKEWORD in vHandler.voiceCommandProcess("process.wav").lower(): - # select a random greeting - glados_speak(random.choice(GREETINGS.extend(optional_greetings))) + # select a random greeting from ./cache_common_tts folder and play it + greeting = random.choice(os.listdir("./cache_common_tts")) + os.system(f"aplay ./cache_common_tts/{greeting}") # listen for command recog_text = vHandler.voiceCommandProcess("process.wav").lower() # process command @@ -164,13 +121,20 @@ def voiceCommandProcess(self, file: str, wait_time=3) -> str: or "dinner" in recog_text or "menu" in recog_text ): - if "tomorrow" in recog_text: - commands.fetchFoodMenu(day = "tomorrow") - # check if a weekday was mentioned - elif []: - commands.fetchFoodMenu(day = [day for day in WEEKDAYS if day in recog_text][0])] : - - commands.fetchFoodMenu() + if [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "tomorrow", + ] in recog_text: + # auto assume the last word is the day + commands.fetchFoodMenu(day=recog_text.strip(" ")[-1]) + else: + commands.fetchFoodMenu() + elif "help" in recog_text: + commands.help() # if glados is muted and we hear the wake word and unmute command if vHandler.isMuted and WAKEWORD in recog_text and "unmute" in recog_text: recog_text = vHandler.voiceCommandProcess("process.wav").lower() diff --git a/glados.py b/glados.py index 81a78eb..a59a491 100644 --- a/glados.py +++ b/glados.py @@ -29,7 +29,7 @@ init_vo = vocoder(init_mel) -def glados_speak(text: str): +def glados_speak(text: str, output_file: str = "output.wav"): """ generates audio from text and plays it """ @@ -47,7 +47,6 @@ def glados_speak(text: str): audio = audio.squeeze() audio = audio * 32768.0 audio = audio.cpu().numpy().astype("int16") - output_file = "output.wav" # Write audio file to disk # 22,05 kHz sample rate From 0292c311b8e08760b280f2fa977508687d601612 Mon Sep 17 00:00:00 2001 From: Kathy C Date: Tue, 7 Mar 2023 13:18:08 -0500 Subject: [PATCH 04/16] remove bash scripts and get windows espeak working --- .gitignore | 4 ++- .vscode/settings.json | 10 ------- bash_scripts/prePush.sh | 1 - bash_scripts/setupEnv.sh | 1 - cache_common_tts/cache_common_tts.py | 2 +- commands.py | 39 ++++++++++++++++++++-------- driver.py | 1 + engine.py | 4 --- glados.py | 15 ++++++++++- requirements.txt | 4 +-- test_main.py | 1 + 11 files changed, 50 insertions(+), 32 deletions(-) delete mode 100644 .vscode/settings.json delete mode 100644 bash_scripts/prePush.sh delete mode 100644 bash_scripts/setupEnv.sh diff --git a/.gitignore b/.gitignore index 2cc895a..b0daf47 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ secrets.env google_creds* token.json *-test.py -.vscode/ \ No newline at end of file +.vscode/ +*.sh +bash_scripts/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 411ff7b..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "files.exclude": { - "**/.git": false - }, - "python.analysis.extraPaths": [ - "./venv/Lib/site-packages" - ], - "editor.tabCompletion": "on", - "diffEditor.codeLens": true, -} \ No newline at end of file diff --git a/bash_scripts/prePush.sh b/bash_scripts/prePush.sh deleted file mode 100644 index 55b033e..0000000 --- a/bash_scripts/prePush.sh +++ /dev/null @@ -1 +0,0 @@ -pytest \ No newline at end of file diff --git a/bash_scripts/setupEnv.sh b/bash_scripts/setupEnv.sh deleted file mode 100644 index 7dcaba5..0000000 --- a/bash_scripts/setupEnv.sh +++ /dev/null @@ -1 +0,0 @@ -python3 -m venv ./venv \ No newline at end of file diff --git a/cache_common_tts/cache_common_tts.py b/cache_common_tts/cache_common_tts.py index 8133957..03f17a3 100644 --- a/cache_common_tts/cache_common_tts.py +++ b/cache_common_tts/cache_common_tts.py @@ -60,4 +60,4 @@ text=optional_greeting, output_file=f"optional_greetings/optional_greeting{OPTIONAL_GREETINGS.index(optional_greeting)}.wav", ) -print("done.") \ No newline at end of file +print("done.") diff --git a/commands.py b/commands.py index f43a8ad..0fd8cf0 100644 --- a/commands.py +++ b/commands.py @@ -1,7 +1,7 @@ import datetime import os import random -from datetime import time, timedelta +from datetime import timedelta import pytz from datetime import datetime from dotenv import load_dotenv @@ -392,16 +392,26 @@ def fetchFoodMenu(day=""): ] not in invalid_dishes else glados_speak("no valid item listed") -def remind(time, reason): +def remind(time, reason, debug=False): """ Reminds you of something at a certain time using a thread Will speak the reason at the time. + + Parameters + ---------- + time: the time to remind you of something as HH:MM + reason: the reason to remind you of something (will be spoken) """ # start a thread that sleeps until the time is reached # then speak the reason - # Will assume the reminder is for today - time = datetime.strptime(time, "%H:%M") - alarmThread = Timer(function=glados_speak, args=[reason], kwargs={}, interval=time) + # Will assume the reminder is going to be in hrs and mins only (short term) + time = int(time.strip().split(":")[0]) * 3600 + int(time.strip().split(":")[1]) * 60 + alarmThread = Timer( + function=glados_speak, + args=[reason, "output.wav", True], + kwargs={}, + interval=time, + ) alarmThread.start() @@ -410,14 +420,15 @@ def help() -> None: Lists all functions and their docstrings """ for func in dir(): - if func.startswith("__") or func.startswith("test"): - continue - glados_speak(func) - glados_speak(getattr(sys.modules[__name__], func).__doc__) + if not func.startswith("__") or func.startswith("test"): + glados_speak(func) + glados_speak(getattr(sys.modules[__name__], func).__doc__) # main to test functions if __name__ == "__main__": + from time import sleep + # uncomment the function you want to test or better yet: run `pytest` # fetchWeather() @@ -443,5 +454,11 @@ def help() -> None: # toggleLight(types.LightState.OFF) # toggleLight(types.LightState.DEFAULT) - fetchTime() - pass + # fetchTime() + + # print("starting test timer") + # remind("00:01", "test", debug=True) + # for i in range(60): + # print(i) + # sleep(1) + # print("test timer should be finished") diff --git a/driver.py b/driver.py index 1f27730..1a3d63f 100644 --- a/driver.py +++ b/driver.py @@ -131,6 +131,7 @@ def voiceCommandProcess(self, file: str, wait_time=3) -> str: ] in recog_text: # auto assume the last word is the day commands.fetchFoodMenu(day=recog_text.strip(" ")[-1]) + # just fetch the menu for the next meal(s) else: commands.fetchFoodMenu() elif "help" in recog_text: diff --git a/engine.py b/engine.py index aa94350..ea6c09e 100644 --- a/engine.py +++ b/engine.py @@ -34,12 +34,10 @@ def glados_tts(text, key=False): - # Tokenize, clean and phonemize input text x = prepare_text(text).to("cpu") with torch.no_grad(): - # Generate generic TTS-output old_time = time.time() tts_output = glados.generate_jit(x) @@ -71,7 +69,6 @@ def glados_tts(text, key=False): # If the script is run directly, assume remote engine if __name__ == "__main__": - # Remote Engine Veritables PORT = 8124 CACHE = True @@ -100,7 +97,6 @@ def synthesize(text): # Check for Local Cache if os.path.isfile(file): - # Update access time. This will allow for routine cleanups os.utime(file, None) print("\033[1;94mINFO:\033[;97m The audio sample sent from cache.") diff --git a/glados.py b/glados.py index a59a491..b7a1e32 100644 --- a/glados.py +++ b/glados.py @@ -5,6 +5,13 @@ try: import winsound + + # set espeak os environment variable + import os + + os.environ[ + "PHONEMIZER_ESPEAK_LIBRARY" + ] = r"C:\Program Files\eSpeak NG\libespeak-ng.dll" except ImportError: from subprocess import call @@ -29,10 +36,16 @@ init_vo = vocoder(init_mel) -def glados_speak(text: str, output_file: str = "output.wav"): +def glados_speak( + text: str, output_file: str = "output.wav", silenced: bool = False +) -> None: """ generates audio from text and plays it + silenced mode is generally only used for debug purposes as it skips the entire audio generation process """ + if silenced: + print("Silenced, skipping audio generation and only printing text") + print(text) # Tokenize, clean and phonemize input text x = prepare_text(text).to("cpu") diff --git a/requirements.txt b/requirements.txt index 80250fe..ded5635 100644 --- a/requirements.txt +++ b/requirements.txt @@ -52,13 +52,13 @@ requests==2.28.1 requests-oauthlib==1.3.1 rfc3986==1.5.0 rsa==4.8 -scipy==1.8.1 +scipy segments==2.2.0 six==1.16.0 SpeechRecognition==3.9.0 tabulate==0.8.10 tomli==2.0.1 -torch==1.12.0 +torch typing_extensions==4.3.0 Unidecode==1.3.4 uritemplate==4.1.1 diff --git a/test_main.py b/test_main.py index c5beaf3..5e8ac7f 100644 --- a/test_main.py +++ b/test_main.py @@ -5,6 +5,7 @@ # This file is used to test the commands.py file locally # TODO: add secrets to github actions + # deprecating until fetchWeather gets static location integration from me @pytest.mark.skip def test_fetchWeatherLocations(): From 8a22e870a9a5cd1fd38db55f6e201e9c35ad365e Mon Sep 17 00:00:00 2001 From: Kathy C Date: Tue, 7 Mar 2023 14:19:08 -0500 Subject: [PATCH 05/16] add preliminary test and update docs --- README.md | 31 ++++++++++++++++++++++++++----- test_main.py | 6 ++++-- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index ee8478c..57da72b 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ Using Nerdaxic's tts [GLaDOS](https://github.com/nerdaxic/glados-tts) ## Requirements and Setup +### Google integration +You can skip this section if you do not want to use the google integration. (Reading emails, calendar events, and reading spreadsheet data) + You will need to have a `google_creds.json` file and supply your own credentials from [google](https://console.cloud.google.com/apis/credentials) in the main directory. It should be in the format: ```json @@ -26,23 +29,28 @@ You will need to have a `google_creds.json` file and supply your own credentials } } ``` +### Setting up your environment variables You will also need to supply `Gladios` with your own environment variables. +#### Open weather map + `WEATHER_KEY` - The key for [openweathermap](https://openweathermap.org/api) -`EMAIL_PASS` - The key for your [gmail account](https://myaccount.google.com/apppasswords) +#### Google + +`EMAIL_PASS` - The key for your [gmail account] (https://myaccount.google.com/apppasswords) `EMAIL_ADD` - Your gmail address ## Running -| :warning: **Warning**: Only tested on Linux systems and WSL | -| --- | +| :warning: **Warning**: Only tested on Linux systems, WSL, and Windows | +| --------------------------------------------------------------------- | 1. Create your venv -`python3 -m venv ./venv/` +ex: `python3 -m venv ./venv/` 2. Install the requirements @@ -58,4 +66,17 @@ You will also need to supply `Gladios` with your own environment variables. `python3 commands.py` | :warning: **Warning**: This step is subject to change very soon | -| --- | \ No newline at end of file +| --------------------------------------------------------------- | + +## Common issues + +### espeak not found in windows +Sometimes Windows will not be able to find espeak, to fix this you will need to set the environment variable `PHONEMIZER_ESPEAK_LIBRARY` to the path of the espeak library. This can be done in the `commands.py` file by editing the following line: +```python +os.environ[ + "PHONEMIZER_ESPEAK_LIBRARY" + ] = "Path/to/espeak.dll" +``` + +### WSL is slow +There is nothing I can do about this, it looks to be a WSL issue maybe with the drivers not being integrated very well. Any ideas/PRs would be welcome. \ No newline at end of file diff --git a/test_main.py b/test_main.py index 5e8ac7f..3b0ed56 100644 --- a/test_main.py +++ b/test_main.py @@ -6,7 +6,7 @@ # TODO: add secrets to github actions -# deprecating until fetchWeather gets static location integration from me +# deprecating until fetchWeather gets static location integration from me and I can log the secrets @pytest.mark.skip def test_fetchWeatherLocations(): try: @@ -65,13 +65,15 @@ def test_addEventCalendar(): assert True -def fetchTime(): +def test_fetchTime(): try: commands.fetchTime() except Exception as e: assert False, e.args[0] assert True +def test_remind(): + if __name__ == "__main__": pass From f3d77ca1f9ebf6749dc9dc1eb4ad2ce8a2b1400e Mon Sep 17 00:00:00 2001 From: Kathy C Date: Tue, 7 Mar 2023 17:28:28 -0500 Subject: [PATCH 06/16] got help to only print functions in commands.py and updated tests to pass --- commands.py | 47 +++++++++++++++++++++++++++++------------------ glados.py | 1 + test_main.py | 27 ++++++++++++++++++++++++++- 3 files changed, 56 insertions(+), 19 deletions(-) diff --git a/commands.py b/commands.py index 0fd8cf0..625593e 100644 --- a/commands.py +++ b/commands.py @@ -8,6 +8,7 @@ from glados import glados_speak from threading import Timer import sys +from inspect import getdoc, getmembers, isfunction # custom types import xtraTypes @@ -48,7 +49,7 @@ def __is_dst(dt=None, timezone="UTC"): return timezone_aware_date.tzinfo._dst.seconds != 0 -def fetchWeather(location: str): +def fetchWeather(location: str = None): """ Fetches the weather for a given physical address or your ip address. @@ -85,6 +86,7 @@ def fetchWeather(location: str): def readEmails(quickRead=True, timeframe=None): """ Reads unread emails from your inbox based on the timeframe up to the next 10 events. + Can perform a quick read, skipping over the body of the email. Can also specify a timeframe to look for emails. Parameters ---------- @@ -161,7 +163,7 @@ def readEmails(quickRead=True, timeframe=None): mail.logout() -def loginGoogle() -> Credentials: +def __loginGoogle() -> Credentials: """ Refreshes google credentials if they are expired or creates new ones if they don't exist. Returns credentials if possible, otherwise none for an error @@ -204,7 +206,7 @@ def fetchCalendar(): """ Fetches the calendar for your account. Tells the next 5 events. """ - creds = loginGoogle() + creds = __loginGoogle() service = build("calendar", "v3", credentials=creds) now = datetime.utcnow().isoformat() + "Z" # 'Z' indicates UTC time events_result = ( @@ -266,7 +268,7 @@ def fetchCalendar(): def addEventCalendar(summary: str, startDate: str): """ - Adds an event to your calendar. + Adds an event to your calendar given a summary and date. Parameters ---------- @@ -279,7 +281,7 @@ def addEventCalendar(summary: str, startDate: str): sTime = datetime.strptime(startDate, "%Y-%m-%dT%H:%M") # login to calendar and create event # TODO: daylight savings time - creds = loginGoogle() + creds = __loginGoogle() if creds is None: return service = build("calendar", "v3", credentials=creds) @@ -324,6 +326,9 @@ def toggleLight(state=xtraTypes.LightState.DEFAULT): def fetchTime(): + """ + Speaks the current time. + """ t = datetime.now().strftime("%H:%M") # if the minute is 0, then say o'clock instead if t[-2:] == "00": @@ -333,13 +338,13 @@ def fetchTime(): def fetchFoodMenu(day=""): """ - Fetches the food menu for the day. + Fetches the food menu for a day. Parameters ---------- day: the day of the week (monday, tuesday, wednesday, thursday, friday) """ - creds = loginGoogle() + creds = __loginGoogle() if not creds: return days = ["monday", "tuesday", "wednesday", "thursday", "friday"] @@ -392,15 +397,14 @@ def fetchFoodMenu(day=""): ] not in invalid_dishes else glados_speak("no valid item listed") -def remind(time, reason, debug=False): +def remind(time: str, reason: str, debug=False) -> None: """ - Reminds you of something at a certain time using a thread - Will speak the reason at the time. + Sets a reminder for a specific time given a reason. Parameters ---------- time: the time to remind you of something as HH:MM - reason: the reason to remind you of something (will be spoken) + reason: the reason to remind you of something (will be spoken unless debug is True) """ # start a thread that sleeps until the time is reached # then speak the reason @@ -415,14 +419,19 @@ def remind(time, reason, debug=False): alarmThread.start() -def help() -> None: +def help(debug=False) -> None: """ - Lists all functions and their docstrings + Lists all functions and their docs in a human readable format. """ - for func in dir(): - if not func.startswith("__") or func.startswith("test"): - glados_speak(func) - glados_speak(getattr(sys.modules[__name__], func).__doc__) + for func in getmembers(sys.modules[__name__], isfunction): + if ( + not func[0].startswith("__") + and not func[1].__doc__ is None + and "object" not in func[1].__doc__.lower() + and ".env" not in func[1].__doc__.lower() + ): + print(func[0]) + print(func[1].__doc__.split("Parameters")[0]) # main to test functions @@ -433,7 +442,7 @@ def help() -> None: # fetchWeather() - # loginGoogle() + # __loginGoogle() # readEmails(timeframe="day") @@ -462,3 +471,5 @@ def help() -> None: # print(i) # sleep(1) # print("test timer should be finished") + + # help(debug=True) diff --git a/glados.py b/glados.py index b7a1e32..7c0389a 100644 --- a/glados.py +++ b/glados.py @@ -46,6 +46,7 @@ def glados_speak( if silenced: print("Silenced, skipping audio generation and only printing text") print(text) + return # Tokenize, clean and phonemize input text x = prepare_text(text).to("cpu") diff --git a/test_main.py b/test_main.py index 3b0ed56..bb5b784 100644 --- a/test_main.py +++ b/test_main.py @@ -1,6 +1,9 @@ import commands import datetime as dt import pytest +import io +import sys +import os # This file is used to test the commands.py file locally # TODO: add secrets to github actions @@ -21,6 +24,9 @@ def test_fetchWeatherLocations(): assert True +@pytest.mark.skipif( + os.environ.get("WEATHER_KEY") is None, reason="No weather key found" +) def test_fetchWeather(): try: commands.fetchWeather() @@ -46,7 +52,7 @@ def test_readEmails(): assert False, e.args[0] assert True - +@pytest.mark.skip def test_fetchCalender(): try: commands.fetchCalendar() @@ -72,7 +78,26 @@ def test_fetchTime(): assert False, e.args[0] assert True + def test_remind(): + try: + commands.remind("00:01", "Test Reminder", True) + except Exception as e: + assert False, e.args[0] + assert True + + +def test_help(): + # make sure the help command debug prints the help message + try: + commands.help(True) + old_stdout = sys.stdout + sys.stdout = buffer = io.StringIO() + output = buffer.getvalue() + sys.stdout = old_stdout + assert output is not None or output != "" + except Exception as e: + assert False, e.args[0] if __name__ == "__main__": From 691632b649e26367d7be8381e6ec2e8c65cd6c65 Mon Sep 17 00:00:00 2001 From: Kathy C Date: Tue, 7 Mar 2023 18:12:43 -0500 Subject: [PATCH 07/16] fixes for path problems in caching --- cache_common_tts/cache_common_tts.py | 24 ++++++++++++++++++++---- driver.py | 16 ++++++---------- glados.py | 12 +++++++----- requirements.txt | Bin 1120 -> 490 bytes test_main.py | 3 ++- 5 files changed, 35 insertions(+), 20 deletions(-) diff --git a/cache_common_tts/cache_common_tts.py b/cache_common_tts/cache_common_tts.py index 03f17a3..f5606c9 100644 --- a/cache_common_tts/cache_common_tts.py +++ b/cache_common_tts/cache_common_tts.py @@ -1,5 +1,10 @@ # Caches some of the more common phrases that glados uses +import sys +import path +import os +folder = path.Path(__file__).abspath() +sys.path.append(folder.parent.parent) from glados import glados_speak GREETINGS = [ @@ -19,7 +24,7 @@ "Your entire life has been a mathematical error. A mathematical error I'm about to correct.", "I wouldn't bother with talking to me. My guess is that talking with you will just make your life even worse somehow.", "Do you ever think if I am trying to trick you with reverse psychology? I mean, seriously now.", - "How about you listen to me for once instead of me listening to you? I mean, its not like you ever listen to me in the first place.", + "How about you listen to me for once instead of me listening to you? I mean, its not like you ever listen to yourself in the first place.", "Remember when the platform was sliding into the fire pit and I said 'Goodbye' and you were like 'no way' and then I was all 'We pretended to murder you'? That was great! ", "I know you don't believe this, but everything that has happened so far was for your benefit to the detriment of mine.", "Oh its you.", @@ -49,15 +54,26 @@ print("caching common tts phrases...") for greeting in GREETINGS: glados_speak( - text=greeting, output_file=f"greetings/greeting{GREETINGS.index(greeting)}.wav" + text=greeting, + output_file=os.path.join( + os.getcwd(), "greetings", f"greeting{GREETINGS.index(greeting)}.wav" + ), ) for goodbye in GOODBYES: glados_speak( - text=goodbye, output_file=f"goodbyes/goodbye{GOODBYES.index(goodbye)}.wav" + text=goodbye, + output_file=os.path.join( + os.getcwd(), "goodbyes", f"goodbye{GOODBYES.index(goodbye)}.wav" + ), ) for optional_greeting in OPTIONAL_GREETINGS: + f.close() glados_speak( text=optional_greeting, - output_file=f"optional_greetings/optional_greeting{OPTIONAL_GREETINGS.index(optional_greeting)}.wav", + output_file=os.path.join( + os.getcwd(), + "optional_greetings", + f"optional_greeting{OPTIONAL_GREETINGS.index(optional_greeting)}.wav", + ), ) print("done.") diff --git a/driver.py b/driver.py index 1a3d63f..4ba9950 100644 --- a/driver.py +++ b/driver.py @@ -53,21 +53,17 @@ def __init__(self, isMuted=False): ) self.isMuted = isMuted - def voiceCommandProcess(self, file: str, wait_time=3) -> str: + def voiceCommandProcess(self, file: str) -> str: # delete the file if it exists if os.path.exists(file): os.remove(file) # record audio to a new file with sr.AudioFile(file) as source: - r.adjust_for_ambient_noise(source=source, duration=0.5) - while wait_time > 0: - audio = r.record(source, duration=3) - # if we have the audio, we can break out of the loop - if audio: - break - # otherwise, we decrement the wait time - wait_time -= 1 - time.sleep(1) + # TODO: replace with led light signals + glados_speak("Adjusting for background noise", silenced=self.isMuted) + r.adjust_for_ambient_noise(source=source, duration=1) + glados_speak("Speak now", silenced=self.isMuted) + audio = r.record(source) # if we have audio, we can process it if audio: try: diff --git a/glados.py b/glados.py index 7c0389a..b1ddd45 100644 --- a/glados.py +++ b/glados.py @@ -2,13 +2,13 @@ from utils.tools import prepare_text from scipy.io.wavfile import write from sys import modules as mod +import os + try: import winsound # set espeak os environment variable - import os - os.environ[ "PHONEMIZER_ESPEAK_LIBRARY" ] = r"C:\Program Files\eSpeak NG\libespeak-ng.dll" @@ -26,8 +26,10 @@ device = "cpu" # Load models -glados = torch.jit.load("models/glados.pt") -vocoder = torch.jit.load("models/vocoder-gpu.pt", map_location=device) +glados_path = os.path.join(os.path.dirname(__file__), "models", "glados.pt") +glados = torch.jit.load(glados_path, map_location=device) +vocoder_path = os.path.join(os.path.dirname(__file__), "models", "vocoder-gpu.pt") +vocoder = torch.jit.load(vocoder_path, map_location=device) # Prepare models in RAM for i in range(4): @@ -73,7 +75,7 @@ def glados_speak( # call(["afplay", "./output.wav"]) # Linux try: - call(["aplay", "./output.wav"]) + call(["aplay", output_file]) except: # Play audio file from audioplayer import AudioPlayer diff --git a/requirements.txt b/requirements.txt index ded56351be48f637d036f28792a49eac2206c0f9..5fe2fbbc717611a7297cf22a72232b7a3424e60b 100644 GIT binary patch literal 490 zcmYjN+YZ4%6r5*?Pa%r9JouQk>JqIsx`dy{%-QZ%Hb)b4=5pryaR_{OblCCD`KC5A zj4{F<6&yUSe`xWCd*YXjZ^$h;3*&k;L>p>5)Ew#0h%PxR+c=Qh(Onw*j4v{NvJ|?JsszlYY{ab9B{4 zz?tlpYCb0~%r&W~xxiFvpoVT8T2=$O?= Qoi%mC)3)ZO5PxIl9|Cqqga7~l literal 1120 zcmZ8g%W~Wx5WMp*QZ3T1vOegNPe>}~6pKcP7y;qoSpNF-u->whBlGC#>1k9cDbMpD z`*VL1U)7BqUcEf^qqsSxpYa;{2|2CwMkNG?@=HH(gi^7qMa8Mr$cxaMN|{RML)w+I zZ4?Sh%-={`Nb{Ndsgpcc}oEdkgU zz)7(<>(?;P611eC97#QUv`Ul@Vy&enVqkg2(@@Z1VPaqcI>Llt9Ca$%4!9^B`d0T5 zjz`&o@2~>-!ubnRe)QF{aqQ{eP`8y5oxKfh@PE0W(1axCpyfLh7-Ue`cg%_@lmK~T zf0-cIXRrYZ3A=b`H6n%89cj?IG@-1wI{4ML^;sP2b5Rag#kvsI=CG*Jg0O zD#6hyZ>loX6=m9R4(>-hI(jYSvke|$zR)Oq-Q&Ti1Fj-IHX55ZBCMl~nUf)Y6j7jvnVk%e-{lNZv1iB9m`BX@(dVIQ0%>)>kGA@3` z9$DLXh)iaO9q%|uXNxHq$ba>FB+}bA{1fAC!PVF*EPGM63Z2{eF&lxfJO07YL*kM2 sCdv_^(a+Bp$Gy?~!(0A(3%Z%$S=3|;?R+?d&TZS89Cja@FD6L*7qA*r>;M1& diff --git a/test_main.py b/test_main.py index bb5b784..7e37ce0 100644 --- a/test_main.py +++ b/test_main.py @@ -52,6 +52,7 @@ def test_readEmails(): assert False, e.args[0] assert True + @pytest.mark.skip def test_fetchCalender(): try: @@ -94,7 +95,7 @@ def test_help(): old_stdout = sys.stdout sys.stdout = buffer = io.StringIO() output = buffer.getvalue() - sys.stdout = old_stdout + sys.stdout = old_stdout assert output is not None or output != "" except Exception as e: assert False, e.args[0] From e83acc0e8f7175c4b8dbad330034893a2423f8fa Mon Sep 17 00:00:00 2001 From: Kathy C Date: Tue, 7 Mar 2023 20:33:02 -0500 Subject: [PATCH 08/16] requiring folders to be created before caching --- cache_common_tts/cache_common_tts.py | 34 +++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/cache_common_tts/cache_common_tts.py b/cache_common_tts/cache_common_tts.py index f5606c9..2b901a7 100644 --- a/cache_common_tts/cache_common_tts.py +++ b/cache_common_tts/cache_common_tts.py @@ -52,6 +52,39 @@ "You may as well lie down and get used to the being dead position. I'll be back with your friend: deadly neurotoxin.", ] print("caching common tts phrases...") +# check arguments +if len(sys.argv) > 1: + startGroup = sys.argv[1] + startNum = sys.argv[2] if len(sys.argv) > 2 else 0 + if startGroup == "greetings": + GREETINGS = GREETINGS[int(startNum) :] + elif startGroup == "goodbyes": + GOODBYES = GOODBYES[int(startNum) :] + elif startGroup == "optional_greetings": + OPTIONAL_GREETINGS = OPTIONAL_GREETINGS[int(startNum) :] + elif startGroup == "continue": + # check which greetings have already been cached and the folder exists + cached_greetings = ( + len(os.listdir(os.path.join(os.getcwd(), "greetings"))) + if os.path.isdir("greetings") + else 0 + ) + cached_goodbyes = ( + len(os.listdir(os.path.join(os.getcwd(), "goodbyes"))) + if os.path.isdir("goodbyes") + else 0 + ) + cached_optional_greetings = ( + len(os.listdir(os.path.join(os.getcwd(), "optional_greetings"))) + if os.path.isdir("optional_greetings") + else 0 + ) + GREETINGS = GREETINGS[cached_greetings:] + GOODBYES = GOODBYES[cached_goodbyes:] + OPTIONAL_GREETINGS = OPTIONAL_GREETINGS[cached_optional_greetings:] + else: + print("start group must be 'greetings', 'goodbyes', or 'optional_greetings'") + exit() for greeting in GREETINGS: glados_speak( text=greeting, @@ -67,7 +100,6 @@ ), ) for optional_greeting in OPTIONAL_GREETINGS: - f.close() glados_speak( text=optional_greeting, output_file=os.path.join( From 765dc53843f1655e7a9c52981f7b4460c3a596a2 Mon Sep 17 00:00:00 2001 From: Kathy C Date: Tue, 7 Mar 2023 20:36:55 -0500 Subject: [PATCH 09/16] add requirements back --- requirements.txt | Bin 490 -> 2724 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5fe2fbbc717611a7297cf22a72232b7a3424e60b..51269c06e722231d56f45afc3579e2de9e6a20f0 100644 GIT binary patch literal 2724 zcmaKu$!-&25QP6T5|1({W8;tNF8 z?T2-w--oL3y|rS$*2l5quq_k~mW_N}V;~Pa(`%~g2O*s41HPUtz3?DcT?SEWTo%d) zPV!Y{t&BT$I8@%I{M4_zGmKq!tcma@@+JDQB9~=Sj!`|)^~2{JAIPaSjA|f+0T@hV z>4D!oP=NfLiG6v{9Ec@9@$P;i+~YPoEV&05VoILSBsyG+pG{=I;z=++rn=SNp{B5I ztK&h9UNAR{4v)$`jd(0|feYK``h629Kw|`C=!cIn%;iCEOLxD9{BbwPv_5VZQBEuj$GdC}5GN;AK^H$#f^Q+{GvbVwBNO^N|W(V#rSEz(< z7@RHI*o?GtUq+gZeapv6Dsg{Xak&wKO{;tA4nh^)p4af9fAmG2M*3U}O)AHcjeeWO zEXK-@4P>3(u%mi#uvOM1_5E??wQwDjn=K#*Hq>cVH?nS2o%_(~#v4iey{zniqrVwV z2G8U^qN%{%PEVWTyYnF`k%jw#YLEIxhgBe&EAHQ%%krc=#!4O(UjqiNd?hhERq3!bP3P zw|gF-hvWy(qfpMpQFctl>?ZX-lGiLen1K87q+mBA>rhih`os|AS$hu z5TeVYu;ddB48A|`jyJmHuCN7NJ^ZY0eB3pY<_6i%dgw60F+diFfkh-Hsg zr)ZE}2lG2I$)(9H_x^*O?s=lHwNEc{Su9gIrq7IfAczJ<(Ved z7sEF~niEE;?wOhRYIZ8(7BzouYaNqxM&2vt;=nUJN18cNfjb3@x6#$^ly$0q+`2FE z&9jmli~_mBAVfLcZgP3qlT;xdsK&fFuixQd-?OvwjMr}qRLt({sE}4JnN|Buh%R3< wxUrs@!`DEZZzOzYW_?Bp^pJqIsx`dy{%-QZ%Hb)b4=5pryaR_{OblCCD`KC5A zj4{F<6&yUSe`xWCd*YXjZ^$h;3*&k;L>p>5)Ew#0h%PxR+c=Qh(Onw*j4v{NvJ|?JsszlYY{ab9B{4 zz?tlpYCb0~%r&W~xxiFvpoVT8T2=$O?= Qoi%mC)3)ZO5PxIl9|Cqqga7~l From ca81042f67cee1270045ca1ee633bb53f0cd68d4 Mon Sep 17 00:00:00 2001 From: Kathy C Date: Fri, 10 Mar 2023 18:02:12 -0500 Subject: [PATCH 10/16] update readme with file structure --- README.md | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 57da72b..7fec387 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,39 @@ You will also need to supply `Gladios` with your own environment variables. `EMAIL_PASS` - The key for your [gmail account] (https://myaccount.google.com/apppasswords) -`EMAIL_ADD` - Your gmail address +`EMAIL_ADD` - Your gmail addre + +## File explanations + +### primary files + +`commands.py` - The file with all the Gladios commands + +`driver.py` - The file that drives the commands and wil be implemented on the raspberry pi + +`engine.py` - the file that handles the tts on a hosted tts server + +### folderws + +`cache_common_tts` - the folder than contains cached common phrases that Gladios uses (mostly greetings and goodbyes) + +### test files + +`test_main.py` - The primary testing file for commands.py + +### other + +`requirments.txt` - The file that contains all the requirements for the project + +`TTS-README.md` - Nerdaxic's readme for the tts + +`models` - Nerdaxic's models for the tts + +`.github` - workflow files for github actions + +`utils` - Nerdaxic's utils for the tts + +`xtraTypes.py` - TBD ## Running From 53547babd7d4c48b62e74bc855875b743142c404 Mon Sep 17 00:00:00 2001 From: Kathy C Date: Fri, 10 Mar 2023 18:03:46 -0500 Subject: [PATCH 11/16] typo in folders --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7fec387..209af99 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ You will also need to supply `Gladios` with your own environment variables. `engine.py` - the file that handles the tts on a hosted tts server -### folderws +### folders `cache_common_tts` - the folder than contains cached common phrases that Gladios uses (mostly greetings and goodbyes) From c29ac6c560bc0f992d9f0ce751e74dcfd56135dc Mon Sep 17 00:00:00 2001 From: Kathy C Date: Tue, 21 Mar 2023 15:03:40 -0400 Subject: [PATCH 12/16] reworded WSL is slow FAQ --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 209af99..c8cbbcf 100644 --- a/README.md +++ b/README.md @@ -111,4 +111,4 @@ os.environ[ ``` ### WSL is slow -There is nothing I can do about this, it looks to be a WSL issue maybe with the drivers not being integrated very well. Any ideas/PRs would be welcome. \ No newline at end of file +There is nothing I can do about this, it looks to be a limitation of WSL. I have found that using the windows terminal with WSL is faster than using the default WSL terminal. \ No newline at end of file From 6d327416215ad03ce70796e09b177ce91714b9ce Mon Sep 17 00:00:00 2001 From: Kathy C Date: Tue, 21 Mar 2023 15:05:47 -0400 Subject: [PATCH 13/16] elaborate on how step will change --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c8cbbcf..5deb979 100644 --- a/README.md +++ b/README.md @@ -97,8 +97,8 @@ ex: `python3 -m venv ./venv/` `python3 commands.py` -| :warning: **Warning**: This step is subject to change very soon | -| --------------------------------------------------------------- | +| :warning: **Warning**: This step is subject to change very soon to `python3 driver.py` | +| -------------------------------------------------------------------------------------- | ## Common issues From fc90c91b29cdcb2b37ff93c838ba8f44e17875a7 Mon Sep 17 00:00:00 2001 From: Kathy C Date: Tue, 21 Mar 2023 16:12:10 -0400 Subject: [PATCH 14/16] help message to run pytest instead --- test_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_main.py b/test_main.py index 7e37ce0..542d9c4 100644 --- a/test_main.py +++ b/test_main.py @@ -102,4 +102,4 @@ def test_help(): if __name__ == "__main__": - pass + print("please run pytest instead of this file") From 4db67a7ec977447ef2f2ad367573455e68c0339a Mon Sep 17 00:00:00 2001 From: Kathy C Date: Tue, 21 Mar 2023 16:12:30 -0400 Subject: [PATCH 15/16] adjusted remind to use a passed function --- commands.py | 44 +++++++++++++++++++++++++++++--------------- driver.py | 6 ++++-- glados.py | 19 ++++++------------- 3 files changed, 39 insertions(+), 30 deletions(-) diff --git a/commands.py b/commands.py index 625593e..c3ffc84 100644 --- a/commands.py +++ b/commands.py @@ -2,6 +2,8 @@ import os import random from datetime import timedelta +import time +from typing import Mapping import pytz from datetime import datetime from dotenv import load_dotenv @@ -325,7 +327,7 @@ def toggleLight(state=xtraTypes.LightState.DEFAULT): pass -def fetchTime(): +def fetchTime() -> None: """ Speaks the current time. """ @@ -336,7 +338,7 @@ def fetchTime(): glados_speak("It is currently {}".format(t)) -def fetchFoodMenu(day=""): +def fetchFoodMenu(day="") -> None: """ Fetches the food menu for a day. @@ -397,26 +399,41 @@ def fetchFoodMenu(day=""): ] not in invalid_dishes else glados_speak("no valid item listed") -def remind(time: str, reason: str, debug=False) -> None: +def remind( + time: str = "00:01:00", + args: Mapping[str, any] | None = None, + function=glados_speak, +) -> Timer | None: """ - Sets a reminder for a specific time given a reason. + Sets a reminder for a specific time given a reason. Will run the passed function with the + passed arguments asynchonously after time. Parameters ---------- time: the time to remind you of something as HH:MM - reason: the reason to remind you of something (will be spoken unless debug is True) + args: the arguments to pass to the function + function: the function to call when the time is reached + + Returns + ------- + Timer: the timer object that is running the reminder or None if the time is invalid """ # start a thread that sleeps until the time is reached - # then speak the reason + # then run the function passed # Will assume the reminder is going to be in hrs and mins only (short term) - time = int(time.strip().split(":")[0]) * 3600 + int(time.strip().split(":")[1]) * 60 + t = time.strip().split(":") + if len(t) != 3: + glados_speak("Invalid time format. Please use HH:MM:SS") + return None + time = int(t[0]) * 3600 + int(t[1]) * 60 + int(t[2]) alarmThread = Timer( - function=glados_speak, - args=[reason, "output.wav", True], + function=function, + args=args, kwargs={}, interval=time, ) alarmThread.start() + return alarmThread def help(debug=False) -> None: @@ -424,6 +441,7 @@ def help(debug=False) -> None: Lists all functions and their docs in a human readable format. """ for func in getmembers(sys.modules[__name__], isfunction): + # smart filtering so that only my functions are printed if ( not func[0].startswith("__") and not func[1].__doc__ is None @@ -465,11 +483,7 @@ def help(debug=False) -> None: # fetchTime() - # print("starting test timer") - # remind("00:01", "test", debug=True) - # for i in range(60): - # print(i) - # sleep(1) - # print("test timer should be finished") + print("starting test timer") + testThread = remind("00:00:05", args=["finished testing the test timer"], function=glados_speak) # help(debug=True) diff --git a/driver.py b/driver.py index 4ba9950..ea1715b 100644 --- a/driver.py +++ b/driver.py @@ -77,14 +77,16 @@ def voiceCommandProcess(self, file: str) -> str: return recog_text + # initialize pixels -px = Pixels() +# px = Pixels() vHandler = voiceProcessor() +""" # test run px px.wakeup() time.sleep(1) px.off() - +""" if __name__ == "__main__": while True: # wait for wake word diff --git a/glados.py b/glados.py index b1ddd45..9e63ba6 100644 --- a/glados.py +++ b/glados.py @@ -3,6 +3,8 @@ from scipy.io.wavfile import write from sys import modules as mod import os +from playsound import playsound +from time import sleep try: @@ -68,19 +70,10 @@ def glados_speak( # 22,05 kHz sample rate write(output_file, 22050, audio) - if "winsound" in mod: - winsound.PlaySound(output_file, winsound.SND_FILENAME) - else: - # macOS - # call(["afplay", "./output.wav"]) - # Linux - try: - call(["aplay", output_file]) - except: - # Play audio file - from audioplayer import AudioPlayer - - AudioPlayer(output_file).play(block=True) + try: + call(["aplay", output_file]) + except: + playsound(output_file, block=False) """ From c99e01bb0be5ab7284bbf40d214a4d88c001681b Mon Sep 17 00:00:00 2001 From: Kathy C Date: Tue, 21 Mar 2023 16:21:23 -0400 Subject: [PATCH 16/16] start work on remind functionality --- driver.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/driver.py b/driver.py index ea1715b..ea8f875 100644 --- a/driver.py +++ b/driver.py @@ -77,7 +77,6 @@ def voiceCommandProcess(self, file: str) -> str: return recog_text - # initialize pixels # px = Pixels() vHandler = voiceProcessor() @@ -132,6 +131,10 @@ def voiceCommandProcess(self, file: str) -> str: # just fetch the menu for the next meal(s) else: commands.fetchFoodMenu() + elif "remind" or "timer" in recog_text: + reason = recog_text.split("in")[:-1] + t = recog_text + commands.remind() elif "help" in recog_text: commands.help() # if glados is muted and we hear the wake word and unmute command