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/README.md b/README.md index ee8478c..5deb979 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,60 @@ 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 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 + +### folders + +`cache_common_tts` - the folder than contains cached common phrases that Gladios uses (mostly greetings and goodbyes) + +### test files -`EMAIL_ADD` - Your gmail address +`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 -| :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 @@ -57,5 +97,18 @@ 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 +| :warning: **Warning**: This step is subject to change very soon to `python3 driver.py` | +| -------------------------------------------------------------------------------------- | + +## 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 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 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 new file mode 100644 index 0000000..2b901a7 --- /dev/null +++ b/cache_common_tts/cache_common_tts.py @@ -0,0 +1,111 @@ +# 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 = [ + "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 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.", + "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...") +# 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, + output_file=os.path.join( + os.getcwd(), "greetings", f"greeting{GREETINGS.index(greeting)}.wav" + ), + ) +for goodbye in GOODBYES: + glados_speak( + text=goodbye, + output_file=os.path.join( + os.getcwd(), "goodbyes", f"goodbye{GOODBYES.index(goodbye)}.wav" + ), + ) +for optional_greeting in OPTIONAL_GREETINGS: + glados_speak( + text=optional_greeting, + output_file=os.path.join( + os.getcwd(), + "optional_greetings", + f"optional_greeting{OPTIONAL_GREETINGS.index(optional_greeting)}.wav", + ), + ) +print("done.") diff --git a/commands.py b/commands.py index 6b5b131..c3ffc84 100644 --- a/commands.py +++ b/commands.py @@ -1,11 +1,16 @@ import datetime import os import random -from datetime import time, timedelta +from datetime import timedelta +import time +from typing import Mapping import pytz from datetime import datetime from dotenv import load_dotenv from glados import glados_speak +from threading import Timer +import sys +from inspect import getdoc, getmembers, isfunction # custom types import xtraTypes @@ -28,7 +33,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. @@ -46,7 +51,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. @@ -83,6 +88,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 ---------- @@ -159,7 +165,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 @@ -198,12 +204,11 @@ def loginGoogle() -> Credentials: return creds -# TODO: test this 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 = ( @@ -263,10 +268,9 @@ def fetchCalendar(): ) -# TODO: test this 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,11 +283,11 @@ 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) - offset = "04:00" if is_dst() else "05:00" + offset = "04:00" if __is_dst() else "05:00" event = { "summary": summary, "start": { @@ -323,7 +327,10 @@ def toggleLight(state=xtraTypes.LightState.DEFAULT): pass -def fetchTime(): +def fetchTime() -> None: + """ + Speaks the current time. + """ t = datetime.now().strftime("%H:%M") # if the minute is 0, then say o'clock instead if t[-2:] == "00": @@ -331,18 +338,19 @@ def fetchTime(): glados_speak("It is currently {}".format(t)) -def fetchFoodMenu(day=""): +def fetchFoodMenu(day="") -> None: """ - 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"] + invalid_dishes = ["", "main dish", "sides", "vegan"] if day == "": day = ( days[datetime.now().weekday()] if datetime.now().weekday() < 5 else "monday" @@ -354,8 +362,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,40 +386,81 @@ 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) - ] != "" 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) - ] != "" else None + ] not in invalid_dishes else glados_speak("no valid item listed") -def remind(time, reason): +def remind( + time: str = "00:01:00", + args: Mapping[str, any] | None = None, + function=glados_speak, +) -> Timer | None: """ - Reminds you of something at a certain time. + 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 + 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 """ - pass + # start a thread that sleeps until the time is reached + # then run the function passed + # Will assume the reminder is going to be in hrs and mins only (short term) + 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=function, + args=args, + kwargs={}, + interval=time, + ) + alarmThread.start() + return alarmThread -def shutdownComputer(computer): +def help(debug=False) -> None: """ - Shuts down a computer. + Lists all functions and their docs in a human readable format. """ - pass + 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 + 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 if __name__ == "__main__": + from time import sleep + # uncomment the function you want to test or better yet: run `pytest` # fetchWeather() - # loginGoogle() + # __loginGoogle() # readEmails(timeframe="day") @@ -432,5 +481,9 @@ def shutdownComputer(computer): # toggleLight(types.LightState.OFF) # toggleLight(types.LightState.DEFAULT) - fetchTime() - pass + # fetchTime() + + 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 5de4625..ea8f875 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,57 +12,11 @@ from pixels import Pixels import valib import response -import glob -import logging 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") @@ -74,19 +30,121 @@ # 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 """ 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], \ No newline at end of file + input_device_index=int(os.environ["RESPEAKER_INDEX"]), + channels=os.environ["RESPEAKER_CHANNELS"], + input=True, + frames_per_buffer=os.environ["CHUNK"], + ) + self.isMuted = isMuted + + 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: + # 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: + 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 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 + 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 [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "tomorrow", + ] 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 "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 + 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") 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 81a78eb..9e63ba6 100644 --- a/glados.py +++ b/glados.py @@ -2,9 +2,18 @@ from utils.tools import prepare_text from scipy.io.wavfile import write from sys import modules as mod +import os +from playsound import playsound +from time import sleep + try: import winsound + + # set espeak os environment variable + os.environ[ + "PHONEMIZER_ESPEAK_LIBRARY" + ] = r"C:\Program Files\eSpeak NG\libespeak-ng.dll" except ImportError: from subprocess import call @@ -19,8 +28,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): @@ -29,10 +40,17 @@ init_vo = vocoder(init_mel) -def glados_speak(text: str): +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) + return # Tokenize, clean and phonemize input text x = prepare_text(text).to("cpu") @@ -47,25 +65,15 @@ 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 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.wav"]) - 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) """ diff --git a/requirements.txt b/requirements.txt index 80250fe..51269c0 100644 Binary files a/requirements.txt and b/requirements.txt differ 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) diff --git a/test_main.py b/test_main.py index c5beaf3..542d9c4 100644 --- a/test_main.py +++ b/test_main.py @@ -1,11 +1,15 @@ 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 -# 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: @@ -20,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,6 +53,7 @@ def test_readEmails(): assert True +@pytest.mark.skip def test_fetchCalender(): try: commands.fetchCalendar() @@ -64,7 +72,7 @@ def test_addEventCalendar(): assert True -def fetchTime(): +def test_fetchTime(): try: commands.fetchTime() except Exception as e: @@ -72,5 +80,26 @@ def fetchTime(): 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__": - pass + print("please run pytest instead of this file")