diff options
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 Binary files differnew file mode 100644 index 0000000..d27c7dc --- /dev/null +++ b/boot/say.wav 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 Binary files differnew file mode 100644 index 0000000..1d6593f --- /dev/null +++ b/client/beep_hi.wav diff --git a/client/beep_lo.wav b/client/beep_lo.wav Binary files differnew file mode 100644 index 0000000..0afb29a --- /dev/null +++ b/client/beep_lo.wav 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) |
