summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore16
-rwxr-xr-xboot/app/app.sh2
-rwxr-xr-xboot/app/cgi-bin/index.cgi42
-rwxr-xr-xboot/app/cgi-bin/wifi.py60
-rwxr-xr-xboot/app/connecting-template.html24
-rwxr-xr-xboot/app/index-template.html30
-rwxr-xr-xboot/app/key-template.html33
-rwxr-xr-xboot/app/pure-min.css16
-rwxr-xr-xboot/app/styles.css64
-rwxr-xr-xboot/boot.py89
-rwxr-xr-xboot/boot.sh6
-rwxr-xr-xboot/broadcast.txt11
-rw-r--r--boot/connect.txt10
-rwxr-xr-xboot/listwifi.py18
-rw-r--r--boot/net.sh17
-rw-r--r--boot/networks.yml1
-rw-r--r--boot/say.wavbin0 -> 151944 bytes
-rw-r--r--boot/vocabcompiler.py52
-rwxr-xr-xboot/wifi.py60
-rw-r--r--client/JOKES.txt47
-rw-r--r--client/alteration.py20
-rw-r--r--client/beep_hi.wavbin0 -> 37656 bytes
-rw-r--r--client/beep_lo.wavbin0 -> 37100 bytes
-rw-r--r--client/brain.py52
-rwxr-xr-xclient/conversation.py47
-rw-r--r--client/dictionary_persona.dic22
-rw-r--r--client/g2p.py55
-rw-r--r--client/languagemodel_persona.lm97
-rw-r--r--client/local_mic.py26
-rw-r--r--client/main.py30
-rw-r--r--client/mic.py296
-rw-r--r--client/modules/Birthday.py64
-rw-r--r--client/modules/Gmail.py136
-rw-r--r--client/modules/HN.py128
-rw-r--r--client/modules/Joke.py63
-rw-r--r--client/modules/Life.py32
-rw-r--r--client/modules/News.py120
-rw-r--r--client/modules/Notifications.py55
-rw-r--r--client/modules/Time.py33
-rw-r--r--client/modules/Unclear.py26
-rw-r--r--client/modules/Weather.py104
-rwxr-xr-xclient/modules/__init__.py1
-rw-r--r--client/modules/app_utils.py123
-rw-r--r--client/music.py260
-rwxr-xr-xclient/musicmode.py179
-rw-r--r--client/notifier.py68
-rw-r--r--client/populate.py90
-rw-r--r--client/requirements.txt23
-rwxr-xr-xclient/start.sh3
-rw-r--r--client/test.py98
-rw-r--r--client/test_mic.py27
51 files changed, 2876 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b40c969
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,16 @@
+client/env
+client/venv
+*.pyc
+client/active.wav
+client/passive.wav
+client/say.wav
+.DS_Store
+*.swn
+*.swo
+*.swp
+server/modules/build/*
+client/build/*
+client/profile.yml
+build/*
+server/dump.rdb
+other/*
diff --git a/boot/app/app.sh b/boot/app/app.sh
new file mode 100755
index 0000000..cf7a383
--- /dev/null
+++ b/boot/app/app.sh
@@ -0,0 +1,2 @@
+cd /home/pi/jasper/boot/app/
+sudo python -m CGIHTTPServer 8000 &
diff --git a/boot/app/cgi-bin/index.cgi b/boot/app/cgi-bin/index.cgi
new file mode 100755
index 0000000..bcb85b5
--- /dev/null
+++ b/boot/app/cgi-bin/index.cgi
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+
+from wifi import *
+import os
+import cgi
+import cgitb; cgitb.enable() # for troubleshooting
+
+# some housekeeping
+print "Content-type: text/html"
+print
+
+# get the form input
+form = cgi.FieldStorage()
+network = form.getvalue("network", "")
+password = form.getvalue("password", "")
+
+# if not submitted yet, show them the network selection form
+if not network:
+
+ wifi = Wifi()
+
+ networks = [x.replace("\n","") for x in wifi.access_points if x.replace("\n","") != "Jasper"]
+ select_code_split = ["<option value=\"%s\">%s</option>" % (network, network) for network in networks]
+ select_code = "".join(select_code_split)
+
+ response = open("index-template.html","r").read().replace("{{ select_code }}", select_code)
+ print response
+
+# otherwise, show them the password form
+elif not password:
+ response = open("key-template.html","r").read().replace("{{ network }}", cgi.escape(network))
+ print response
+
+# in the final case, process the wifi password and network data
+else:
+ # show the confirmation page
+ response = open("connecting-template.html","r").read()
+ print response
+
+ # save the configuration
+ wifi = Wifi()
+ wifi.add_wifi(network, password) \ No newline at end of file
diff --git a/boot/app/cgi-bin/wifi.py b/boot/app/cgi-bin/wifi.py
new file mode 100755
index 0000000..5cede72
--- /dev/null
+++ b/boot/app/cgi-bin/wifi.py
@@ -0,0 +1,60 @@
+import yaml
+import os, subprocess
+
+class Wifi:
+
+ access_points = []
+
+ def __init__(self):
+
+ subprocess.call("iwlist wlan0 scan > temp.txt", shell = True)
+ output = open("temp.txt")
+ lines = output.readlines()
+
+ for index, line in enumerate(lines):
+
+ if "Address" in line:
+
+ address = line[29:-1]
+ name = lines[index + 1].split("\"")[1]
+ self.access_points.append(name)
+
+ self.access_points = list(set(self.access_points))
+
+ os.system("rm temp.txt")
+
+ def add_wifi(self, SSID, KEY=""):
+ networks = yaml.safe_load(open("../networks.yml", "r"))
+
+ # add new network to front of list
+ network = {"SSID": SSID, "KEY": KEY}
+ networks.insert(0, network)
+
+ with open('../networks.yml', 'w') as outputFile:
+ output = yaml.dump(networks)
+ outputFile.write(output)
+
+ def set_default_wifi(self, SSID, KEY=""):
+
+ text = open("connect.txt", "r").read()
+ text = text.replace("{{ SSID }}", SSID)
+ text = text.replace("{{ KEY }}", KEY)
+
+ outputFile = open("/etc/network/interfaces", "w")
+ outputFile.write(text)
+ outputFile.flush()
+ outputFile.close()
+
+ subprocess.call(["sudo", "/etc/init.d/networking", "restart"])
+
+ def setup_adhoc(self):
+
+ text = open("broadcast.txt", "r").read()
+ outputFile = open("/etc/network/interfaces","w")
+ outputFile.write(text)
+ outputFile.flush()
+ outputFile.close()
+
+ subprocess.call(["sudo", "/etc/init.d/networking", "restart"])
+ subprocess.call(['sudo', '/usr/sbin/dhcpd', '-q', '-cf', '/etc/dhcp/dhcpd.conf', '-pf', '/var/run/dhcpd.pid'])
+ subprocess.call(["sudo", "/etc/init.d/networking", "restart"])
diff --git a/boot/app/connecting-template.html b/boot/app/connecting-template.html
new file mode 100755
index 0000000..f9d745c
--- /dev/null
+++ b/boot/app/connecting-template.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Welcome to Jasper</title>
+ <link rel="stylesheet" type="text/css" href="http://fonts.googleapis.com/css?family=Open+Sans">
+ <link rel="stylesheet" href="../pure-min.css">
+ <link rel="stylesheet" href="../styles.css">
+ </head>
+ <body>
+ <div class="centered">
+ <center>
+ <h1>Great!</h1>
+ <h3>
+ Jasper is now attempting to connect with your wifi network and will speak after it has restarted.
+
+ <br /><br />
+
+ You may now disconnect your laptop from the "Jasper" network.
+ </h3>
+ </center>
+ </body>
+</html>
diff --git a/boot/app/index-template.html b/boot/app/index-template.html
new file mode 100755
index 0000000..e8ad45b
--- /dev/null
+++ b/boot/app/index-template.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Welcome to Jasper</title>
+ <link rel="stylesheet" type="text/css" href="http://fonts.googleapis.com/css?family=Open+Sans">
+ <link rel="stylesheet" href="../pure-min.css">
+ <link rel="stylesheet" href="../styles.css">
+ </head>
+ <body>
+ <div class="centered">
+ <center>
+ <h1>Welcome to Jasper</h1>
+ <h3>Please select a wifi network</h3>
+
+ <form target="index.cgi" method="POST">
+
+ <select name="network">
+ {{ select_code }}
+ </select>
+
+ <br /><br />
+
+ <button type="submit" class="pure-button pure-input-1 notice">Continue</button>
+
+ </form>
+ </center>
+ </body>
+</html>
diff --git a/boot/app/key-template.html b/boot/app/key-template.html
new file mode 100755
index 0000000..a1a076b
--- /dev/null
+++ b/boot/app/key-template.html
@@ -0,0 +1,33 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Welcome to Jasper</title>
+ <link rel="stylesheet" type="text/css" href="http://fonts.googleapis.com/css?family=Open+Sans">
+ <link rel="stylesheet" href="../pure-min.css">
+ <link rel="stylesheet" href="../styles.css">
+ </head>
+ <body>
+ <div class="centered">
+ <center>
+ <h2>{{ network }}</h2>
+ <h4>If the network requires one, please enter the password. Otherwise, leave it blank.</h4>
+
+ <form target="index.cgi" method="POST">
+
+ <input id="password" name="password" type="password" tabindex="1"></input>
+ <input id="network" name="network" type="hidden" value="{{ network }}"></input>
+
+ <br /><br />
+
+ <button type="submit" class="pure-button pure-input-1" tabindex="3">Back</button> <button type="submit" class="pure-button pure-input-1 notice" tabindex="2">Continue</button>
+
+ </form>
+ </center>
+
+ <script>
+ document.getElementById("password").focus();
+ </script>
+ </body>
+</html>
diff --git a/boot/app/pure-min.css b/boot/app/pure-min.css
new file mode 100755
index 0000000..5f0118d
--- /dev/null
+++ b/boot/app/pure-min.css
@@ -0,0 +1,16 @@
+/*!
+Pure v0.2.0
+Copyright 2013 Yahoo! Inc. All rights reserved.
+Licensed under the BSD License.
+https://github.com/yui/pure/blob/master/LICENSE.md
+*/
+/*!
+normalize.css v1.1.2 | MIT License | git.io/normalize
+Copyright (c) Nicolas Gallagher and Jonathan Neal
+*/
+/*! normalize.css v1.1.2 | MIT License | git.io/normalize */article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none;height:0}[hidden]{display:none}html{font-size:100%;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}html,button,input,select,textarea{font-family:sans-serif}body{margin:0}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{font-size:2em;margin:.67em 0}h2{font-size:1.5em;margin:.83em 0}h3{font-size:1.17em;margin:1em 0}h4{font-size:1em;margin:1.33em 0}h5{font-size:.83em;margin:1.67em 0}h6{font-size:.67em;margin:2.33em 0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:1em 40px}dfn{font-style:italic}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}mark{background:#ff0;color:#000}p,pre{margin:1em 0}code,kbd,pre,samp{font-family:monospace,serif;_font-family:'courier new',monospace;font-size:1em}pre{white-space:pre;white-space:pre-wrap;word-wrap:break-word}q{quotes:none}q:before,q:after{content:'';content:none}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,menu,ol,ul{margin:1em 0}dd{margin:0 0 0 40px}menu,ol,ul{padding:0 0 0 40px}nav ul,nav ol{list-style:none;list-style-image:none}img{border:0;-ms-interpolation-mode:bicubic}svg:not(:root){overflow:hidden}figure{margin:0}form{margin:0}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0;white-space:normal;*margin-left:-7px}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;*overflow:visible}button[disabled],html input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0;*height:13px;*width:13px}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}
+.pure-button{display:inline-block;*display:inline;zoom:1;line-height:normal;white-space:nowrap;vertical-align:baseline;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button{font-size:100%;*font-size:90%;*overflow:visible;padding:.5em 1.5em;color:#444;color:rgba(0,0,0,.8);*color:#444;border:1px solid #999;border:0 rgba(0,0,0,0);background-color:#E6E6E6;text-decoration:none;border-radius:2px;-webkit-font-smoothing:antialiased;-webkit-transition:.1s linear -webkit-box-shadow;-moz-transition:.1s linear -moz-box-shadow;-ms-transition:.1s linear box-shadow;-o-transition:.1s linear box-shadow;transition:.1s linear box-shadow}.pure-button-hover,.pure-button:hover,.pure-button:focus{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#1a000000', GradientType=0);background-image:-webkit-gradient(linear,0 0,0 100%,from(transparent),color-stop(40%,rgba(0,0,0,.05)),to(rgba(0,0,0,.1)));background-image:-webkit-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:-moz-linear-gradient(top,rgba(0,0,0,.05) 0,rgba(0,0,0,.1));background-image:-ms-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:-o-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset}.pure-button[disabled],.pure-button-disabled,.pure-button-disabled:hover,.pure-button-disabled:focus,.pure-button-disabled:active{border:0;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);filter:alpha(opacity=40);-khtml-opacity:.4;-moz-opacity:.4;opacity:.4;cursor:not-allowed;box-shadow:none}.pure-button-hidden{display:none}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}
+.pure-form{margin:0}.pure-form fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}.pure-form legend{border:0;padding:0;white-space:normal;*margin-left:-7px}.pure-form button,.pure-form input,.pure-form select,.pure-form textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.pure-form button,.pure-form input{line-height:normal}.pure-form button,.pure-form input[type=button],.pure-form input[type=reset],.pure-form input[type=submit]{-webkit-appearance:button;cursor:pointer;*overflow:visible}.pure-form button[disabled],.pure-form input[disabled]{cursor:default}.pure-form input[type=checkbox],.pure-form input[type=radio]{box-sizing:border-box;padding:0;*height:13px;*width:13px}.pure-form input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}.pure-form input[type=search]::-webkit-search-cancel-button,.pure-form input[type=search]::-webkit-search-decoration{-webkit-appearance:none}.pure-form button::-moz-focus-inner,.pure-form input::-moz-focus-inner{border:0;padding:0}.pure-form textarea{overflow:auto;vertical-align:top}.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;font-size:.8em;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-transition:.3s linear border;-moz-transition:.3s linear border;-ms-transition:.3s linear border;-o-transition:.3s linear border;transition:.3s linear border;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-font-smoothing:antialiased}.pure-form input[type=text]:focus,.pure-form input[type=password]:focus,.pure-form input[type=email]:focus,.pure-form input[type=url]:focus,.pure-form input[type=date]:focus,.pure-form input[type=month]:focus,.pure-form input[type=time]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=week]:focus,.pure-form input[type=number]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=color]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;outline:thin dotted \9;border-color:#129FEA}.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus,.pure-form input[type=checkbox]:focus{outline:thin dotted #333;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=text][disabled],.pure-form input[type=password][disabled],.pure-form input[type=email][disabled],.pure-form input[type=url][disabled],.pure-form input[type=date][disabled],.pure-form input[type=month][disabled],.pure-form input[type=time][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=week][disabled],.pure-form input[type=number][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=color][disabled],.pure-form select[disabled],.pure-form textarea[disabled],.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3;border-color:transparent}.pure-form input:focus:invalid,.pure-form textarea:focus:invalid,.pure-form select:focus:invalid{color:#b94a48;border:1px solid #ee5f5b}.pure-form input:focus:invalid:focus,.pure-form textarea:focus:invalid:focus,.pure-form select:focus:invalid:focus{border-color:#e9322d}.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus,.pure-form input[type=checkbox]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em;color:#999;font-size:90%}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;font-size:125%;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=text],.pure-form-stacked input[type=password],.pure-form-stacked input[type=email],.pure-form-stacked input[type=url],.pure-form-stacked input[type=date],.pure-form-stacked input[type=month],.pure-form-stacked input[type=time],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=week],.pure-form-stacked input[type=number],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=color],.pure-form-stacked select,.pure-form-stacked label{display:block}.pure-form-aligned input,.pure-form-aligned textarea,.pure-form-aligned select,.pure-form-aligned .pure-help-inline,.pure-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 10em}.pure-form .pure-input-rounded{border-radius:20px;padding-left:1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input{display:block;padding:10px;margin:0;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus{z-index:2}.pure-form .pure-group input:first-child{top:1px;border-radius:4px 4px 0 0}.pure-form .pure-group input:last-child{top:-2px;border-radius:0 0 4px 4px}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form .pure-help-inline,.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:90%}.pure-form-message{display:block;color:#666;font-size:90%}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form label{margin-bottom:.3em;display:block}.pure-group input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0}.pure-form .pure-help-inline,.pure-form-message-inline,.pure-form-message{display:block;font-size:80%;padding:.2em 0 .8em}}
+.pure-g{letter-spacing:-.31em;*letter-spacing:normal;*word-spacing:-.43em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1,.pure-u-1-2,.pure-u-1-3,.pure-u-2-3,.pure-u-1-4,.pure-u-3-4,.pure-u-1-5,.pure-u-2-5,.pure-u-3-5,.pure-u-4-5,.pure-u-1-6,.pure-u-5-6,.pure-u-1-8,.pure-u-3-8,.pure-u-5-8,.pure-u-7-8,.pure-u-1-12,.pure-u-5-12,.pure-u-7-12,.pure-u-11-12,.pure-u-1-24,.pure-u-5-24,.pure-u-7-24,.pure-u-11-24,.pure-u-13-24,.pure-u-17-24,.pure-u-19-24,.pure-u-23-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1{display:block}.pure-u-1-2{width:50%}.pure-u-1-3{width:33.33333%}.pure-u-2-3{width:66.66666%}.pure-u-1-4{width:25%}.pure-u-3-4{width:75%}.pure-u-1-5{width:20%}.pure-u-2-5{width:40%}.pure-u-3-5{width:60%}.pure-u-4-5{width:80%}.pure-u-1-6{width:16.656%}.pure-u-5-6{width:83.33%}.pure-u-1-8{width:12.5%}.pure-u-3-8{width:37.5%}.pure-u-5-8{width:62.5%}.pure-u-7-8{width:87.5%}.pure-u-1-12{width:8.3333%}.pure-u-5-12{width:41.6666%}.pure-u-7-12{width:58.3333%}.pure-u-11-12{width:91.6666%}.pure-u-1-24{width:4.1666%}.pure-u-5-24{width:20.8333%}.pure-u-7-24{width:29.1666%}.pure-u-11-24{width:45.8333%}.pure-u-13-24{width:54.1666%}.pure-u-17-24{width:70.8333%}.pure-u-19-24{width:79.1666%}.pure-u-23-24{width:95.8333%}.pure-g-r{letter-spacing:-.31em;*letter-spacing:normal;*word-spacing:-.43em}.opera-only :-o-prefocus,.pure-g-r{word-spacing:-.43em}.pure-g-r img{max-width:100%}@media (min-width:980px){.pure-visible-phone{display:none}.pure-visible-tablet{display:none}.pure-hidden-desktop{display:none}}@media (max-width:480px){.pure-g-r>.pure-u,.pure-g-r>[class *="pure-u-"]{width:100%}}@media (max-width:767px){.pure-g-r>.pure-u,.pure-g-r>[class *="pure-u-"]{width:100%}.pure-hidden-phone{display:none}.pure-visible-desktop{display:none}}@media (min-width:768px) and (max-width:979px){.pure-hidden-tablet{display:none}.pure-visible-desktop{display:none}}
+.pure-menu ul{position:absolute;visibility:hidden}.pure-menu.pure-menu-open{visibility:visible;z-index:2;width:100%}.pure-menu ul{left:-10000px;list-style:none;margin:0;padding:0;top:-10000px;z-index:1}.pure-menu>ul{position:relative}.pure-menu-open>ul{left:0;top:0;visibility:visible}.pure-menu-open>ul:focus{outline:0}.pure-menu li{position:relative}.pure-menu a,.pure-menu .pure-menu-heading{display:block;color:inherit;line-height:1.5em;padding:5px 20px;text-decoration:none;white-space:nowrap}.pure-menu.pure-menu-horizontal>.pure-menu-heading{display:inline-block;*display:inline;zoom:1;margin:0;vertical-align:middle}.pure-menu.pure-menu-horizontal>ul{display:inline-block;*display:inline;zoom:1;vertical-align:middle;height:2.4em}.pure-menu li a{padding:5px 20px}.pure-menu-can-have-children>.pure-menu-label:after{content:'\25B8';float:right;font-family:'Lucida Grande','Lucida Sans Unicode','DejaVu Sans',sans-serif;margin-right:-20px;margin-top:-1px}.pure-menu-can-have-children>.pure-menu-label{padding-right:30px}.pure-menu-separator{background-color:#dfdfdf;display:block;height:1px;font-size:0;margin:7px 2px;overflow:hidden}.pure-menu-hidden{display:none}.pure-menu-fixed{position:fixed;top:0;left:0;width:100%}.pure-menu-horizontal li{display:inline-block;*display:inline;zoom:1;vertical-align:middle}.pure-menu-horizontal li li{display:block}.pure-menu-horizontal>.pure-menu-children>.pure-menu-can-have-children>.pure-menu-label:after{content:"\25BE"}.pure-menu-horizontal>.pure-menu-children>.pure-menu-can-have-children>.pure-menu-label{padding-right:30px}.pure-menu-horizontal li.pure-menu-separator{height:50%;width:1px;margin:0 7px}.pure-menu-horizontal li li.pure-menu-separator{height:1px;width:auto;margin:7px 2px}.pure-menu.pure-menu-open,.pure-menu.pure-menu-horizontal li .pure-menu-children{background:#fff;border:1px solid #b7b7b7}.pure-menu.pure-menu-horizontal,.pure-menu.pure-menu-horizontal .pure-menu-heading{border:0}.pure-menu a{border:1px solid transparent;border-left:0;border-right:0}.pure-menu a,.pure-menu .pure-menu-can-have-children>li:after{color:#777}.pure-menu .pure-menu-can-have-children>li:hover:after{color:#fff}.pure-menu .pure-menu-open{background:#dedede}.pure-menu li a:hover,.pure-menu li a:focus{background:#eee}.pure-menu li.pure-menu-disabled a:hover,.pure-menu li.pure-menu-disabled a:focus{background:#fff;color:#bfbfbf}.pure-menu .pure-menu-disabled>a{background-image:none;border-color:transparent;cursor:default}.pure-menu .pure-menu-disabled>a,.pure-menu .pure-menu-can-have-children.pure-menu-disabled>a:after{color:#bfbfbf}.pure-menu .pure-menu-heading{color:#565d64;text-transform:uppercase;font-size:90%;margin-top:.5em;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:#dfdfdf}.pure-menu .pure-menu-selected a{color:#000}.pure-menu.pure-menu-open.pure-menu-fixed{border:0;border-bottom:1px solid #b7b7b7}.pure-paginator{list-style:none;margin:0;padding:0}.pure-paginator li{display:inline-block;*display:inline;zoom:1;margin:0 -.35em 0 0}.pure-paginator .pure-button{border-radius:0;padding:.8em 1.4em;vertical-align:top;height:1.1em}.pure-paginator .pure-button:focus{outline-style:none}.pure-paginator .prev,.pure-paginator .next{color:#C0C1C3;text-shadow:0 -1px 0 rgba(0,0,0,.45)}.pure-paginator .prev{border-radius:2px 0 0 2px}.pure-paginator .next{border-radius:0 2px 2px 0}@media (max-width:480px){.pure-menu-horizontal{width:100%}.pure-menu-children li{display:block;border-bottom:1px solid #000}}
+.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:6px 12px}.pure-table td:first-child,.pure-table th:first-child{border-left-width:0}.pure-table thead{background:#e0e0e0;color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child td,.pure-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child td{border-bottom-width:0} \ No newline at end of file
diff --git a/boot/app/styles.css b/boot/app/styles.css
new file mode 100755
index 0000000..b6b3155
--- /dev/null
+++ b/boot/app/styles.css
@@ -0,0 +1,64 @@
+body {
+ margin: 0 auto;
+ line-height: 1.7em;
+ font-family:Open Sans;
+}
+
+h1, h2, h3, h4 {
+ font-weight:300;
+ color: #888;
+}
+
+.pure-button-success,
+.pure-button-error,
+.pure-button-warning,
+.pure-button-secondary {
+ color: white;
+ text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
+}
+
+.pure-button-success {
+ background: rgb(28, 184, 65); /* this is a green */
+}
+
+.pure-button-error {
+ background: rgb(202, 60, 60); /* this is a maroon */
+}
+
+.pure-button-warning {
+ background: rgb(223, 117, 20); /* this is an orange */
+}
+
+.pure-button-secondary {
+ background: rgb(66, 184, 221); /* this is a light blue */
+}
+
+
+#menu {
+ margin-left: -300px;
+ width: 300px;
+ position: fixed;
+ top: 0;
+ left: 300px;
+ bottom: 0;
+ z-index: 1000;
+ padding:20px;
+}
+
+#main {
+ margin-left:80px;
+}
+
+.centered{
+ width:300px;
+ height:200px;
+ position:absolute;
+ left:50%;
+ top:50%;
+ margin:-100px 0 0 -150px;
+}
+
+.notice {
+ background-color: #61B842;
+ color: white;
+} \ No newline at end of file
diff --git a/boot/boot.py b/boot/boot.py
new file mode 100755
index 0000000..658748a
--- /dev/null
+++ b/boot/boot.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python
+
+import os, json
+import urllib2
+import subprocess
+import yaml
+from wifi import *
+
+import vocabcompiler
+
+def say(phrase, OPTIONS = " -vdefault+m3 -p 40 -s 160 --stdout > say.wav"):
+
+ os.system("espeak " + json.dumps(phrase) + OPTIONS)
+ os.system("aplay -D hw:1,0 say.wav")
+
+
+
+# check if there is network connection
+def configure():
+ try:
+
+ urllib2.urlopen("http://www.google.com").getcode()
+
+ print "CONNECTED TO INTERNET"
+ print "COMPILING DICTIONARY"
+ vocabcompiler.compile()
+
+ print "STARTING CLIENT PROGRAM"
+
+ try:
+ os.system("/home/pi/jasper/client/start.sh &")
+ except:
+ os.system("/home/pi/jasper/backup/start.sh &")
+ finally:
+ return
+
+ except:
+
+ networks = yaml.safe_load(open("networks.yml", "r"))
+
+ wifi = Wifi()
+
+ for network in networks:
+ wifi.set_default_wifi(network['SSID'], network['KEY'])
+ say("Attempting to connect to network " + network['SSID'])
+
+ try:
+
+ urllib2.urlopen("http://www.google.com").getcode()
+
+ print "CONNECTED TO INTERNET"
+ print "COMPILING DICTIONARY"
+ vocabcompiler.compile()
+
+ print "STARTING CLIENT PROGRAM"
+
+ try:
+ os.system("/home/pi/jasper/client/start.sh &")
+ except:
+ os.system("/home/pi/jasper/backup/start.sh &")
+ finally:
+ return
+
+ except:
+ pass
+
+ print "NOT CONNECTED TO INTERNET. RUNNING AD HOC NETWORK."
+
+ wifi.setup_adhoc()
+
+ os.system("sudo app/app.sh &")
+ say("Hello.... I could not connect to a wifi network... Please log in with your computer to help me")
+
+ original = open("networks.yml",'r').readlines()
+ while True:
+ list = open("networks.yml",'r').readlines()
+ if list != original:
+ break
+
+ say("Thank you for adding a wifi network. Just give me a few minutes to restart.")
+ os.system("sudo shutdown -r now")
+
+if __name__ == "__main__":
+ print "==========STARTING JASPER CLIENT=========="
+ print "=========================================="
+ print "COPYRIGHT 2013 SHUBHRO SAHA, CHARLIE MARSH"
+ print "=========================================="
+ say("Hello.... I am Jasper... Please wait one moment.")
+ configure()
diff --git a/boot/boot.sh b/boot/boot.sh
new file mode 100755
index 0000000..5ea1b3d
--- /dev/null
+++ b/boot/boot.sh
@@ -0,0 +1,6 @@
+cd /home/pi/jasper/boot/
+LD_LIBRARY_PATH="/usr/local/lib"
+export LD_LIBRARY_PATH
+PATH=$PATH:/usr/local/lib/
+export PATH
+python boot.py & \ No newline at end of file
diff --git a/boot/broadcast.txt b/boot/broadcast.txt
new file mode 100755
index 0000000..36942c4
--- /dev/null
+++ b/boot/broadcast.txt
@@ -0,0 +1,11 @@
+auto lo
+iface lo inet loopback
+iface eth0 inet dhcp
+
+auto wlan0
+iface wlan0 inet static
+ address 192.168.1.1
+ netmask 255.255.255.0
+ wireless-channel 1
+ wireless-essid Jasper
+ wireless-mode ad-hoc
diff --git a/boot/connect.txt b/boot/connect.txt
new file mode 100644
index 0000000..b30d831
--- /dev/null
+++ b/boot/connect.txt
@@ -0,0 +1,10 @@
+auto lo
+
+iface lo inet loopback
+iface eth0 inet dhcp
+
+allow-hotplug wlan0
+auto wlan0
+iface wlan0 inet dhcp
+ wpa-ssid "{{ SSID }}"
+ wpa-psk "{{ KEY }}" \ No newline at end of file
diff --git a/boot/listwifi.py b/boot/listwifi.py
new file mode 100755
index 0000000..5ee14de
--- /dev/null
+++ b/boot/listwifi.py
@@ -0,0 +1,18 @@
+import os
+import subprocess
+
+subprocess.call("iwlist wlan0 scan > temp.txt", shell = True)
+output = open("temp.txt")
+
+lines = output.readlines()
+
+for index, line in enumerate(lines):
+
+ if "Address" in line:
+
+ address = line[29:-1]
+ name = lines[index + 1].split("\"")[1]
+
+ print "%s is %s" % (address, name)
+
+os.system("rm temp.txt")
diff --git a/boot/net.sh b/boot/net.sh
new file mode 100644
index 0000000..a7bee21
--- /dev/null
+++ b/boot/net.sh
@@ -0,0 +1,17 @@
+sudo sysctl net.ipv4.ip_forward=1
+
+sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 1.2.3.4:80
+sudo iptables -t nat -A POSTROUTING -j MASQUERADE
+sudo /etc/init.d/networking restart
+
+#sudo iptables -A POSTROUTING -t nat -o wlan0 -j MASQUERADE
+
+#sudo iptables -t mangle -N internet
+
+#sudo iptables -t mangle -A PREROUTING -i wlan0 -p tcp -m tcp --dport 80 -j internet
+
+#sudo iptables -t mangle -A internet -j MARK --set-mark 99
+
+#sudo iptables -t nat -A PREROUTING -i wlan0 -p tcp -m mark --mark 99 -m tcp --dport 80 -j DNAT --to-destination 192.168.3.1
+
+#sudo /etc/init.d/networking restart
diff --git a/boot/networks.yml b/boot/networks.yml
new file mode 100644
index 0000000..0637a08
--- /dev/null
+++ b/boot/networks.yml
@@ -0,0 +1 @@
+[] \ No newline at end of file
diff --git a/boot/say.wav b/boot/say.wav
new file mode 100644
index 0000000..d27c7dc
--- /dev/null
+++ b/boot/say.wav
Binary files differ
diff --git a/boot/vocabcompiler.py b/boot/vocabcompiler.py
new file mode 100644
index 0000000..d5455d3
--- /dev/null
+++ b/boot/vocabcompiler.py
@@ -0,0 +1,52 @@
+"""
+ This iterates over all the WORDS variables in the modules and creates a dictionary that the client program will use
+"""
+
+import os
+import sys
+import pkgutil
+
+lib_path = os.path.abspath('../client')
+sys.path.append(lib_path)
+import modules
+import g2p
+
+
+def compile():
+ """
+ Gets the words and creates the dictionary
+ """
+
+ m = dir(modules)
+
+ words = []
+ for module_name in m:
+ try:
+ eval("words.extend(modules.%s.WORDS)" % module_name)
+ except:
+ pass # module probably doesn't have the property
+
+ words = list(set(words))
+
+ # for spotify module
+ words.extend(["MUSIC","SPOTIFY"])
+
+ # create the dictionary
+ pronounced = g2p.translateWords(words)
+ zipped = zip(words, pronounced)
+ lines = ["%s %s" % (x, y) for x, y in zipped]
+
+ with open("../client/dictionary.dic", "w") as f:
+ f.write("\n".join(lines) + "\n")
+
+ # create the language model
+ with open("../client/sentences.txt", "w") as f:
+ f.write("\n".join(words) + "\n")
+ f.write("<s> \n </s> \n")
+ f.close()
+
+ # make language model
+ os.system(
+ "text2idngram -vocab ../client/sentences.txt < ../client/sentences.txt -idngram temp.idngram")
+ os.system(
+ "idngram2lm -idngram temp.idngram -vocab ../client/sentences.txt -arpa ../client/languagemodel.lm")
diff --git a/boot/wifi.py b/boot/wifi.py
new file mode 100755
index 0000000..7e2fa43
--- /dev/null
+++ b/boot/wifi.py
@@ -0,0 +1,60 @@
+import yaml
+import os, subprocess
+
+class Wifi:
+
+ access_points = []
+
+ def __init__(self):
+
+ subprocess.call("iwlist wlan0 scan > temp.txt", shell = True)
+ output = open("temp.txt")
+ lines = output.readlines()
+
+ for index, line in enumerate(lines):
+
+ if "Address" in line:
+
+ address = line[29:-1]
+ name = lines[index + 1].split("\"")[1]
+ self.access_points.append(name)
+
+ self.access_points = list(set(self.access_points))
+
+ os.system("rm temp.txt")
+
+ def add_wifi(self, SSID, KEY=""):
+ networks = yaml.safe_load(open("networks.yml", "r"))
+
+ # add new network to front of list
+ network = {"SSID": SSID, "KEY": KEY}
+ networks.insert(0, network)
+
+ with open('networks.yml', 'w') as outputFile:
+ output = yaml.dump(networks)
+ outputFile.write(output)
+
+ def set_default_wifi(self, SSID, KEY=""):
+
+ text = open("connect.txt", "r").read()
+ text = text.replace("{{ SSID }}", SSID)
+ text = text.replace("{{ KEY }}", KEY)
+
+ outputFile = open("/etc/network/interfaces", "w")
+ outputFile.write(text)
+ outputFile.flush()
+ outputFile.close()
+
+ subprocess.call(["sudo", "/etc/init.d/networking", "restart"])
+
+ def setup_adhoc(self):
+
+ text = open("broadcast.txt", "r").read()
+ outputFile = open("/etc/network/interfaces","w")
+ outputFile.write(text)
+ outputFile.flush()
+ outputFile.close()
+
+ subprocess.call(["sudo", "/etc/init.d/networking", "restart"])
+ subprocess.call(['sudo', '/usr/sbin/dhcpd', '-q', '-cf', '/etc/dhcp/dhcpd.conf', '-pf', '/var/run/dhcpd.pid'])
+ subprocess.call(["sudo", "/etc/init.d/networking", "restart"]) \ No newline at end of file
diff --git a/client/JOKES.txt b/client/JOKES.txt
new file mode 100644
index 0000000..05780e4
--- /dev/null
+++ b/client/JOKES.txt
@@ -0,0 +1,47 @@
+little old lady
+wow... I didn't know you could yodel... get it... because it sounded like you were yodeling
+
+oink oink
+make you your mind... are you a pig or an owl... get it... because an owl goes who but a pig goes oink... oink
+
+cows go
+no... cows go moo... didn't you know
+
+jarvis
+jarvis... it's me you idiot
+
+me
+no... seriously... it's just me... I was telling a knock knock joke ha ha ha
+
+madam
+my damn foot got stuck in the door, so open it he he he... but actually... I think I hurt my foot
+
+doris
+door is locked... that's why I'm knocking, you idiot
+
+cash
+no thanks... but I would like a peanut instead... get it? because you said cashew... ha ha ha
+
+orange
+orange you glad I am your friend
+
+alex
+I'll ask the questions around here, thank you
+
+viper
+viper nose... it's running
+
+canoe
+can you scratch my back... it itches
+
+pete
+pizza delivery guy... you idiot
+
+doctor
+that is the best show ever
+
+arizona
+arizona room for only one of us in this room
+
+spider
+in spider what everyone says, I still feel like a human \ No newline at end of file
diff --git a/client/alteration.py b/client/alteration.py
new file mode 100644
index 0000000..e381cd4
--- /dev/null
+++ b/client/alteration.py
@@ -0,0 +1,20 @@
+import re
+
+
+def detectYears(input):
+ YEAR_REGEX = re.compile(r'(\b)(\d\d)([1-9]\d)(\b)')
+ return YEAR_REGEX.sub('\g<1>\g<2> \g<3>\g<4>', input)
+
+
+def clean(input):
+ """
+ Manually adjust output text before it's translated into
+ actual speech by the TTS system. This is to fix minior
+ idiomatic issues, for example, that 1901 is pronounced
+ "one thousand, ninehundred and one" rather than
+ "nineteen ninety one".
+
+ Arguments:
+ input -- original speech text to-be modified
+ """
+ return detectYears(input)
diff --git a/client/beep_hi.wav b/client/beep_hi.wav
new file mode 100644
index 0000000..1d6593f
--- /dev/null
+++ b/client/beep_hi.wav
Binary files differ
diff --git a/client/beep_lo.wav b/client/beep_lo.wav
new file mode 100644
index 0000000..0afb29a
--- /dev/null
+++ b/client/beep_lo.wav
Binary files differ
diff --git a/client/brain.py b/client/brain.py
new file mode 100644
index 0000000..b5e0c1d
--- /dev/null
+++ b/client/brain.py
@@ -0,0 +1,52 @@
+import logging
+from modules import *
+
+
+def logError():
+ logger = logging.getLogger('jasper')
+ fh = logging.FileHandler('jasper.log')
+ fh.setLevel(logging.WARNING)
+ formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
+ fh.setFormatter(formatter)
+ logger.addHandler(fh)
+ logger.error('Failed to execute module', exc_info=True)
+
+
+class Brain(object):
+
+ def __init__(self, mic, profile):
+ """
+ Instantiates a new Brain object, which cross-references user
+ input with a list of modules. Note that the order of brain.modules
+ matters, as the Brain will cease execution on the first module
+ that accepts a given input.
+
+ Arguments:
+ mic -- used to interact with the user (for both input and output)
+ profile -- contains information related to the user (e.g., phone number)
+ """
+ self.mic = mic
+ self.profile = profile
+ self.modules = [
+ Gmail, Notifications, Birthday, Weather, HN, News, Time, Joke, Life]
+ self.modules.append(Unclear)
+
+ def query(self, text):
+ """
+ Passes user input to the appropriate module, testing it against
+ each candidate module's isValid function.
+
+ Arguments:
+ text -- user input, typically speech, to be parsed by a module
+ """
+ for module in self.modules:
+ if module.isValid(text):
+
+ try:
+ module.handle(text, self.mic, self.profile)
+ break
+ except:
+ logError()
+ self.mic.say(
+ "I'm sorry. I had some trouble with that operation. Please try again later.")
+ break
diff --git a/client/conversation.py b/client/conversation.py
new file mode 100755
index 0000000..6e994a2
--- /dev/null
+++ b/client/conversation.py
@@ -0,0 +1,47 @@
+from notifier import Notifier
+from musicmode import *
+from brain import Brain
+
+
+class Conversation(object):
+
+ def __init__(self, persona, mic, profile):
+ self.persona = persona
+ self.mic = mic
+ self.profile = profile
+ self.brain = Brain(mic, profile)
+ self.notifier = Notifier(profile)
+
+ def delegateInput(self, text):
+ """A wrapper for querying brain."""
+
+ # check if input is meant to start the music module
+ if any(x in text.upper() for x in ["SPOTIFY","MUSIC"]):
+ self.mic.say("Please give me a moment, I'm loading your Spotify playlists.")
+ music_mode = MusicMode(self.persona, self.mic)
+ music_mode.handleForever()
+ return
+
+
+ self.brain.query(text)
+
+ def handleForever(self):
+ """Delegates user input to the handling function when activated."""
+ while True:
+
+ # Print notifications until empty
+ notifications = self.notifier.getAllNotifications()
+ for notif in notifications:
+ print notif
+
+ try:
+ threshold, transcribed = self.mic.passiveListen(self.persona)
+ except:
+ continue
+
+ if threshold:
+ input = self.mic.activeListen(threshold)
+ if input:
+ self.delegateInput(input)
+ else:
+ self.mic.say("Pardon?")
diff --git a/client/dictionary_persona.dic b/client/dictionary_persona.dic
new file mode 100644
index 0000000..8b813ac
--- /dev/null
+++ b/client/dictionary_persona.dic
@@ -0,0 +1,22 @@
+BE B IY
+BEING B IY IH NG
+BUT B AH T
+DID D IH D
+FIRST F ER S T
+IN IH N
+IS IH Z
+IT IH T
+JASPER JH AE S P ER
+NOW N AW
+OF AH V
+ON AA N
+ON(2) AO N
+RIGHT R AY T
+SAY S EY
+WHAT W AH T
+WHAT(2) HH W AH T
+WHICH W IH CH
+WHICH(2) HH W IH CH
+WITH W IH DH
+WITH(2) W IH TH
+WORK W ER K \ No newline at end of file
diff --git a/client/g2p.py b/client/g2p.py
new file mode 100644
index 0000000..06fef14
--- /dev/null
+++ b/client/g2p.py
@@ -0,0 +1,55 @@
+import os
+import subprocess
+import re
+
+TEMP_FILENAME = "g2ptemp"
+PHONE_MATCH = re.compile(r'<s> (.*) </s>')
+
+
+def parseLine(line):
+ return PHONE_MATCH.search(line).group(1)
+
+
+def parseOutput(output):
+ return PHONE_MATCH.findall(output)
+
+
+def translateWord(word):
+ out = subprocess.check_output(['phonetisaurus-g2p', '--model=%s' %
+ os.path.expanduser("~/phonetisaurus/g014b2b.fst"), '--input=%s' % word])
+ return parseLine(out)
+
+
+def translateWords(words):
+ full_text = '\n'.join(words)
+
+ f = open(TEMP_FILENAME, "wb")
+ f.write(full_text)
+ f.flush()
+
+ output = translateFile(TEMP_FILENAME)
+ os.remove(TEMP_FILENAME)
+
+ return output
+
+
+def translateFile(input_filename, output_filename=None):
+ out = subprocess.check_output(['phonetisaurus-g2p', '--model=%s' % os.path.expanduser(
+ "~/phonetisaurus/g014b2b.fst"), '--input=%s' % input_filename, '--words', '--isfile'])
+ out = parseOutput(out)
+
+ if output_filename:
+ out = '\n'.join(out)
+
+ f = open(output_filename, "wb")
+ f.write(out)
+ f.close()
+
+ return None
+
+ return out
+
+if __name__ == "__main__":
+
+ translateFile(os.path.expanduser("~/phonetisaurus/sentences.txt"),
+ os.path.expanduser("~/phonetisaurus/dictionary.dic"))
diff --git a/client/languagemodel_persona.lm b/client/languagemodel_persona.lm
new file mode 100644
index 0000000..f290a31
--- /dev/null
+++ b/client/languagemodel_persona.lm
@@ -0,0 +1,97 @@
+Language model created by QuickLM on Wed Sep 4 14:21:33 EDT 2013
+Copyright (c) 1996-2010 Carnegie Mellon University and Alexander I. Rudnicky
+
+The model is in standard ARPA format, designed by Doug Paul while he was at MITRE.
+
+The code that was used to produce this language model is available in Open Source.
+Please visit http://www.speech.cs.cmu.edu/tools/ for more information
+
+The (fixed) discount mass is 0.5. The backoffs are computed using the ratio method.
+This model based on a corpus of 18 sentences and 20 words
+
+\data\
+ngram 1=20
+ngram 2=36
+ngram 3=18
+
+\1-grams:
+-0.7782 </s> -0.3010
+-0.7782 <s> -0.2218
+-2.0334 BE -0.2218
+-2.0334 BEING -0.2218
+-2.0334 BUT -0.2218
+-2.0334 DID -0.2218
+-2.0334 FIRST -0.2218
+-2.0334 IN -0.2218
+-2.0334 IS -0.2218
+-2.0334 IT -0.2218
+-2.0334 JASPER -0.2218
+-2.0334 NOW -0.2218
+-2.0334 OF -0.2218
+-2.0334 ON -0.2218
+-2.0334 RIGHT -0.2218
+-2.0334 SAY -0.2218
+-2.0334 WHAT -0.2218
+-2.0334 WHICH -0.2218
+-2.0334 WITH -0.2218
+-2.0334 WORK -0.2218
+
+\2-grams:
+-1.5563 <s> BE 0.0000
+-1.5563 <s> BEING 0.0000
+-1.5563 <s> BUT 0.0000
+-1.5563 <s> DID 0.0000
+-1.5563 <s> FIRST 0.0000
+-1.5563 <s> IN 0.0000
+-1.5563 <s> IS 0.0000
+-1.5563 <s> IT 0.0000
+-1.5563 <s> JASPER 0.0000
+-1.5563 <s> NOW 0.0000
+-1.5563 <s> OF 0.0000
+-1.5563 <s> ON 0.0000
+-1.5563 <s> RIGHT 0.0000
+-1.5563 <s> SAY 0.0000
+-1.5563 <s> WHAT 0.0000
+-1.5563 <s> WHICH 0.0000
+-1.5563 <s> WITH 0.0000
+-1.5563 <s> WORK 0.0000
+-0.3010 BE </s> -0.3010
+-0.3010 BEING </s> -0.3010
+-0.3010 BUT </s> -0.3010
+-0.3010 DID </s> -0.3010
+-0.3010 FIRST </s> -0.3010
+-0.3010 IN </s> -0.3010
+-0.3010 IS </s> -0.3010
+-0.3010 IT </s> -0.3010
+-0.3010 JASPER </s> -0.3010
+-0.3010 NOW </s> -0.3010
+-0.3010 OF </s> -0.3010
+-0.3010 ON </s> -0.3010
+-0.3010 RIGHT </s> -0.3010
+-0.3010 SAY </s> -0.3010
+-0.3010 WHAT </s> -0.3010
+-0.3010 WHICH </s> -0.3010
+-0.3010 WITH </s> -0.3010
+-0.3010 WORK </s> -0.3010
+
+\3-grams:
+-0.3010 <s> BE </s>
+-0.3010 <s> BEING </s>
+-0.3010 <s> BUT </s>
+-0.3010 <s> DID </s>
+-0.3010 <s> FIRST </s>
+-0.3010 <s> IN </s>
+-0.3010 <s> IS </s>
+-0.3010 <s> IT </s>
+-0.3010 <s> JASPER </s>
+-0.3010 <s> NOW </s>
+-0.3010 <s> OF </s>
+-0.3010 <s> ON </s>
+-0.3010 <s> RIGHT </s>
+-0.3010 <s> SAY </s>
+-0.3010 <s> WHAT </s>
+-0.3010 <s> WHICH </s>
+-0.3010 <s> WITH </s>
+-0.3010 <s> WORK </s>
+
+\end\ \ No newline at end of file
diff --git a/client/local_mic.py b/client/local_mic.py
new file mode 100644
index 0000000..3fc742e
--- /dev/null
+++ b/client/local_mic.py
@@ -0,0 +1,26 @@
+"""
+A drop-in replacement for the Mic class that allows for all I/O to occur
+over the terminal. Useful for debugging. Unlike with the typical Mic
+implementation, Jasper is always active listening with local_mic.
+"""
+
+
+class Mic:
+ prev = None
+
+ def __init__(self, lmd, dictd, lmd_persona, dictd_persona):
+ return
+
+ def passiveListen(self, PERSONA):
+ return True, "JASPER"
+
+ def activeListen(self, THRESHOLD=None, LISTEN=True, MUSIC=False):
+ if not LISTEN:
+ return self.prev
+
+ input = raw_input("YOU: ")
+ self.prev = input
+ return input
+
+ def say(self, phrase, OPTIONS=None):
+ print "JASPER: " + phrase
diff --git a/client/main.py b/client/main.py
new file mode 100644
index 0000000..c5805c4
--- /dev/null
+++ b/client/main.py
@@ -0,0 +1,30 @@
+import yaml
+import sys
+from conversation import Conversation
+
+
+def isLocal():
+ return len(sys.argv) > 1 and sys.argv[1] == "--local"
+
+if isLocal():
+ from local_mic import Mic
+else:
+ from mic import Mic
+
+if __name__ == "__main__":
+
+ print "==========================================================="
+ print " JASPER The Talking Computer "
+ print " Copyright 2013 Shubhro Saha & Charlie Marsh "
+ print "==========================================================="
+
+ profile = yaml.safe_load(open("profile.yml", "r"))
+
+ mic = Mic("languagemodel.lm", "dictionary.dic",
+ "languagemodel_persona.lm", "dictionary_persona.dic")
+
+ mic.say("How can I be of service?")
+
+ conversation = Conversation("JASPER", mic, profile)
+
+ conversation.handleForever()
diff --git a/client/mic.py b/client/mic.py
new file mode 100644
index 0000000..71f49ab
--- /dev/null
+++ b/client/mic.py
@@ -0,0 +1,296 @@
+"""
+ The Mic class handles all interactions with the microphone and speaker.
+"""
+
+import os
+import json
+from wave import open as open_audio
+import audioop
+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, lmd, dictd, lmd_persona, dictd_persona, lmd_music=None, dictd_music=None):
+ """
+ Initiates the pocketsphinx instance.
+
+ Arguments:
+ 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)
+ """
+
+ 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 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
+ return score
+
+ def fetchThreshold(self):
+
+ # TODO: Consolidate all of these variables from the next three
+ # functions
+ THRESHOLD_MULTIPLIER = 1.8
+ AUDIO_FILE = "passive.wav"
+ RATE = 16000
+ CHUNK = 1024
+
+ # number of seconds to allow to establish threshold
+ THRESHOLD_TIME = 1
+
+ # number of seconds to listen before forcing restart
+ LISTEN_TIME = 10
+
+ # prepare recording stream
+ audio = pyaudio.PyAudio()
+ stream = audio.open(format=pyaudio.paInt16,
+ channels=1,
+ rate=RATE,
+ input=True,
+ frames_per_buffer=CHUNK)
+
+ # stores the audio data
+ frames = []
+
+ # stores the lastN score values
+ lastN = [i for i in range(20)]
+
+ # calculate the long run average, and thereby the proper threshold
+ for i in range(0, RATE / CHUNK * THRESHOLD_TIME):
+
+ data = stream.read(CHUNK)
+ frames.append(data)
+
+ # save this data point as a score
+ lastN.pop(0)
+ lastN.append(self.getScore(data))
+ average = sum(lastN) / len(lastN)
+
+ # this will be the benchmark to cause a disturbance over!
+ THRESHOLD = average * THRESHOLD_MULTIPLIER
+
+ return THRESHOLD
+
+ def passiveListen(self, PERSONA):
+ """
+ Listens for PERSONA in everyday sound
+ Times out after LISTEN_TIME, so needs to be restarted
+ """
+
+ THRESHOLD_MULTIPLIER = 1.8
+ AUDIO_FILE = "passive.wav"
+ RATE = 16000
+ CHUNK = 1024
+
+ # number of seconds to allow to establish threshold
+ THRESHOLD_TIME = 1
+
+ # number of seconds to listen before forcing restart
+ LISTEN_TIME = 10
+
+ # prepare recording stream
+ audio = pyaudio.PyAudio()
+ stream = audio.open(format=pyaudio.paInt16,
+ channels=1,
+ rate=RATE,
+ input=True,
+ frames_per_buffer=CHUNK)
+
+ # stores the audio data
+ frames = []
+
+ # stores the lastN score values
+ lastN = [i for i in range(30)]
+
+ # calculate the long run average, and thereby the proper threshold
+ for i in range(0, RATE / CHUNK * THRESHOLD_TIME):
+
+ data = stream.read(CHUNK)
+ frames.append(data)
+
+ # save this data point as a score
+ lastN.pop(0)
+ lastN.append(self.getScore(data))
+ average = sum(lastN) / len(lastN)
+
+ # this will be the benchmark to cause a disturbance over!
+ THRESHOLD = average * THRESHOLD_MULTIPLIER
+
+ # save some memory for sound data
+ frames = []
+
+ # flag raised when sound disturbance detected
+ didDetect = False
+
+ # start passively listening for disturbance above threshold
+ for i in range(0, RATE / CHUNK * LISTEN_TIME):
+
+ data = stream.read(CHUNK)
+ frames.append(data)
+ score = self.getScore(data)
+
+ if score > THRESHOLD:
+ didDetect = True
+ break
+
+ # no use continuing if no flag raised
+ if not didDetect:
+ print "No disturbance detected"
+ return
+
+ # cutoff any recording before this disturbance was detected
+ frames = frames[-20:]
+
+ # otherwise, let's keep recording for few seconds and save the file
+ DELAY_MULTIPLIER = 1
+ for i in range(0, RATE / CHUNK * DELAY_MULTIPLIER):
+
+ data = stream.read(CHUNK)
+ frames.append(data)
+
+ # save the audio data
+ stream.stop_stream()
+ stream.close()
+ audio.terminate()
+ write_frames = open_audio(AUDIO_FILE, 'wb')
+ write_frames.setnchannels(1)
+ write_frames.setsampwidth(audio.get_sample_size(pyaudio.paInt16))
+ write_frames.setframerate(RATE)
+ write_frames.writeframes(''.join(frames))
+ write_frames.close()
+
+ # check if PERSONA was said
+ transcribed = self.transcribe(AUDIO_FILE, PERSONA_ONLY=True)
+
+ if PERSONA in transcribed:
+ return (THRESHOLD, PERSONA)
+
+ return (False, transcribed)
+
+ def activeListen(self, THRESHOLD=None, LISTEN=True, MUSIC=False):
+ """
+ Records until a second of silence or times out after 12 seconds
+ """
+
+ AUDIO_FILE = "active.wav"
+ RATE = 16000
+ CHUNK = 1024
+ LISTEN_TIME = 12
+
+ # user can request pre-recorded sound
+ if not LISTEN:
+ if not os.path.exists(AUDIO_FILE):
+ return None
+
+ return self.transcribe(AUDIO_FILE)
+
+ # check if no threshold provided
+ if THRESHOLD == None:
+ THRESHOLD = self.fetchThreshold()
+
+ os.system("aplay -D hw:1,0 beep_hi.wav")
+
+ # prepare recording stream
+ audio = pyaudio.PyAudio()
+ stream = audio.open(format=pyaudio.paInt16,
+ channels=1,
+ rate=RATE,
+ input=True,
+ frames_per_buffer=CHUNK)
+
+ frames = []
+ # increasing the range # results in longer pause after command
+ # generation
+ lastN = [THRESHOLD * 1.2 for i in range(30)]
+
+ for i in range(0, RATE / CHUNK * LISTEN_TIME):
+
+ data = stream.read(CHUNK)
+ frames.append(data)
+ score = self.getScore(data)
+
+ lastN.pop(0)
+ lastN.append(score)
+
+ average = sum(lastN) / float(len(lastN))
+
+ # TODO: 0.8 should not be a MAGIC NUMBER!
+ if average < THRESHOLD * 0.8:
+ break
+
+ os.system("aplay -D hw:1,0 beep_lo.wav")
+
+ # save the audio data
+ stream.stop_stream()
+ stream.close()
+ audio.terminate()
+ write_frames = open_audio(AUDIO_FILE, 'wb')
+ write_frames.setnchannels(1)
+ write_frames.setsampwidth(audio.get_sample_size(pyaudio.paInt16))
+ write_frames.setframerate(RATE)
+ write_frames.writeframes(''.join(frames))
+ write_frames.close()
+
+ # DO SOME AMPLIFICATION
+ # os.system("sox "+AUDIO_FILE+" temp.wav vol 20dB")
+
+ if MUSIC:
+ return self.transcribe(AUDIO_FILE, MUSIC=True)
+
+ return self.transcribe(AUDIO_FILE)
+
+ def say(self, phrase, OPTIONS=" -vdefault+m3 -p 40 -s 160 --stdout > say.wav"):
+ # alter phrase before speaking
+ phrase = alteration.clean(phrase)
+
+ os.system("espeak " + json.dumps(phrase) + OPTIONS)
+ os.system("aplay -D hw:1,0 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..16aeab8
--- /dev/null
+++ b/client/modules/HN.py
@@ -0,0 +1,128 @@
+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"]
+
+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 = "<ul>"
+
+ def formatArticle(article):
+ tiny_url = app_utils.generateTinyURL(article.URL)
+
+ if profile['prefers_email']:
+ return "<li><a href=\'%s\'>%s</a></li>" % (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 += "</ul>"
+ 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 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..454280c
--- /dev/null
+++ b/client/modules/Joke.py
@@ -0,0 +1,63 @@
+import random
+import re
+
+WORDS = ["JOKE", "KNOCK KNOCK"]
+
+
+def getRandomJoke(filename="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..0cedd3a
--- /dev/null
+++ b/client/modules/News.py
@@ -0,0 +1,120 @@
+import feedparser
+import app_utils
+import re
+from semantic.numbers import NumberService
+
+WORDS = ["NEWS", "YES", "NO", "FIRST", "SECOND", "THIRD"]
+
+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 = "<ul>"
+
+ def formatArticle(article):
+ tiny_url = app_utils.generateTinyURL(article.URL)
+
+ if profile['prefers_email']:
+ return "<li><a href=\'%s\'>%s</a></li>" % (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 += "</ul>"
+ 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..39f56d0
--- /dev/null
+++ b/client/modules/Unclear.py
@@ -0,0 +1,26 @@
+import random
+
+WORDS = []
+
+
+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..85a8f1d
--- /dev/null
+++ b/client/modules/Weather.py
@@ -0,0 +1,104 @@
+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.parseDay(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()
+
+ weather_desc = entry['summary'].split('-')[1]
+
+ 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..febb4fd
--- /dev/null
+++ b/client/modules/__init__.py
@@ -0,0 +1 @@
+#!/usr/bin/env python import Gmail import Notifications import Birthday import Weather import HN import News import Time import Joke import Life import Unclear \ No newline at end of file
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'] + \
+ ",<br><br>Here are your top headlines:" + BODY
+ BODY += "<br>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 <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/music.py b/client/music.py
new file mode 100644
index 0000000..2b22b81
--- /dev/null
+++ b/client/music.py
@@ -0,0 +1,260 @@
+import re
+import difflib
+from mpd import MPDClient
+
+
+def reconnect(func, *default_args, **default_kwargs):
+ """
+ Reconnects before running
+ """
+
+ def wrap(self, *default_args, **default_kwargs):
+ try:
+ self.client.connect("localhost", 6600)
+ except:
+ pass
+
+ # sometimes not enough to just connect
+ try:
+ func(self, *default_args, **default_kwargs)
+ except:
+ self.client = MPDClient()
+ self.client.timeout = None
+ self.client.idletimeout = None
+ self.client.connect("localhost", 6600)
+
+ func(self, *default_args, **default_kwargs)
+
+ return wrap
+
+
+class Song:
+
+ def __init__(self, id, title, artist, album):
+
+ self.id = id
+ self.title = title
+ self.artist = artist
+ self.album = album
+
+
+class Music:
+
+ client = None
+ songs = [] # may have duplicates
+ playlists = []
+
+ # capitalized strings
+ song_titles = []
+ song_artists = []
+
+ def __init__(self):
+ """
+ Prepare the client and music variables
+ """
+ # prepare client
+ self.client = MPDClient()
+ self.client.timeout = None
+ self.client.idletimeout = None
+ self.client.connect("localhost", 6600)
+
+ # gather playlists
+ self.playlists = [x["playlist"] for x in self.client.listplaylists()]
+
+ # gather songs
+ self.client.clear()
+ for playlist in self.playlists:
+ self.client.load(playlist)
+
+ soup = self.client.playlist()
+ for i in range(0, len(soup) / 10):
+ index = i * 10
+ id = soup[index].strip()
+ title = soup[index + 3].strip().upper()
+ artist = soup[index + 2].strip().upper()
+ album = soup[index + 4].strip().upper()
+
+ self.songs.append(Song(id, title, artist, album))
+
+ self.song_titles.append(title)
+ self.song_artists.append(artist)
+
+ @reconnect
+ def play(self, songs=False, playlist_name=False):
+ """
+ Plays the current song or accepts a song to play.
+
+ Arguments:
+ songs -- a list of song objects
+ playlist_name -- user-defined, something like "Love Song Playlist"
+ """
+ if songs:
+ self.client.clear()
+ for song in songs:
+ try: # for some reason, certain ids don't work
+ self.client.add(song.id)
+ except:
+ pass
+
+ if playlist_name:
+ self.client.clear()
+ self.client.load(playlist_name)
+
+ self.client.play()
+
+ #@reconnect -- this makes the function return None for some reason!
+ def current_song(self):
+ item = self.client.playlistinfo(int(self.client.status()["song"]))[0]
+ result = "%s by %s" % (item["title"], item["artist"])
+ return result
+
+ @reconnect
+ def volume(self, level=None, interval=None):
+
+ if level:
+ self.client.setvol(int(level))
+ return
+
+ if interval:
+ level = int(self.client.status()['volume']) + int(interval)
+ self.client.setvol(int(level))
+ return
+
+ @reconnect
+ def pause(self):
+ self.client.pause()
+
+ @reconnect
+ def stop(self):
+ self.client.stop()
+
+ @reconnect
+ def next(self):
+ self.client.next()
+ return
+
+ @reconnect
+ def previous(self):
+ self.client.previous()
+ return
+
+ def get_soup(self):
+ """
+ returns the list of unique words that comprise song and artist titles
+ """
+
+ soup = []
+
+ for song in self.songs:
+ song_words = song.title.split(" ")
+ artist_words = song.artist.split(" ")
+ soup.extend(song_words)
+ soup.extend(artist_words)
+
+ title_trans = ''.join(
+ chr(c) if chr(c).isupper() or chr(c).islower() else '_' for c in range(256))
+ soup = [x.decode('utf-8').encode("ascii", "ignore").upper().translate(title_trans).replace("_", "")
+ for x in soup]
+ soup = [x for x in soup if x != ""]
+
+ return list(set(soup))
+
+ def get_soup_playlist(self):
+ """
+ returns the list of unique words that comprise playlist names
+ """
+
+ soup = []
+
+ for name in self.playlists:
+ soup.extend(name.split(" "))
+
+ title_trans = ''.join(
+ chr(c) if chr(c).isupper() or chr(c).islower() else '_' for c in range(256))
+ soup = [x.decode('utf-8').encode("ascii", "ignore").upper().translate(title_trans).replace("_", "")
+ for x in soup]
+ soup = [x for x in soup if x != ""]
+
+ return list(set(soup))
+
+ def get_soup_separated(self):
+ """
+ returns the list of PHRASES that comprise song and artist titles
+ """
+
+ title_soup = [song.title for song in self.songs]
+ artist_soup = [song.artist for song in self.songs]
+
+ soup = list(set(title_soup + artist_soup))
+
+ title_trans = ''.join(
+ chr(c) if chr(c).isupper() or chr(c).islower() else '_' for c in range(256))
+ soup = [x.decode('utf-8').encode("ascii", "ignore").upper().translate(title_trans).replace("_", " ")
+ for x in soup]
+ soup = [re.sub(' +', ' ', x) for x in soup if x != ""]
+
+ return soup
+
+ def fuzzy_songs(self, query):
+ """
+ Returns songs matching a query best as possible on either artist field, etc
+ """
+
+ query = query.upper()
+
+ matched_song_titles = difflib.get_close_matches(
+ query, self.song_titles)
+ matched_song_artists = difflib.get_close_matches(
+ query, self.song_artists)
+
+ # if query is beautifully matched, then forget about everything else
+ strict_priority_title = [x for x in matched_song_titles if x == query]
+ strict_priority_artists = [
+ x for x in matched_song_artists if x == query]
+
+ if strict_priority_title:
+ matched_song_titles = strict_priority_title
+ if strict_priority_artists:
+ matched_song_artists = strict_priority_artists
+
+ matched_songs_bytitle = [
+ song for song in self.songs if song.title in matched_song_titles]
+ matched_songs_byartist = [
+ song for song in self.songs if song.artist in matched_song_artists]
+
+ matches = list(set(matched_songs_bytitle + matched_songs_byartist))
+
+ return matches
+
+ def fuzzy_playlists(self, query):
+ """
+ returns playlist names that match query best as possible
+ """
+ query = query.upper()
+ lookup = {n.upper(): n for n in self.playlists}
+ results = [lookup[r] for r in difflib.get_close_matches(query, lookup)]
+ return results
+
+if __name__ == "__main__":
+ """
+ Plays music and performs unit-testing
+ """
+
+ print "Creating client"
+
+ music = Music()
+
+ print "Playing"
+
+ music.play(songs=[music.songs[3]])
+
+ print music.get_soup_separated()
+
+ while True:
+ query = raw_input("Query: ")
+ songs = music.fuzzy_songs(query)
+ print "Results"
+ print "======="
+ for song in songs:
+ print "Title: %s Artist: %s" % (song.title, song.artist)
+ music.play(songs=songs)
diff --git a/client/musicmode.py b/client/musicmode.py
new file mode 100755
index 0000000..79c4597
--- /dev/null
+++ b/client/musicmode.py
@@ -0,0 +1,179 @@
+"""
+ Manages the conversation
+"""
+
+import os
+from mic import Mic
+import g2p
+from music import *
+
+
+class MusicMode:
+
+ def __init__(self, PERSONA, mic):
+ self.persona = PERSONA
+ # self.mic - we're actually going to ignore the mic they passed in
+ self.music = Music()
+
+ # index spotify playlists into new dictionary and language models
+ original = self.music.get_soup_playlist(
+ ) + ["STOP", "CLOSE", "PLAY", "PAUSE",
+ "NEXT", "PREVIOUS", "LOUDER", "SOFTER", "LOWER", "HIGHER", "VOLUME", "PLAYLIST"]
+ pronounced = g2p.translateWords(original)
+ zipped = zip(original, pronounced)
+ lines = ["%s %s" % (x, y) for x, y in zipped]
+
+ with open("dictionary_spotify.dic", "w") as f:
+ f.write("\n".join(lines) + "\n")
+
+ with open("sentences_spotify.txt", "w") as f:
+ f.write("\n".join(original) + "\n")
+ f.write("<s> \n </s> \n")
+ f.close()
+
+ # make language model
+ os.system(
+ "text2idngram -vocab sentences_spotify.txt < sentences_spotify.txt -idngram spotify.idngram")
+ os.system(
+ "idngram2lm -idngram spotify.idngram -vocab sentences_spotify.txt -arpa languagemodel_spotify.lm")
+
+ # create a new mic with the new music models
+ self.mic = Mic(
+ "languagemodel.lm", "dictionary.dic", "languagemodel_persona.lm",
+ "dictionary_persona.dic", "languagemodel_spotify.lm", "dictionary_spotify.dic")
+
+ def delegateInput(self, input):
+
+ command = input.upper()
+
+ # check if input is meant to start the music module
+ if "PLAYLIST" in command:
+ command = command.replace("PLAYLIST", "")
+ elif "STOP" in command:
+ self.mic.say("Stopping music")
+ self.music.stop()
+ return
+ elif "PLAY" in command:
+ self.mic.say("Playing %s" % self.music.current_song())
+ self.music.play()
+ return
+ elif "PAUSE" in command:
+ self.mic.say("Pausing music")
+ # not pause because would need a way to keep track of pause/play
+ # state
+ self.music.stop()
+ return
+ elif any(ext in command for ext in ["LOUDER", "HIGHER"]):
+ self.mic.say("Louder")
+ self.music.volume(interval=10)
+ self.music.play()
+ return
+ elif any(ext in command for ext in ["SOFTER", "LOWER"]):
+ self.mic.say("Softer")
+ self.music.volume(interval=-10)
+ self.music.play()
+ return
+ elif "NEXT" in command:
+ self.mic.say("Next song")
+ self.music.play() # backwards necessary to get mopidy to work
+ self.music.next()
+ self.mic.say("Playing %s" % self.music.current_song())
+ return
+ elif "PREVIOUS" in command:
+ self.mic.say("Previous song")
+ self.music.play() # backwards necessary to get mopidy to work
+ self.music.previous()
+ self.mic.say("Playing %s" % self.music.current_song())
+ return
+
+ # SONG SELECTION... requires long-loading dictionary and language model
+ # songs = self.music.fuzzy_songs(query = command.replace("PLAY", ""))
+ # if songs:
+ # self.mic.say("Found songs")
+ # self.music.play(songs = songs)
+
+ # print "SONG RESULTS"
+ # print "============"
+ # for song in songs:
+ # print "Song: %s Artist: %s" % (song.title, song.artist)
+
+ # self.mic.say("Playing %s" % self.music.current_song())
+
+ # else:
+ # self.mic.say("No songs found. Resuming current song.")
+ # self.music.play()
+
+ # PLAYLIST SELECTION
+ playlists = self.music.fuzzy_playlists(query=command)
+ if playlists:
+ self.mic.say("Loading playlist %s" % playlists[0])
+ self.music.play(playlist_name=playlists[0])
+ self.mic.say("Playing %s" % self.music.current_song())
+ else:
+ self.mic.say("No playlists found. Resuming current song.")
+ self.music.play()
+
+ return
+
+ def handleForever(self):
+
+ self.music.play()
+ self.mic.say("Playing %s" % self.music.current_song())
+
+ while True:
+
+ try:
+ threshold, transcribed = self.mic.passiveListen(self.persona)
+ except:
+ continue
+
+ if threshold:
+
+ self.music.pause()
+
+ input = self.mic.activeListen(MUSIC=True)
+
+ if "close" in input.lower():
+ self.mic.say("Closing Spotify")
+ return
+
+ if input:
+ self.delegateInput(input)
+ else:
+ self.mic.say("Pardon?")
+ self.music.play()
+
+if __name__ == "__main__":
+ """
+ Indexes the Spotify music library to dictionary_spotify.dic and languagemodel_spotify.lm
+ """
+
+ musicmode = MusicMode("JASPER", None)
+ music = musicmode.music
+
+ original = music.get_soup() + ["STOP", "CLOSE", "PLAY",
+ "PAUSE", "NEXT", "PREVIOUS", "LOUDER", "SOFTER"]
+ pronounced = g2p.translateWords(original)
+ zipped = zip(original, pronounced)
+ lines = ["%s %s" % (x, y) for x, y in zipped]
+
+ with open("dictionary_spotify.dic", "w") as f:
+ f.write("\n".join(lines) + "\n")
+
+ with open("sentences_spotify.txt", "w") as f:
+ f.write("\n".join(original) + "\n")
+ f.write("<s> \n </s> \n")
+ f.close()
+
+ with open("sentences_spotify_separated.txt", "w") as f:
+ f.write("\n".join(music.get_soup_separated()) + "\n")
+ f.write("<s> \n </s> \n")
+ f.close()
+
+ # make language model
+ os.system(
+ "text2idngram -vocab sentences_spotify.txt < sentences_spotify_separated.txt -idngram spotify.idngram")
+ os.system(
+ "idngram2lm -idngram spotify.idngram -vocab sentences_spotify.txt -arpa languagemodel_spotify.lm")
+
+ print "Language Model and Dictionary Done"
diff --git a/client/notifier.py b/client/notifier.py
new file mode 100644
index 0000000..341ae04
--- /dev/null
+++ b/client/notifier.py
@@ -0,0 +1,68 @@
+import Queue
+from modules import Gmail
+from apscheduler.scheduler import Scheduler
+import logging
+logging.basicConfig()
+
+
+class Notifier(object):
+
+ class NotificationClient(object):
+
+ def __init__(self, gather, timestamp):
+ self.gather = gather
+ self.timestamp = timestamp
+
+ def run(self):
+ self.timestamp = self.gather(self.timestamp)
+
+ def __init__(self, profile):
+ self.q = Queue.Queue()
+ self.profile = profile
+ self.notifiers = [
+ self.NotificationClient(self.handleEmailNotifications, None),
+ ]
+
+ sched = Scheduler()
+ sched.start()
+ sched.add_interval_job(self.gather, seconds=30)
+
+ def gather(self):
+ [client.run() for client in self.notifiers]
+
+ def handleEmailNotifications(self, lastDate):
+ """Places new Gmail notifications in the Notifier's queue."""
+ emails = Gmail.fetchUnreadEmails(self.profile, since=lastDate)
+ if emails:
+ lastDate = Gmail.getMostRecentDate(emails)
+
+ def styleEmail(e):
+ return "New email from %s." % Gmail.getSender(e)
+
+ for e in emails:
+ self.q.put(styleEmail(e))
+
+ return lastDate
+
+ def getNotification(self):
+ """Returns a notification. Note that this function is consuming."""
+ try:
+ notif = self.q.get(block=False)
+ return notif
+ except Queue.Empty:
+ return None
+
+ def getAllNotifications(self):
+ """
+ Return a list of notifications in chronological order.
+ Note that this function is consuming, so consecutive calls
+ will yield different results.
+ """
+ notifs = []
+
+ notif = self.getNotification()
+ while notif:
+ notifs.append(notif)
+ notif = self.getNotification()
+
+ return notifs
diff --git a/client/populate.py b/client/populate.py
new file mode 100644
index 0000000..8181dc5
--- /dev/null
+++ b/client/populate.py
@@ -0,0 +1,90 @@
+import re
+from getpass import getpass
+import yaml
+from pytz import timezone
+
+
+def run():
+ profile = {}
+
+ print "Welcome to the profile populator. If, at any step, you'd prefer not to enter the requested information, just hit 'Enter' with a blank field to continue."
+
+ def simple_request(var, cleanVar, cleanInput=None):
+ input = raw_input(cleanVar + ": ")
+ if input:
+ if cleanInput:
+ input = cleanInput(input)
+ profile[var] = input
+
+ # name
+ simple_request('first_name', 'First name')
+ simple_request('last_name', 'Last name')
+
+ # gmail
+ print "\nJasper uses your Gmail to send notifications. Alternatively, you can skip this step (or just fill in the email address if you want to receive email notifications) and setup a Mailgun account, as at http://jasperproject.github.io/documentation/software/#mailgun.\n"
+ simple_request('gmail_address', 'Gmail address')
+ profile['gmail_password'] = getpass()
+
+
+ # phone number
+ clean_number = lambda s: re.sub(r'[^0-9]', '', s)
+ phone_number = clean_number(raw_input(
+ "Phone number. 10 digits (no country code). Any dashes or spaces will be removed for you: "))
+ while phone_number and len(phone_number) != 10:
+ print("Invalid phone number. Must be 10 digits.")
+ phone_number = clean_number(raw_input("Phone number: "))
+ if phone_number:
+ profile['phone_number'] = phone_number
+
+ # carrier
+ print("Phone carrier (for sending text notifications).")
+ print(
+ "Enter one of the following: 'AT&T', 'Verizon', 'T-Mobile' OR go to http://www.emailtextmessages.com and enter the email suffix for your carrier (e.g., for Virgin Mobile, enter 'vmobl.com').")
+ carrier = raw_input('Carrier: ')
+ if carrier:
+ if carrier == 'AT&T':
+ profile['carrier'] = 'txt.att.net'
+ elif carrier == 'Verizon':
+ profile['carrier'] = 'vtext.com'
+ elif carrier == 'T-Mobile':
+ profile['carrier'] = 'tmomail.net'
+ else:
+ profile['carrier'] = carrier
+
+ # location
+ print(
+ "Location should be a 5-digit US zipcode (e.g., 08544). For weather requests.")
+ zipcode = clean_number(raw_input("Zipcode: "))
+ while zipcode and len(zipcode) != 5:
+ print("Invalid zipcode. Must be 5 digits.")
+ zipcode = clean_number(raw_input("Zipcode: "))
+ if zipcode:
+ profile['location'] = zipcode
+
+ # timezone
+ print(
+ "Please enter a timezone from the list located in the TZ* column at http://en.wikipedia.org/wiki/List_of_tz_database_time_zones, or none at all.")
+ tz = raw_input("Timezone: ")
+ while tz:
+ try:
+ timezone(tz)
+ profile['timezone'] = tz
+ break
+ except:
+ print("Not a valid timezone. Try again.")
+ tz = raw_input("Timezone: ")
+
+ response = raw_input(
+ "Would you prefer to have notifications sent by email (E) or text message (T)? ")
+ while not response or (response != 'E' and response != 'T'):
+ response = raw_input("Please choose email (E) or text message (T): ")
+ profile['prefers_email'] = (response == 'E')
+
+ # write to profile
+ print("Writing to profile...")
+ outputFile = open("profile.yml", "w")
+ yaml.dump(profile, outputFile, default_flow_style=False)
+ print("Done.")
+
+if __name__ == "__main__":
+ run()
diff --git a/client/requirements.txt b/client/requirements.txt
new file mode 100644
index 0000000..ce2f0f7
--- /dev/null
+++ b/client/requirements.txt
@@ -0,0 +1,23 @@
+APScheduler==2.1.2
+CherryPy==3.2.2
+PocketSphinx==0.8
+PyYAML==3.10
+Pykka==1.2.0
+RPi.GPIO==0.5.4
+SphinxBase==0.8
+argparse==1.2.1
+beautifulsoup4==4.2.1
+facebook-sdk==0.4.0
+feedparser==5.1.3
+numpy==1.8.0
+pifacecommon==4.0.0
+pifacedigitalio==3.0.4
+pygame==1.9.1release
+python-dateutil==2.1
+python-mpd==0.3.0
+pytz==2013b
+quantities==0.10.1
+semantic==1.0.1
+six==1.6.1
+ws4py==0.3.2
+wsgiref==0.1.2
diff --git a/client/start.sh b/client/start.sh
new file mode 100755
index 0000000..f3eea8b
--- /dev/null
+++ b/client/start.sh
@@ -0,0 +1,3 @@
+cd /home/pi/jasper/client/
+rm -rf ../old_client
+python main.py &
diff --git a/client/test.py b/client/test.py
new file mode 100644
index 0000000..461ceba
--- /dev/null
+++ b/client/test.py
@@ -0,0 +1,98 @@
+import unittest
+from urllib2 import URLError, urlopen
+import yaml
+from test_mic import Mic
+from modules import *
+
+
+def activeInternet():
+ try:
+ urlopen('http://www.google.com', timeout=1)
+ return True
+ except URLError:
+ return False
+
+
+class TestModules(unittest.TestCase):
+
+ def setUp(self):
+ self.profile = yaml.safe_load(open("profile.yml", "r"))
+ self.send = False
+
+ def runConversation(self, query, inputs, module):
+ """Generic method for spoofing conversation.
+
+ Arguments:
+ query -- The initial input to the server.
+ inputs -- Additional input, if conversation is extended.
+
+ Returns:
+ The server's responses, in a list.
+ """
+ self.assertTrue(module.isValid(query))
+ mic = Mic(inputs)
+ module.handle(query, mic, self.profile)
+ return mic.outputs
+
+ def testLife(self):
+ query = "What is the meaning of life?"
+ inputs = []
+ outputs = self.runConversation(query, inputs, Life)
+ self.assertEqual(len(outputs), 1)
+ self.assertTrue("42" in outputs[0])
+
+ def testJoke(self):
+ query = "Tell me a joke."
+ inputs = ["Who's there?", "Random response"]
+ outputs = self.runConversation(query, inputs, Joke)
+ self.assertEqual(len(outputs), 3)
+ allJokes = open("JOKES.txt", "r").read()
+ self.assertTrue(outputs[2] in allJokes)
+
+ def testTime(self):
+ query = "What time is it?"
+ inputs = []
+ self.runConversation(query, inputs, Time)
+
+ @unittest.skipIf(not activeInternet(), "No internet connection")
+ def testGmail(self):
+ key = 'gmail_password'
+ if not key in self.profile or not self.profile[key]:
+ return
+
+ query = "Check my email"
+ inputs = []
+ self.runConversation(query, inputs, Gmail)
+
+ @unittest.skipIf(not activeInternet(), "No internet connection")
+ def testHN(self):
+ query = "find me some of the top hacker news stories"
+ if self.send:
+ inputs = ["the first and third"]
+ else:
+ inputs = ["no"]
+ outputs = self.runConversation(query, inputs, HN)
+ self.assertTrue("front-page articles" in outputs[1])
+
+ @unittest.skipIf(not activeInternet(), "No internet connection")
+ def testNews(self):
+ query = "find me some of the top news stories"
+ if self.send:
+ inputs = ["the first"]
+ else:
+ inputs = ["no"]
+ outputs = self.runConversation(query, inputs, News)
+ self.assertTrue("top headlines" in outputs[1])
+
+ @unittest.skipIf(not activeInternet(), "No internet connection")
+ def testWeather(self):
+ query = "what's the weather like tomorrow"
+ inputs = []
+ outputs = self.runConversation(query, inputs, Weather)
+ self.assertTrue(
+ "can't see that far ahead" in outputs[0]
+ or "Tomorrow" in outputs[0])
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/client/test_mic.py b/client/test_mic.py
new file mode 100644
index 0000000..b01240f
--- /dev/null
+++ b/client/test_mic.py
@@ -0,0 +1,27 @@
+"""
+A drop-in replacement for the Mic class used during unit testing.
+Designed to take pre-arranged inputs as an argument and store any
+outputs for inspection. Requires a populated profile (profile.yml).
+"""
+
+
+class Mic:
+
+ def __init__(self, inputs):
+ self.inputs = inputs
+ self.idx = 0
+ self.outputs = []
+
+ def passiveListen(self, PERSONA):
+ return True, "JASPER"
+
+ def activeListen(self, THRESHOLD=None, LISTEN=True, MUSIC=False):
+ if not LISTEN:
+ return self.inputs[self.idx - 1]
+
+ input = self.inputs[self.idx]
+ self.idx += 1
+ return input
+
+ def say(self, phrase, OPTIONS=None):
+ self.outputs.append(phrase)