From 2c55f4da462383ee6c7b89ea44e5c40cd5badf0c Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 23 Jul 2014 21:21:17 -0700 Subject: Google STT works on OS X --- .gitignore | 1 + client/conversation.py | 1 + client/main.py | 6 +- client/mic.py | 33 ++++------ client/modules/Birthday.py | 64 ------------------- client/modules/Gmail.py | 136 ---------------------------------------- client/modules/HN.py | 130 -------------------------------------- client/modules/Joke.py | 63 ------------------- client/modules/Life.py | 32 ---------- client/modules/News.py | 122 ----------------------------------- client/modules/Notifications.py | 55 ---------------- client/modules/Time.py | 33 ---------- client/modules/Unclear.py | 29 --------- client/modules/Weather.py | 110 -------------------------------- client/modules/__init__.py | 0 client/modules/app_utils.py | 123 ------------------------------------ client/notifier.py | 4 +- client/populate.py | 3 + client/stt.py | 101 +++++++++++++++++++++++++++++ 19 files changed, 125 insertions(+), 921 deletions(-) delete mode 100644 client/modules/Birthday.py delete mode 100644 client/modules/Gmail.py delete mode 100644 client/modules/HN.py delete mode 100644 client/modules/Joke.py delete mode 100644 client/modules/Life.py delete mode 100644 client/modules/News.py delete mode 100644 client/modules/Notifications.py delete mode 100644 client/modules/Time.py delete mode 100644 client/modules/Unclear.py delete mode 100644 client/modules/Weather.py delete mode 100755 client/modules/__init__.py delete mode 100644 client/modules/app_utils.py create mode 100644 client/stt.py diff --git a/.gitignore b/.gitignore index 08cf7c5..5a2333a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ client/say.wav *.swp server/modules/build/* client/build/* +client/modules/* client/profile.yml build/* server/dump.rdb diff --git a/client/conversation.py b/client/conversation.py index bad5073..9f7cc22 100755 --- a/client/conversation.py +++ b/client/conversation.py @@ -51,6 +51,7 @@ class Conversation(object): if threshold: input = self.mic.activeListen(threshold) + print "I just heard '%s'" % input if input: self.delegateInput(input) else: diff --git a/client/main.py b/client/main.py index 35ef715..4a54f3f 100644 --- a/client/main.py +++ b/client/main.py @@ -1,6 +1,8 @@ import yaml import sys import speaker +import stt +from stt import PocketSphinxSTT from conversation import Conversation @@ -21,8 +23,8 @@ if __name__ == "__main__": profile = yaml.safe_load(open("profile.yml", "r")) - mic = Mic(speaker.newSpeaker(), "languagemodel.lm", "dictionary.dic", - "languagemodel_persona.lm", "dictionary_persona.dic") + # TODO: rename 'api_key' to 'google_stt_api_key' + mic = Mic(speaker.newSpeaker(), PocketSphinxSTT(), stt.newSTTEngine(profile['api_key'])) mic.say("How can I be of service?") diff --git a/client/mic.py b/client/mic.py index f4b70df..48f14de 100644 --- a/client/mic.py +++ b/client/mic.py @@ -10,19 +10,12 @@ import pyaudio import alteration -# quirky bug where first import doesn't work -try: - import pocketsphinx as ps -except: - import pocketsphinx as ps - - class Mic: speechRec = None speechRec_persona = None - def __init__(self, speaker, lmd, dictd, lmd_persona, dictd_persona, lmd_music=None, dictd_music=None): + def __init__(self, speaker, passive_stt_engine, active_stt_engine): """ Initiates the pocketsphinx instance. @@ -34,13 +27,8 @@ class Mic: dictd_persona -- filename of the 'Persona' dictionary (.dic) """ self.speaker = speaker - hmdir = "/usr/local/share/pocketsphinx/model/hmm/en_US/hub4wsj_sc_8k" - - if lmd_music and dictd_music: - self.speechRec_music = ps.Decoder(hmm = hmdir, lm = lmd_music, dict = dictd_music) - self.speechRec_persona = ps.Decoder( - hmm=hmdir, lm=lmd_persona, dict=dictd_persona) - self.speechRec = ps.Decoder(hmm=hmdir, lm=lmd, dict=dictd) + self.passive_stt_engine = passive_stt_engine + self.active_stt_engine = active_stt_engine def transcribe(self, audio_file_path, PERSONA_ONLY=False, MUSIC=False): """ @@ -210,7 +198,8 @@ class Mic: write_frames.close() # check if PERSONA was said - transcribed = self.transcribe(AUDIO_FILE, PERSONA_ONLY=True) + #transcribed = self.transcribe(AUDIO_FILE, PERSONA_ONLY=True) + transcribed = self.passive_stt_engine.transcribe(AUDIO_FILE, PERSONA_ONLY=True) if PERSONA in transcribed: return (THRESHOLD, PERSONA) @@ -223,7 +212,8 @@ class Mic: """ AUDIO_FILE = "active.wav" - RATE = 16000 + #RATE = 16000 + RATE = 44100 CHUNK = 1024 LISTEN_TIME = 12 @@ -232,7 +222,8 @@ class Mic: if not os.path.exists(AUDIO_FILE): return None - return self.transcribe(AUDIO_FILE) + #return self.transcribe(AUDIO_FILE) + return self.active_stt_engine.transcribe(AUDIO_FILE) # check if no threshold provided if THRESHOLD == None: @@ -285,9 +276,11 @@ class Mic: # os.system("sox "+AUDIO_FILE+" temp.wav vol 20dB") if MUSIC: - return self.transcribe(AUDIO_FILE, MUSIC=True) + #return self.transcribe(AUDIO_FILE, MUSIC=True) + return self.active_stt_engine.transcribe(AUDIO_FILE, MUSIC=True) - return self.transcribe(AUDIO_FILE) + #return self.transcribe(AUDIO_FILE) + return self.active_stt_engine.transcribe(AUDIO_FILE) def say(self, phrase, OPTIONS=" -vdefault+m3 -p 40 -s 160 --stdout > say.wav"): # alter phrase before speaking diff --git a/client/modules/Birthday.py b/client/modules/Birthday.py deleted file mode 100644 index 1388443..0000000 --- a/client/modules/Birthday.py +++ /dev/null @@ -1,64 +0,0 @@ -import datetime -import re -from facebook import * -from app_utils import getTimezone - -WORDS = ["BIRTHDAY"] - - -def handle(text, mic, profile): - """ - Responds to user-input, typically speech text, by listing the user's - Facebook friends with birthdays today. - - Arguments: - text -- user-input, typically transcribed speech - mic -- used to interact with the user (for both input and output) - profile -- contains information related to the user (e.g., phone number) - """ - oauth_access_token = profile['keys']["FB_TOKEN"] - - graph = GraphAPI(oauth_access_token) - - try: - results = graph.request( - "me/friends", args={'fields': 'id,name,birthday'}) - except GraphAPIError: - mic.say( - "I have not been authorized to query your Facebook. If you would like to check birthdays in the future, please visit the Jasper dashboard.") - return - except: - mic.say( - "I apologize, there's a problem with that service at the moment.") - return - - needle = datetime.datetime.now(tz=getTimezone(profile)).strftime("%m/%d") - - people = [] - for person in results['data']: - try: - if needle in person['birthday']: - people.append(person['name']) - except: - continue - - if len(people) > 0: - if len(people) == 1: - output = people[0] + " has a birthday today." - else: - output = "Your friends with birthdays today are " + \ - ", ".join(people[:-1]) + " and " + people[-1] + "." - else: - output = "None of your friends have birthdays today." - - mic.say(output) - - -def isValid(text): - """ - Returns True if the input is related to birthdays. - - Arguments: - text -- user-input, typically transcribed speech - """ - return bool(re.search(r'birthday', text, re.IGNORECASE)) diff --git a/client/modules/Gmail.py b/client/modules/Gmail.py deleted file mode 100644 index b401ae6..0000000 --- a/client/modules/Gmail.py +++ /dev/null @@ -1,136 +0,0 @@ -import imaplib -import email -import re -from dateutil import parser -from app_utils import * - -WORDS = ["EMAIL", "INBOX"] - - -def getSender(email): - """ - Returns the best-guess sender of an email. - - Arguments: - email -- the email whose sender is desired - - Returns: - Sender of the email. - """ - sender = email['From'] - m = re.match(r'(.*)\s<.*>', sender) - if m: - return m.group(1) - return sender - - -def getDate(email): - return parser.parse(email.get('date')) - - -def getMostRecentDate(emails): - """ - Returns the most recent date of any email in the list provided. - - Arguments: - emails -- a list of emails to check - - Returns: - Date of the most recent email. - """ - dates = [getDate(e) for e in emails] - dates.sort(reverse=True) - if dates: - return dates[0] - return None - - -def fetchUnreadEmails(profile, since=None, markRead=False, limit=None): - """ - Fetches a list of unread email objects from a user's Gmail inbox. - - Arguments: - profile -- contains information related to the user (e.g., Gmail address) - since -- if provided, no emails before this date will be returned - markRead -- if True, marks all returned emails as read in target inbox - - Returns: - A list of unread email objects. - """ - conn = imaplib.IMAP4_SSL('imap.gmail.com') - conn.debug = 0 - conn.login(profile['gmail_address'], profile['gmail_password']) - conn.select(readonly=(not markRead)) - - msgs = [] - (retcode, messages) = conn.search(None, '(UNSEEN)') - - if retcode == 'OK' and messages != ['']: - numUnread = len(messages[0].split(' ')) - if limit and numUnread > limit: - return numUnread - - for num in messages[0].split(' '): - # parse email RFC822 format - ret, data = conn.fetch(num, '(RFC822)') - msg = email.message_from_string(data[0][1]) - - if not since or getDate(msg) > since: - msgs.append(msg) - conn.close() - conn.logout() - - return msgs - - -def handle(text, mic, profile): - """ - Responds to user-input, typically speech text, with a summary of - the user's Gmail inbox, reporting on the number of unread emails - in the inbox, as well as their senders. - - Arguments: - text -- user-input, typically transcribed speech - mic -- used to interact with the user (for both input and output) - profile -- contains information related to the user (e.g., Gmail address) - """ - try: - msgs = fetchUnreadEmails(profile, limit=5) - - if isinstance(msgs, int): - response = "You have %d unread emails." % msgs - mic.say(response) - return - - senders = [getSender(e) for e in msgs] - except imaplib.IMAP4.error: - mic.say( - "I'm sorry. I'm not authenticated to work with your Gmail.") - return - - if not senders: - mic.say("You have no unread emails.") - elif len(senders) == 1: - mic.say("You have one unread email from " + senders[0] + ".") - else: - response = "You have %d unread emails" % len( - senders) - unique_senders = list(set(senders)) - if len(unique_senders) > 1: - unique_senders[-1] = 'and ' + unique_senders[-1] - response += ". Senders include: " - response += '...'.join(senders) - else: - response += " from " + unittest[0] - - mic.say(response) - - -def isValid(text): - """ - Returns True if the input is related to email. - - Arguments: - text -- user-input, typically transcribed speech - """ - return bool(re.search(r'\bemail\b', text, re.IGNORECASE)) diff --git a/client/modules/HN.py b/client/modules/HN.py deleted file mode 100644 index 493fa1d..0000000 --- a/client/modules/HN.py +++ /dev/null @@ -1,130 +0,0 @@ -import urllib2 -import re -import random -from bs4 import BeautifulSoup -import app_utils -from semantic.numbers import NumberService - -WORDS = ["HACKER", "NEWS", "YES", "NO", "FIRST", "SECOND", "THIRD"] - -PRIORITY = 4 - -URL = 'http://news.ycombinator.com' - - -class HNStory: - - def __init__(self, title, URL): - self.title = title - self.URL = URL - - -def getTopStories(maxResults=None): - """ - Returns the top headlines from Hacker News. - - Arguments: - maxResults -- if provided, returns a random sample of size maxResults - """ - hdr = {'User-Agent': 'Mozilla/5.0'} - req = urllib2.Request(URL, headers=hdr) - page = urllib2.urlopen(req).read() - soup = BeautifulSoup(page) - matches = soup.findAll('td', class_="title") - matches = [m.a for m in matches if m.a and m.text != u'More'] - matches = [HNStory(m.text, m['href']) for m in matches] - - if maxResults: - num_stories = min(maxResults, len(matches)) - return random.sample(matches, num_stories) - - return matches - - -def handle(text, mic, profile): - """ - Responds to user-input, typically speech text, with a sample of - Hacker News's top headlines, sending them to the user over email - if desired. - - Arguments: - text -- user-input, typically transcribed speech - mic -- used to interact with the user (for both input and output) - profile -- contains information related to the user (e.g., phone number) - """ - mic.say("Pulling up some stories.") - stories = getTopStories(maxResults=3) - all_titles = '... '.join(str(idx + 1) + ") " + - story.title for idx, story in enumerate(stories)) - - def handleResponse(text): - - def extractOrdinals(text): - output = [] - service = NumberService() - for w in text.split(): - if w in service.__ordinals__: - output.append(service.__ordinals__[w]) - return [service.parse(w) for w in output] - - chosen_articles = extractOrdinals(text) - send_all = chosen_articles is [] and app_utils.isPositive(text) - - if send_all or chosen_articles: - mic.say("Sure, just give me a moment") - - if profile['prefers_email']: - body = "" - if not app_utils.emailUser(profile, SUBJECT="From the Front Page of Hacker News", BODY=body): - mic.say( - "I'm having trouble sending you these articles. Please make sure that your phone number and carrier are correct on the dashboard.") - return - - mic.say("All done.") - - else: - mic.say("OK I will not send any articles") - - if not profile['prefers_email'] and profile['phone_number']: - mic.say("Here are some front-page articles. " + - all_titles + ". Would you like me to send you these? If so, which?") - handleResponse(mic.activeListen()) - - else: - mic.say( - "Here are some front-page articles. " + all_titles) - - -def isValid(text): - """ - Returns True if the input is related to Hacker News. - - Arguments: - text -- user-input, typically transcribed speech - """ - return bool(re.search(r'\b(hack(er)?|HN)\b', text, re.IGNORECASE)) diff --git a/client/modules/Joke.py b/client/modules/Joke.py deleted file mode 100644 index 52b3eb9..0000000 --- a/client/modules/Joke.py +++ /dev/null @@ -1,63 +0,0 @@ -import random -import re - -WORDS = ["JOKE", "KNOCK KNOCK"] - - -def getRandomJoke(filename="../static/text/JOKES.txt"): - jokeFile = open(filename, "r") - jokes = [] - start = "" - end = "" - for line in jokeFile.readlines(): - line = line.replace("\n", "") - - if start == "": - start = line - continue - - if end == "": - end = line - continue - - jokes.append((start, end)) - start = "" - end = "" - - jokes.append((start, end)) - joke = random.choice(jokes) - return joke - - -def handle(text, mic, profile): - """ - Responds to user-input, typically speech text, by telling a joke. - - Arguments: - text -- user-input, typically transcribed speech - mic -- used to interact with the user (for both input and output) - profile -- contains information related to the user (e.g., phone number) - """ - joke = getRandomJoke() - - mic.say("Knock knock") - - def firstLine(text): - mic.say(joke[0]) - - def punchLine(text): - mic.say(joke[1]) - - punchLine(mic.activeListen()) - - firstLine(mic.activeListen()) - - -def isValid(text): - """ - Returns True if the input is related to jokes/humor. - - Arguments: - text -- user-input, typically transcribed speech - """ - return bool(re.search(r'\bjoke\b', text, re.IGNORECASE)) diff --git a/client/modules/Life.py b/client/modules/Life.py deleted file mode 100644 index 2c79ee1..0000000 --- a/client/modules/Life.py +++ /dev/null @@ -1,32 +0,0 @@ -import random -import re - -WORDS = ["MEANING", "OF", "LIFE"] - - -def handle(text, mic, profile): - """ - Responds to user-input, typically speech text, by relaying the - meaning of life. - - Arguments: - text -- user-input, typically transcribed speech - mic -- used to interact with the user (for both input and output) - profile -- contains information related to the user (e.g., phone number) - """ - messages = ["It's 42, you idiot.", - "It's 42. How many times do I have to tell you?"] - - message = random.choice(messages) - - mic.say(message) - - -def isValid(text): - """ - Returns True if the input is related to the meaning of life. - - Arguments: - text -- user-input, typically transcribed speech - """ - return bool(re.search(r'\bmeaning of life\b', text, re.IGNORECASE)) diff --git a/client/modules/News.py b/client/modules/News.py deleted file mode 100644 index 59495a3..0000000 --- a/client/modules/News.py +++ /dev/null @@ -1,122 +0,0 @@ -import feedparser -import app_utils -import re -from semantic.numbers import NumberService - -WORDS = ["NEWS", "YES", "NO", "FIRST", "SECOND", "THIRD"] - -PRIORITY = 3 - -URL = 'http://news.ycombinator.com' - - -class Article: - - def __init__(self, title, URL): - self.title = title - self.URL = URL - - -def getTopArticles(maxResults=None): - d = feedparser.parse("http://news.google.com/?output=rss") - - count = 0 - articles = [] - for item in d['items']: - articles.append(Article(item['title'], item['link'].split("&url=")[1])) - count += 1 - if maxResults and count > maxResults: - break - - return articles - - -def handle(text, mic, profile): - """ - Responds to user-input, typically speech text, with a summary of - the day's top news headlines, sending them to the user over email - if desired. - - Arguments: - text -- user-input, typically transcribed speech - mic -- used to interact with the user (for both input and output) - profile -- contains information related to the user (e.g., phone number) - """ - mic.say("Pulling up the news") - articles = getTopArticles(maxResults=3) - titles = [" ".join(x.title.split(" - ")[:-1]) for x in articles] - all_titles = "... ".join(str(idx + 1) + ")" + - title for idx, title in enumerate(titles)) - - def handleResponse(text): - - def extractOrdinals(text): - output = [] - service = NumberService() - for w in text.split(): - if w in service.__ordinals__: - output.append(service.__ordinals__[w]) - return [service.parse(w) for w in output] - - chosen_articles = extractOrdinals(text) - send_all = chosen_articles is [] and app_utils.isPositive(text) - - if send_all or chosen_articles: - mic.say("Sure, just give me a moment") - - if profile['prefers_email']: - body = "" - if not app_utils.emailUser(profile, SUBJECT="Your Top Headlines", BODY=body): - mic.say( - "I'm having trouble sending you these articles. Please make sure that your phone number and carrier are correct on the dashboard.") - return - - mic.say("All set") - - else: - - mic.say("OK I will not send any articles") - - if 'phone_number' in profile: - mic.say("Here are the current top headlines. " + all_titles + - ". Would you like me to send you these articles? If so, which?") - handleResponse(mic.activeListen()) - - else: - mic.say( - "Here are the current top headlines. " + all_titles) - - -def isValid(text): - """ - Returns True if the input is related to the news. - - Arguments: - text -- user-input, typically transcribed speech - """ - return bool(re.search(r'\b(news|headline)\b', text, re.IGNORECASE)) diff --git a/client/modules/Notifications.py b/client/modules/Notifications.py deleted file mode 100644 index fc91da1..0000000 --- a/client/modules/Notifications.py +++ /dev/null @@ -1,55 +0,0 @@ -import re -from facebook import * - - -WORDS = ["FACEBOOK", "NOTIFICATION"] - - -def handle(text, mic, profile): - """ - Responds to user-input, typically speech text, with a summary of - the user's Facebook notifications, including a count and details - related to each individual notification. - - Arguments: - text -- user-input, typically transcribed speech - mic -- used to interact with the user (for both input and output) - profile -- contains information related to the user (e.g., phone number) - """ - oauth_access_token = profile['keys']['FB_TOKEN'] - - graph = GraphAPI(oauth_access_token) - - try: - results = graph.request("me/notifications") - except GraphAPIError: - mic.say( - "I have not been authorized to query your Facebook. If you would like to check your notifications in the future, please visit the Jasper dashboard.") - return - except: - mic.say( - "I apologize, there's a problem with that service at the moment.") - - if not len(results['data']): - mic.say("You have no Facebook notifications. ") - return - - updates = [] - for notification in results['data']: - updates.append(notification['title']) - - count = len(results['data']) - mic.say("You have " + str(count) + - " Facebook notifications. " + " ".join(updates) + ". ") - - return - - -def isValid(text): - """ - Returns True if the input is related to Facebook notifications. - - Arguments: - text -- user-input, typically transcribed speech - """ - return bool(re.search(r'\bnotification|Facebook\b', text, re.IGNORECASE)) diff --git a/client/modules/Time.py b/client/modules/Time.py deleted file mode 100644 index 2fe8a10..0000000 --- a/client/modules/Time.py +++ /dev/null @@ -1,33 +0,0 @@ -import datetime -import re -from app_utils import getTimezone -from semantic.dates import DateService - -WORDS = ["TIME"] - - -def handle(text, mic, profile): - """ - Reports the current time based on the user's timezone. - - Arguments: - text -- user-input, typically transcribed speech - mic -- used to interact with the user (for both input and output) - profile -- contains information related to the user (e.g., phone number) - """ - - tz = getTimezone(profile) - now = datetime.datetime.now(tz=tz) - service = DateService() - response = service.convertTime(now) - mic.say("It is %s right now." % response) - - -def isValid(text): - """ - Returns True if input is related to the time. - - Arguments: - text -- user-input, typically transcribed speech - """ - return bool(re.search(r'\btime\b', text, re.IGNORECASE)) diff --git a/client/modules/Unclear.py b/client/modules/Unclear.py deleted file mode 100644 index 23ab2ed..0000000 --- a/client/modules/Unclear.py +++ /dev/null @@ -1,29 +0,0 @@ -from sys import maxint -import random - -WORDS = [] - -PRIORITY = -(maxint + 1) - - -def handle(text, mic, profile): - """ - Reports that the user has unclear or unusable input. - - Arguments: - text -- user-input, typically transcribed speech - mic -- used to interact with the user (for both input and output) - profile -- contains information related to the user (e.g., phone number) - """ - - messages = ["I'm sorry, could you repeat that?", - "My apologies, could you try saying that again?", - "Say that again?", "I beg your pardon?"] - - message = random.choice(messages) - - mic.say(message) - - -def isValid(text): - return True diff --git a/client/modules/Weather.py b/client/modules/Weather.py deleted file mode 100644 index ad1ac29..0000000 --- a/client/modules/Weather.py +++ /dev/null @@ -1,110 +0,0 @@ -import re -import datetime -import feedparser -from app_utils import getTimezone -from semantic.dates import DateService - -WORDS = ["WEATHER", "TODAY", "TOMORROW"] - - -def replaceAcronyms(text): - """Replaces some commonly-used acronyms for an improved verbal weather report.""" - - def parseDirections(text): - words = { - 'N': 'north', - 'S': 'south', - 'E': 'east', - 'W': 'west', - } - output = [words[w] for w in list(text)] - return ' '.join(output) - acronyms = re.findall(r'\b([NESW]+)\b', text) - - for w in acronyms: - text = text.replace(w, parseDirections(w)) - - text = re.sub(r'(\b\d+)F(\b)', '\g<1> Fahrenheit\g<2>', text) - text = re.sub(r'(\b)mph(\b)', '\g<1>miles per hour\g<2>', text) - text = re.sub(r'(\b)in\.', '\g<1>inches', text) - - return text - - -def getForecast(profile): - return feedparser.parse("http://rss.wunderground.com/auto/rss_full/" - + str(profile['location']))['entries'] - - -def handle(text, mic, profile): - """ - Responds to user-input, typically speech text, with a summary of - the relevant weather for the requested date (typically, weather - information will not be available for days beyond tomorrow). - - Arguments: - text -- user-input, typically transcribed speech - mic -- used to interact with the user (for both input and output) - profile -- contains information related to the user (e.g., phone number) - """ - - if not profile['location']: - mic.say( - "I'm sorry, I can't seem to access that information. Please make sure that you've set your location on the dashboard.") - return - - tz = getTimezone(profile) - - service = DateService(tz=tz) - date = service.extractDay(text) - if not date: - date = datetime.datetime.now(tz=tz) - weekday = service.__daysOfWeek__[date.weekday()] - - if date.weekday() == datetime.datetime.now(tz=tz).weekday(): - date_keyword = "Today" - elif date.weekday() == ( - datetime.datetime.now(tz=tz).weekday() + 1) % 7: - date_keyword = "Tomorrow" - else: - date_keyword = "On " + weekday - - forecast = getForecast(profile) - - output = None - - for entry in forecast: - try: - date_desc = entry['title'].split()[0].strip().lower() - if date_desc == 'forecast': #For global forecasts - date_desc = entry['title'].split()[2].strip().lower() - weather_desc = entry['summary'] - - elif date_desc == 'current': #For first item of global forecasts - continue - else: - weather_desc = entry['summary'].split('-')[1] #US forecasts - - if weekday == date_desc: - output = date_keyword + \ - ", the weather will be " + weather_desc + "." - break - except: - continue - - if output: - output = replaceAcronyms(output) - mic.say(output) - else: - mic.say( - "I'm sorry. I can't see that far ahead.") - - -def isValid(text): - """ - Returns True if the text is related to the weather. - - Arguments: - text -- user-input, typically transcribed speech - """ - return bool(re.search(r'\b(weathers?|temperature|forecast|outside|hot|cold|jacket|coat|rain)\b', text, re.IGNORECASE)) diff --git a/client/modules/__init__.py b/client/modules/__init__.py deleted file mode 100755 index e69de29..0000000 diff --git a/client/modules/app_utils.py b/client/modules/app_utils.py deleted file mode 100644 index 7e39cbf..0000000 --- a/client/modules/app_utils.py +++ /dev/null @@ -1,123 +0,0 @@ -import smtplib -from email.MIMEText import MIMEText -import urllib2 -import re -import requests -from pytz import timezone - - -def sendEmail(SUBJECT, BODY, TO, FROM, SENDER, PASSWORD, SMTP_SERVER): - """Sends an HTML email.""" - for body_charset in 'US-ASCII', 'ISO-8859-1', 'UTF-8': - try: - BODY.encode(body_charset) - except UnicodeError: - pass - else: - break - msg = MIMEText(BODY.encode(body_charset), 'html', body_charset) - msg['From'] = SENDER - msg['To'] = TO - msg['Subject'] = SUBJECT - - SMTP_PORT = 587 - session = smtplib.SMTP(SMTP_SERVER, SMTP_PORT) - session.starttls() - session.login(FROM, PASSWORD) - session.sendmail(SENDER, TO, msg.as_string()) - session.quit() - - -def emailUser(profile, SUBJECT="", BODY=""): - """ - Sends an email. - - Arguments: - profile -- contains information related to the user (e.g., email address) - SUBJECT -- subject line of the email - BODY -- body text of the email - """ - def generateSMSEmail(profile): - """Generates an email from a user's phone number based on their carrier.""" - if profile['carrier'] is None or not profile['phone_number']: - return None - - return str(profile['phone_number']) + "@" + profile['carrier'] - - if profile['prefers_email'] and profile['gmail_address']: - # add footer - if BODY: - BODY = profile['first_name'] + \ - ",

Here are your top headlines:" + BODY - BODY += "
Sent from your Jasper" - - recipient = profile['gmail_address'] - if profile['first_name'] and profile['last_name']: - recipient = profile['first_name'] + " " + \ - profile['last_name'] + " <%s>" % recipient - else: - recipient = generateSMSEmail(profile) - - if not recipient: - return False - - try: - if 'mailgun' in profile: - user = profile['mailgun']['username'] - password = profile['mailgun']['password'] - server = 'smtp.mailgun.org' - else: - user = profile['gmail_address'] - password = profile['gmail_password'] - server = 'smtp.gmail.com' - sendEmail(SUBJECT, BODY, recipient, user, - "Jasper ", password, server) - - return True - except: - return False - - -def getTimezone(profile): - """ - Returns the pytz timezone for a given profile. - - Arguments: - profile -- contains information related to the user (e.g., email address) - """ - try: - return timezone(profile['timezone']) - except: - return None - - -def generateTinyURL(URL): - """ - Generates a compressed URL. - - Arguments: - URL -- the original URL to-be compressed - """ - target = "http://tinyurl.com/api-create.php?url=" + URL - response = urllib2.urlopen(target) - return response.read() - - -def isNegative(phrase): - """ - Returns True if the input phrase has a negative sentiment. - - Arguments: - phrase -- the input phrase to-be evaluated - """ - return bool(re.search(r'\b(no(t)?|don\'t|stop|end)\b', phrase, re.IGNORECASE)) - - -def isPositive(phrase): - """ - Returns True if the input phrase has a positive sentiment. - - Arguments: - phrase -- the input phrase to-be evaluated - """ - return re.search(r'\b(sure|yes|yeah|go)\b', phrase, re.IGNORECASE) diff --git a/client/notifier.py b/client/notifier.py index 341ae04..5e33c5b 100644 --- a/client/notifier.py +++ b/client/notifier.py @@ -1,5 +1,5 @@ import Queue -from modules import Gmail +#from modules import Gmail from apscheduler.scheduler import Scheduler import logging logging.basicConfig() @@ -24,7 +24,7 @@ class Notifier(object): ] sched = Scheduler() - sched.start() + #sched.start() sched.add_interval_job(self.gather, seconds=30) def gather(self): diff --git a/client/populate.py b/client/populate.py index 48b8670..4778a74 100644 --- a/client/populate.py +++ b/client/populate.py @@ -84,6 +84,9 @@ def run(): response = raw_input("Please choose email (E) or text message (T): ") profile['prefers_email'] = (response == 'E') + print ("\nIf you wish to depend on the Google Speech To Text API, please enter your API key, or leave blank to use Jasper's default speech to text implementation.") + simple_request('api_key', 'API Key') + # write to profile print("Writing to profile...") outputFile = open("profile.yml", "w") diff --git a/client/stt.py b/client/stt.py new file mode 100644 index 0000000..a62cf38 --- /dev/null +++ b/client/stt.py @@ -0,0 +1,101 @@ +import os +import traceback +import json +import urllib2 + +class PocketSphinxSTT(object): + + def __init__(self, lmd = "languagemodel.lm", dictd = "dictionary.dic", lmd_persona = "languagemodel_persona.lm", dictd_persona = "dictionary_persona.dic", lmd_music=None, dictd_music=None): + """ + Initiates the pocketsphinx instance. + + Arguments: + speaker -- handles platform-independent audio output + lmd -- filename of the full language model + dictd -- filename of the full dictionary (.dic) + lmd_persona -- filename of the 'Persona' language model (containing, e.g., 'Jasper') + dictd_persona -- filename of the 'Persona' dictionary (.dic) + """ + + # quirky bug where first import doesn't work + try: + import pocketsphinx as ps + except: + import pocketsphinx as ps + + hmdir = "/usr/local/share/pocketsphinx/model/hmm/en_US/hub4wsj_sc_8k" + + if lmd_music and dictd_music: + self.speechRec_music = ps.Decoder(hmm = hmdir, lm = lmd_music, dict = dictd_music) + self.speechRec_persona = ps.Decoder( + hmm=hmdir, lm=lmd_persona, dict=dictd_persona) + self.speechRec = ps.Decoder(hmm=hmdir, lm=lmd, dict=dictd) + + def transcribe(self, audio_file_path, PERSONA_ONLY=False, MUSIC=False): + """ + Performs STT, transcribing an audio file and returning the result. + + Arguments: + audio_file_path -- the path to the audio file to-be transcribed + PERSONA_ONLY -- if True, uses the 'Persona' language model and dictionary + MUSIC -- if True, uses the 'Music' language model and dictionary + """ + + wavFile = file(audio_file_path, 'rb') + wavFile.seek(44) + + if MUSIC: + self.speechRec_music.decode_raw(wavFile) + result = self.speechRec_music.get_hyp() + elif PERSONA_ONLY: + self.speechRec_persona.decode_raw(wavFile) + result = self.speechRec_persona.get_hyp() + else: + self.speechRec.decode_raw(wavFile) + result = self.speechRec.get_hyp() + + print "===================" + print "JASPER: " + result[0] + print "===================" + + return result[0] + + +class GoogleSTT(object): + + RATE = 44100 + + def __init__(self, api_key): + self.api_key = api_key + + def transcribe(self, audio_file): + + AUDIO_FILE_FLAC = "active.flac" + os.system("ffmpeg -y -i %s -c:a flac -ab 44100 %s" % (audio_file, AUDIO_FILE_FLAC)) + + url = "https://www.google.com/speech-api/v2/recognize?output=json&client=chromium&key=%s&lang=%s&maxresults=6&pfilter=2" % (self.api_key, "en-us") + flac = open(AUDIO_FILE_FLAC, 'rb') + data = flac.read() + flac.close() + try: + req = urllib2.Request( + url, + data=data, + headers={ + 'Content-type': 'audio/x-flac; rate=%s' % GoogleSTT.RATE}) + response_url = urllib2.urlopen(req) + response_read = response_url.read() + response_read = response_read.decode('utf-8') + decoded = json.loads(response_read.split("\n")[1]) + print response_read + text = decoded['result'][0]['alternative'][0]['transcript'] + print text + return text + except Exception: + traceback.print_exc() + +def newSTTEngine(api_key = None): + if api_key: + return GoogleSTT(api_key) + else: + return PocketSphinxSTT() -- cgit v1.3.1 From 78389f252a021d642a7a0146b658f0465da6e264 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sat, 26 Jul 2014 23:31:25 +0000 Subject: Work on Google STT on RPi. --- client/main.py | 9 ++- client/mic.py | 39 +----------- client/modules/Birthday.py | 64 +++++++++++++++++++ client/modules/Gmail.py | 136 ++++++++++++++++++++++++++++++++++++++++ client/modules/HN.py | 130 ++++++++++++++++++++++++++++++++++++++ client/modules/Joke.py | 63 +++++++++++++++++++ client/modules/Life.py | 32 ++++++++++ client/modules/News.py | 122 +++++++++++++++++++++++++++++++++++ client/modules/Notifications.py | 55 ++++++++++++++++ client/modules/Time.py | 33 ++++++++++ client/modules/Unclear.py | 29 +++++++++ client/modules/Weather.py | 110 ++++++++++++++++++++++++++++++++ client/modules/__init__.py | 0 client/modules/app_utils.py | 123 ++++++++++++++++++++++++++++++++++++ client/populate.py | 2 +- client/stt.py | 60 ++++++++++++++++-- 16 files changed, 962 insertions(+), 45 deletions(-) create mode 100644 client/modules/Birthday.py create mode 100644 client/modules/Gmail.py create mode 100644 client/modules/HN.py create mode 100644 client/modules/Joke.py create mode 100644 client/modules/Life.py create mode 100644 client/modules/News.py create mode 100644 client/modules/Notifications.py create mode 100644 client/modules/Time.py create mode 100644 client/modules/Unclear.py create mode 100644 client/modules/Weather.py create mode 100755 client/modules/__init__.py create mode 100644 client/modules/app_utils.py diff --git a/client/main.py b/client/main.py index 4a54f3f..7920832 100644 --- a/client/main.py +++ b/client/main.py @@ -23,8 +23,13 @@ if __name__ == "__main__": profile = yaml.safe_load(open("profile.yml", "r")) - # TODO: rename 'api_key' to 'google_stt_api_key' - mic = Mic(speaker.newSpeaker(), PocketSphinxSTT(), stt.newSTTEngine(profile['api_key'])) + try: + google_api_key = profile['google_api_key'] + except KeyError: + print "Google STT API Key not present in profile - defaulting to PocketSphinx..." + google_api_key = None + + mic = Mic(speaker.newSpeaker(), PocketSphinxSTT(), stt.newSTTEngine(google_api_key)) mic.say("How can I be of service?") diff --git a/client/mic.py b/client/mic.py index 48f14de..4798790 100644 --- a/client/mic.py +++ b/client/mic.py @@ -21,44 +21,13 @@ class Mic: Arguments: speaker -- handles platform-independent audio output - lmd -- filename of the full language model - dictd -- filename of the full dictionary (.dic) - lmd_persona -- filename of the 'Persona' language model (containing, e.g., 'Jasper') - dictd_persona -- filename of the 'Persona' dictionary (.dic) + passive_stt_engine -- performs STT while Jasper is in passive listen mode + acive_stt_engine -- performs STT while Jasper is in active listen mode """ self.speaker = speaker self.passive_stt_engine = passive_stt_engine self.active_stt_engine = active_stt_engine - def transcribe(self, audio_file_path, PERSONA_ONLY=False, MUSIC=False): - """ - Performs TTS, transcribing an audio file and returning the result. - - Arguments: - audio_file_path -- the path to the audio file to-be transcribed - PERSONA_ONLY -- if True, uses the 'Persona' language model and dictionary - MUSIC -- if True, uses the 'Music' language model and dictionary - """ - - wavFile = file(audio_file_path, 'rb') - wavFile.seek(44) - - if MUSIC: - self.speechRec_music.decode_raw(wavFile) - result = self.speechRec_music.get_hyp() - elif PERSONA_ONLY: - self.speechRec_persona.decode_raw(wavFile) - result = self.speechRec_persona.get_hyp() - else: - self.speechRec.decode_raw(wavFile) - result = self.speechRec.get_hyp() - - print "===================" - print "JASPER: " + result[0] - print "===================" - - return result[0] - def getScore(self, data): rms = audioop.rms(data, 2) score = rms / 3 @@ -198,7 +167,6 @@ class Mic: write_frames.close() # check if PERSONA was said - #transcribed = self.transcribe(AUDIO_FILE, PERSONA_ONLY=True) transcribed = self.passive_stt_engine.transcribe(AUDIO_FILE, PERSONA_ONLY=True) if PERSONA in transcribed: @@ -222,7 +190,6 @@ class Mic: if not os.path.exists(AUDIO_FILE): return None - #return self.transcribe(AUDIO_FILE) return self.active_stt_engine.transcribe(AUDIO_FILE) # check if no threshold provided @@ -276,10 +243,8 @@ class Mic: # os.system("sox "+AUDIO_FILE+" temp.wav vol 20dB") if MUSIC: - #return self.transcribe(AUDIO_FILE, MUSIC=True) return self.active_stt_engine.transcribe(AUDIO_FILE, MUSIC=True) - #return self.transcribe(AUDIO_FILE) return self.active_stt_engine.transcribe(AUDIO_FILE) def say(self, phrase, OPTIONS=" -vdefault+m3 -p 40 -s 160 --stdout > say.wav"): diff --git a/client/modules/Birthday.py b/client/modules/Birthday.py new file mode 100644 index 0000000..1388443 --- /dev/null +++ b/client/modules/Birthday.py @@ -0,0 +1,64 @@ +import datetime +import re +from facebook import * +from app_utils import getTimezone + +WORDS = ["BIRTHDAY"] + + +def handle(text, mic, profile): + """ + Responds to user-input, typically speech text, by listing the user's + Facebook friends with birthdays today. + + Arguments: + text -- user-input, typically transcribed speech + mic -- used to interact with the user (for both input and output) + profile -- contains information related to the user (e.g., phone number) + """ + oauth_access_token = profile['keys']["FB_TOKEN"] + + graph = GraphAPI(oauth_access_token) + + try: + results = graph.request( + "me/friends", args={'fields': 'id,name,birthday'}) + except GraphAPIError: + mic.say( + "I have not been authorized to query your Facebook. If you would like to check birthdays in the future, please visit the Jasper dashboard.") + return + except: + mic.say( + "I apologize, there's a problem with that service at the moment.") + return + + needle = datetime.datetime.now(tz=getTimezone(profile)).strftime("%m/%d") + + people = [] + for person in results['data']: + try: + if needle in person['birthday']: + people.append(person['name']) + except: + continue + + if len(people) > 0: + if len(people) == 1: + output = people[0] + " has a birthday today." + else: + output = "Your friends with birthdays today are " + \ + ", ".join(people[:-1]) + " and " + people[-1] + "." + else: + output = "None of your friends have birthdays today." + + mic.say(output) + + +def isValid(text): + """ + Returns True if the input is related to birthdays. + + Arguments: + text -- user-input, typically transcribed speech + """ + return bool(re.search(r'birthday', text, re.IGNORECASE)) diff --git a/client/modules/Gmail.py b/client/modules/Gmail.py new file mode 100644 index 0000000..b401ae6 --- /dev/null +++ b/client/modules/Gmail.py @@ -0,0 +1,136 @@ +import imaplib +import email +import re +from dateutil import parser +from app_utils import * + +WORDS = ["EMAIL", "INBOX"] + + +def getSender(email): + """ + Returns the best-guess sender of an email. + + Arguments: + email -- the email whose sender is desired + + Returns: + Sender of the email. + """ + sender = email['From'] + m = re.match(r'(.*)\s<.*>', sender) + if m: + return m.group(1) + return sender + + +def getDate(email): + return parser.parse(email.get('date')) + + +def getMostRecentDate(emails): + """ + Returns the most recent date of any email in the list provided. + + Arguments: + emails -- a list of emails to check + + Returns: + Date of the most recent email. + """ + dates = [getDate(e) for e in emails] + dates.sort(reverse=True) + if dates: + return dates[0] + return None + + +def fetchUnreadEmails(profile, since=None, markRead=False, limit=None): + """ + Fetches a list of unread email objects from a user's Gmail inbox. + + Arguments: + profile -- contains information related to the user (e.g., Gmail address) + since -- if provided, no emails before this date will be returned + markRead -- if True, marks all returned emails as read in target inbox + + Returns: + A list of unread email objects. + """ + conn = imaplib.IMAP4_SSL('imap.gmail.com') + conn.debug = 0 + conn.login(profile['gmail_address'], profile['gmail_password']) + conn.select(readonly=(not markRead)) + + msgs = [] + (retcode, messages) = conn.search(None, '(UNSEEN)') + + if retcode == 'OK' and messages != ['']: + numUnread = len(messages[0].split(' ')) + if limit and numUnread > limit: + return numUnread + + for num in messages[0].split(' '): + # parse email RFC822 format + ret, data = conn.fetch(num, '(RFC822)') + msg = email.message_from_string(data[0][1]) + + if not since or getDate(msg) > since: + msgs.append(msg) + conn.close() + conn.logout() + + return msgs + + +def handle(text, mic, profile): + """ + Responds to user-input, typically speech text, with a summary of + the user's Gmail inbox, reporting on the number of unread emails + in the inbox, as well as their senders. + + Arguments: + text -- user-input, typically transcribed speech + mic -- used to interact with the user (for both input and output) + profile -- contains information related to the user (e.g., Gmail address) + """ + try: + msgs = fetchUnreadEmails(profile, limit=5) + + if isinstance(msgs, int): + response = "You have %d unread emails." % msgs + mic.say(response) + return + + senders = [getSender(e) for e in msgs] + except imaplib.IMAP4.error: + mic.say( + "I'm sorry. I'm not authenticated to work with your Gmail.") + return + + if not senders: + mic.say("You have no unread emails.") + elif len(senders) == 1: + mic.say("You have one unread email from " + senders[0] + ".") + else: + response = "You have %d unread emails" % len( + senders) + unique_senders = list(set(senders)) + if len(unique_senders) > 1: + unique_senders[-1] = 'and ' + unique_senders[-1] + response += ". Senders include: " + response += '...'.join(senders) + else: + response += " from " + unittest[0] + + mic.say(response) + + +def isValid(text): + """ + Returns True if the input is related to email. + + Arguments: + text -- user-input, typically transcribed speech + """ + return bool(re.search(r'\bemail\b', text, re.IGNORECASE)) diff --git a/client/modules/HN.py b/client/modules/HN.py new file mode 100644 index 0000000..493fa1d --- /dev/null +++ b/client/modules/HN.py @@ -0,0 +1,130 @@ +import urllib2 +import re +import random +from bs4 import BeautifulSoup +import app_utils +from semantic.numbers import NumberService + +WORDS = ["HACKER", "NEWS", "YES", "NO", "FIRST", "SECOND", "THIRD"] + +PRIORITY = 4 + +URL = 'http://news.ycombinator.com' + + +class HNStory: + + def __init__(self, title, URL): + self.title = title + self.URL = URL + + +def getTopStories(maxResults=None): + """ + Returns the top headlines from Hacker News. + + Arguments: + maxResults -- if provided, returns a random sample of size maxResults + """ + hdr = {'User-Agent': 'Mozilla/5.0'} + req = urllib2.Request(URL, headers=hdr) + page = urllib2.urlopen(req).read() + soup = BeautifulSoup(page) + matches = soup.findAll('td', class_="title") + matches = [m.a for m in matches if m.a and m.text != u'More'] + matches = [HNStory(m.text, m['href']) for m in matches] + + if maxResults: + num_stories = min(maxResults, len(matches)) + return random.sample(matches, num_stories) + + return matches + + +def handle(text, mic, profile): + """ + Responds to user-input, typically speech text, with a sample of + Hacker News's top headlines, sending them to the user over email + if desired. + + Arguments: + text -- user-input, typically transcribed speech + mic -- used to interact with the user (for both input and output) + profile -- contains information related to the user (e.g., phone number) + """ + mic.say("Pulling up some stories.") + stories = getTopStories(maxResults=3) + all_titles = '... '.join(str(idx + 1) + ") " + + story.title for idx, story in enumerate(stories)) + + def handleResponse(text): + + def extractOrdinals(text): + output = [] + service = NumberService() + for w in text.split(): + if w in service.__ordinals__: + output.append(service.__ordinals__[w]) + return [service.parse(w) for w in output] + + chosen_articles = extractOrdinals(text) + send_all = chosen_articles is [] and app_utils.isPositive(text) + + if send_all or chosen_articles: + mic.say("Sure, just give me a moment") + + if profile['prefers_email']: + body = "
    " + + def formatArticle(article): + tiny_url = app_utils.generateTinyURL(article.URL) + + if profile['prefers_email']: + return "
  • %s
  • " % (tiny_url, + article.title) + else: + return article.title + " -- " + tiny_url + + for idx, article in enumerate(stories): + if send_all or (idx + 1) in chosen_articles: + article_link = formatArticle(article) + + if profile['prefers_email']: + body += article_link + else: + if not app_utils.emailUser(profile, SUBJECT="", BODY=article_link): + mic.say( + "I'm having trouble sending you these articles. Please make sure that your phone number and carrier are correct on the dashboard.") + return + + # if prefers email, we send once, at the end + if profile['prefers_email']: + body += "
" + if not app_utils.emailUser(profile, SUBJECT="From the Front Page of Hacker News", BODY=body): + mic.say( + "I'm having trouble sending you these articles. Please make sure that your phone number and carrier are correct on the dashboard.") + return + + mic.say("All done.") + + else: + mic.say("OK I will not send any articles") + + if not profile['prefers_email'] and profile['phone_number']: + mic.say("Here are some front-page articles. " + + all_titles + ". Would you like me to send you these? If so, which?") + handleResponse(mic.activeListen()) + + else: + mic.say( + "Here are some front-page articles. " + all_titles) + + +def isValid(text): + """ + Returns True if the input is related to Hacker News. + + Arguments: + text -- user-input, typically transcribed speech + """ + return bool(re.search(r'\b(hack(er)?|HN)\b', text, re.IGNORECASE)) diff --git a/client/modules/Joke.py b/client/modules/Joke.py new file mode 100644 index 0000000..52b3eb9 --- /dev/null +++ b/client/modules/Joke.py @@ -0,0 +1,63 @@ +import random +import re + +WORDS = ["JOKE", "KNOCK KNOCK"] + + +def getRandomJoke(filename="../static/text/JOKES.txt"): + jokeFile = open(filename, "r") + jokes = [] + start = "" + end = "" + for line in jokeFile.readlines(): + line = line.replace("\n", "") + + if start == "": + start = line + continue + + if end == "": + end = line + continue + + jokes.append((start, end)) + start = "" + end = "" + + jokes.append((start, end)) + joke = random.choice(jokes) + return joke + + +def handle(text, mic, profile): + """ + Responds to user-input, typically speech text, by telling a joke. + + Arguments: + text -- user-input, typically transcribed speech + mic -- used to interact with the user (for both input and output) + profile -- contains information related to the user (e.g., phone number) + """ + joke = getRandomJoke() + + mic.say("Knock knock") + + def firstLine(text): + mic.say(joke[0]) + + def punchLine(text): + mic.say(joke[1]) + + punchLine(mic.activeListen()) + + firstLine(mic.activeListen()) + + +def isValid(text): + """ + Returns True if the input is related to jokes/humor. + + Arguments: + text -- user-input, typically transcribed speech + """ + return bool(re.search(r'\bjoke\b', text, re.IGNORECASE)) diff --git a/client/modules/Life.py b/client/modules/Life.py new file mode 100644 index 0000000..2c79ee1 --- /dev/null +++ b/client/modules/Life.py @@ -0,0 +1,32 @@ +import random +import re + +WORDS = ["MEANING", "OF", "LIFE"] + + +def handle(text, mic, profile): + """ + Responds to user-input, typically speech text, by relaying the + meaning of life. + + Arguments: + text -- user-input, typically transcribed speech + mic -- used to interact with the user (for both input and output) + profile -- contains information related to the user (e.g., phone number) + """ + messages = ["It's 42, you idiot.", + "It's 42. How many times do I have to tell you?"] + + message = random.choice(messages) + + mic.say(message) + + +def isValid(text): + """ + Returns True if the input is related to the meaning of life. + + Arguments: + text -- user-input, typically transcribed speech + """ + return bool(re.search(r'\bmeaning of life\b', text, re.IGNORECASE)) diff --git a/client/modules/News.py b/client/modules/News.py new file mode 100644 index 0000000..59495a3 --- /dev/null +++ b/client/modules/News.py @@ -0,0 +1,122 @@ +import feedparser +import app_utils +import re +from semantic.numbers import NumberService + +WORDS = ["NEWS", "YES", "NO", "FIRST", "SECOND", "THIRD"] + +PRIORITY = 3 + +URL = 'http://news.ycombinator.com' + + +class Article: + + def __init__(self, title, URL): + self.title = title + self.URL = URL + + +def getTopArticles(maxResults=None): + d = feedparser.parse("http://news.google.com/?output=rss") + + count = 0 + articles = [] + for item in d['items']: + articles.append(Article(item['title'], item['link'].split("&url=")[1])) + count += 1 + if maxResults and count > maxResults: + break + + return articles + + +def handle(text, mic, profile): + """ + Responds to user-input, typically speech text, with a summary of + the day's top news headlines, sending them to the user over email + if desired. + + Arguments: + text -- user-input, typically transcribed speech + mic -- used to interact with the user (for both input and output) + profile -- contains information related to the user (e.g., phone number) + """ + mic.say("Pulling up the news") + articles = getTopArticles(maxResults=3) + titles = [" ".join(x.title.split(" - ")[:-1]) for x in articles] + all_titles = "... ".join(str(idx + 1) + ")" + + title for idx, title in enumerate(titles)) + + def handleResponse(text): + + def extractOrdinals(text): + output = [] + service = NumberService() + for w in text.split(): + if w in service.__ordinals__: + output.append(service.__ordinals__[w]) + return [service.parse(w) for w in output] + + chosen_articles = extractOrdinals(text) + send_all = chosen_articles is [] and app_utils.isPositive(text) + + if send_all or chosen_articles: + mic.say("Sure, just give me a moment") + + if profile['prefers_email']: + body = "
    " + + def formatArticle(article): + tiny_url = app_utils.generateTinyURL(article.URL) + + if profile['prefers_email']: + return "
  • %s
  • " % (tiny_url, + article.title) + else: + return article.title + " -- " + tiny_url + + for idx, article in enumerate(articles): + if send_all or (idx + 1) in chosen_articles: + article_link = formatArticle(article) + + if profile['prefers_email']: + body += article_link + else: + if not app_utils.emailUser(profile, SUBJECT="", BODY=article_link): + mic.say( + "I'm having trouble sending you these articles. Please make sure that your phone number and carrier are correct on the dashboard.") + return + + # if prefers email, we send once, at the end + if profile['prefers_email']: + body += "
" + if not app_utils.emailUser(profile, SUBJECT="Your Top Headlines", BODY=body): + mic.say( + "I'm having trouble sending you these articles. Please make sure that your phone number and carrier are correct on the dashboard.") + return + + mic.say("All set") + + else: + + mic.say("OK I will not send any articles") + + if 'phone_number' in profile: + mic.say("Here are the current top headlines. " + all_titles + + ". Would you like me to send you these articles? If so, which?") + handleResponse(mic.activeListen()) + + else: + mic.say( + "Here are the current top headlines. " + all_titles) + + +def isValid(text): + """ + Returns True if the input is related to the news. + + Arguments: + text -- user-input, typically transcribed speech + """ + return bool(re.search(r'\b(news|headline)\b', text, re.IGNORECASE)) diff --git a/client/modules/Notifications.py b/client/modules/Notifications.py new file mode 100644 index 0000000..fc91da1 --- /dev/null +++ b/client/modules/Notifications.py @@ -0,0 +1,55 @@ +import re +from facebook import * + + +WORDS = ["FACEBOOK", "NOTIFICATION"] + + +def handle(text, mic, profile): + """ + Responds to user-input, typically speech text, with a summary of + the user's Facebook notifications, including a count and details + related to each individual notification. + + Arguments: + text -- user-input, typically transcribed speech + mic -- used to interact with the user (for both input and output) + profile -- contains information related to the user (e.g., phone number) + """ + oauth_access_token = profile['keys']['FB_TOKEN'] + + graph = GraphAPI(oauth_access_token) + + try: + results = graph.request("me/notifications") + except GraphAPIError: + mic.say( + "I have not been authorized to query your Facebook. If you would like to check your notifications in the future, please visit the Jasper dashboard.") + return + except: + mic.say( + "I apologize, there's a problem with that service at the moment.") + + if not len(results['data']): + mic.say("You have no Facebook notifications. ") + return + + updates = [] + for notification in results['data']: + updates.append(notification['title']) + + count = len(results['data']) + mic.say("You have " + str(count) + + " Facebook notifications. " + " ".join(updates) + ". ") + + return + + +def isValid(text): + """ + Returns True if the input is related to Facebook notifications. + + Arguments: + text -- user-input, typically transcribed speech + """ + return bool(re.search(r'\bnotification|Facebook\b', text, re.IGNORECASE)) diff --git a/client/modules/Time.py b/client/modules/Time.py new file mode 100644 index 0000000..2fe8a10 --- /dev/null +++ b/client/modules/Time.py @@ -0,0 +1,33 @@ +import datetime +import re +from app_utils import getTimezone +from semantic.dates import DateService + +WORDS = ["TIME"] + + +def handle(text, mic, profile): + """ + Reports the current time based on the user's timezone. + + Arguments: + text -- user-input, typically transcribed speech + mic -- used to interact with the user (for both input and output) + profile -- contains information related to the user (e.g., phone number) + """ + + tz = getTimezone(profile) + now = datetime.datetime.now(tz=tz) + service = DateService() + response = service.convertTime(now) + mic.say("It is %s right now." % response) + + +def isValid(text): + """ + Returns True if input is related to the time. + + Arguments: + text -- user-input, typically transcribed speech + """ + return bool(re.search(r'\btime\b', text, re.IGNORECASE)) diff --git a/client/modules/Unclear.py b/client/modules/Unclear.py new file mode 100644 index 0000000..23ab2ed --- /dev/null +++ b/client/modules/Unclear.py @@ -0,0 +1,29 @@ +from sys import maxint +import random + +WORDS = [] + +PRIORITY = -(maxint + 1) + + +def handle(text, mic, profile): + """ + Reports that the user has unclear or unusable input. + + Arguments: + text -- user-input, typically transcribed speech + mic -- used to interact with the user (for both input and output) + profile -- contains information related to the user (e.g., phone number) + """ + + messages = ["I'm sorry, could you repeat that?", + "My apologies, could you try saying that again?", + "Say that again?", "I beg your pardon?"] + + message = random.choice(messages) + + mic.say(message) + + +def isValid(text): + return True diff --git a/client/modules/Weather.py b/client/modules/Weather.py new file mode 100644 index 0000000..ad1ac29 --- /dev/null +++ b/client/modules/Weather.py @@ -0,0 +1,110 @@ +import re +import datetime +import feedparser +from app_utils import getTimezone +from semantic.dates import DateService + +WORDS = ["WEATHER", "TODAY", "TOMORROW"] + + +def replaceAcronyms(text): + """Replaces some commonly-used acronyms for an improved verbal weather report.""" + + def parseDirections(text): + words = { + 'N': 'north', + 'S': 'south', + 'E': 'east', + 'W': 'west', + } + output = [words[w] for w in list(text)] + return ' '.join(output) + acronyms = re.findall(r'\b([NESW]+)\b', text) + + for w in acronyms: + text = text.replace(w, parseDirections(w)) + + text = re.sub(r'(\b\d+)F(\b)', '\g<1> Fahrenheit\g<2>', text) + text = re.sub(r'(\b)mph(\b)', '\g<1>miles per hour\g<2>', text) + text = re.sub(r'(\b)in\.', '\g<1>inches', text) + + return text + + +def getForecast(profile): + return feedparser.parse("http://rss.wunderground.com/auto/rss_full/" + + str(profile['location']))['entries'] + + +def handle(text, mic, profile): + """ + Responds to user-input, typically speech text, with a summary of + the relevant weather for the requested date (typically, weather + information will not be available for days beyond tomorrow). + + Arguments: + text -- user-input, typically transcribed speech + mic -- used to interact with the user (for both input and output) + profile -- contains information related to the user (e.g., phone number) + """ + + if not profile['location']: + mic.say( + "I'm sorry, I can't seem to access that information. Please make sure that you've set your location on the dashboard.") + return + + tz = getTimezone(profile) + + service = DateService(tz=tz) + date = service.extractDay(text) + if not date: + date = datetime.datetime.now(tz=tz) + weekday = service.__daysOfWeek__[date.weekday()] + + if date.weekday() == datetime.datetime.now(tz=tz).weekday(): + date_keyword = "Today" + elif date.weekday() == ( + datetime.datetime.now(tz=tz).weekday() + 1) % 7: + date_keyword = "Tomorrow" + else: + date_keyword = "On " + weekday + + forecast = getForecast(profile) + + output = None + + for entry in forecast: + try: + date_desc = entry['title'].split()[0].strip().lower() + if date_desc == 'forecast': #For global forecasts + date_desc = entry['title'].split()[2].strip().lower() + weather_desc = entry['summary'] + + elif date_desc == 'current': #For first item of global forecasts + continue + else: + weather_desc = entry['summary'].split('-')[1] #US forecasts + + if weekday == date_desc: + output = date_keyword + \ + ", the weather will be " + weather_desc + "." + break + except: + continue + + if output: + output = replaceAcronyms(output) + mic.say(output) + else: + mic.say( + "I'm sorry. I can't see that far ahead.") + + +def isValid(text): + """ + Returns True if the text is related to the weather. + + Arguments: + text -- user-input, typically transcribed speech + """ + return bool(re.search(r'\b(weathers?|temperature|forecast|outside|hot|cold|jacket|coat|rain)\b', text, re.IGNORECASE)) diff --git a/client/modules/__init__.py b/client/modules/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/client/modules/app_utils.py b/client/modules/app_utils.py new file mode 100644 index 0000000..7e39cbf --- /dev/null +++ b/client/modules/app_utils.py @@ -0,0 +1,123 @@ +import smtplib +from email.MIMEText import MIMEText +import urllib2 +import re +import requests +from pytz import timezone + + +def sendEmail(SUBJECT, BODY, TO, FROM, SENDER, PASSWORD, SMTP_SERVER): + """Sends an HTML email.""" + for body_charset in 'US-ASCII', 'ISO-8859-1', 'UTF-8': + try: + BODY.encode(body_charset) + except UnicodeError: + pass + else: + break + msg = MIMEText(BODY.encode(body_charset), 'html', body_charset) + msg['From'] = SENDER + msg['To'] = TO + msg['Subject'] = SUBJECT + + SMTP_PORT = 587 + session = smtplib.SMTP(SMTP_SERVER, SMTP_PORT) + session.starttls() + session.login(FROM, PASSWORD) + session.sendmail(SENDER, TO, msg.as_string()) + session.quit() + + +def emailUser(profile, SUBJECT="", BODY=""): + """ + Sends an email. + + Arguments: + profile -- contains information related to the user (e.g., email address) + SUBJECT -- subject line of the email + BODY -- body text of the email + """ + def generateSMSEmail(profile): + """Generates an email from a user's phone number based on their carrier.""" + if profile['carrier'] is None or not profile['phone_number']: + return None + + return str(profile['phone_number']) + "@" + profile['carrier'] + + if profile['prefers_email'] and profile['gmail_address']: + # add footer + if BODY: + BODY = profile['first_name'] + \ + ",

Here are your top headlines:" + BODY + BODY += "
Sent from your Jasper" + + recipient = profile['gmail_address'] + if profile['first_name'] and profile['last_name']: + recipient = profile['first_name'] + " " + \ + profile['last_name'] + " <%s>" % recipient + else: + recipient = generateSMSEmail(profile) + + if not recipient: + return False + + try: + if 'mailgun' in profile: + user = profile['mailgun']['username'] + password = profile['mailgun']['password'] + server = 'smtp.mailgun.org' + else: + user = profile['gmail_address'] + password = profile['gmail_password'] + server = 'smtp.gmail.com' + sendEmail(SUBJECT, BODY, recipient, user, + "Jasper ", password, server) + + return True + except: + return False + + +def getTimezone(profile): + """ + Returns the pytz timezone for a given profile. + + Arguments: + profile -- contains information related to the user (e.g., email address) + """ + try: + return timezone(profile['timezone']) + except: + return None + + +def generateTinyURL(URL): + """ + Generates a compressed URL. + + Arguments: + URL -- the original URL to-be compressed + """ + target = "http://tinyurl.com/api-create.php?url=" + URL + response = urllib2.urlopen(target) + return response.read() + + +def isNegative(phrase): + """ + Returns True if the input phrase has a negative sentiment. + + Arguments: + phrase -- the input phrase to-be evaluated + """ + return bool(re.search(r'\b(no(t)?|don\'t|stop|end)\b', phrase, re.IGNORECASE)) + + +def isPositive(phrase): + """ + Returns True if the input phrase has a positive sentiment. + + Arguments: + phrase -- the input phrase to-be evaluated + """ + return re.search(r'\b(sure|yes|yeah|go)\b', phrase, re.IGNORECASE) diff --git a/client/populate.py b/client/populate.py index 4778a74..5e9fa4a 100644 --- a/client/populate.py +++ b/client/populate.py @@ -85,7 +85,7 @@ def run(): profile['prefers_email'] = (response == 'E') print ("\nIf you wish to depend on the Google Speech To Text API, please enter your API key, or leave blank to use Jasper's default speech to text implementation.") - simple_request('api_key', 'API Key') + simple_request('google_api_key', 'API Key') # write to profile print("Writing to profile...") diff --git a/client/stt.py b/client/stt.py index a62cf38..5da0889 100644 --- a/client/stt.py +++ b/client/stt.py @@ -3,9 +3,14 @@ import traceback import json import urllib2 +""" +The default Speech-To-Text implementation which relies on PocketSphinx. +""" class PocketSphinxSTT(object): - def __init__(self, lmd = "languagemodel.lm", dictd = "dictionary.dic", lmd_persona = "languagemodel_persona.lm", dictd_persona = "dictionary_persona.dic", lmd_music=None, dictd_music=None): + def __init__(self, lmd = "languagemodel.lm", dictd = "dictionary.dic", + lmd_persona = "languagemodel_persona.lm", dictd_persona = "dictionary_persona.dic", + lmd_music=None, dictd_music=None): """ Initiates the pocketsphinx instance. @@ -60,18 +65,48 @@ class PocketSphinxSTT(object): return result[0] +""" +Speech-To-Text implementation which relies on the Google Speech API. +This implementation requires a Google API key to be present in profile.yml + +To obtain an API key: +1. Join the Chromium Dev group: https://groups.google.com/a/chromium.org/forum/?fromgroups#!forum/chromium-dev +2. Create a project through the Google Developers console: https://console.developers.google.com/project +3. Select your project. In the sidebar, navigate to "APIs & Auth." Activate the Speech API. +4. Under "APIs & Auth," navigate to "Credentials." Create a new key for public API access. +5. Copy your API key and run client/populate.py. When prompted, paste this key for access to the Speech API. + +This implementation also requires that the avconv audio utility be present on your $PATH. On RPi, simply run: + sudo apt-get install avconv +""" class GoogleSTT(object): RATE = 44100 def __init__(self, api_key): - self.api_key = api_key + """ + Arguments: + api_key - the public api key which allows access to Google APIs + """ - def transcribe(self, audio_file): + self.api_key = api_key + for tool in ("avconv", "ffmpeg"): + if os.system("which %s" % tool) == 0: + self.audio_tool = tool + break + if not self.audio_tool: + raise Exception("Could not find an audio tool to convert .wav files to .flac") + + def transcribe(self, audio_file_path): + """ + Performs STT via the Google Speech API, transcribing an audio file + and returning an English string. + audio_file_path -- the path to the audio file to-be transcribed + """ AUDIO_FILE_FLAC = "active.flac" - os.system("ffmpeg -y -i %s -c:a flac -ab 44100 %s" % (audio_file, AUDIO_FILE_FLAC)) + os.system("%s -y -i %s -f flac -b:a 44100 %s" % (self.audio_tool, audio_file_path, AUDIO_FILE_FLAC)) url = "https://www.google.com/speech-api/v2/recognize?output=json&client=chromium&key=%s&lang=%s&maxresults=6&pfilter=2" % (self.api_key, "en-us") flac = open(AUDIO_FILE_FLAC, 'rb') @@ -89,11 +124,26 @@ class GoogleSTT(object): decoded = json.loads(response_read.split("\n")[1]) print response_read text = decoded['result'][0]['alternative'][0]['transcript'] - print text return text except Exception: traceback.print_exc() +""" +Returns a Speech-To-Text engine. + +If api_key is not supplied, Jasper will rely on the PocketSphinx STT engine for +audio transcription. + +If api_key is supplied, Jasper will use the Google Speech API for transcribing +audio while in the active listen phase. Jasper will continue to rely on the +PocketSphinx engine during the passive listen phase, as the Google Speech API +is rate limited to 50 requests/day. + +Arguments: +api_key - if supplied, Jasper will use the Google Speech API for transcribing +audio in the active listen phase. + +""" def newSTTEngine(api_key = None): if api_key: return GoogleSTT(api_key) -- cgit v1.3.1 From 50057bec57447247846d615865fe225ffc814771 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sat, 26 Jul 2014 16:44:12 -0700 Subject: Cleanup before pull request. --- client/stt.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/stt.py b/client/stt.py index 5da0889..3b3b81d 100644 --- a/client/stt.py +++ b/client/stt.py @@ -124,6 +124,10 @@ class GoogleSTT(object): decoded = json.loads(response_read.split("\n")[1]) print response_read text = decoded['result'][0]['alternative'][0]['transcript'] + if text: + print "===================" + print "JASPER: " + text + print "===================" return text except Exception: traceback.print_exc() -- cgit v1.3.1 From 44c9bacf3b3ef24a0f0ef971db45f145b54ff77c Mon Sep 17 00:00:00 2001 From: schneefux Date: Sat, 26 Jul 2014 17:00:39 -0700 Subject: Fixing unit tests. --- .gitignore | 1 + client/local_mic.py | 2 +- client/test.py | 10 ++++++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 5a2333a..64e06fa 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ client/say.wav server/modules/build/* client/build/* client/modules/* +client/modules-bak/* client/profile.yml build/* server/dump.rdb diff --git a/client/local_mic.py b/client/local_mic.py index 3fc742e..44e232d 100644 --- a/client/local_mic.py +++ b/client/local_mic.py @@ -8,7 +8,7 @@ implementation, Jasper is always active listening with local_mic. class Mic: prev = None - def __init__(self, lmd, dictd, lmd_persona, dictd_persona): + def __init__(self, speaker, passive_stt_engine, active_stt_engine): return def passiveListen(self, PERSONA): diff --git a/client/test.py b/client/test.py index fb7d147..69567bb 100644 --- a/client/test.py +++ b/client/test.py @@ -29,17 +29,19 @@ class TestMic(unittest.TestCase): self.time_clip = "../static/audio/time.wav" from mic import Mic - self.m = Mic(speaker.newSpeaker(), "languagemodel.lm", "dictionary.dic", - "languagemodel_persona.lm", "dictionary_persona.dic") + from stt import PocketSphinxSTT + + self.stt = PocketSphinxSTT() def testTranscribeJasper(self): """Does Jasper recognize his name (i.e., passive listen)?""" - transcription = self.m.transcribe(self.jasper_clip, PERSONA_ONLY=True) + transcription = self.stt.transcribe(self.jasper_clip, PERSONA_ONLY=True) self.assertTrue("JASPER" in transcription) def testTranscribe(self): """Does Jasper recognize 'time' (i.e., active listen)?""" - transcription = self.m.transcribe(self.time_clip) + transcription = self.stt.transcribe(self.time_clip) + print transcription self.assertTrue("TIME" in transcription) -- cgit v1.3.1 From 526f86a138a2145c4342e76e2d7dd795f60013a7 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sun, 27 Jul 2014 00:14:17 +0000 Subject: Cleanup before pull request. --- .gitignore | 2 -- client/conversation.py | 1 - client/mic.py | 1 - client/notifier.py | 4 ++-- 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 64e06fa..08cf7c5 100644 --- a/.gitignore +++ b/.gitignore @@ -11,8 +11,6 @@ client/say.wav *.swp server/modules/build/* client/build/* -client/modules/* -client/modules-bak/* client/profile.yml build/* server/dump.rdb diff --git a/client/conversation.py b/client/conversation.py index 9f7cc22..bad5073 100755 --- a/client/conversation.py +++ b/client/conversation.py @@ -51,7 +51,6 @@ class Conversation(object): if threshold: input = self.mic.activeListen(threshold) - print "I just heard '%s'" % input if input: self.delegateInput(input) else: diff --git a/client/mic.py b/client/mic.py index 4798790..8d66053 100644 --- a/client/mic.py +++ b/client/mic.py @@ -180,7 +180,6 @@ class Mic: """ AUDIO_FILE = "active.wav" - #RATE = 16000 RATE = 44100 CHUNK = 1024 LISTEN_TIME = 12 diff --git a/client/notifier.py b/client/notifier.py index 5e33c5b..341ae04 100644 --- a/client/notifier.py +++ b/client/notifier.py @@ -1,5 +1,5 @@ import Queue -#from modules import Gmail +from modules import Gmail from apscheduler.scheduler import Scheduler import logging logging.basicConfig() @@ -24,7 +24,7 @@ class Notifier(object): ] sched = Scheduler() - #sched.start() + sched.start() sched.add_interval_job(self.gather, seconds=30) def gather(self): -- cgit v1.3.1 From 577d630ad2110fbde73008c2ba96f38319024334 Mon Sep 17 00:00:00 2001 From: schneefux Date: Tue, 29 Jul 2014 19:44:10 -0700 Subject: Addressed CR comments. --- client/main.py | 14 +++++++---- client/mic.py | 7 ++---- client/populate.py | 3 --- client/stt.py | 72 ++++++++++++++++++++++++++---------------------------- client/test.py | 3 --- 5 files changed, 46 insertions(+), 53 deletions(-) diff --git a/client/main.py b/client/main.py index ccafdb9..3fc728f 100644 --- a/client/main.py +++ b/client/main.py @@ -2,7 +2,6 @@ import yaml import sys import speaker import stt -from stt import PocketSphinxSTT from conversation import Conversation @@ -24,12 +23,17 @@ if __name__ == "__main__": profile = yaml.safe_load(open("profile.yml", "r")) try: - google_api_key = profile['google_api_key'] + api_key = profile['keys']['GOOGLE_SPEECH'] except KeyError: - print "Google STT API Key not present in profile - defaulting to PocketSphinx..." - google_api_key = None + api_key = None - mic = Mic(speaker.newSpeaker(), PocketSphinxSTT(), stt.newSTTEngine(google_api_key)) + try: + stt_engine_type = profile['stt_engine'] + except KeyError: + print "stt_engine not specified in profile, defaulting to PocketSphinx" + stt_engine_type = "sphinx" + + mic = Mic(speaker.newSpeaker(), stt.PocketSphinxSTT(), stt.newSTTEngine(stt_engine_type, api_key=api_key)) addendum = "" if 'first_name' in profile: diff --git a/client/mic.py b/client/mic.py index 8d66053..b4ef668 100644 --- a/client/mic.py +++ b/client/mic.py @@ -180,7 +180,7 @@ class Mic: """ AUDIO_FILE = "active.wav" - RATE = 44100 + RATE = 16000 CHUNK = 1024 LISTEN_TIME = 12 @@ -241,10 +241,7 @@ class Mic: # DO SOME AMPLIFICATION # os.system("sox "+AUDIO_FILE+" temp.wav vol 20dB") - if MUSIC: - return self.active_stt_engine.transcribe(AUDIO_FILE, MUSIC=True) - - return self.active_stt_engine.transcribe(AUDIO_FILE) + return self.active_stt_engine.transcribe(AUDIO_FILE, MUSIC) def say(self, phrase, OPTIONS=" -vdefault+m3 -p 40 -s 160 --stdout > say.wav"): # alter phrase before speaking diff --git a/client/populate.py b/client/populate.py index 5e9fa4a..48b8670 100644 --- a/client/populate.py +++ b/client/populate.py @@ -84,9 +84,6 @@ def run(): response = raw_input("Please choose email (E) or text message (T): ") profile['prefers_email'] = (response == 'E') - print ("\nIf you wish to depend on the Google Speech To Text API, please enter your API key, or leave blank to use Jasper's default speech to text implementation.") - simple_request('google_api_key', 'API Key') - # write to profile print("Writing to profile...") outputFile = open("profile.yml", "w") diff --git a/client/stt.py b/client/stt.py index 3b3b81d..d261464 100644 --- a/client/stt.py +++ b/client/stt.py @@ -10,7 +10,7 @@ class PocketSphinxSTT(object): def __init__(self, lmd = "languagemodel.lm", dictd = "dictionary.dic", lmd_persona = "languagemodel_persona.lm", dictd_persona = "dictionary_persona.dic", - lmd_music=None, dictd_music=None): + lmd_music=None, dictd_music=None, **kwargs): """ Initiates the pocketsphinx instance. @@ -75,49 +75,50 @@ To obtain an API key: 2. Create a project through the Google Developers console: https://console.developers.google.com/project 3. Select your project. In the sidebar, navigate to "APIs & Auth." Activate the Speech API. 4. Under "APIs & Auth," navigate to "Credentials." Create a new key for public API access. -5. Copy your API key and run client/populate.py. When prompted, paste this key for access to the Speech API. +5. Add your credentials to your profile.yml. Add an entry to the 'keys' section using the key name 'GOOGLE_SPEECH.' Sample configuration: +6. Set the value of the 'stt_engine' key in your profile.yml to 'google' + + +Excerpt from sample profile.yml: + + ... + timezone: US/Pacific + stt_engine: google + keys: + GOOGLE_SPEECH: $YOUR_KEY_HERE -This implementation also requires that the avconv audio utility be present on your $PATH. On RPi, simply run: - sudo apt-get install avconv """ class GoogleSTT(object): - RATE = 44100 + RATE = 16000 - def __init__(self, api_key): + def __init__(self, api_key, **kwargs): """ Arguments: api_key - the public api key which allows access to Google APIs """ - self.api_key = api_key - for tool in ("avconv", "ffmpeg"): - if os.system("which %s" % tool) == 0: - self.audio_tool = tool - break - if not self.audio_tool: - raise Exception("Could not find an audio tool to convert .wav files to .flac") - - def transcribe(self, audio_file_path): + + def transcribe(self, audio_file_path, PERSONA_ONLY=False, MUSIC=False): """ Performs STT via the Google Speech API, transcribing an audio file and returning an English string. - audio_file_path -- the path to the audio file to-be transcribed + Arguments: + audio_file_path -- the path to the .wav file to be transcribed """ - AUDIO_FILE_FLAC = "active.flac" - os.system("%s -y -i %s -f flac -b:a 44100 %s" % (self.audio_tool, audio_file_path, AUDIO_FILE_FLAC)) - url = "https://www.google.com/speech-api/v2/recognize?output=json&client=chromium&key=%s&lang=%s&maxresults=6&pfilter=2" % (self.api_key, "en-us") - flac = open(AUDIO_FILE_FLAC, 'rb') - data = flac.read() - flac.close() + + wav = open(audio_file_path, 'rb') + data = wav.read() + wav.close() + try: req = urllib2.Request( url, data=data, headers={ - 'Content-type': 'audio/x-flac; rate=%s' % GoogleSTT.RATE}) + 'Content-type': 'audio/l16; rate=%s' % GoogleSTT.RATE}) response_url = urllib2.urlopen(req) response_read = response_url.read() response_read = response_read.decode('utf-8') @@ -135,21 +136,18 @@ class GoogleSTT(object): """ Returns a Speech-To-Text engine. -If api_key is not supplied, Jasper will rely on the PocketSphinx STT engine for -audio transcription. - -If api_key is supplied, Jasper will use the Google Speech API for transcribing -audio while in the active listen phase. Jasper will continue to rely on the -PocketSphinx engine during the passive listen phase, as the Google Speech API -is rate limited to 50 requests/day. +Currently, the supported implementations are the default Pocket Sphinx and +the Google Speech API Arguments: -api_key - if supplied, Jasper will use the Google Speech API for transcribing -audio in the active listen phase. - + engine_type - one of "sphinx" or "google" + kwargs - keyword arguments passed to the constructor of the STT engine """ -def newSTTEngine(api_key = None): - if api_key: - return GoogleSTT(api_key) +def newSTTEngine(engine_type, **kwargs): + t = engine_type.lower() + if t == "sphinx": + return PocketSphinxSTT(**kwargs) + elif t == "google": + return GoogleSTT(**kwargs) else: - return PocketSphinxSTT() + raise ValueError("Unsupported STT engine type: " + engine_type) diff --git a/client/test.py b/client/test.py index 69567bb..6186642 100644 --- a/client/test.py +++ b/client/test.py @@ -28,9 +28,7 @@ class TestMic(unittest.TestCase): self.jasper_clip = "../static/audio/jasper.wav" self.time_clip = "../static/audio/time.wav" - from mic import Mic from stt import PocketSphinxSTT - self.stt = PocketSphinxSTT() def testTranscribeJasper(self): @@ -41,7 +39,6 @@ class TestMic(unittest.TestCase): def testTranscribe(self): """Does Jasper recognize 'time' (i.e., active listen)?""" transcription = self.stt.transcribe(self.time_clip) - print transcription self.assertTrue("TIME" in transcription) -- cgit v1.3.1 From 1eb5e58379f463f322e62a37ece2c2bd6d61c67d Mon Sep 17 00:00:00 2001 From: schneefux Date: Wed, 30 Jul 2014 03:36:53 +0000 Subject: Prompt user to populate stt_engine in profile --- client/populate.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/client/populate.py b/client/populate.py index 48b8670..dd5ca48 100644 --- a/client/populate.py +++ b/client/populate.py @@ -4,7 +4,6 @@ import yaml from pytz import timezone import feedparser - def run(): profile = {} @@ -84,6 +83,26 @@ def run(): response = raw_input("Please choose email (E) or text message (T): ") profile['prefers_email'] = (response == 'E') + stt_engines = { + "sphinx" : None, + "google" : "GOOGLE_SPEECH" + } + + response = raw_input( + "\nIf you would like to choose a specific STT engine, please specify which." + + "\nAvailable implementations: %s. (Press Enter to default to PocketSphinx): " % stt_engines.keys()) + if (response in stt_engines): + profile["stt_engine"] = response + api_key_name = stt_engines[response] + if api_key_name: + key = raw_input("\nPlease enter your API key: ") + profile["keys"] = { api_key_name : key } + else: + print("Unrecognized STT engine. Available implementations: %s" % stt_engines.keys()) + profile["stt_engine"] = "sphinx" + + + # write to profile print("Writing to profile...") outputFile = open("profile.yml", "w") -- cgit v1.3.1