From 6ea7c3c65bb14fded486acb4b8e9e2bb70efe40e Mon Sep 17 00:00:00 2001 From: Jezra Date: Tue, 9 Apr 2013 22:49:54 -0700 Subject: Init --- README | 8 ++++++ TTS.py | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ blather.py | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++ commands.tmp | 6 +++++ 4 files changed, 172 insertions(+) create mode 100644 README create mode 100755 TTS.py create mode 100755 blather.py create mode 100644 commands.tmp diff --git a/README b/README new file mode 100644 index 0000000..ab8248b --- /dev/null +++ b/README @@ -0,0 +1,8 @@ +1. Run blather.py, this will generate a 'sentences.corpus' file based on sentences in the 'commands' file +2. quit blather (there is a good chance it will just segfault) +3. go to http://www.speech.cs.cmu.edu/tools/lmtool-new.html and upload the sentences.corpus file +4. download the resulting XXXX.lm file to the 'language' directory and rename to file to 'lm' +5. download the resulting XXXX.dic file to the 'language' directory and rename to file to 'dic' +6. run blather.py +7. start talking + diff --git a/TTS.py b/TTS.py new file mode 100755 index 0000000..96a6f64 --- /dev/null +++ b/TTS.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python2 +import gst +import subprocess +import os.path +import time +import gobject + +#define some global variables +this_dir = os.path.dirname( os.path.abspath(__file__) ) +lang_dir = os.path.join(this_dir, "language") +command_file = os.path.join(this_dir, "commands") +strings_file = os.path.join(this_dir, "sentences.corpus") + +class TTS(gobject.GObject): + __gsignals__ = { + 'finished' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_BOOLEAN,)) + } + def __init__(self): + gobject.GObject.__init__(self) + self.commands = {} + #build the pipeline + cmd = 'autoaudiosrc ! audioconvert ! audioresample ! vader name=vad ! pocketsphinx name=asr ! appsink sync=false' + self.pipeline=gst.parse_launch( cmd ) + #get the Auto Speech Recognition piece + asr=self.pipeline.get_by_name('asr') + asr.connect('result', self.result) + asr.set_property('lm', os.path.join(lang_dir, 'lm')) + asr.set_property('dict', os.path.join(lang_dir, 'dic')) + asr.set_property('configured', True) + #get the Voice Activity DEtectoR + self.vad = self.pipeline.get_by_name('vad') + self.vad.set_property('auto-threshold',True) + self.read_commands() + #init gobject threads + gobject.threads_init() + + def listen(self): + self.pipeline.set_state(gst.STATE_PLAYING) + + def pause(self): + self.vad.set_property('silent', True) + self.pipeline.set_state(gst.STATE_PAUSED) + + def result(self, asr, text, uttid): + self.emit("finished", True) + print text + #is there a matching command? + if self.commands.has_key( text ): + cmd = self.commands[text] + print cmd + subprocess.call(cmd, shell=True) + else: + print "no matching command" + #emit finished + + + def read_commands(self): + #read the.commands file + file_lines = open(command_file) + strings = open(strings_file, "w") + for line in file_lines: + #trim the white spaces + line = line.strip() + #if the line has length and the first char isn't a hash + if len(line) and line[0]!="#": + #this is a parsible line + (key,value) = line.split(":",1) + print key, value + self.commands[key.strip()] = value.strip() + strings.write( key.strip()+"\n") + #close the strings file + strings.close() + +if __name__ == "__main__": + b = Blather() + b.listen() + main_loop = gobject.MainLoop() + #start the main loop + try: + main_loop.run() + except: + main_loop.quit() + + + diff --git a/blather.py b/blather.py new file mode 100755 index 0000000..230f055 --- /dev/null +++ b/blather.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python2 +import sys +import signal +import gobject +# Qt stuff +from PySide.QtCore import Signal, Qt +from PySide.QtGui import QApplication, QWidget, QMainWindow, QVBoxLayout +from PySide.QtGui import QLabel, QPushButton, QCheckBox +from TTS import TTS + +class Blather: + def __init__(self): + self.tts = TTS(); + self.tts.connect('finished',self.tts_finished) + #make a window + self.window = QMainWindow() + center = QWidget() + self.window.setCentralWidget(center) + + layout = QVBoxLayout() + center.setLayout(layout) + #make a listen/stop button + self.lsbutton = QPushButton("Listen") + layout.addWidget(self.lsbutton) + #make a continuous button + self.ccheckbox = QCheckBox("Continuous Listen") + layout.addWidget(self.ccheckbox) + + #connect the buttonsc + self.lsbutton.clicked.connect(self.lsbutton_clicked) + self.ccheckbox.clicked.connect(self.ccheckbox_clicked) + + def tts_finished(self, x, y): + if self.ccheckbox.isChecked(): + pass + else: + self.lsbutton_stopped() + + + def ccheckbox_clicked(self): + checked = self.ccheckbox.isChecked() + if checked: + #disable lsbutton + self.lsbutton.setEnabled(False) + self.tts.listen() + else: + self.lsbutton.setEnabled(True) + + def lsbutton_stopped(self): + self.tts.pause() + self.lsbutton.setText("Listen") + + def lsbutton_clicked(self): + val = self.lsbutton.text() + print val + if val == "Listen": + self.tts.listen() + self.lsbutton.setText("Stop") + else: + self.lsbutton_stopped() + + def run(self): + self.window.show() + +if __name__ == "__main__": + app = QApplication(sys.argv) + b = Blather() + b.run() + + signal.signal(signal.SIGINT, signal.SIG_DFL) + #start the app running + sys.exit(app.exec_()) + diff --git a/commands.tmp b/commands.tmp new file mode 100644 index 0000000..3835915 --- /dev/null +++ b/commands.tmp @@ -0,0 +1,6 @@ +# commands are key:value pairs +# key is the sentence to listen for +# key must be in ALL CAPS +# value is the command to run when the key is spoken + +HELLO WORLD:echo "hello world" -- cgit 1.4.1 From d81856fa08dda68dfef2b162934e2e598cb88d59 Mon Sep 17 00:00:00 2001 From: Jezra Date: Tue, 9 Apr 2013 22:53:54 -0700 Subject: Updated readme --- README | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README b/README index ab8248b..4c2966d 100644 --- a/README +++ b/README @@ -1,3 +1,12 @@ +Requirements +=========== + +pocketsphinx +gstreamer (and what ever plugin has pocket sphinx support) +pyside + +0. move commands.tmp to commands and fill the file with sentences and command to run + 1. Run blather.py, this will generate a 'sentences.corpus' file based on sentences in the 'commands' file 2. quit blather (there is a good chance it will just segfault) 3. go to http://www.speech.cs.cmu.edu/tools/lmtool-new.html and upload the sentences.corpus file -- cgit 1.4.1 From 41c518ae922194ecaa1f1d868000a3d4d9b7a868 Mon Sep 17 00:00:00 2001 From: Jezra Date: Wed, 10 Apr 2013 11:10:54 -0700 Subject: Added language dir and and README --- language/README | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 language/README diff --git a/language/README b/language/README new file mode 100644 index 0000000..dea25cb --- /dev/null +++ b/language/README @@ -0,0 +1,2 @@ +This is the directory where the language files go. +Please read ../README -- cgit 1.4.1 From b9632d0ab2f236e949d2d23c0360891c2c83d24b Mon Sep 17 00:00:00 2001 From: Jezra Date: Wed, 10 Apr 2013 18:40:44 -0700 Subject: Added pygst and a requirement for a specific version --- TTS.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TTS.py b/TTS.py index 96a6f64..4473986 100755 --- a/TTS.py +++ b/TTS.py @@ -1,4 +1,6 @@ #!/usr/bin/env python2 +import pygst +pygst.require('0.10') import gst import subprocess import os.path -- cgit 1.4.1 From 9efaa1b700689d1276a55560f9c13a2af8debe8f Mon Sep 17 00:00:00 2001 From: Jezra Date: Fri, 12 Apr 2013 12:24:56 -0700 Subject: Updated TTS.py to run standalone --- TTS.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/TTS.py b/TTS.py index 4473986..de4b61a 100755 --- a/TTS.py +++ b/TTS.py @@ -44,6 +44,7 @@ class TTS(gobject.GObject): self.pipeline.set_state(gst.STATE_PAUSED) def result(self, asr, text, uttid): + #emit finished self.emit("finished", True) print text #is there a matching command? @@ -53,8 +54,6 @@ class TTS(gobject.GObject): subprocess.call(cmd, shell=True) else: print "no matching command" - #emit finished - def read_commands(self): #read the.commands file @@ -74,8 +73,8 @@ class TTS(gobject.GObject): strings.close() if __name__ == "__main__": - b = Blather() - b.listen() + tts = TTS() + tts.listen() main_loop = gobject.MainLoop() #start the main loop try: -- cgit 1.4.1 From d9ddd711158e69a6eeb4900f68c80946705d1305 Mon Sep 17 00:00:00 2001 From: Jezra Date: Fri, 12 Apr 2013 12:31:39 -0700 Subject: That shit wasn't a TTS it was a Recognizer! --- Recognizer.py | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ TTS.py | 86 ----------------------------------------------------------- blather.py | 14 +++++----- 3 files changed, 93 insertions(+), 93 deletions(-) create mode 100755 Recognizer.py delete mode 100755 TTS.py diff --git a/Recognizer.py b/Recognizer.py new file mode 100755 index 0000000..685e41d --- /dev/null +++ b/Recognizer.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python2 +import pygst +pygst.require('0.10') +import gst +import subprocess +import os.path +import time +import gobject + +#define some global variables +this_dir = os.path.dirname( os.path.abspath(__file__) ) +lang_dir = os.path.join(this_dir, "language") +command_file = os.path.join(this_dir, "commands") +strings_file = os.path.join(this_dir, "sentences.corpus") + +class Recognizer(gobject.GObject): + __gsignals__ = { + 'finished' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_BOOLEAN,)) + } + def __init__(self): + gobject.GObject.__init__(self) + self.commands = {} + #build the pipeline + cmd = 'autoaudiosrc ! audioconvert ! audioresample ! vader name=vad ! pocketsphinx name=asr ! appsink sync=false' + self.pipeline=gst.parse_launch( cmd ) + #get the Auto Speech Recognition piece + asr=self.pipeline.get_by_name('asr') + asr.connect('result', self.result) + asr.set_property('lm', os.path.join(lang_dir, 'lm')) + asr.set_property('dict', os.path.join(lang_dir, 'dic')) + asr.set_property('configured', True) + #get the Voice Activity DEtectoR + self.vad = self.pipeline.get_by_name('vad') + self.vad.set_property('auto-threshold',True) + self.read_commands() + #init gobject threads + gobject.threads_init() + + def listen(self): + self.pipeline.set_state(gst.STATE_PLAYING) + + def pause(self): + self.vad.set_property('silent', True) + self.pipeline.set_state(gst.STATE_PAUSED) + + def result(self, asr, text, uttid): + #emit finished + self.emit("finished", True) + print text + #is there a matching command? + if self.commands.has_key( text ): + cmd = self.commands[text] + print cmd + subprocess.call(cmd, shell=True) + else: + print "no matching command" + + def read_commands(self): + #read the.commands file + file_lines = open(command_file) + strings = open(strings_file, "w") + for line in file_lines: + #trim the white spaces + line = line.strip() + #if the line has length and the first char isn't a hash + if len(line) and line[0]!="#": + #this is a parsible line + (key,value) = line.split(":",1) + print key, value + self.commands[key.strip()] = value.strip() + strings.write( key.strip()+"\n") + #close the strings file + strings.close() + +if __name__ == "__main__": + recognizer = Recognizer() + recognizer.listen() + main_loop = gobject.MainLoop() + #start the main loop + try: + main_loop.run() + except: + main_loop.quit() + + + diff --git a/TTS.py b/TTS.py deleted file mode 100755 index de4b61a..0000000 --- a/TTS.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env python2 -import pygst -pygst.require('0.10') -import gst -import subprocess -import os.path -import time -import gobject - -#define some global variables -this_dir = os.path.dirname( os.path.abspath(__file__) ) -lang_dir = os.path.join(this_dir, "language") -command_file = os.path.join(this_dir, "commands") -strings_file = os.path.join(this_dir, "sentences.corpus") - -class TTS(gobject.GObject): - __gsignals__ = { - 'finished' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_BOOLEAN,)) - } - def __init__(self): - gobject.GObject.__init__(self) - self.commands = {} - #build the pipeline - cmd = 'autoaudiosrc ! audioconvert ! audioresample ! vader name=vad ! pocketsphinx name=asr ! appsink sync=false' - self.pipeline=gst.parse_launch( cmd ) - #get the Auto Speech Recognition piece - asr=self.pipeline.get_by_name('asr') - asr.connect('result', self.result) - asr.set_property('lm', os.path.join(lang_dir, 'lm')) - asr.set_property('dict', os.path.join(lang_dir, 'dic')) - asr.set_property('configured', True) - #get the Voice Activity DEtectoR - self.vad = self.pipeline.get_by_name('vad') - self.vad.set_property('auto-threshold',True) - self.read_commands() - #init gobject threads - gobject.threads_init() - - def listen(self): - self.pipeline.set_state(gst.STATE_PLAYING) - - def pause(self): - self.vad.set_property('silent', True) - self.pipeline.set_state(gst.STATE_PAUSED) - - def result(self, asr, text, uttid): - #emit finished - self.emit("finished", True) - print text - #is there a matching command? - if self.commands.has_key( text ): - cmd = self.commands[text] - print cmd - subprocess.call(cmd, shell=True) - else: - print "no matching command" - - def read_commands(self): - #read the.commands file - file_lines = open(command_file) - strings = open(strings_file, "w") - for line in file_lines: - #trim the white spaces - line = line.strip() - #if the line has length and the first char isn't a hash - if len(line) and line[0]!="#": - #this is a parsible line - (key,value) = line.split(":",1) - print key, value - self.commands[key.strip()] = value.strip() - strings.write( key.strip()+"\n") - #close the strings file - strings.close() - -if __name__ == "__main__": - tts = TTS() - tts.listen() - main_loop = gobject.MainLoop() - #start the main loop - try: - main_loop.run() - except: - main_loop.quit() - - - diff --git a/blather.py b/blather.py index 230f055..ddc2ce7 100755 --- a/blather.py +++ b/blather.py @@ -6,12 +6,12 @@ import gobject from PySide.QtCore import Signal, Qt from PySide.QtGui import QApplication, QWidget, QMainWindow, QVBoxLayout from PySide.QtGui import QLabel, QPushButton, QCheckBox -from TTS import TTS +from Recognizer import Recognizer class Blather: def __init__(self): - self.tts = TTS(); - self.tts.connect('finished',self.tts_finished) + self.recognizer = Recognizer(); + self.recognizer.connect('finished',self.recognizer_finished) #make a window self.window = QMainWindow() center = QWidget() @@ -30,7 +30,7 @@ class Blather: self.lsbutton.clicked.connect(self.lsbutton_clicked) self.ccheckbox.clicked.connect(self.ccheckbox_clicked) - def tts_finished(self, x, y): + def recognizer_finished(self, x, y): if self.ccheckbox.isChecked(): pass else: @@ -42,19 +42,19 @@ class Blather: if checked: #disable lsbutton self.lsbutton.setEnabled(False) - self.tts.listen() + self.recognizer.listen() else: self.lsbutton.setEnabled(True) def lsbutton_stopped(self): - self.tts.pause() + self.recognizer.pause() self.lsbutton.setText("Listen") def lsbutton_clicked(self): val = self.lsbutton.text() print val if val == "Listen": - self.tts.listen() + self.recognizer.listen() self.lsbutton.setText("Stop") else: self.lsbutton_stopped() -- cgit 1.4.1 From 73c3ca50ddd83e49952b93b70c8bf0e660b77f87 Mon Sep 17 00:00:00 2001 From: Jezra Date: Wed, 17 Apr 2013 22:01:39 -0700 Subject: Config and language files are now in ~/.config/blather --- Blather.py | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++ QtUI.py | 82 +++++++++++++++++++++++++++++++++++++++ README | 15 +++++--- Recognizer.py | 56 +++------------------------ blather.py | 73 ----------------------------------- language/README | 2 - language_updater.sh | 24 ++++++++++++ 7 files changed, 229 insertions(+), 131 deletions(-) create mode 100755 Blather.py create mode 100644 QtUI.py delete mode 100755 blather.py delete mode 100644 language/README create mode 100755 language_updater.sh diff --git a/Blather.py b/Blather.py new file mode 100755 index 0000000..c9e4800 --- /dev/null +++ b/Blather.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python2 +import sys +import signal +import gobject +import os.path +import subprocess +from Recognizer import Recognizer + +#where are the files? +conf_dir = os.path.expanduser("~/.config/blather") +lang_dir = os.path.join(conf_dir, "language") +command_file = os.path.join(conf_dir, "commands") +strings_file = os.path.join(conf_dir, "sentences.corpus") +lang_file = os.path.join(lang_dir,'lm') +dic_file = os.path.join(lang_dir,'dic') +#make the lang_dir if it doesn't exist +if not os.path.exists(lang_dir): + os.makedirs(lang_dir) + +class Blather: + def __init__(self): + self.continuous_listen = False + self.commands = {} + self.read_commands() + self.recognizer = Recognizer(lang_file, dic_file) + self.recognizer.connect('finished',self.recognizer_finished) + + def read_commands(self): + #read the.commands file + file_lines = open(command_file) + strings = open(strings_file, "w") + for line in file_lines: + print line + #trim the white spaces + line = line.strip() + #if the line has length and the first char isn't a hash + if len(line) and line[0]!="#": + #this is a parsible line + (key,value) = line.split(":",1) + print key, value + self.commands[key.strip()] = value.strip() + strings.write( key.strip()+"\n") + #close the strings file + strings.close() + + + def recognizer_finished(self, recognizer, text): + #is there a matching command? + if self.commands.has_key( text ): + cmd = self.commands[text] + print cmd + subprocess.call(cmd, shell=True) + else: + print "no matching command" + #if there is a UI and we are not continuous listen + if self.ui: + if not self.continuous_listen: + #stop listening + self.recognizer.pause() + #let the UI know that there is a finish + self.ui.finished(text) + + def run(self, args): + #TODO check for UI request + #is there an arg? + if len(args) > 1: + if args[1] == "-qt": + #import the ui from qt + from QtUI import UI + elif args[1] == "-gtk": + from GtkUI import UI + else: + print "no GUI defined" + sys.exit() + self.ui = UI(args) + self.ui.connect("command", self.process_command) + self.ui.run() + else: + blather.recognizer.listen() + + def process_command(self, UI, command): + if command == "listen": + self.recognizer.listen() + elif command == "stop": + self.recognizer.pause() + elif command == "continuous_listen": + self.continuous_listen = True + self.recognizer.listen() + elif command == "continuous_stop": + self.continuous_listen = False + self.recognizer.pause() + +if __name__ == "__main__": + #make our blather object + blather = Blather() + #init gobject threads + gobject.threads_init() + #we want a main loop + main_loop = gobject.MainLoop() + #run the blather + blather.run(sys.argv) + #start the main loop + try: + main_loop.run() + except: + main_loop.quit() + sys.exit() + diff --git a/QtUI.py b/QtUI.py new file mode 100644 index 0000000..032e93b --- /dev/null +++ b/QtUI.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python2 +import sys +import signal +import gobject +# Qt stuff +from PySide.QtCore import Signal, Qt +from PySide.QtGui import QApplication, QWidget, QMainWindow, QVBoxLayout +from PySide.QtGui import QLabel, QPushButton, QCheckBox + +class UI(gobject.GObject): + __gsignals__ = { + 'command' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)) + } + + def __init__(self,args): + gobject.GObject.__init__(self) + #start by making our app + self.app = QApplication(args) + #make a window + self.window = QMainWindow() + #give the window a name + self.window.setWindowTitle("BlatherQt") + center = QWidget() + self.window.setCentralWidget(center) + + layout = QVBoxLayout() + center.setLayout(layout) + #make a listen/stop button + self.lsbutton = QPushButton("Listen") + layout.addWidget(self.lsbutton) + #make a continuous button + self.ccheckbox = QCheckBox("Continuous Listen") + layout.addWidget(self.ccheckbox) + + #connect the buttonsc + self.lsbutton.clicked.connect(self.lsbutton_clicked) + self.ccheckbox.clicked.connect(self.ccheckbox_clicked) + + def recognizer_finished(self, x, y): + if self.ccheckbox.isChecked(): + pass + else: + self.lsbutton_stopped() + + + def ccheckbox_clicked(self): + checked = self.ccheckbox.isChecked() + if checked: + #disable lsbutton + self.lsbutton.setEnabled(False) + self.lsbutton_stopped() + self.emit('command', "continuous_listen") + else: + self.lsbutton.setEnabled(True) + self.emit('command', "continuous_stop") + + def lsbutton_stopped(self): + self.lsbutton.setText("Listen") + + def lsbutton_clicked(self): + val = self.lsbutton.text() + print val + if val == "Listen": + self.emit("command", "listen") + self.lsbutton.setText("Stop") + else: + self.lsbutton_stopped() + self.emit("command", "stop") + + def run(self): + self.window.show() + self.app.exec_() + + def finished(self, text): + print text + #if the continuous isn't pressed + if not self.ccheckbox.isChecked(): + self.lsbutton_stopped() + + def quit(self): + #sys.exit() + pass diff --git a/README b/README index 4c2966d..4df3169 100644 --- a/README +++ b/README @@ -3,15 +3,18 @@ Requirements pocketsphinx gstreamer (and what ever plugin has pocket sphinx support) -pyside +pyside (only required for the Qt based UI) -0. move commands.tmp to commands and fill the file with sentences and command to run +0. move commands.tmp to ~/.config/blather/commands and fill the file with sentences and command to run -1. Run blather.py, this will generate a 'sentences.corpus' file based on sentences in the 'commands' file +1. Run blather.py, this will generate ~/.config/blather/sentences.corpus based on sentences in the 'commands' file 2. quit blather (there is a good chance it will just segfault) -3. go to http://www.speech.cs.cmu.edu/tools/lmtool-new.html and upload the sentences.corpus file -4. download the resulting XXXX.lm file to the 'language' directory and rename to file to 'lm' -5. download the resulting XXXX.dic file to the 'language' directory and rename to file to 'dic' +3. go to and upload the sentences.corpus file +4. download the resulting XXXX.lm file to the ~/.config/blather/language directory and rename to file to 'lm' +5. download the resulting XXXX.dic file to the ~/.config/blather/language directory and rename to file to 'dic' 6. run blather.py + * for Qt GUI, run blather.py -qt 7. start talking +####Bonus +once the sentences.corpus file has been created, run the language_updater.sh to automate the process of creating and downloading language files. \ No newline at end of file diff --git a/Recognizer.py b/Recognizer.py index 685e41d..8320cae 100755 --- a/Recognizer.py +++ b/Recognizer.py @@ -2,22 +2,18 @@ import pygst pygst.require('0.10') import gst -import subprocess import os.path -import time import gobject #define some global variables this_dir = os.path.dirname( os.path.abspath(__file__) ) -lang_dir = os.path.join(this_dir, "language") -command_file = os.path.join(this_dir, "commands") -strings_file = os.path.join(this_dir, "sentences.corpus") + class Recognizer(gobject.GObject): __gsignals__ = { - 'finished' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_BOOLEAN,)) + 'finished' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)) } - def __init__(self): + def __init__(self, language_file, dictionary_file): gobject.GObject.__init__(self) self.commands = {} #build the pipeline @@ -26,15 +22,12 @@ class Recognizer(gobject.GObject): #get the Auto Speech Recognition piece asr=self.pipeline.get_by_name('asr') asr.connect('result', self.result) - asr.set_property('lm', os.path.join(lang_dir, 'lm')) - asr.set_property('dict', os.path.join(lang_dir, 'dic')) + asr.set_property('lm', language_file) + asr.set_property('dict', dictionary_file) asr.set_property('configured', True) #get the Voice Activity DEtectoR self.vad = self.pipeline.get_by_name('vad') self.vad.set_property('auto-threshold',True) - self.read_commands() - #init gobject threads - gobject.threads_init() def listen(self): self.pipeline.set_state(gst.STATE_PLAYING) @@ -45,42 +38,5 @@ class Recognizer(gobject.GObject): def result(self, asr, text, uttid): #emit finished - self.emit("finished", True) - print text - #is there a matching command? - if self.commands.has_key( text ): - cmd = self.commands[text] - print cmd - subprocess.call(cmd, shell=True) - else: - print "no matching command" + self.emit("finished", text) - def read_commands(self): - #read the.commands file - file_lines = open(command_file) - strings = open(strings_file, "w") - for line in file_lines: - #trim the white spaces - line = line.strip() - #if the line has length and the first char isn't a hash - if len(line) and line[0]!="#": - #this is a parsible line - (key,value) = line.split(":",1) - print key, value - self.commands[key.strip()] = value.strip() - strings.write( key.strip()+"\n") - #close the strings file - strings.close() - -if __name__ == "__main__": - recognizer = Recognizer() - recognizer.listen() - main_loop = gobject.MainLoop() - #start the main loop - try: - main_loop.run() - except: - main_loop.quit() - - - diff --git a/blather.py b/blather.py deleted file mode 100755 index ddc2ce7..0000000 --- a/blather.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python2 -import sys -import signal -import gobject -# Qt stuff -from PySide.QtCore import Signal, Qt -from PySide.QtGui import QApplication, QWidget, QMainWindow, QVBoxLayout -from PySide.QtGui import QLabel, QPushButton, QCheckBox -from Recognizer import Recognizer - -class Blather: - def __init__(self): - self.recognizer = Recognizer(); - self.recognizer.connect('finished',self.recognizer_finished) - #make a window - self.window = QMainWindow() - center = QWidget() - self.window.setCentralWidget(center) - - layout = QVBoxLayout() - center.setLayout(layout) - #make a listen/stop button - self.lsbutton = QPushButton("Listen") - layout.addWidget(self.lsbutton) - #make a continuous button - self.ccheckbox = QCheckBox("Continuous Listen") - layout.addWidget(self.ccheckbox) - - #connect the buttonsc - self.lsbutton.clicked.connect(self.lsbutton_clicked) - self.ccheckbox.clicked.connect(self.ccheckbox_clicked) - - def recognizer_finished(self, x, y): - if self.ccheckbox.isChecked(): - pass - else: - self.lsbutton_stopped() - - - def ccheckbox_clicked(self): - checked = self.ccheckbox.isChecked() - if checked: - #disable lsbutton - self.lsbutton.setEnabled(False) - self.recognizer.listen() - else: - self.lsbutton.setEnabled(True) - - def lsbutton_stopped(self): - self.recognizer.pause() - self.lsbutton.setText("Listen") - - def lsbutton_clicked(self): - val = self.lsbutton.text() - print val - if val == "Listen": - self.recognizer.listen() - self.lsbutton.setText("Stop") - else: - self.lsbutton_stopped() - - def run(self): - self.window.show() - -if __name__ == "__main__": - app = QApplication(sys.argv) - b = Blather() - b.run() - - signal.signal(signal.SIGINT, signal.SIG_DFL) - #start the app running - sys.exit(app.exec_()) - diff --git a/language/README b/language/README deleted file mode 100644 index dea25cb..0000000 --- a/language/README +++ /dev/null @@ -1,2 +0,0 @@ -This is the directory where the language files go. -Please read ../README diff --git a/language_updater.sh b/language_updater.sh new file mode 100755 index 0000000..211793e --- /dev/null +++ b/language_updater.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +blatherdir=~/.config/blather +sourcefile=$blatherdir/sentences.corpus +langdir=$blatherdir/language +tempfile=$blatherdir/url.txt +lmtoolurl=http://www.speech.cs.cmu.edu/cgi-bin/tools/lmtool/run + +cd $blatherdir + +# upload corpus file, find the resulting dictionary file url +curl -L -F corpus=@"$sourcefile" -F formtype=simple $lmtoolurl \ + |grep -A 1 "base name" |grep http \ + | sed -e 's/^.*\="//' | sed -e 's/\.tgz.*$//' | sed -e 's/TAR//' > $tempfile + +# download the .dic and .lm files +curl -C - -O $(cat $tempfile).dic +curl -C - -O $(cat $tempfile).lm + +# mv em to the right name/place +mv *.dic $langdir/dic +mv *.lm $langdir/lm + +rm $tempfile -- cgit 1.4.1 From c61a0c311fa2a795f9369cf94988b0c7c7ff3973 Mon Sep 17 00:00:00 2001 From: Jezra Date: Thu, 18 Apr 2013 21:14:43 -0700 Subject: Cleaner quiting for Qt, added Gtk UI --- Blather.py | 49 +++++++++++++++++++++++++--------------- GtkUI.py | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ QtUI.py | 20 +++++++++-------- 3 files changed, 118 insertions(+), 27 deletions(-) create mode 100644 GtkUI.py diff --git a/Blather.py b/Blather.py index c9e4800..af36edb 100755 --- a/Blather.py +++ b/Blather.py @@ -18,12 +18,25 @@ if not os.path.exists(lang_dir): os.makedirs(lang_dir) class Blather: - def __init__(self): + def __init__(self, args): + self.ui = None self.continuous_listen = False self.commands = {} self.read_commands() self.recognizer = Recognizer(lang_file, dic_file) self.recognizer.connect('finished',self.recognizer_finished) + #is there an arg? + if len(args) > 1: + if args[1] == "-qt": + #import the ui from qt + from QtUI import UI + elif args[1] == "-gtk": + from GtkUI import UI + else: + print "no GUI defined" + sys.exit() + self.ui = UI(args) + self.ui.connect("command", self.process_command) def read_commands(self): #read the.commands file @@ -60,25 +73,19 @@ class Blather: #let the UI know that there is a finish self.ui.finished(text) - def run(self, args): - #TODO check for UI request - #is there an arg? - if len(args) > 1: - if args[1] == "-qt": - #import the ui from qt - from QtUI import UI - elif args[1] == "-gtk": - from GtkUI import UI - else: - print "no GUI defined" - sys.exit() - self.ui = UI(args) - self.ui.connect("command", self.process_command) + def run(self): + if self.ui: self.ui.run() else: blather.recognizer.listen() + def quit(self): + if self.ui: + self.ui.quit() + sys.exit() + def process_command(self, UI, command): + print command if command == "listen": self.recognizer.listen() elif command == "stop": @@ -89,20 +96,26 @@ class Blather: elif command == "continuous_stop": self.continuous_listen = False self.recognizer.pause() + elif command == "quit": + self.quit() if __name__ == "__main__": #make our blather object - blather = Blather() + blather = Blather(sys.argv) #init gobject threads gobject.threads_init() #we want a main loop main_loop = gobject.MainLoop() + #handle sigint + signal.signal(signal.SIGINT, signal.SIG_DFL) #run the blather - blather.run(sys.argv) + blather.run() #start the main loop + try: main_loop.run() except: + print "time to quit" main_loop.quit() sys.exit() - + diff --git a/GtkUI.py b/GtkUI.py new file mode 100644 index 0000000..6b33dd7 --- /dev/null +++ b/GtkUI.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python2 +import sys +import gobject +#Gtk +import pygtk +import gtk + +class UI(gobject.GObject): + __gsignals__ = { + 'command' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)) + } + + def __init__(self,args): + gobject.GObject.__init__(self) + #make a window + self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) + self.window.connect("delete_event", self.delete_event) + #give the window a name + self.window.set_title("BlatherGtk") + + layout = gtk.VBox() + self.window.add(layout) + #make a listen/stop button + self.lsbutton = gtk.Button("Listen") + layout.add(self.lsbutton) + #make a continuous button + self.ccheckbox = gtk.CheckButton("Continuous Listen") + layout.add(self.ccheckbox) + + #connect the buttonsc + self.lsbutton.connect("clicked",self.lsbutton_clicked) + self.ccheckbox.connect("clicked",self.ccheckbox_clicked) + + #add a label to the UI to display the last command + self.label = gtk.Label() + layout.add(self.label) + + def ccheckbox_clicked(self, widget): + checked = self.ccheckbox.get_active() + self.lsbutton.set_sensitive(not checked) + if checked: + self.lsbutton_stopped() + self.emit('command', "continuous_listen") + else: + self.emit('command', "continuous_stop") + + def lsbutton_stopped(self): + self.lsbutton.set_label("Listen") + + def lsbutton_clicked(self, button): + val = self.lsbutton.get_label() + if val == "Listen": + self.emit("command", "listen") + self.lsbutton.set_label("Stop") + #clear the label + self.label.set_text("") + else: + self.lsbutton_stopped() + self.emit("command", "stop") + + def run(self): + self.window.show_all() + + def quit(self): + pass + + def delete_event(self, x, y ): + self.emit("command", "quit") + + def finished(self, text): + print text + #if the continuous isn't pressed + if not self.ccheckbox.get_active(): + self.lsbutton_stopped() + self.label.set_text(text) + diff --git a/QtUI.py b/QtUI.py index 032e93b..35e2c38 100644 --- a/QtUI.py +++ b/QtUI.py @@ -1,6 +1,5 @@ #!/usr/bin/env python2 import sys -import signal import gobject # Qt stuff from PySide.QtCore import Signal, Qt @@ -35,14 +34,11 @@ class UI(gobject.GObject): #connect the buttonsc self.lsbutton.clicked.connect(self.lsbutton_clicked) self.ccheckbox.clicked.connect(self.ccheckbox_clicked) + + #add a label to the UI to display the last command + self.label = QLabel() + layout.addWidget(self.label) - def recognizer_finished(self, x, y): - if self.ccheckbox.isChecked(): - pass - else: - self.lsbutton_stopped() - - def ccheckbox_clicked(self): checked = self.ccheckbox.isChecked() if checked: @@ -59,10 +55,11 @@ class UI(gobject.GObject): def lsbutton_clicked(self): val = self.lsbutton.text() - print val if val == "Listen": self.emit("command", "listen") self.lsbutton.setText("Stop") + #clear the label + self.label.setText("") else: self.lsbutton_stopped() self.emit("command", "stop") @@ -70,12 +67,17 @@ class UI(gobject.GObject): def run(self): self.window.show() self.app.exec_() + self.emit("command", "quit") + def quit(self): + pass + def finished(self, text): print text #if the continuous isn't pressed if not self.ccheckbox.isChecked(): self.lsbutton_stopped() + self.label.setText(text) def quit(self): #sys.exit() -- cgit 1.4.1 From a14686b782b16740d9ee4a287efc4a5417ca1a9e Mon Sep 17 00:00:00 2001 From: Jezra Date: Fri, 19 Apr 2013 11:29:13 -0700 Subject: Updated the README to better reflect the new UIs --- README | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/README b/README index 4df3169..8e70d2b 100644 --- a/README +++ b/README @@ -1,20 +1,23 @@ -Requirements -=========== +#Blather +Blather is a speech recognizer that will run commands when a user speaks preset sentences. -pocketsphinx -gstreamer (and what ever plugin has pocket sphinx support) -pyside (only required for the Qt based UI) +##Requirements +1. pocketsphinx +2. gstreamer (and what ever plugin has pocket sphinx support) +3. pyside (only required for the Qt based UI) +4. pygtk (only required for the Gtk based UI) +##Usage 0. move commands.tmp to ~/.config/blather/commands and fill the file with sentences and command to run - -1. Run blather.py, this will generate ~/.config/blather/sentences.corpus based on sentences in the 'commands' file +1. Run Blather.py, this will generate ~/.config/blather/sentences.corpus based on sentences in the 'commands' file 2. quit blather (there is a good chance it will just segfault) 3. go to and upload the sentences.corpus file 4. download the resulting XXXX.lm file to the ~/.config/blather/language directory and rename to file to 'lm' 5. download the resulting XXXX.dic file to the ~/.config/blather/language directory and rename to file to 'dic' -6. run blather.py - * for Qt GUI, run blather.py -qt +6. run Blather.py + * for Qt GUI, run Blather.py -qt + * for Gtk GUI, run Blather.py -gtk 7. start talking ####Bonus -once the sentences.corpus file has been created, run the language_updater.sh to automate the process of creating and downloading language files. \ No newline at end of file +once the sentences.corpus file has been created, run the language_updater.sh script to automate the process of creating and downloading language files. \ No newline at end of file -- cgit 1.4.1 From abd0eacddb3f5ad44ad3e859e32826b4cd2fc8e4 Mon Sep 17 00:00:00 2001 From: Jezra Date: Sat, 20 Apr 2013 09:44:20 -0700 Subject: Whoa bub! you don't have to yell. Case insensitive commands --- Blather.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Blather.py b/Blather.py index af36edb..8985cd6 100755 --- a/Blather.py +++ b/Blather.py @@ -51,16 +51,17 @@ class Blather: #this is a parsible line (key,value) = line.split(":",1) print key, value - self.commands[key.strip()] = value.strip() + self.commands[key.strip().lower()] = value.strip() strings.write( key.strip()+"\n") #close the strings file strings.close() def recognizer_finished(self, recognizer, text): + t = text.lower() #is there a matching command? - if self.commands.has_key( text ): - cmd = self.commands[text] + if self.commands.has_key( t ): + cmd = self.commands[t] print cmd subprocess.call(cmd, shell=True) else: @@ -71,7 +72,7 @@ class Blather: #stop listening self.recognizer.pause() #let the UI know that there is a finish - self.ui.finished(text) + self.ui.finished(t) def run(self): if self.ui: -- cgit 1.4.1 From 40b219723f2ac3775c66a0547b5c7ece061edee3 Mon Sep 17 00:00:00 2001 From: Jezra Date: Mon, 22 Apr 2013 18:13:08 -0700 Subject: patched language_updater.sh to generate corpus from command file --- language_updater.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/language_updater.sh b/language_updater.sh index 211793e..383e140 100755 --- a/language_updater.sh +++ b/language_updater.sh @@ -1,15 +1,22 @@ #!/bin/bash blatherdir=~/.config/blather -sourcefile=$blatherdir/sentences.corpus +sentences=$blatherdir/sentences.corpus +sourcefile=$blatherdir/commands langdir=$blatherdir/language tempfile=$blatherdir/url.txt lmtoolurl=http://www.speech.cs.cmu.edu/cgi-bin/tools/lmtool/run cd $blatherdir +sed -f - $sourcefile > $sentences < + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. -- cgit 1.4.1 From 8fdc5cf0cdf84f25b84f9c0db003c1bc0f0d096e Mon Sep 17 00:00:00 2001 From: Jezra Date: Fri, 3 May 2013 10:22:25 -0700 Subject: Control the size --- GtkUI.py | 1 + QtUI.py | 1 + 2 files changed, 2 insertions(+) diff --git a/GtkUI.py b/GtkUI.py index d371aac..9d68fcc 100644 --- a/GtkUI.py +++ b/GtkUI.py @@ -19,6 +19,7 @@ class UI(gobject.GObject): self.window.connect("delete_event", self.delete_event) #give the window a name self.window.set_title("BlatherGtk") + self.window.set_resizable(False) layout = gtk.VBox() self.window.add(layout) diff --git a/QtUI.py b/QtUI.py index bfe2358..37910a5 100644 --- a/QtUI.py +++ b/QtUI.py @@ -21,6 +21,7 @@ class UI(gobject.GObject): self.window = QMainWindow() #give the window a name self.window.setWindowTitle("BlatherQt") + self.window.setMaximumSize(400,200) center = QWidget() self.window.setCentralWidget(center) -- cgit 1.4.1 From fcb1172f7d0cfe52b6e00c984001a0b01ee3ab9e Mon Sep 17 00:00:00 2001 From: Jezra Date: Sat, 1 Jun 2013 12:26:34 -0700 Subject: Added some graphical assets for blather (icon related) --- assets/blather.png | Bin 0 -> 13571 bytes assets/blather.svg | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++ assets/blather.xcf | Bin 0 -> 23830 bytes 3 files changed, 85 insertions(+) create mode 100644 assets/blather.png create mode 100644 assets/blather.svg create mode 100644 assets/blather.xcf diff --git a/assets/blather.png b/assets/blather.png new file mode 100644 index 0000000..e1a83cb Binary files /dev/null and b/assets/blather.png differ diff --git a/assets/blather.svg b/assets/blather.svg new file mode 100644 index 0000000..1e10ee7 --- /dev/null +++ b/assets/blather.svg @@ -0,0 +1,85 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + B + + diff --git a/assets/blather.xcf b/assets/blather.xcf new file mode 100644 index 0000000..9a11666 Binary files /dev/null and b/assets/blather.xcf differ -- cgit 1.4.1 From bb744e1556db4fe11be9c0fb6a29ba5643ec6fff Mon Sep 17 00:00:00 2001 From: Jezra Date: Mon, 10 Jun 2013 20:10:30 -0700 Subject: Implemented -i flag to select UI and -c flag to start UI in 'continuous listen' mode giggity --- Blather.py | 32 +++++++++++++++++++++++--------- GtkUI.py | 7 +++++-- QtUI.py | 6 +++++- README | 12 +++++++++--- 4 files changed, 42 insertions(+), 15 deletions(-) diff --git a/Blather.py b/Blather.py index 6b431a7..c534e71 100755 --- a/Blather.py +++ b/Blather.py @@ -8,7 +8,8 @@ import signal import gobject import os.path import subprocess -from Recognizer import Recognizer +from optparse import OptionParser + #where are the files? conf_dir = os.path.expanduser("~/.config/blather") @@ -22,26 +23,30 @@ if not os.path.exists(lang_dir): os.makedirs(lang_dir) class Blather: - def __init__(self, args): + def __init__(self, opts): + #import the recognizer so Gst doesn't clobber our -h + from Recognizer import Recognizer self.ui = None + ui_continuous_listen = False self.continuous_listen = False self.commands = {} self.read_commands() self.recognizer = Recognizer(lang_file, dic_file) self.recognizer.connect('finished',self.recognizer_finished) - #is there an arg? - if len(args) > 1: - if args[1] == "-qt": + + if opts.interface != None: + if opts.interface == "q": #import the ui from qt from QtUI import UI - elif args[1] == "-gtk": + elif opts.interface == "g": from GtkUI import UI else: print "no GUI defined" sys.exit() - self.ui = UI(args) + + self.ui = UI(args,opts.continuous) self.ui.connect("command", self.process_command) - + def read_commands(self): #read the.commands file file_lines = open(command_file) @@ -105,8 +110,17 @@ class Blather: self.quit() if __name__ == "__main__": + parser = OptionParser() + parser.add_option("-i", "--interface", type="string", dest="interface", + action='store', + help="Interface to use (if any). 'q' for Qt, 'g' for GTK") + parser.add_option("-c", "--continuous", + action="store_true", dest="continuous", default=False, + help="starts interface with 'continuous' listen enabled") + + (options, args) = parser.parse_args() #make our blather object - blather = Blather(sys.argv) + blather = Blather(options) #init gobject threads gobject.threads_init() #we want a main loop diff --git a/GtkUI.py b/GtkUI.py index 9d68fcc..d9aae7d 100644 --- a/GtkUI.py +++ b/GtkUI.py @@ -12,8 +12,9 @@ class UI(gobject.GObject): 'command' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)) } - def __init__(self,args): + def __init__(self,args, continuous): gobject.GObject.__init__(self) + self.continuous = continuous #make a window self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) self.window.connect("delete_event", self.delete_event) @@ -37,7 +38,7 @@ class UI(gobject.GObject): #add a label to the UI to display the last command self.label = gtk.Label() layout.add(self.label) - + def ccheckbox_clicked(self, widget): checked = self.ccheckbox.get_active() self.lsbutton.set_sensitive(not checked) @@ -63,6 +64,8 @@ class UI(gobject.GObject): def run(self): self.window.show_all() + if self.continuous: + self.ccheckbox.set_active(True) def quit(self): pass diff --git a/QtUI.py b/QtUI.py index 37910a5..c4a5a54 100644 --- a/QtUI.py +++ b/QtUI.py @@ -13,7 +13,8 @@ class UI(gobject.GObject): 'command' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)) } - def __init__(self,args): + def __init__(self,args,continuous): + self.continuous = continuous gobject.GObject.__init__(self) #start by making our app self.app = QApplication(args) @@ -69,6 +70,9 @@ class UI(gobject.GObject): def run(self): self.window.show() + if self.continuous: + self.ccheckbox.setCheckState(Qt.Checked) + self.ccheckbox_clicked() self.app.exec_() self.emit("command", "quit") diff --git a/README b/README index 8e70d2b..d5414c1 100644 --- a/README +++ b/README @@ -15,9 +15,15 @@ Blather is a speech recognizer that will run commands when a user speaks preset 4. download the resulting XXXX.lm file to the ~/.config/blather/language directory and rename to file to 'lm' 5. download the resulting XXXX.dic file to the ~/.config/blather/language directory and rename to file to 'dic' 6. run Blather.py - * for Qt GUI, run Blather.py -qt - * for Gtk GUI, run Blather.py -gtk + * for Qt GUI, run Blather.py -i q + * for Gtk GUI, run Blather.py -i g + * to start a UI in 'continuous' listen mode, use the -c flag + 7. start talking ####Bonus -once the sentences.corpus file has been created, run the language_updater.sh script to automate the process of creating and downloading language files. \ No newline at end of file +once the sentences.corpus file has been created, run the language_updater.sh script to automate the process of creating and downloading language files. + +**Example** +To run blather with the GTK UI and start in continuous listen mode: +./Blather.py -i g -c \ No newline at end of file -- cgit 1.4.1 From 2888974007584d59c0949e3bf65c2f172ba49edd Mon Sep 17 00:00:00 2001 From: Jezra Date: Tue, 25 Jun 2013 14:01:11 -0700 Subject: added -H Num to keep track of blather history this shit is untested! --- Blather.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Blather.py b/Blather.py index c534e71..bdf38e2 100755 --- a/Blather.py +++ b/Blather.py @@ -16,6 +16,7 @@ conf_dir = os.path.expanduser("~/.config/blather") lang_dir = os.path.join(conf_dir, "language") command_file = os.path.join(conf_dir, "commands") strings_file = os.path.join(conf_dir, "sentences.corpus") +history_file = os.path.join(conf_dir, "blather.history") lang_file = os.path.join(lang_dir,'lm') dic_file = os.path.join(lang_dir,'dic') #make the lang_dir if it doesn't exist @@ -27,6 +28,8 @@ class Blather: #import the recognizer so Gst doesn't clobber our -h from Recognizer import Recognizer self.ui = None + #keep track of the opts + self.opts = opts ui_continuous_listen = False self.continuous_listen = False self.commands = {} @@ -46,6 +49,10 @@ class Blather: self.ui = UI(args,opts.continuous) self.ui.connect("command", self.process_command) + + if self.opts.history: + self.history = [] + def read_commands(self): #read the.commands file @@ -65,6 +72,19 @@ class Blather: #close the strings file strings.close() + def log_history(self,text): + if self.opts.history: + self.history.append(text) + if len(self.history) > self.opts.history: + #pop off the first item + self.history.pop(0) + + #open and truncate the blather history file + hfile = open(history_file, "w") + for line in self.history: + hfile.write( line+"\n") + #close the file + hfile.close() def recognizer_finished(self, recognizer, text): t = text.lower() @@ -73,6 +93,7 @@ class Blather: cmd = self.commands[t] print cmd subprocess.call(cmd, shell=True) + self.log_history(text) else: print "no matching command" #if there is a UI and we are not continuous listen @@ -117,6 +138,9 @@ if __name__ == "__main__": parser.add_option("-c", "--continuous", action="store_true", dest="continuous", default=False, help="starts interface with 'continuous' listen enabled") + parser.add_option("-H", "--history", type="int", + action="store", dest="history", + help="number of commands to store in history file") (options, args) = parser.parse_args() #make our blather object -- cgit 1.4.1 From 5ea5c74cf9620256ffd89d25323adddca9a94290 Mon Sep 17 00:00:00 2001 From: Jezra Date: Wed, 26 Jun 2013 08:09:24 -0700 Subject: Something .conf --- Blather.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Blather.py b/Blather.py index bdf38e2..223d417 100755 --- a/Blather.py +++ b/Blather.py @@ -14,7 +14,7 @@ from optparse import OptionParser #where are the files? conf_dir = os.path.expanduser("~/.config/blather") lang_dir = os.path.join(conf_dir, "language") -command_file = os.path.join(conf_dir, "commands") +command_file = os.path.join(conf_dir, "commands.conf") strings_file = os.path.join(conf_dir, "sentences.corpus") history_file = os.path.join(conf_dir, "blather.history") lang_file = os.path.join(lang_dir,'lm') -- cgit 1.4.1 From 3d1a7ff6d07380af777194d1b95e2dafa65033cb Mon Sep 17 00:00:00 2001 From: Jezra Date: Mon, 1 Jul 2013 20:11:00 -0700 Subject: Added a data dir with the blather icon made sure the UIs would use the icon booyah? --- data/icon.png | Bin 0 -> 13571 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 data/icon.png diff --git a/data/icon.png b/data/icon.png new file mode 100644 index 0000000..e1a83cb Binary files /dev/null and b/data/icon.png differ -- cgit 1.4.1 From bd290f006130827b0630bf36ab058bb82f4ce29d Mon Sep 17 00:00:00 2001 From: Jezra Date: Tue, 2 Jul 2013 07:05:51 -0700 Subject: icon, dumbass --- Blather.py | 18 +++++++++++++++++- GtkUI.py | 3 +++ QtUI.py | 8 ++++---- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Blather.py b/Blather.py index 223d417..461a456 100755 --- a/Blather.py +++ b/Blather.py @@ -49,6 +49,10 @@ class Blather: self.ui = UI(args,opts.continuous) self.ui.connect("command", self.process_command) + #can we load the icon resource? + icon = self.load_resource("icon.png") + if icon: + self.ui.set_icon(icon) if self.opts.history: self.history = [] @@ -129,7 +133,19 @@ class Blather: self.recognizer.pause() elif command == "quit": self.quit() - + + def load_resource(self,string): + local_data = os.path.join(os.path.dirname(__file__), 'data') + paths = ["/usr/share/blather/","/usr/local/share/blather", local_data] + for path in paths: + resource = os.path.join(path, string) + if os.path.exists( resource ): + return resource + #if we get this far, no resource was found + return False + + + if __name__ == "__main__": parser = OptionParser() parser.add_option("-i", "--interface", type="string", dest="interface", diff --git a/GtkUI.py b/GtkUI.py index d9aae7d..720fb10 100644 --- a/GtkUI.py +++ b/GtkUI.py @@ -80,3 +80,6 @@ class UI(gobject.GObject): self.lsbutton_stopped() self.label.set_text(text) + def set_icon(self, icon): + gtk.window_set_default_icon_from_file(icon) + diff --git a/QtUI.py b/QtUI.py index c4a5a54..071dab4 100644 --- a/QtUI.py +++ b/QtUI.py @@ -6,7 +6,7 @@ import gobject # Qt stuff from PySide.QtCore import Signal, Qt from PySide.QtGui import QApplication, QWidget, QMainWindow, QVBoxLayout -from PySide.QtGui import QLabel, QPushButton, QCheckBox +from PySide.QtGui import QLabel, QPushButton, QCheckBox, QIcon class UI(gobject.GObject): __gsignals__ = { @@ -76,9 +76,6 @@ class UI(gobject.GObject): self.app.exec_() self.emit("command", "quit") - def quit(self): - pass - def finished(self, text): print text #if the continuous isn't pressed @@ -89,3 +86,6 @@ class UI(gobject.GObject): def quit(self): #sys.exit() pass + + def set_icon(self, icon): + self.window.setWindowIcon(QIcon(icon)) -- cgit 1.4.1 From 9a052e1c5c4beaac376b79309556e0d934c95e0e Mon Sep 17 00:00:00 2001 From: Jezra Date: Tue, 16 Jul 2013 20:47:39 -0700 Subject: The updater uses the correct commands file --- language_updater.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/language_updater.sh b/language_updater.sh index 383e140..ec5c868 100755 --- a/language_updater.sh +++ b/language_updater.sh @@ -2,7 +2,7 @@ blatherdir=~/.config/blather sentences=$blatherdir/sentences.corpus -sourcefile=$blatherdir/commands +sourcefile=$blatherdir/commands.conf langdir=$blatherdir/language tempfile=$blatherdir/url.txt lmtoolurl=http://www.speech.cs.cmu.edu/cgi-bin/tools/lmtool/run -- cgit 1.4.1 From a230f50f56aeeef8ef1c8b21ea8384e079d77aae Mon Sep 17 00:00:00 2001 From: Jezra Date: Tue, 23 Jul 2013 21:18:37 -0700 Subject: ctrl+q will quit the UI if it is running --- Blather.py | 36 +++++++++++++++++------------------- GtkUI.py | 39 ++++++++++++++++++++++++--------------- QtUI.py | 38 ++++++++++++++++++++++---------------- 3 files changed, 63 insertions(+), 50 deletions(-) diff --git a/Blather.py b/Blather.py index 461a456..f5d8a67 100755 --- a/Blather.py +++ b/Blather.py @@ -36,7 +36,7 @@ class Blather: self.read_commands() self.recognizer = Recognizer(lang_file, dic_file) self.recognizer.connect('finished',self.recognizer_finished) - + if opts.interface != None: if opts.interface == "q": #import the ui from qt @@ -46,18 +46,18 @@ class Blather: else: print "no GUI defined" sys.exit() - + self.ui = UI(args,opts.continuous) self.ui.connect("command", self.process_command) #can we load the icon resource? icon = self.load_resource("icon.png") if icon: self.ui.set_icon(icon) - + if self.opts.history: self.history = [] - - + + def read_commands(self): #read the.commands file file_lines = open(command_file) @@ -75,21 +75,21 @@ class Blather: strings.write( key.strip()+"\n") #close the strings file strings.close() - + def log_history(self,text): if self.opts.history: self.history.append(text) if len(self.history) > self.opts.history: #pop off the first item self.history.pop(0) - + #open and truncate the blather history file hfile = open(history_file, "w") for line in self.history: hfile.write( line+"\n") #close the file hfile.close() - + def recognizer_finished(self, recognizer, text): t = text.lower() #is there a matching command? @@ -107,16 +107,14 @@ class Blather: self.recognizer.pause() #let the UI know that there is a finish self.ui.finished(t) - + def run(self): if self.ui: self.ui.run() else: - blather.recognizer.listen() + blather.recognizer.listen() def quit(self): - if self.ui: - self.ui.quit() sys.exit() def process_command(self, UI, command): @@ -133,7 +131,7 @@ class Blather: self.recognizer.pause() elif command == "quit": self.quit() - + def load_resource(self,string): local_data = os.path.join(os.path.dirname(__file__), 'data') paths = ["/usr/share/blather/","/usr/local/share/blather", local_data] @@ -143,9 +141,9 @@ class Blather: return resource #if we get this far, no resource was found return False - - - + + + if __name__ == "__main__": parser = OptionParser() parser.add_option("-i", "--interface", type="string", dest="interface", @@ -155,7 +153,7 @@ if __name__ == "__main__": action="store_true", dest="continuous", default=False, help="starts interface with 'continuous' listen enabled") parser.add_option("-H", "--history", type="int", - action="store", dest="history", + action="store", dest="history", help="number of commands to store in history file") (options, args) = parser.parse_args() @@ -170,11 +168,11 @@ if __name__ == "__main__": #run the blather blather.run() #start the main loop - + try: main_loop.run() except: print "time to quit" main_loop.quit() sys.exit() - + diff --git a/GtkUI.py b/GtkUI.py index 720fb10..56a6252 100644 --- a/GtkUI.py +++ b/GtkUI.py @@ -11,7 +11,7 @@ class UI(gobject.GObject): __gsignals__ = { 'command' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)) } - + def __init__(self,args, continuous): gobject.GObject.__init__(self) self.continuous = continuous @@ -21,7 +21,7 @@ class UI(gobject.GObject): #give the window a name self.window.set_title("BlatherGtk") self.window.set_resizable(False) - + layout = gtk.VBox() self.window.add(layout) #make a listen/stop button @@ -30,15 +30,24 @@ class UI(gobject.GObject): #make a continuous button self.ccheckbox = gtk.CheckButton("Continuous Listen") layout.add(self.ccheckbox) - - #connect the buttonsc + + #connect the buttons self.lsbutton.connect("clicked",self.lsbutton_clicked) self.ccheckbox.connect("clicked",self.ccheckbox_clicked) - + #add a label to the UI to display the last command self.label = gtk.Label() layout.add(self.label) + #create an accellerator group for this window + accel = gtk.AccelGroup() + #add the ctrl+q to quit + accel.connect_group(gtk.keysyms.q, gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE, self.accel_quit ) + #lock the group + accel.lock() + #add the group to the window + self.window.add_accel_group(accel) + def ccheckbox_clicked(self, widget): checked = self.ccheckbox.get_active() self.lsbutton.set_sensitive(not checked) @@ -47,10 +56,10 @@ class UI(gobject.GObject): self.emit('command', "continuous_listen") else: self.emit('command', "continuous_stop") - + def lsbutton_stopped(self): self.lsbutton.set_label("Listen") - + def lsbutton_clicked(self, button): val = self.lsbutton.get_label() if val == "Listen": @@ -61,25 +70,25 @@ class UI(gobject.GObject): else: self.lsbutton_stopped() self.emit("command", "stop") - + def run(self): self.window.show_all() if self.continuous: self.ccheckbox.set_active(True) - - def quit(self): - pass - + + def accel_quit(self, accel_group, acceleratable, keyval, modifier): + self.emit("command", "quit") + def delete_event(self, x, y ): self.emit("command", "quit") - + def finished(self, text): print text #if the continuous isn't pressed if not self.ccheckbox.get_active(): self.lsbutton_stopped() self.label.set_text(text) - + def set_icon(self, icon): gtk.window_set_default_icon_from_file(icon) - + diff --git a/QtUI.py b/QtUI.py index 071dab4..c772abf 100644 --- a/QtUI.py +++ b/QtUI.py @@ -6,13 +6,13 @@ import gobject # Qt stuff from PySide.QtCore import Signal, Qt from PySide.QtGui import QApplication, QWidget, QMainWindow, QVBoxLayout -from PySide.QtGui import QLabel, QPushButton, QCheckBox, QIcon +from PySide.QtGui import QLabel, QPushButton, QCheckBox, QIcon, QAction class UI(gobject.GObject): __gsignals__ = { 'command' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)) } - + def __init__(self,args,continuous): self.continuous = continuous gobject.GObject.__init__(self) @@ -25,7 +25,7 @@ class UI(gobject.GObject): self.window.setMaximumSize(400,200) center = QWidget() self.window.setCentralWidget(center) - + layout = QVBoxLayout() center.setLayout(layout) #make a listen/stop button @@ -34,15 +34,25 @@ class UI(gobject.GObject): #make a continuous button self.ccheckbox = QCheckBox("Continuous Listen") layout.addWidget(self.ccheckbox) - - #connect the buttonsc + + #connect the buttons self.lsbutton.clicked.connect(self.lsbutton_clicked) self.ccheckbox.clicked.connect(self.ccheckbox_clicked) - + #add a label to the UI to display the last command self.label = QLabel() layout.addWidget(self.label) - + + #add the actions for quiting + quit_action = QAction(self.window) + quit_action.setShortcut('Ctrl+Q') + quit_action.triggered.connect(self.accel_quit) + self.window.addAction(quit_action) + + def accel_quit(self): + #emit the quit + self.emit("command", "quit") + def ccheckbox_clicked(self): checked = self.ccheckbox.isChecked() if checked: @@ -53,10 +63,10 @@ class UI(gobject.GObject): else: self.lsbutton.setEnabled(True) self.emit('command', "continuous_stop") - + def lsbutton_stopped(self): self.lsbutton.setText("Listen") - + def lsbutton_clicked(self): val = self.lsbutton.text() if val == "Listen": @@ -67,7 +77,7 @@ class UI(gobject.GObject): else: self.lsbutton_stopped() self.emit("command", "stop") - + def run(self): self.window.show() if self.continuous: @@ -75,17 +85,13 @@ class UI(gobject.GObject): self.ccheckbox_clicked() self.app.exec_() self.emit("command", "quit") - + def finished(self, text): print text #if the continuous isn't pressed if not self.ccheckbox.isChecked(): self.lsbutton_stopped() self.label.setText(text) - - def quit(self): - #sys.exit() - pass def set_icon(self, icon): - self.window.setWindowIcon(QIcon(icon)) + self.window.setWindowIcon(QIcon(icon)) -- cgit 1.4.1 From add80c4852edd641dbee89b3ada1c6f832471a31 Mon Sep 17 00:00:00 2001 From: Jezra Date: Fri, 23 Aug 2013 11:12:19 -0700 Subject: Updated the README to correct instructions for copying commands file --- README | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README b/README index d5414c1..a4a47d7 100644 --- a/README +++ b/README @@ -8,7 +8,7 @@ Blather is a speech recognizer that will run commands when a user speaks preset 4. pygtk (only required for the Gtk based UI) ##Usage -0. move commands.tmp to ~/.config/blather/commands and fill the file with sentences and command to run +0. move commands.tmp to ~/.config/blather/commands.conf and fill the file with sentences and command to run 1. Run Blather.py, this will generate ~/.config/blather/sentences.corpus based on sentences in the 'commands' file 2. quit blather (there is a good chance it will just segfault) 3. go to and upload the sentences.corpus file -- cgit 1.4.1 From 00b0cdb291d2e025366f153fd504bc7f9218e6bb Mon Sep 17 00:00:00 2001 From: Jezra Date: Tue, 14 Jan 2014 20:53:24 -0800 Subject: Added '-m --microphone' flag to let the user pick a mic other than the system default --- Blather.py | 5 ++++- README | 25 +++++++++++++++++-------- Recognizer.py | 15 ++++++++++----- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/Blather.py b/Blather.py index f5d8a67..6ec6b02 100755 --- a/Blather.py +++ b/Blather.py @@ -34,7 +34,7 @@ class Blather: self.continuous_listen = False self.commands = {} self.read_commands() - self.recognizer = Recognizer(lang_file, dic_file) + self.recognizer = Recognizer(lang_file, dic_file, opts.microphone ) self.recognizer.connect('finished',self.recognizer_finished) if opts.interface != None: @@ -155,6 +155,9 @@ if __name__ == "__main__": parser.add_option("-H", "--history", type="int", action="store", dest="history", help="number of commands to store in history file") + parser.add_option("-m", "--microphone", type="int", + action="store", dest="microphone", default=None, + help="Audio input card to use (if other than system default)") (options, args) = parser.parse_args() #make our blather object diff --git a/README b/README index a4a47d7..b88048d 100644 --- a/README +++ b/README @@ -1,9 +1,9 @@ #Blather -Blather is a speech recognizer that will run commands when a user speaks preset sentences. +Blather is a speech recognizer that will run commands when a user speaks preset sentences. ##Requirements -1. pocketsphinx -2. gstreamer (and what ever plugin has pocket sphinx support) +1. pocketsphinx +2. gstreamer (and what ever plugin has pocket sphinx support) 3. pyside (only required for the Qt based UI) 4. pygtk (only required for the Gtk based UI) @@ -17,13 +17,22 @@ Blather is a speech recognizer that will run commands when a user speaks preset 6. run Blather.py * for Qt GUI, run Blather.py -i q * for Gtk GUI, run Blather.py -i g - * to start a UI in 'continuous' listen mode, use the -c flag - + * to start a UI in 'continuous' listen mode, use the -c flag + * to use a microphone other than the system default, use the -d flag 7. start talking ####Bonus once the sentences.corpus file has been created, run the language_updater.sh script to automate the process of creating and downloading language files. -**Example** -To run blather with the GTK UI and start in continuous listen mode: -./Blather.py -i g -c \ No newline at end of file +####Examples +To run blather with the GTK UI and start in continuous listen mode: +./Blather.py -i g -c + +To run blather with no UI and using a USB microphone recognized and device 2: +./Blather.py -d 2 + +####Finding the Device Number of a USB microphone +There are a few ways to find the device number of a USB microphone. + +* `cat /proc/asound/cards` +* `arecord -l` \ No newline at end of file diff --git a/Recognizer.py b/Recognizer.py index 8497839..26ccd80 100755 --- a/Recognizer.py +++ b/Recognizer.py @@ -16,11 +16,16 @@ class Recognizer(gobject.GObject): __gsignals__ = { 'finished' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)) } - def __init__(self, language_file, dictionary_file): + def __init__(self, language_file, dictionary_file, src = None): gobject.GObject.__init__(self) self.commands = {} + if src: + audio_src = 'alsasrc device="hw:%d,0"' % (src) + else: + audio_src = 'autoaudiosrc' + #build the pipeline - cmd = 'autoaudiosrc ! audioconvert ! audioresample ! vader name=vad ! pocketsphinx name=asr ! appsink sync=false' + cmd = audio_src+' ! audioconvert ! audioresample ! vader name=vad ! pocketsphinx name=asr ! appsink sync=false' self.pipeline=gst.parse_launch( cmd ) #get the Auto Speech Recognition piece asr=self.pipeline.get_by_name('asr') @@ -31,10 +36,10 @@ class Recognizer(gobject.GObject): #get the Voice Activity DEtectoR self.vad = self.pipeline.get_by_name('vad') self.vad.set_property('auto-threshold',True) - + def listen(self): self.pipeline.set_state(gst.STATE_PLAYING) - + def pause(self): self.vad.set_property('silent', True) self.pipeline.set_state(gst.STATE_PAUSED) @@ -42,4 +47,4 @@ class Recognizer(gobject.GObject): def result(self, asr, text, uttid): #emit finished self.emit("finished", text) - + -- cgit 1.4.1 From 87da5d0aaa5eeed007f04b868bb693e0be074608 Mon Sep 17 00:00:00 2001 From: Jezra Date: Fri, 17 Jan 2014 09:49:32 -0800 Subject: Changed README to reference a working corpus processing web page --- README | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README b/README index b88048d..fe41ef9 100644 --- a/README +++ b/README @@ -11,7 +11,7 @@ Blather is a speech recognizer that will run commands when a user speaks preset 0. move commands.tmp to ~/.config/blather/commands.conf and fill the file with sentences and command to run 1. Run Blather.py, this will generate ~/.config/blather/sentences.corpus based on sentences in the 'commands' file 2. quit blather (there is a good chance it will just segfault) -3. go to and upload the sentences.corpus file +3. go to and upload the sentences.corpus file 4. download the resulting XXXX.lm file to the ~/.config/blather/language directory and rename to file to 'lm' 5. download the resulting XXXX.dic file to the ~/.config/blather/language directory and rename to file to 'dic' 6. run Blather.py @@ -35,4 +35,4 @@ To run blather with no UI and using a USB microphone recognized and device 2: There are a few ways to find the device number of a USB microphone. * `cat /proc/asound/cards` -* `arecord -l` \ No newline at end of file +* `arecord -l` -- cgit 1.4.1 From df39e3e2c63a643906268e484cde1e89a23f2329 Mon Sep 17 00:00:00 2001 From: Jezra Date: Tue, 21 Jan 2014 12:29:22 -0800 Subject: added gstreamer0.10-base-plugins to the requirements in the README commands don't need to be in all caps --- README | 7 ++++--- commands.tmp | 3 +-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README b/README index fe41ef9..1ab0da0 100644 --- a/README +++ b/README @@ -3,9 +3,10 @@ Blather is a speech recognizer that will run commands when a user speaks preset ##Requirements 1. pocketsphinx -2. gstreamer (and what ever plugin has pocket sphinx support) -3. pyside (only required for the Qt based UI) -4. pygtk (only required for the Gtk based UI) +2. gstreamer-0.10 (and what ever plugin has pocket sphinx support) +3. gstreamer-0.10 base plugins (required for alsa) +4. pyside (only required for the Qt based UI) +5. pygtk (only required for the Gtk based UI) ##Usage 0. move commands.tmp to ~/.config/blather/commands.conf and fill the file with sentences and command to run diff --git a/commands.tmp b/commands.tmp index 3835915..9e41147 100644 --- a/commands.tmp +++ b/commands.tmp @@ -1,6 +1,5 @@ # commands are key:value pairs # key is the sentence to listen for -# key must be in ALL CAPS # value is the command to run when the key is spoken -HELLO WORLD:echo "hello world" +hello world:echo "hello world" -- cgit 1.4.1 From 5575072b9b66d2b4aa63fa6cba0530a7d8bc5e76 Mon Sep 17 00:00:00 2001 From: Jezra Date: Sat, 26 Apr 2014 13:10:52 -0700 Subject: Added Gtk Tray UI (thanks padfoot) Added active/inactive icons --- GtkTrayUI.py | 100 ++++++++++++++++++++++++++++++++++++++++++++ assets/blather.svg | 91 +++++++++++++++++++++++++++------------- assets/blather_inactive.png | Bin 0 -> 13379 bytes assets/blathersrc.png | Bin 0 -> 27324 bytes data/icon_inactive.png | Bin 0 -> 13379 bytes 5 files changed, 161 insertions(+), 30 deletions(-) create mode 100644 GtkTrayUI.py create mode 100644 assets/blather_inactive.png create mode 100644 assets/blathersrc.png create mode 100644 data/icon_inactive.png diff --git a/GtkTrayUI.py b/GtkTrayUI.py new file mode 100644 index 0000000..24e2b57 --- /dev/null +++ b/GtkTrayUI.py @@ -0,0 +1,100 @@ +import sys +import gobject + +import pygtk +import gtk + +class UI(gobject.GObject): + __gsignals__ = { + 'command' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)) + } + + def __init__(self,args, continuous): + gobject.GObject.__init__(self) + self.continuous = continuous + + self.statusicon = gtk.StatusIcon() + self.statusicon.set_title("Blather") + self.statusicon.set_name("Blather") + self.statusicon.set_tooltip_text("Blather - Idle") + self.statusicon.set_has_tooltip(True) + self.statusicon.connect("activate", self.continuous_toggle) + self.statusicon.connect("popup-menu", self.popup_menu) + + self.menu = gtk.Menu() + self.menu_listen = gtk.MenuItem('Listen') + self.menu_continuous = gtk.CheckMenuItem('Continuous') + self.menu_quit = gtk.MenuItem('Quit') + self.menu.append(self.menu_listen) + self.menu.append(self.menu_continuous) + self.menu.append(self.menu_quit) + self.menu_listen.connect("activate", self.toggle_listen) + self.menu_continuous.connect("toggled", self.toggle_continuous) + self.menu_quit.connect("activate", self.quit) + self.menu.show_all() + + def continuous_toggle(self, item): + checked = self.menu_continuous.get_active() + self.menu_continuous.set_active(not checked) + + def toggle_continuous(self, item): + checked = self.menu_continuous.get_active() + self.menu_listen.set_sensitive(not checked) + if checked: + self.menu_listen.set_label("Listen") + self.emit('command', "continuous_listen") + self.statusicon.set_tooltip_text("Blather - Listening") + self.set_icon_active() + else: + self.set_icon_inactive() + self.statusicon.set_tooltip_text("Blather - Idle") + self.emit('command', "continuous_stop") + + def toggle_listen(self, item): + val = self.menu_listen.get_label() + if val == "Listen": + self.emit("command", "listen") + self.menu_listen.set_label("Stop") + self.statusicon.set_tooltip_text("Blather - Listening") + else: + self.icon_inactive() + self.menu_listen.set_label("Listen") + self.emit("command", "stop") + self.statusicon.set_tooltip_text("Blather - Idle") + + def popup_menu(self, item, button, time): + self.menu.popup(None, None, gtk.status_icon_position_menu, button, time, item) + + def run(self): + #set the icon + self.set_icon_inactive() + if self.continuous: + self.menu_continuous.set_active(True) + self.set_icon_active() + else: + self.menu_continuous.set_active(False) + self.statusicon.set_visible(True) + + def quit(self, item): + self.statusicon.set_visible(False) + self.emit("command", "quit") + + def finished(self, text): + print text + if not self.menu_continuous.get_active(): + self.menu_listen.set_label("Listen") + self.statusicon.set_from_icon_name("blather_stopped") + self.statusicon.set_tooltip_text("Blather - Idle") + + def set_icon_active_asset(self, i): + self.icon_active = i + + def set_icon_inactive_asset(self, i): + self.icon_inactive = i + + def set_icon_active(self): + self.statusicon.set_from_file( self.icon_active ) + + def set_icon_inactive(self): + self.statusicon.set_from_file( self.icon_inactive ) + diff --git a/assets/blather.svg b/assets/blather.svg index 1e10ee7..e7e3446 100644 --- a/assets/blather.svg +++ b/assets/blather.svg @@ -14,7 +14,10 @@ id="svg2" version="1.1" inkscape:version="0.48.4 r9939" - sodipodi:docname="blather.svg"> + sodipodi:docname="blather.svg" + inkscape:export-filename="/storage/projects/blather/assets/blathersrc.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90"> @@ -43,7 +46,7 @@ image/svg+xml - + @@ -51,35 +54,63 @@ inkscape:groupmode="layer" id="layer2" inkscape:label="Layer" - style="display:inline"> - - + style="display:inline" /> - - + + + B + x="185.41168" + style="font-size:130.46780396px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:#008900;stroke-width:5.77600002;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;font-family:Sans" + xml:space="preserve">B + + + + + B + diff --git a/assets/blather_inactive.png b/assets/blather_inactive.png new file mode 100644 index 0000000..63d1572 Binary files /dev/null and b/assets/blather_inactive.png differ diff --git a/assets/blathersrc.png b/assets/blathersrc.png new file mode 100644 index 0000000..fb187e4 Binary files /dev/null and b/assets/blathersrc.png differ diff --git a/data/icon_inactive.png b/data/icon_inactive.png new file mode 100644 index 0000000..63d1572 Binary files /dev/null and b/data/icon_inactive.png differ -- cgit 1.4.1 From 810d17c7a66c240c84e7818ece0c3e63caea4bc6 Mon Sep 17 00:00:00 2001 From: Jezra Date: Sun, 4 May 2014 10:40:06 -0700 Subject: Reads default startup options from an options.yaml file Added some missing files because I'm an idiot --- Blather.py | 54 ++++++++++++++++++++++++++++++++++++++++-------------- GtkUI.py | 22 ++++++++++++++++++++-- QtUI.py | 19 +++++++++++++++++++ README | 39 --------------------------------------- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ options.yaml.tmp | 6 ++++++ 6 files changed, 127 insertions(+), 55 deletions(-) delete mode 100644 README create mode 100644 README.md create mode 100644 options.yaml.tmp diff --git a/Blather.py b/Blather.py index 6ec6b02..60ca6bb 100755 --- a/Blather.py +++ b/Blather.py @@ -9,7 +9,10 @@ import gobject import os.path import subprocess from optparse import OptionParser - +try: + import yaml +except: + print "YAML is not supported. ~/.config/blather/options.yaml will not function" #where are the files? conf_dir = os.path.expanduser("~/.config/blather") @@ -17,6 +20,7 @@ lang_dir = os.path.join(conf_dir, "language") command_file = os.path.join(conf_dir, "commands.conf") strings_file = os.path.join(conf_dir, "sentences.corpus") history_file = os.path.join(conf_dir, "blather.history") +opt_file = os.path.join(conf_dir, "options.yaml") lang_file = os.path.join(lang_dir,'lm') dic_file = os.path.join(lang_dir,'dic') #make the lang_dir if it doesn't exist @@ -28,8 +32,7 @@ class Blather: #import the recognizer so Gst doesn't clobber our -h from Recognizer import Recognizer self.ui = None - #keep track of the opts - self.opts = opts + self.options = {} ui_continuous_listen = False self.continuous_listen = False self.commands = {} @@ -37,27 +40,40 @@ class Blather: self.recognizer = Recognizer(lang_file, dic_file, opts.microphone ) self.recognizer.connect('finished',self.recognizer_finished) - if opts.interface != None: - if opts.interface == "q": - #import the ui from qt + #load the options file + self.load_options() + #merge the opts + for k,v in opts.__dict__.items(): + if not k in self.options: + self.options[k] = v + + print "Using Options: ", self.options + + if self.options['interface'] != None: + if self.options['interface'] == "q": from QtUI import UI - elif opts.interface == "g": + elif self.options['interface'] == "g": from GtkUI import UI + elif self.options['interface'] == "gt": + from GtkTrayUI import UI else: print "no GUI defined" sys.exit() - self.ui = UI(args,opts.continuous) + self.ui = UI(args, self.options['continuous']) self.ui.connect("command", self.process_command) #can we load the icon resource? icon = self.load_resource("icon.png") if icon: - self.ui.set_icon(icon) + self.ui.set_icon_active_asset(icon) + #can we load the icon_inactive resource? + icon_inactive = self.load_resource("icon_inactive.png") + if icon_inactive: + self.ui.set_icon_inactive_asset(icon_inactive) - if self.opts.history: + if self.options['history']: self.history = [] - def read_commands(self): #read the.commands file file_lines = open(command_file) @@ -76,10 +92,20 @@ class Blather: #close the strings file strings.close() + def load_options(self): + #is there an opt file? + try: + opt_fh = open(opt_file) + text = opt_fh.read() + self.options = yaml.load(text) + except: + pass + + def log_history(self,text): - if self.opts.history: + if self.options['history']: self.history.append(text) - if len(self.history) > self.opts.history: + if len(self.history) > self.options['history']: #pop off the first item self.history.pop(0) @@ -148,7 +174,7 @@ if __name__ == "__main__": parser = OptionParser() parser.add_option("-i", "--interface", type="string", dest="interface", action='store', - help="Interface to use (if any). 'q' for Qt, 'g' for GTK") + help="Interface to use (if any). 'q' for Qt, 'g' for GTK, 'gt' for GTK system tray icon") parser.add_option("-c", "--continuous", action="store_true", dest="continuous", default=False, help="starts interface with 'continuous' listen enabled") diff --git a/GtkUI.py b/GtkUI.py index 56a6252..71255fc 100644 --- a/GtkUI.py +++ b/GtkUI.py @@ -54,8 +54,10 @@ class UI(gobject.GObject): if checked: self.lsbutton_stopped() self.emit('command', "continuous_listen") + self.set_icon_active() else: self.emit('command', "continuous_stop") + self.set_icon_inactive() def lsbutton_stopped(self): self.lsbutton.set_label("Listen") @@ -67,13 +69,18 @@ class UI(gobject.GObject): self.lsbutton.set_label("Stop") #clear the label self.label.set_text("") + self.set_icon_active() else: self.lsbutton_stopped() self.emit("command", "stop") + self.set_icon_inactive() def run(self): + #set the default icon + self.set_icon_inactive() self.window.show_all() if self.continuous: + self.set_icon_active() self.ccheckbox.set_active(True) def accel_quit(self, accel_group, acceleratable, keyval, modifier): @@ -87,8 +94,19 @@ class UI(gobject.GObject): #if the continuous isn't pressed if not self.ccheckbox.get_active(): self.lsbutton_stopped() + self.set_icon_inactive() self.label.set_text(text) - def set_icon(self, icon): - gtk.window_set_default_icon_from_file(icon) + def set_icon_active_asset(self, i): + self.icon_active = i + + def set_icon_inactive_asset(self, i): + self.icon_inactive = i + + def set_icon_active(self): + gtk.window_set_default_icon_from_file(self.icon_active) + + def set_icon_inactive(self): + gtk.window_set_default_icon_from_file(self.icon_inactive) + diff --git a/QtUI.py b/QtUI.py index c772abf..4b17d9f 100644 --- a/QtUI.py +++ b/QtUI.py @@ -60,9 +60,11 @@ class UI(gobject.GObject): self.lsbutton.setEnabled(False) self.lsbutton_stopped() self.emit('command', "continuous_listen") + self.set_icon_active() else: self.lsbutton.setEnabled(True) self.emit('command', "continuous_stop") + self.set_icon_inactive() def lsbutton_stopped(self): self.lsbutton.setText("Listen") @@ -74,13 +76,17 @@ class UI(gobject.GObject): self.lsbutton.setText("Stop") #clear the label self.label.setText("") + self.set_icon_active() else: self.lsbutton_stopped() self.emit("command", "stop") + self.set_icon_inactive() def run(self): + self.set_icon_inactive() self.window.show() if self.continuous: + self.set_icon_active() self.ccheckbox.setCheckState(Qt.Checked) self.ccheckbox_clicked() self.app.exec_() @@ -95,3 +101,16 @@ class UI(gobject.GObject): def set_icon(self, icon): self.window.setWindowIcon(QIcon(icon)) + + def set_icon_active_asset(self, i): + self.icon_active = i + + def set_icon_inactive_asset(self, i): + self.icon_inactive = i + + def set_icon_active(self): + self.window.setWindowIcon(QIcon(self.icon_active)) + + def set_icon_inactive(self): + self.window.setWindowIcon(QIcon(self.icon_inactive)) + diff --git a/README b/README deleted file mode 100644 index 1ab0da0..0000000 --- a/README +++ /dev/null @@ -1,39 +0,0 @@ -#Blather -Blather is a speech recognizer that will run commands when a user speaks preset sentences. - -##Requirements -1. pocketsphinx -2. gstreamer-0.10 (and what ever plugin has pocket sphinx support) -3. gstreamer-0.10 base plugins (required for alsa) -4. pyside (only required for the Qt based UI) -5. pygtk (only required for the Gtk based UI) - -##Usage -0. move commands.tmp to ~/.config/blather/commands.conf and fill the file with sentences and command to run -1. Run Blather.py, this will generate ~/.config/blather/sentences.corpus based on sentences in the 'commands' file -2. quit blather (there is a good chance it will just segfault) -3. go to and upload the sentences.corpus file -4. download the resulting XXXX.lm file to the ~/.config/blather/language directory and rename to file to 'lm' -5. download the resulting XXXX.dic file to the ~/.config/blather/language directory and rename to file to 'dic' -6. run Blather.py - * for Qt GUI, run Blather.py -i q - * for Gtk GUI, run Blather.py -i g - * to start a UI in 'continuous' listen mode, use the -c flag - * to use a microphone other than the system default, use the -d flag -7. start talking - -####Bonus -once the sentences.corpus file has been created, run the language_updater.sh script to automate the process of creating and downloading language files. - -####Examples -To run blather with the GTK UI and start in continuous listen mode: -./Blather.py -i g -c - -To run blather with no UI and using a USB microphone recognized and device 2: -./Blather.py -d 2 - -####Finding the Device Number of a USB microphone -There are a few ways to find the device number of a USB microphone. - -* `cat /proc/asound/cards` -* `arecord -l` diff --git a/README.md b/README.md new file mode 100644 index 0000000..6b3e365 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +#Blather +Blather is a speech recognizer that will run commands when a user speaks preset sentences. + +##Requirements +1. pocketsphinx +2. gstreamer-0.10 (and what ever plugin has pocket sphinx support) +3. gstreamer-0.10 base plugins (required for alsa) +4. pyside (only required for the Qt based UI) +5. pygtk (only required for the Gtk based UI) +6. pyyaml (only required for reading the options file) + +##Usage +0. move commands.tmp to ~/.config/blather/commands.conf and fill the file with sentences and command to run +1. Run Blather.py, this will generate ~/.config/blather/sentences.corpus based on sentences in the 'commands' file +2. quit blather (there is a good chance it will just segfault) +3. go to and upload the sentences.corpus file +4. download the resulting XXXX.lm file to the ~/.config/blather/language directory and rename to file to 'lm' +5. download the resulting XXXX.dic file to the ~/.config/blather/language directory and rename to file to 'dic' +6. run Blather.py + * for Qt GUI, run Blather.py -i q + * for Gtk GUI, run Blather.py -i g + * to start a UI in 'continuous' listen mode, use the -c flag + * to use a microphone other than the system default, use the -m flag +7. start talking + +**Note:** to start Blather without needing to enter command line options all the time, copy options.yaml.tmp to ~/.config/blather/options.yaml and edit accordingly. + +###Bonus +once the sentences.corpus file has been created, run the language_updater.sh script to automate the process of creating and downloading language files. + +###Examples +To run blather with the GTK UI and start in continuous listen mode: +`./Blather.py -i g -c` + +To run blather with no UI and using a USB microphone recognized and device 2: +`./Blather.py -m 2` + +###Finding the Device Number of a USB microphone +There are a few ways to find the device number of a USB microphone. + +* `cat /proc/asound/cards` +* `arecord -l` diff --git a/options.yaml.tmp b/options.yaml.tmp new file mode 100644 index 0000000..e399b56 --- /dev/null +++ b/options.yaml.tmp @@ -0,0 +1,6 @@ +#This is a YAML file +#these options can be over-ridden by commandline arguments +continuous: false +history: null +microphone: null +interface: null -- cgit 1.4.1 From 126f14d6fb57d8cf601c5e816b27fa63582860e5 Mon Sep 17 00:00:00 2001 From: Jezra Date: Fri, 4 Jul 2014 12:24:21 -0700 Subject: Added a bit of error checking for the most common problem: missing gstreamer pocketsphinx plugin --- Recognizer.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Recognizer.py b/Recognizer.py index 26ccd80..7bc2023 100755 --- a/Recognizer.py +++ b/Recognizer.py @@ -26,7 +26,12 @@ class Recognizer(gobject.GObject): #build the pipeline cmd = audio_src+' ! audioconvert ! audioresample ! vader name=vad ! pocketsphinx name=asr ! appsink sync=false' - self.pipeline=gst.parse_launch( cmd ) + try: + self.pipeline=gst.parse_launch( cmd ) + except Exception, e: + print e.message + print "You may need to install gstreamer0.10-pocketsphinx" + #get the Auto Speech Recognition piece asr=self.pipeline.get_by_name('asr') asr.connect('result', self.result) -- cgit 1.4.1 From a13c16cf21c92e4b3a36744acd910bf3b04ab55a Mon Sep 17 00:00:00 2001 From: Jezra Date: Sun, 24 Aug 2014 15:54:08 -0700 Subject: Added the ability to define a command to run whenever a valid sentence is detected, or when an invalid sentence is detected --- Blather.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/Blather.py b/Blather.py index 60ca6bb..a67feab 100755 --- a/Blather.py +++ b/Blather.py @@ -29,12 +29,14 @@ if not os.path.exists(lang_dir): class Blather: def __init__(self, opts): + #import the recognizer so Gst doesn't clobber our -h from Recognizer import Recognizer self.ui = None self.options = {} ui_continuous_listen = False self.continuous_listen = False + self.commands = {} self.read_commands() self.recognizer = Recognizer(lang_file, dic_file, opts.microphone ) @@ -44,7 +46,7 @@ class Blather: self.load_options() #merge the opts for k,v in opts.__dict__.items(): - if not k in self.options: + if (not k in self.options) or opts.override: self.options[k] = v print "Using Options: ", self.options @@ -120,12 +122,18 @@ class Blather: t = text.lower() #is there a matching command? if self.commands.has_key( t ): + #run the valid_sentence_command if there is a valid sentence command + if self.options['valid_sentence_command']: + subprocess.call(self.options['valid_sentence_command'], shell=True) cmd = self.commands[t] print cmd subprocess.call(cmd, shell=True) self.log_history(text) else: - print "no matching command" + #run the invalid_sentence_command if there is a valid sentence command + if self.options['invalid_sentence_command']: + subprocess.call(self.options['invalid_sentence_command'], shell=True) + print "no matching command %s" %(t) #if there is a UI and we are not continuous listen if self.ui: if not self.continuous_listen: @@ -169,22 +177,36 @@ class Blather: return False - if __name__ == "__main__": parser = OptionParser() parser.add_option("-i", "--interface", type="string", dest="interface", action='store', help="Interface to use (if any). 'q' for Qt, 'g' for GTK, 'gt' for GTK system tray icon") + parser.add_option("-c", "--continuous", action="store_true", dest="continuous", default=False, help="starts interface with 'continuous' listen enabled") + + parser.add_option("-o", "--override", + action="store_true", dest="override", default=False, + help="override config file with command line options") + parser.add_option("-H", "--history", type="int", action="store", dest="history", help="number of commands to store in history file") + parser.add_option("-m", "--microphone", type="int", action="store", dest="microphone", default=None, help="Audio input card to use (if other than system default)") + parser.add_option("--valid-sentence-command", type="string", dest="valid_sentence_command", + action='store', + help="command to run when a valid sentence is detected") + + parser.add_option( "--invalid-sentence-command", type="string", dest="invalid_sentence_command", + action='store', + help="command to run when an invalid sentence is detected") + (options, args) = parser.parse_args() #make our blather object blather = Blather(options) -- cgit 1.4.1 From e50facf6742d283731ee9cdae1d38f24cde6b6da Mon Sep 17 00:00:00 2001 From: Jezra Date: Sun, 24 Aug 2014 16:16:21 -0700 Subject: modified recognizer instance creation to use self.options['microphone'] --- Blather.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Blather.py b/Blather.py index a67feab..b0487c6 100755 --- a/Blather.py +++ b/Blather.py @@ -38,12 +38,13 @@ class Blather: self.continuous_listen = False self.commands = {} + + #read the commands self.read_commands() - self.recognizer = Recognizer(lang_file, dic_file, opts.microphone ) - self.recognizer.connect('finished',self.recognizer_finished) #load the options file self.load_options() + #merge the opts for k,v in opts.__dict__.items(): if (not k in self.options) or opts.override: @@ -76,6 +77,10 @@ class Blather: if self.options['history']: self.history = [] + #create the recognizer + self.recognizer = Recognizer(lang_file, dic_file, self.options['microphone'] ) + self.recognizer.connect('finished',self.recognizer_finished) + def read_commands(self): #read the.commands file file_lines = open(command_file) -- cgit 1.4.1 From 7f6b9270e56433072afaa1c56810847362920d8c Mon Sep 17 00:00:00 2001 From: Jezra Date: Sun, 24 Aug 2014 16:21:05 -0700 Subject: pull your shit together jez. moved the options print out --- Blather.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Blather.py b/Blather.py index b0487c6..f2a932a 100755 --- a/Blather.py +++ b/Blather.py @@ -50,8 +50,6 @@ class Blather: if (not k in self.options) or opts.override: self.options[k] = v - print "Using Options: ", self.options - if self.options['interface'] != None: if self.options['interface'] == "q": from QtUI import UI @@ -81,6 +79,8 @@ class Blather: self.recognizer = Recognizer(lang_file, dic_file, self.options['microphone'] ) self.recognizer.connect('finished',self.recognizer_finished) + print "Using Options: ", self.options + def read_commands(self): #read the.commands file file_lines = open(command_file) -- cgit 1.4.1 From 87585c7b20ace497d2497ee916f84d7e8e5e9a8d Mon Sep 17 00:00:00 2001 From: Jezra Date: Fri, 12 Sep 2014 09:24:14 -0700 Subject: Added -p --pass-words flag that will pass the recognized words to the shell command a few print commands were remove too. --- Blather.py | 17 +++++++++++++++-- GtkTrayUI.py | 1 - GtkUI.py | 1 - QtUI.py | 1 - 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Blather.py b/Blather.py index f2a932a..9ad5e14 100755 --- a/Blather.py +++ b/Blather.py @@ -123,6 +123,11 @@ class Blather: #close the file hfile.close() + # Print the cmd and then run the command + def run_command(self, cmd): + print cmd + subprocess.call(cmd, shell=True) + def recognizer_finished(self, recognizer, text): t = text.lower() #is there a matching command? @@ -131,8 +136,12 @@ class Blather: if self.options['valid_sentence_command']: subprocess.call(self.options['valid_sentence_command'], shell=True) cmd = self.commands[t] - print cmd - subprocess.call(cmd, shell=True) + #should we be passing words? + if self.options['pass_words']: + cmd+=" "+t + self.run_command(cmd) + else: + self.run_command(cmd) self.log_history(text) else: #run the invalid_sentence_command if there is a valid sentence command @@ -192,6 +201,10 @@ if __name__ == "__main__": action="store_true", dest="continuous", default=False, help="starts interface with 'continuous' listen enabled") + parser.add_option("-p", "--pass-words", + action="store_true", dest="pass_words", default=False, + help="passes the recognized words as arguments to the shell command") + parser.add_option("-o", "--override", action="store_true", dest="override", default=False, help="override config file with command line options") diff --git a/GtkTrayUI.py b/GtkTrayUI.py index 24e2b57..f0d1c2e 100644 --- a/GtkTrayUI.py +++ b/GtkTrayUI.py @@ -80,7 +80,6 @@ class UI(gobject.GObject): self.emit("command", "quit") def finished(self, text): - print text if not self.menu_continuous.get_active(): self.menu_listen.set_label("Listen") self.statusicon.set_from_icon_name("blather_stopped") diff --git a/GtkUI.py b/GtkUI.py index 71255fc..17c7b50 100644 --- a/GtkUI.py +++ b/GtkUI.py @@ -90,7 +90,6 @@ class UI(gobject.GObject): self.emit("command", "quit") def finished(self, text): - print text #if the continuous isn't pressed if not self.ccheckbox.get_active(): self.lsbutton_stopped() diff --git a/QtUI.py b/QtUI.py index 4b17d9f..728ff80 100644 --- a/QtUI.py +++ b/QtUI.py @@ -93,7 +93,6 @@ class UI(gobject.GObject): self.emit("command", "quit") def finished(self, text): - print text #if the continuous isn't pressed if not self.ccheckbox.isChecked(): self.lsbutton_stopped() -- cgit 1.4.1 From 8433aac1ed4c6084beb3fccf964af7bed59caa45 Mon Sep 17 00:00:00 2001 From: Jezra Date: Sun, 9 Nov 2014 18:57:59 -0800 Subject: *slightly* improved the error handling.... slightly --- Blather.py | 7 ++++++- Recognizer.py | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Blather.py b/Blather.py index 9ad5e14..ed5e6ed 100755 --- a/Blather.py +++ b/Blather.py @@ -76,7 +76,12 @@ class Blather: self.history = [] #create the recognizer - self.recognizer = Recognizer(lang_file, dic_file, self.options['microphone'] ) + try: + self.recognizer = Recognizer(lang_file, dic_file, self.options['microphone'] ) + except Exception, e: + #no recognizer? bummer + sys.exit() + self.recognizer.connect('finished',self.recognizer_finished) print "Using Options: ", self.options diff --git a/Recognizer.py b/Recognizer.py index 7bc2023..e9cb648 100755 --- a/Recognizer.py +++ b/Recognizer.py @@ -7,6 +7,7 @@ pygst.require('0.10') import gst import os.path import gobject +import sys #define some global variables this_dir = os.path.dirname( os.path.abspath(__file__) ) @@ -31,6 +32,7 @@ class Recognizer(gobject.GObject): except Exception, e: print e.message print "You may need to install gstreamer0.10-pocketsphinx" + raise e #get the Auto Speech Recognition piece asr=self.pipeline.get_by_name('asr') -- cgit 1.4.1 From e1294938233b49d9be8f08d7e4ec7868c538e728 Mon Sep 17 00:00:00 2001 From: Jezra Date: Sun, 15 Mar 2015 10:17:07 -0700 Subject: Added example for -p flag to README --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 6b3e365..0367252 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,17 @@ To run blather with the GTK UI and start in continuous listen mode: To run blather with no UI and using a USB microphone recognized and device 2: `./Blather.py -m 2` + +To have blather pass the matched sentence to the executing command: + `./Blather -p` + + **explanation:** if the commands.conf contains: + **good morning world : example_command.sh** + then 3 arguments, 'good', 'morning', and 'world' would get passed to example_command.sh as + `example_command.sh good morning world` + + + ###Finding the Device Number of a USB microphone There are a few ways to find the device number of a USB microphone. -- cgit 1.4.1 From 5fc770044c75a797ddfc4184be33f7d77dc32be5 Mon Sep 17 00:00:00 2001 From: Jezra Date: Sat, 1 Aug 2015 08:17:40 -0700 Subject: Added invalid sentence command and valid sentence command to the documentation --- README.md | 25 ++++++++++++++++--------- options.yaml.tmp | 2 ++ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0367252..93d030b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,11 @@ Blather is a speech recognizer that will run commands when a user speaks preset 3. gstreamer-0.10 base plugins (required for alsa) 4. pyside (only required for the Qt based UI) 5. pygtk (only required for the Gtk based UI) -6. pyyaml (only required for reading the options file) +6. pyyaml (only required for reading the options file) + +**Note:** it may also be required to install `pocketsphinx-hmm-en-hub4wsj` + + ##Usage 0. move commands.tmp to ~/.config/blather/commands.conf and fill the file with sentences and command to run @@ -29,23 +33,26 @@ Blather is a speech recognizer that will run commands when a user speaks preset once the sentences.corpus file has been created, run the language_updater.sh script to automate the process of creating and downloading language files. ###Examples -To run blather with the GTK UI and start in continuous listen mode: +* To run blather with the GTK UI and start in continuous listen mode: `./Blather.py -i g -c` -To run blather with no UI and using a USB microphone recognized and device 2: +* To run blather with no UI and using a USB microphone recognized and device 2: `./Blather.py -m 2` +* To have blather pass the matched sentence to the executing command: + `./Blather.py -p` -To have blather pass the matched sentence to the executing command: - `./Blather -p` - - **explanation:** if the commands.conf contains: + **explanation:** if the commands.conf contains: **good morning world : example_command.sh** then 3 arguments, 'good', 'morning', and 'world' would get passed to example_command.sh as `example_command.sh good morning world` - - +* To run a command when a valid sentence has been detected: + `./Blather.py --valid-sentence-command=/path/to/command` + **note:** this can be set in the options.yml file +* To run a command when a invalid sentence has been detected: + `./Blather.py --invalid-sentence-command=/path/to/command` + **note:** this can be set in the options.yml file ###Finding the Device Number of a USB microphone There are a few ways to find the device number of a USB microphone. diff --git a/options.yaml.tmp b/options.yaml.tmp index e399b56..fd53a97 100644 --- a/options.yaml.tmp +++ b/options.yaml.tmp @@ -4,3 +4,5 @@ continuous: false history: null microphone: null interface: null +valid_sentence_command: null +invalid_sentence_command: null -- cgit 1.4.1 From 67f8e0f5cd4db13cb448316d6eaba69d7cde9fd2 Mon Sep 17 00:00:00 2001 From: Jezra Date: Sat, 1 Aug 2015 08:21:24 -0700 Subject: Added missing return --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 93d030b..82e559d 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ once the sentences.corpus file has been created, run the language_updater.sh scr * To run a command when a invalid sentence has been detected: `./Blather.py --invalid-sentence-command=/path/to/command` **note:** this can be set in the options.yml file + ###Finding the Device Number of a USB microphone There are a few ways to find the device number of a USB microphone. -- cgit 1.4.1 From 2d374bf13d3a1bfd3b723737127bba457e0c4da3 Mon Sep 17 00:00:00 2001 From: Jezra Date: Sat, 1 Aug 2015 08:23:59 -0700 Subject: Baaaahhh! gitlab markdown processing leaves something to be desired --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 82e559d..66990b5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ #Blather + Blather is a speech recognizer that will run commands when a user speaks preset sentences. ##Requirements + 1. pocketsphinx 2. gstreamer-0.10 (and what ever plugin has pocket sphinx support) 3. gstreamer-0.10 base plugins (required for alsa) @@ -12,8 +14,8 @@ Blather is a speech recognizer that will run commands when a user speaks preset **Note:** it may also be required to install `pocketsphinx-hmm-en-hub4wsj` - ##Usage + 0. move commands.tmp to ~/.config/blather/commands.conf and fill the file with sentences and command to run 1. Run Blather.py, this will generate ~/.config/blather/sentences.corpus based on sentences in the 'commands' file 2. quit blather (there is a good chance it will just segfault) @@ -30,9 +32,11 @@ Blather is a speech recognizer that will run commands when a user speaks preset **Note:** to start Blather without needing to enter command line options all the time, copy options.yaml.tmp to ~/.config/blather/options.yaml and edit accordingly. ###Bonus + once the sentences.corpus file has been created, run the language_updater.sh script to automate the process of creating and downloading language files. ###Examples + * To run blather with the GTK UI and start in continuous listen mode: `./Blather.py -i g -c` -- cgit 1.4.1 From fab5cc73faa98cdc941ce0da05ee89a974a461a1 Mon Sep 17 00:00:00 2001 From: Jezra Date: Sat, 1 Aug 2015 08:27:17 -0700 Subject: More format fuckery --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 66990b5..768f38a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -#Blather +# Blather Blather is a speech recognizer that will run commands when a user speaks preset sentences. -##Requirements +## Requirements 1. pocketsphinx 2. gstreamer-0.10 (and what ever plugin has pocket sphinx support) @@ -14,7 +14,7 @@ Blather is a speech recognizer that will run commands when a user speaks preset **Note:** it may also be required to install `pocketsphinx-hmm-en-hub4wsj` -##Usage +## Usage 0. move commands.tmp to ~/.config/blather/commands.conf and fill the file with sentences and command to run 1. Run Blather.py, this will generate ~/.config/blather/sentences.corpus based on sentences in the 'commands' file @@ -31,11 +31,11 @@ Blather is a speech recognizer that will run commands when a user speaks preset **Note:** to start Blather without needing to enter command line options all the time, copy options.yaml.tmp to ~/.config/blather/options.yaml and edit accordingly. -###Bonus +### Bonus once the sentences.corpus file has been created, run the language_updater.sh script to automate the process of creating and downloading language files. -###Examples +### Examples * To run blather with the GTK UI and start in continuous listen mode: `./Blather.py -i g -c` @@ -58,7 +58,7 @@ once the sentences.corpus file has been created, run the language_updater.sh scr `./Blather.py --invalid-sentence-command=/path/to/command` **note:** this can be set in the options.yml file -###Finding the Device Number of a USB microphone +### Finding the Device Number of a USB microphone There are a few ways to find the device number of a USB microphone. * `cat /proc/asound/cards` -- cgit 1.4.1 From a6e27df2ccf8a22d76b2ff795dee2f86f52b3970 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sat, 26 Dec 2015 22:24:06 -0500 Subject: Ported to new pocketsphinx and pygi Gotta keep that stuff up-to-date, yo. Pocketsphinx changed so it needs GStreamer 1.0, and that required rewriting everything to use GObject Introspection instead of the old, static Python bindings for GObject. --- .gitignore | 1 + Blather.py | 448 +++++++++++++++++++++++++++++----------------------------- GtkTrayUI.py | 194 +++++++++++++------------ GtkUI.py | 214 ++++++++++++++-------------- README.md | 59 ++++---- Recognizer.py | 102 +++++++------ 6 files changed, 513 insertions(+), 505 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/Blather.py b/Blather.py index ed5e6ed..1f59ee0 100755 --- a/Blather.py +++ b/Blather.py @@ -5,16 +5,16 @@ import sys import signal -import gobject +from gi.repository import GObject import os.path import subprocess from optparse import OptionParser try: - import yaml + import yaml except: - print "YAML is not supported. ~/.config/blather/options.yaml will not function" + print "YAML is not supported. ~/.config/blather/options.yaml will not function" -#where are the files? +# Where are the files? conf_dir = os.path.expanduser("~/.config/blather") lang_dir = os.path.join(conf_dir, "language") command_file = os.path.join(conf_dir, "commands.conf") @@ -23,230 +23,230 @@ history_file = os.path.join(conf_dir, "blather.history") opt_file = os.path.join(conf_dir, "options.yaml") lang_file = os.path.join(lang_dir,'lm') dic_file = os.path.join(lang_dir,'dic') -#make the lang_dir if it doesn't exist +# Make the lang_dir if it doesn't exist if not os.path.exists(lang_dir): - os.makedirs(lang_dir) + os.makedirs(lang_dir) class Blather: - def __init__(self, opts): - - #import the recognizer so Gst doesn't clobber our -h - from Recognizer import Recognizer - self.ui = None - self.options = {} - ui_continuous_listen = False - self.continuous_listen = False - - self.commands = {} - - #read the commands - self.read_commands() - - #load the options file - self.load_options() - - #merge the opts - for k,v in opts.__dict__.items(): - if (not k in self.options) or opts.override: - self.options[k] = v - - if self.options['interface'] != None: - if self.options['interface'] == "q": - from QtUI import UI - elif self.options['interface'] == "g": - from GtkUI import UI - elif self.options['interface'] == "gt": - from GtkTrayUI import UI - else: - print "no GUI defined" - sys.exit() - - self.ui = UI(args, self.options['continuous']) - self.ui.connect("command", self.process_command) - #can we load the icon resource? - icon = self.load_resource("icon.png") - if icon: - self.ui.set_icon_active_asset(icon) - #can we load the icon_inactive resource? - icon_inactive = self.load_resource("icon_inactive.png") - if icon_inactive: - self.ui.set_icon_inactive_asset(icon_inactive) - - if self.options['history']: - self.history = [] - - #create the recognizer - try: - self.recognizer = Recognizer(lang_file, dic_file, self.options['microphone'] ) - except Exception, e: - #no recognizer? bummer - sys.exit() - - self.recognizer.connect('finished',self.recognizer_finished) - - print "Using Options: ", self.options - - def read_commands(self): - #read the.commands file - file_lines = open(command_file) - strings = open(strings_file, "w") - for line in file_lines: - print line - #trim the white spaces - line = line.strip() - #if the line has length and the first char isn't a hash - if len(line) and line[0]!="#": - #this is a parsible line - (key,value) = line.split(":",1) - print key, value - self.commands[key.strip().lower()] = value.strip() - strings.write( key.strip()+"\n") - #close the strings file - strings.close() - - def load_options(self): - #is there an opt file? - try: - opt_fh = open(opt_file) - text = opt_fh.read() - self.options = yaml.load(text) - except: - pass - - - def log_history(self,text): - if self.options['history']: - self.history.append(text) - if len(self.history) > self.options['history']: - #pop off the first item - self.history.pop(0) - - #open and truncate the blather history file - hfile = open(history_file, "w") - for line in self.history: - hfile.write( line+"\n") - #close the file - hfile.close() - - # Print the cmd and then run the command - def run_command(self, cmd): - print cmd - subprocess.call(cmd, shell=True) - - def recognizer_finished(self, recognizer, text): - t = text.lower() - #is there a matching command? - if self.commands.has_key( t ): - #run the valid_sentence_command if there is a valid sentence command - if self.options['valid_sentence_command']: - subprocess.call(self.options['valid_sentence_command'], shell=True) - cmd = self.commands[t] - #should we be passing words? - if self.options['pass_words']: - cmd+=" "+t - self.run_command(cmd) - else: - self.run_command(cmd) - self.log_history(text) - else: - #run the invalid_sentence_command if there is a valid sentence command - if self.options['invalid_sentence_command']: - subprocess.call(self.options['invalid_sentence_command'], shell=True) - print "no matching command %s" %(t) - #if there is a UI and we are not continuous listen - if self.ui: - if not self.continuous_listen: - #stop listening - self.recognizer.pause() - #let the UI know that there is a finish - self.ui.finished(t) - - def run(self): - if self.ui: - self.ui.run() - else: - blather.recognizer.listen() - - def quit(self): - sys.exit() - - def process_command(self, UI, command): - print command - if command == "listen": - self.recognizer.listen() - elif command == "stop": - self.recognizer.pause() - elif command == "continuous_listen": - self.continuous_listen = True - self.recognizer.listen() - elif command == "continuous_stop": - self.continuous_listen = False - self.recognizer.pause() - elif command == "quit": - self.quit() - - def load_resource(self,string): - local_data = os.path.join(os.path.dirname(__file__), 'data') - paths = ["/usr/share/blather/","/usr/local/share/blather", local_data] - for path in paths: - resource = os.path.join(path, string) - if os.path.exists( resource ): - return resource - #if we get this far, no resource was found - return False + + def __init__(self, opts): + # Import the recognizer so Gst doesn't clobber our -h + from Recognizer import Recognizer + self.ui = None + self.options = {} + ui_continuous_listen = False + self.continuous_listen = False + + self.commands = {} + + # Read the commands + self.read_commands() + + # Load the options file + self.load_options() + + # Merge the opts + for k,v in opts.__dict__.items(): + if (not k in self.options) or opts.override: + self.options[k] = v + + if self.options['interface'] != None: + if self.options['interface'] == "q": + from QtUI import UI + elif self.options['interface'] == "g": + from GtkUI import UI + elif self.options['interface'] == "gt": + from GtkTrayUI import UI + else: + print "no GUI defined" + sys.exit() + + self.ui = UI(args, self.options['continuous']) + self.ui.connect("command", self.process_command) + #can we load the icon resource? + icon = self.load_resource("icon.png") + if icon: + self.ui.set_icon_active_asset(icon) + #can we load the icon_inactive resource? + icon_inactive = self.load_resource("icon_inactive.png") + if icon_inactive: + self.ui.set_icon_inactive_asset(icon_inactive) + + if self.options['history']: + self.history = [] + + # Create the recognizer + try: + self.recognizer = Recognizer(lang_file, dic_file, self.options['microphone']) + except Exception, e: + #no recognizer? bummer + print 'error making recognizer' + sys.exit() + + self.recognizer.connect('finished', self.recognizer_finished) + + print "Using Options: ", self.options + + def read_commands(self): + # Read the commands file + file_lines = open(command_file) + strings = open(strings_file, "w") + for line in file_lines: + print line + # Trim the white spaces + line = line.strip() + # If the line has length and the first char isn't a hash + if len(line) and line[0]!="#": + # This is a parsible line + (key,value) = line.split(":",1) + print key, value + self.commands[key.strip().lower()] = value.strip() + strings.write( key.strip()+"\n") + # Close the strings file + strings.close() + + def load_options(self): + # Is there an opt file? + try: + opt_fh = open(opt_file) + text = opt_fh.read() + self.options = yaml.load(text) + except: + pass + + + def log_history(self,text): + if self.options['history']: + self.history.append(text) + if len(self.history) > self.options['history']: + # Pop off the first item + self.history.pop(0) + + # Open and truncate the blather history file + hfile = open(history_file, "w") + for line in self.history: + hfile.write( line+"\n") + # Close the file + hfile.close() + + def run_command(self, cmd): + '''Print the command, then run it''' + print cmd + subprocess.call(cmd, shell=True) + + def recognizer_finished(self, recognizer, text): + t = text.lower() + # Is there a matching command? + if self.commands.has_key( t ): + # Run the valid_sentence_command if there is a valid sentence command + if self.options['valid_sentence_command']: + subprocess.call(self.options['valid_sentence_command'], shell=True) + cmd = self.commands[t] + # Should we be passing words? + if self.options['pass_words']: + cmd += " " + t + self.run_command(cmd) + else: + self.run_command(cmd) + self.log_history(text) + else: + # Run the invalid_sentence_command if there is an invalid sentence command + if self.options['invalid_sentence_command']: + subprocess.call(self.options['invalid_sentence_command'], shell=True) + print "no matching command %s" % t + # If there is a UI and we are not continuous listen + if self.ui: + if not self.continuous_listen: + # Stop listening + self.recognizer.pause() + # Let the UI know that there is a finish + self.ui.finished(t) + + def run(self): + if self.ui: + self.ui.run() + else: + blather.recognizer.listen() + + def quit(self): + sys.exit() + + def process_command(self, UI, command): + print command + if command == "listen": + self.recognizer.listen() + elif command == "stop": + self.recognizer.pause() + elif command == "continuous_listen": + self.continuous_listen = True + self.recognizer.listen() + elif command == "continuous_stop": + self.continuous_listen = False + self.recognizer.pause() + elif command == "quit": + self.quit() + + def load_resource(self,string): + local_data = os.path.join(os.path.dirname(__file__), 'data') + paths = ["/usr/share/blather/","/usr/local/share/blather", local_data] + for path in paths: + resource = os.path.join(path, string) + if os.path.exists( resource ): + return resource + # If we get this far, no resource was found + return False if __name__ == "__main__": - parser = OptionParser() - parser.add_option("-i", "--interface", type="string", dest="interface", - action='store', - help="Interface to use (if any). 'q' for Qt, 'g' for GTK, 'gt' for GTK system tray icon") - - parser.add_option("-c", "--continuous", - action="store_true", dest="continuous", default=False, - help="starts interface with 'continuous' listen enabled") - - parser.add_option("-p", "--pass-words", - action="store_true", dest="pass_words", default=False, - help="passes the recognized words as arguments to the shell command") - - parser.add_option("-o", "--override", - action="store_true", dest="override", default=False, - help="override config file with command line options") - - parser.add_option("-H", "--history", type="int", - action="store", dest="history", - help="number of commands to store in history file") - - parser.add_option("-m", "--microphone", type="int", - action="store", dest="microphone", default=None, - help="Audio input card to use (if other than system default)") - - parser.add_option("--valid-sentence-command", type="string", dest="valid_sentence_command", - action='store', - help="command to run when a valid sentence is detected") - - parser.add_option( "--invalid-sentence-command", type="string", dest="invalid_sentence_command", - action='store', - help="command to run when an invalid sentence is detected") - - (options, args) = parser.parse_args() - #make our blather object - blather = Blather(options) - #init gobject threads - gobject.threads_init() - #we want a main loop - main_loop = gobject.MainLoop() - #handle sigint - signal.signal(signal.SIGINT, signal.SIG_DFL) - #run the blather - blather.run() - #start the main loop - - try: - main_loop.run() - except: - print "time to quit" - main_loop.quit() - sys.exit() + parser = OptionParser() + parser.add_option("-i", "--interface", type="string", dest="interface", + action='store', + help="Interface to use (if any). 'q' for Qt, 'g' for GTK, 'gt' for GTK system tray icon") + + parser.add_option("-c", "--continuous", + action="store_true", dest="continuous", default=False, + help="starts interface with 'continuous' listen enabled") + + parser.add_option("-p", "--pass-words", + action="store_true", dest="pass_words", default=False, + help="passes the recognized words as arguments to the shell command") + + parser.add_option("-o", "--override", + action="store_true", dest="override", default=False, + help="override config file with command line options") + + parser.add_option("-H", "--history", type="int", + action="store", dest="history", + help="number of commands to store in history file") + + parser.add_option("-m", "--microphone", type="int", + action="store", dest="microphone", default=None, + help="Audio input card to use (if other than system default)") + + parser.add_option("--valid-sentence-command", type="string", dest="valid_sentence_command", + action='store', + help="command to run when a valid sentence is detected") + + parser.add_option( "--invalid-sentence-command", type="string", dest="invalid_sentence_command", + action='store', + help="command to run when an invalid sentence is detected") + + (options, args) = parser.parse_args() + # Make our blather object + blather = Blather(options) + # Init gobject threads + GObject.threads_init() + # We want a main loop + main_loop = GObject.MainLoop() + # Handle sigint + signal.signal(signal.SIGINT, signal.SIG_DFL) + # Run the blather + blather.run() + # Start the main loop + try: + main_loop.run() + except: + print "time to quit" + main_loop.quit() + sys.exit() diff --git a/GtkTrayUI.py b/GtkTrayUI.py index f0d1c2e..dda153d 100644 --- a/GtkTrayUI.py +++ b/GtkTrayUI.py @@ -1,99 +1,97 @@ import sys -import gobject - -import pygtk -import gtk - -class UI(gobject.GObject): - __gsignals__ = { - 'command' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)) - } - - def __init__(self,args, continuous): - gobject.GObject.__init__(self) - self.continuous = continuous - - self.statusicon = gtk.StatusIcon() - self.statusicon.set_title("Blather") - self.statusicon.set_name("Blather") - self.statusicon.set_tooltip_text("Blather - Idle") - self.statusicon.set_has_tooltip(True) - self.statusicon.connect("activate", self.continuous_toggle) - self.statusicon.connect("popup-menu", self.popup_menu) - - self.menu = gtk.Menu() - self.menu_listen = gtk.MenuItem('Listen') - self.menu_continuous = gtk.CheckMenuItem('Continuous') - self.menu_quit = gtk.MenuItem('Quit') - self.menu.append(self.menu_listen) - self.menu.append(self.menu_continuous) - self.menu.append(self.menu_quit) - self.menu_listen.connect("activate", self.toggle_listen) - self.menu_continuous.connect("toggled", self.toggle_continuous) - self.menu_quit.connect("activate", self.quit) - self.menu.show_all() - - def continuous_toggle(self, item): - checked = self.menu_continuous.get_active() - self.menu_continuous.set_active(not checked) - - def toggle_continuous(self, item): - checked = self.menu_continuous.get_active() - self.menu_listen.set_sensitive(not checked) - if checked: - self.menu_listen.set_label("Listen") - self.emit('command', "continuous_listen") - self.statusicon.set_tooltip_text("Blather - Listening") - self.set_icon_active() - else: - self.set_icon_inactive() - self.statusicon.set_tooltip_text("Blather - Idle") - self.emit('command', "continuous_stop") - - def toggle_listen(self, item): - val = self.menu_listen.get_label() - if val == "Listen": - self.emit("command", "listen") - self.menu_listen.set_label("Stop") - self.statusicon.set_tooltip_text("Blather - Listening") - else: - self.icon_inactive() - self.menu_listen.set_label("Listen") - self.emit("command", "stop") - self.statusicon.set_tooltip_text("Blather - Idle") - - def popup_menu(self, item, button, time): - self.menu.popup(None, None, gtk.status_icon_position_menu, button, time, item) - - def run(self): - #set the icon - self.set_icon_inactive() - if self.continuous: - self.menu_continuous.set_active(True) - self.set_icon_active() - else: - self.menu_continuous.set_active(False) - self.statusicon.set_visible(True) - - def quit(self, item): - self.statusicon.set_visible(False) - self.emit("command", "quit") - - def finished(self, text): - if not self.menu_continuous.get_active(): - self.menu_listen.set_label("Listen") - self.statusicon.set_from_icon_name("blather_stopped") - self.statusicon.set_tooltip_text("Blather - Idle") - - def set_icon_active_asset(self, i): - self.icon_active = i - - def set_icon_inactive_asset(self, i): - self.icon_inactive = i - - def set_icon_active(self): - self.statusicon.set_from_file( self.icon_active ) - - def set_icon_inactive(self): - self.statusicon.set_from_file( self.icon_inactive ) - +from gi.repository import GObject +# Gtk +from gi.repository import Gtk, Gdk + +class UI(GObject.GObject): + __gsignals__ = { + 'command' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)) + } + + def __init__(self, args, continuous): + GObject.GObject.__init__(self) + self.continuous = continuous + + self.statusicon = Gtk.StatusIcon() + self.statusicon.set_title("Blather") + self.statusicon.set_name("Blather") + self.statusicon.set_tooltip_text("Blather - Idle") + self.statusicon.set_has_tooltip(True) + self.statusicon.connect("activate", self.continuous_toggle) + self.statusicon.connect("popup-menu", self.popup_menu) + + self.menu = Gtk.Menu() + self.menu_listen = Gtk.MenuItem('Listen') + self.menu_continuous = Gtk.CheckMenuItem('Continuous') + self.menu_quit = Gtk.MenuItem('Quit') + self.menu.append(self.menu_listen) + self.menu.append(self.menu_continuous) + self.menu.append(self.menu_quit) + self.menu_listen.connect("activate", self.toggle_listen) + self.menu_continuous.connect("toggled", self.toggle_continuous) + self.menu_quit.connect("activate", self.quit) + self.menu.show_all() + + def continuous_toggle(self, item): + checked = self.menu_continuous.get_active() + self.menu_continuous.set_active(not checked) + + def toggle_continuous(self, item): + checked = self.menu_continuous.get_active() + self.menu_listen.set_sensitive(not checked) + if checked: + self.menu_listen.set_label("Listen") + self.emit('command', "continuous_listen") + self.statusicon.set_tooltip_text("Blather - Listening") + self.set_icon_active() + else: + self.set_icon_inactive() + self.statusicon.set_tooltip_text("Blather - Idle") + self.emit('command', "continuous_stop") + + def toggle_listen(self, item): + val = self.menu_listen.get_label() + if val == "Listen": + self.emit("command", "listen") + self.menu_listen.set_label("Stop") + self.statusicon.set_tooltip_text("Blather - Listening") + else: + self.icon_inactive() + self.menu_listen.set_label("Listen") + self.emit("command", "stop") + self.statusicon.set_tooltip_text("Blather - Idle") + + def popup_menu(self, item, button, time): + self.menu.popup(None, None, Gtk.StatusIcon.position_menu, item, button, time) + + def run(self): + # Set the icon + self.set_icon_inactive() + if self.continuous: + self.menu_continuous.set_active(True) + self.set_icon_active() + else: + self.menu_continuous.set_active(False) + self.statusicon.set_visible(True) + + def quit(self, item): + self.statusicon.set_visible(False) + self.emit("command", "quit") + + def finished(self, text): + if not self.menu_continuous.get_active(): + self.menu_listen.set_label("Listen") + self.statusicon.set_from_icon_name("blather_stopped") + self.statusicon.set_tooltip_text("Blather - Idle") + + def set_icon_active_asset(self, i): + self.icon_active = i + + def set_icon_inactive_asset(self, i): + self.icon_inactive = i + + def set_icon_active(self): + self.statusicon.set_from_file(self.icon_active) + + def set_icon_inactive(self): + self.statusicon.set_from_file(self.icon_inactive) diff --git a/GtkUI.py b/GtkUI.py index 17c7b50..2e0fc17 100644 --- a/GtkUI.py +++ b/GtkUI.py @@ -1,111 +1,109 @@ -#This is part of Blather +# This is part of Blather # -- this code is licensed GPLv3 # Copyright 2013 Jezra import sys -import gobject -#Gtk -import pygtk -import gtk - -class UI(gobject.GObject): - __gsignals__ = { - 'command' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)) - } - - def __init__(self,args, continuous): - gobject.GObject.__init__(self) - self.continuous = continuous - #make a window - self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) - self.window.connect("delete_event", self.delete_event) - #give the window a name - self.window.set_title("BlatherGtk") - self.window.set_resizable(False) - - layout = gtk.VBox() - self.window.add(layout) - #make a listen/stop button - self.lsbutton = gtk.Button("Listen") - layout.add(self.lsbutton) - #make a continuous button - self.ccheckbox = gtk.CheckButton("Continuous Listen") - layout.add(self.ccheckbox) - - #connect the buttons - self.lsbutton.connect("clicked",self.lsbutton_clicked) - self.ccheckbox.connect("clicked",self.ccheckbox_clicked) - - #add a label to the UI to display the last command - self.label = gtk.Label() - layout.add(self.label) - - #create an accellerator group for this window - accel = gtk.AccelGroup() - #add the ctrl+q to quit - accel.connect_group(gtk.keysyms.q, gtk.gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE, self.accel_quit ) - #lock the group - accel.lock() - #add the group to the window - self.window.add_accel_group(accel) - - def ccheckbox_clicked(self, widget): - checked = self.ccheckbox.get_active() - self.lsbutton.set_sensitive(not checked) - if checked: - self.lsbutton_stopped() - self.emit('command', "continuous_listen") - self.set_icon_active() - else: - self.emit('command', "continuous_stop") - self.set_icon_inactive() - - def lsbutton_stopped(self): - self.lsbutton.set_label("Listen") - - def lsbutton_clicked(self, button): - val = self.lsbutton.get_label() - if val == "Listen": - self.emit("command", "listen") - self.lsbutton.set_label("Stop") - #clear the label - self.label.set_text("") - self.set_icon_active() - else: - self.lsbutton_stopped() - self.emit("command", "stop") - self.set_icon_inactive() - - def run(self): - #set the default icon - self.set_icon_inactive() - self.window.show_all() - if self.continuous: - self.set_icon_active() - self.ccheckbox.set_active(True) - - def accel_quit(self, accel_group, acceleratable, keyval, modifier): - self.emit("command", "quit") - - def delete_event(self, x, y ): - self.emit("command", "quit") - - def finished(self, text): - #if the continuous isn't pressed - if not self.ccheckbox.get_active(): - self.lsbutton_stopped() - self.set_icon_inactive() - self.label.set_text(text) - - def set_icon_active_asset(self, i): - self.icon_active = i - - def set_icon_inactive_asset(self, i): - self.icon_inactive = i - - def set_icon_active(self): - gtk.window_set_default_icon_from_file(self.icon_active) - - def set_icon_inactive(self): - gtk.window_set_default_icon_from_file(self.icon_inactive) - - +from gi.repository import GObject +# Gtk +from gi.repository import Gtk, Gdk + +class UI(GObject.GObject): + __gsignals__ = { + 'command' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)) + } + + def __init__(self,args, continuous): + GObject.GObject.__init__(self) + self.continuous = continuous + # Make a window + self.window = Gtk.Window(Gtk.WindowType.TOPLEVEL) + self.window.connect("delete_event", self.delete_event) + # Give the window a name + self.window.set_title("BlatherGtk") + self.window.set_resizable(False) + + layout = Gtk.VBox() + self.window.add(layout) + # Make a listen/stop button + self.lsbutton = Gtk.Button("Listen") + layout.add(self.lsbutton) + # Make a continuous button + self.ccheckbox = Gtk.CheckButton("Continuous Listen") + layout.add(self.ccheckbox) + + # Connect the buttons + self.lsbutton.connect("clicked",self.lsbutton_clicked) + self.ccheckbox.connect("clicked",self.ccheckbox_clicked) + + # Add a label to the UI to display the last command + self.label = Gtk.Label() + layout.add(self.label) + + # Create an accellerator group for this window + accel = Gtk.AccelGroup() + # Add the ctrl+q to quit + accel.connect(Gdk.keyval_from_name('q'), Gdk.ModifierType.CONTROL_MASK, + Gtk.AccelFlags.VISIBLE, self.accel_quit) + # Lock the group + accel.lock() + # Add the group to the window + self.window.add_accel_group(accel) + + def ccheckbox_clicked(self, widget): + checked = self.ccheckbox.get_active() + self.lsbutton.set_sensitive(not checked) + if checked: + self.lsbutton_stopped() + self.emit('command', "continuous_listen") + self.set_icon_active() + else: + self.emit('command', "continuous_stop") + self.set_icon_inactive() + + def lsbutton_stopped(self): + self.lsbutton.set_label("Listen") + + def lsbutton_clicked(self, button): + val = self.lsbutton.get_label() + if val == "Listen": + self.emit("command", "listen") + self.lsbutton.set_label("Stop") + # Clear the label + self.label.set_text("") + self.set_icon_active() + else: + self.lsbutton_stopped() + self.emit("command", "stop") + self.set_icon_inactive() + + def run(self): + # Set the default icon + self.set_icon_inactive() + self.window.show_all() + if self.continuous: + self.set_icon_active() + self.ccheckbox.set_active(True) + + def accel_quit(self, accel_group, acceleratable, keyval, modifier): + self.emit("command", "quit") + + def delete_event(self, x, y): + self.emit("command", "quit") + + def finished(self, text): + # If the continuous isn't pressed + if not self.ccheckbox.get_active(): + self.lsbutton_stopped() + self.set_icon_inactive() + self.label.set_text(text) + + def set_icon_active_asset(self, i): + self.icon_active = i + + def set_icon_inactive_asset(self, i): + self.icon_inactive = i + + def set_icon_active(self): + Gtk.Window.set_default_icon_from_file(self.icon_active) + + def set_icon_inactive(self): + Gtk.Window.set_default_icon_from_file(self.icon_inactive) diff --git a/README.md b/README.md index 768f38a..feb0b46 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,64 @@ -# Blather +# Kaylee -Blather is a speech recognizer that will run commands when a user speaks preset sentences. +Kaylee is a somewhat fancy speech recognizer that will run commands and perform +other functions when a user speaks loosely preset sentences. It is based on +[Blather](https://gitlab.com/jezra/blather) by [Jezra](http://www.jezra.net/), +but adds a lot of features that go beyond the original purpose of Blather. ## Requirements 1. pocketsphinx -2. gstreamer-0.10 (and what ever plugin has pocket sphinx support) -3. gstreamer-0.10 base plugins (required for alsa) +2. gstreamer-1.0 (and what ever plugin has pocket sphinx support) +3. gstreamer-1.0 base plugins (required for alsa) 4. pyside (only required for the Qt based UI) 5. pygtk (only required for the Gtk based UI) -6. pyyaml (only required for reading the options file) +6. pyyaml (only required for reading the options file) **Note:** it may also be required to install `pocketsphinx-hmm-en-hub4wsj` ## Usage -0. move commands.tmp to ~/.config/blather/commands.conf and fill the file with sentences and command to run +0. Move commands.tmp to ~/.config/blather/commands.conf and fill the file with sentences and command to run 1. Run Blather.py, this will generate ~/.config/blather/sentences.corpus based on sentences in the 'commands' file -2. quit blather (there is a good chance it will just segfault) -3. go to and upload the sentences.corpus file -4. download the resulting XXXX.lm file to the ~/.config/blather/language directory and rename to file to 'lm' -5. download the resulting XXXX.dic file to the ~/.config/blather/language directory and rename to file to 'dic' -6. run Blather.py - * for Qt GUI, run Blather.py -i q - * for Gtk GUI, run Blather.py -i g - * to start a UI in 'continuous' listen mode, use the -c flag - * to use a microphone other than the system default, use the -m flag -7. start talking +2. Quit blather (there is a good chance it will just segfault) +3. Go to and upload the sentences.corpus file +4. Download the resulting XXXX.lm file to the ~/.config/blather/language directory and rename to file to 'lm' +5. Download the resulting XXXX.dic file to the ~/.config/blather/language directory and rename to file to 'dic' +6. Run Blather.py + * For Qt GUI, run Blather.py -i q + * For Gtk GUI, run Blather.py -i g + * To start a UI in 'continuous' listen mode, use the -c flag + * To use a microphone other than the system default, use the -m flag +7. Start talking **Note:** to start Blather without needing to enter command line options all the time, copy options.yaml.tmp to ~/.config/blather/options.yaml and edit accordingly. ### Bonus -once the sentences.corpus file has been created, run the language_updater.sh script to automate the process of creating and downloading language files. +Once the sentences.corpus file has been created, run the language_updater.sh script to automate the process of creating and downloading language files. ### Examples -* To run blather with the GTK UI and start in continuous listen mode: +* To run blather with the GTK UI and start in continuous listen mode: `./Blather.py -i g -c` -* To run blather with no UI and using a USB microphone recognized and device 2: +* To run blather with no UI and using a USB microphone recognized and device 2: `./Blather.py -m 2` -* To have blather pass the matched sentence to the executing command: - `./Blather.py -p` +* To have blather pass the matched sentence to the executing command: + `./Blather.py -p` - **explanation:** if the commands.conf contains: - **good morning world : example_command.sh** - then 3 arguments, 'good', 'morning', and 'world' would get passed to example_command.sh as + **explanation:** if the commands.conf contains: + **good morning world : example_command.sh** + then 3 arguments, 'good', 'morning', and 'world' would get passed to example_command.sh as `example_command.sh good morning world` -* To run a command when a valid sentence has been detected: - `./Blather.py --valid-sentence-command=/path/to/command` +* To run a command when a valid sentence has been detected: + `./Blather.py --valid-sentence-command=/path/to/command` **note:** this can be set in the options.yml file -* To run a command when a invalid sentence has been detected: - `./Blather.py --invalid-sentence-command=/path/to/command` +* To run a command when a invalid sentence has been detected: + `./Blather.py --invalid-sentence-command=/path/to/command` **note:** this can be set in the options.yml file ### Finding the Device Number of a USB microphone diff --git a/Recognizer.py b/Recognizer.py index e9cb648..e962ea3 100755 --- a/Recognizer.py +++ b/Recognizer.py @@ -1,57 +1,65 @@ -#This is part of Blather +# This is part of Blather # -- this code is licensed GPLv3 # Copyright 2013 Jezra -import pygst -pygst.require('0.10') -import gst +import gi +gi.require_version('Gst', '1.0') +from gi.repository import GObject, Gst +GObject.threads_init() +Gst.init(None) import os.path -import gobject import sys -#define some global variables +# Define some global variables this_dir = os.path.dirname( os.path.abspath(__file__) ) -class Recognizer(gobject.GObject): - __gsignals__ = { - 'finished' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)) - } - def __init__(self, language_file, dictionary_file, src = None): - gobject.GObject.__init__(self) - self.commands = {} - if src: - audio_src = 'alsasrc device="hw:%d,0"' % (src) - else: - audio_src = 'autoaudiosrc' - - #build the pipeline - cmd = audio_src+' ! audioconvert ! audioresample ! vader name=vad ! pocketsphinx name=asr ! appsink sync=false' - try: - self.pipeline=gst.parse_launch( cmd ) - except Exception, e: - print e.message - print "You may need to install gstreamer0.10-pocketsphinx" - raise e - - #get the Auto Speech Recognition piece - asr=self.pipeline.get_by_name('asr') - asr.connect('result', self.result) - asr.set_property('lm', language_file) - asr.set_property('dict', dictionary_file) - asr.set_property('configured', True) - #get the Voice Activity DEtectoR - self.vad = self.pipeline.get_by_name('vad') - self.vad.set_property('auto-threshold',True) - - def listen(self): - self.pipeline.set_state(gst.STATE_PLAYING) - - def pause(self): - self.vad.set_property('silent', True) - self.pipeline.set_state(gst.STATE_PAUSED) - - def result(self, asr, text, uttid): - #emit finished - self.emit("finished", text) +class Recognizer(GObject.GObject): + __gsignals__ = { + 'finished' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)) + } + def __init__(self, language_file, dictionary_file, src = None): + GObject.GObject.__init__(self) + self.commands = {} + if src: + audio_src = 'alsasrc device="hw:%d,0"' % (src) + else: + audio_src = 'autoaudiosrc' + + # Build the pipeline + cmd = audio_src+' ! audioconvert ! audioresample ! pocketsphinx name=asr ! appsink sync=false' + try: + self.pipeline=Gst.parse_launch( cmd ) + except Exception, e: + print e.message + print "You may need to install gstreamer1.0-pocketsphinx" + raise e + + bus = self.pipeline.get_bus() + bus.add_signal_watch() + + # Get the Auto Speech Recognition piece + asr=self.pipeline.get_by_name('asr') + bus.connect('message::element', self.result) + asr.set_property('lm', language_file) + asr.set_property('dict', dictionary_file) + asr.set_property('configured', True) + + def listen(self): + self.pipeline.set_state(Gst.State.PLAYING) + + def pause(self): + self.pipeline.set_state(Gst.State.PAUSED) + + def result(self, bus, msg): + msg_struct = msg.get_structure() + # Ignore messages that aren't from pocketsphinx + msgtype = msg_struct.get_name() + if msgtype != 'pocketsphinx': + return + + # If we have a final command, send it for processing + command = msg_struct.get_string('hypothesis') + if command != '' and msg_struct.get_boolean('final')[1]: + self.emit("finished", command) -- cgit 1.4.1 From 2a641364bf8fc3cc4069d2d2c42b75241e6dc3f2 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sat, 26 Dec 2015 22:43:12 -0500 Subject: Removed QT interface, renamed interfaces to Kaylee I'm a GTK man myself. I don't know if I have Python QT bindings installed on any of my computers. It follows then that I would not maintain the QT interface well, let alone use it at all. It has therefore been removed to avoid having someone try to use it only to find that it's broken. --- Blather.py | 252 --------------------------------------------------------- GtkTrayUI.py | 97 ---------------------- GtkUI.py | 109 ------------------------- QtUI.py | 115 -------------------------- Recognizer.py | 65 --------------- blather.py | 253 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ gtktrayui.py | 105 ++++++++++++++++++++++++ gtkui.py | 111 ++++++++++++++++++++++++++ recognizer.py | 66 +++++++++++++++ 9 files changed, 535 insertions(+), 638 deletions(-) delete mode 100755 Blather.py delete mode 100644 GtkTrayUI.py delete mode 100644 GtkUI.py delete mode 100644 QtUI.py delete mode 100755 Recognizer.py create mode 100755 blather.py create mode 100644 gtktrayui.py create mode 100644 gtkui.py create mode 100755 recognizer.py diff --git a/Blather.py b/Blather.py deleted file mode 100755 index 1f59ee0..0000000 --- a/Blather.py +++ /dev/null @@ -1,252 +0,0 @@ -#!/usr/bin/env python2 - -# -- this code is licensed GPLv3 -# Copyright 2013 Jezra - -import sys -import signal -from gi.repository import GObject -import os.path -import subprocess -from optparse import OptionParser -try: - import yaml -except: - print "YAML is not supported. ~/.config/blather/options.yaml will not function" - -# Where are the files? -conf_dir = os.path.expanduser("~/.config/blather") -lang_dir = os.path.join(conf_dir, "language") -command_file = os.path.join(conf_dir, "commands.conf") -strings_file = os.path.join(conf_dir, "sentences.corpus") -history_file = os.path.join(conf_dir, "blather.history") -opt_file = os.path.join(conf_dir, "options.yaml") -lang_file = os.path.join(lang_dir,'lm') -dic_file = os.path.join(lang_dir,'dic') -# Make the lang_dir if it doesn't exist -if not os.path.exists(lang_dir): - os.makedirs(lang_dir) - -class Blather: - - def __init__(self, opts): - # Import the recognizer so Gst doesn't clobber our -h - from Recognizer import Recognizer - self.ui = None - self.options = {} - ui_continuous_listen = False - self.continuous_listen = False - - self.commands = {} - - # Read the commands - self.read_commands() - - # Load the options file - self.load_options() - - # Merge the opts - for k,v in opts.__dict__.items(): - if (not k in self.options) or opts.override: - self.options[k] = v - - if self.options['interface'] != None: - if self.options['interface'] == "q": - from QtUI import UI - elif self.options['interface'] == "g": - from GtkUI import UI - elif self.options['interface'] == "gt": - from GtkTrayUI import UI - else: - print "no GUI defined" - sys.exit() - - self.ui = UI(args, self.options['continuous']) - self.ui.connect("command", self.process_command) - #can we load the icon resource? - icon = self.load_resource("icon.png") - if icon: - self.ui.set_icon_active_asset(icon) - #can we load the icon_inactive resource? - icon_inactive = self.load_resource("icon_inactive.png") - if icon_inactive: - self.ui.set_icon_inactive_asset(icon_inactive) - - if self.options['history']: - self.history = [] - - # Create the recognizer - try: - self.recognizer = Recognizer(lang_file, dic_file, self.options['microphone']) - except Exception, e: - #no recognizer? bummer - print 'error making recognizer' - sys.exit() - - self.recognizer.connect('finished', self.recognizer_finished) - - print "Using Options: ", self.options - - def read_commands(self): - # Read the commands file - file_lines = open(command_file) - strings = open(strings_file, "w") - for line in file_lines: - print line - # Trim the white spaces - line = line.strip() - # If the line has length and the first char isn't a hash - if len(line) and line[0]!="#": - # This is a parsible line - (key,value) = line.split(":",1) - print key, value - self.commands[key.strip().lower()] = value.strip() - strings.write( key.strip()+"\n") - # Close the strings file - strings.close() - - def load_options(self): - # Is there an opt file? - try: - opt_fh = open(opt_file) - text = opt_fh.read() - self.options = yaml.load(text) - except: - pass - - - def log_history(self,text): - if self.options['history']: - self.history.append(text) - if len(self.history) > self.options['history']: - # Pop off the first item - self.history.pop(0) - - # Open and truncate the blather history file - hfile = open(history_file, "w") - for line in self.history: - hfile.write( line+"\n") - # Close the file - hfile.close() - - def run_command(self, cmd): - '''Print the command, then run it''' - print cmd - subprocess.call(cmd, shell=True) - - def recognizer_finished(self, recognizer, text): - t = text.lower() - # Is there a matching command? - if self.commands.has_key( t ): - # Run the valid_sentence_command if there is a valid sentence command - if self.options['valid_sentence_command']: - subprocess.call(self.options['valid_sentence_command'], shell=True) - cmd = self.commands[t] - # Should we be passing words? - if self.options['pass_words']: - cmd += " " + t - self.run_command(cmd) - else: - self.run_command(cmd) - self.log_history(text) - else: - # Run the invalid_sentence_command if there is an invalid sentence command - if self.options['invalid_sentence_command']: - subprocess.call(self.options['invalid_sentence_command'], shell=True) - print "no matching command %s" % t - # If there is a UI and we are not continuous listen - if self.ui: - if not self.continuous_listen: - # Stop listening - self.recognizer.pause() - # Let the UI know that there is a finish - self.ui.finished(t) - - def run(self): - if self.ui: - self.ui.run() - else: - blather.recognizer.listen() - - def quit(self): - sys.exit() - - def process_command(self, UI, command): - print command - if command == "listen": - self.recognizer.listen() - elif command == "stop": - self.recognizer.pause() - elif command == "continuous_listen": - self.continuous_listen = True - self.recognizer.listen() - elif command == "continuous_stop": - self.continuous_listen = False - self.recognizer.pause() - elif command == "quit": - self.quit() - - def load_resource(self,string): - local_data = os.path.join(os.path.dirname(__file__), 'data') - paths = ["/usr/share/blather/","/usr/local/share/blather", local_data] - for path in paths: - resource = os.path.join(path, string) - if os.path.exists( resource ): - return resource - # If we get this far, no resource was found - return False - - -if __name__ == "__main__": - parser = OptionParser() - parser.add_option("-i", "--interface", type="string", dest="interface", - action='store', - help="Interface to use (if any). 'q' for Qt, 'g' for GTK, 'gt' for GTK system tray icon") - - parser.add_option("-c", "--continuous", - action="store_true", dest="continuous", default=False, - help="starts interface with 'continuous' listen enabled") - - parser.add_option("-p", "--pass-words", - action="store_true", dest="pass_words", default=False, - help="passes the recognized words as arguments to the shell command") - - parser.add_option("-o", "--override", - action="store_true", dest="override", default=False, - help="override config file with command line options") - - parser.add_option("-H", "--history", type="int", - action="store", dest="history", - help="number of commands to store in history file") - - parser.add_option("-m", "--microphone", type="int", - action="store", dest="microphone", default=None, - help="Audio input card to use (if other than system default)") - - parser.add_option("--valid-sentence-command", type="string", dest="valid_sentence_command", - action='store', - help="command to run when a valid sentence is detected") - - parser.add_option( "--invalid-sentence-command", type="string", dest="invalid_sentence_command", - action='store', - help="command to run when an invalid sentence is detected") - - (options, args) = parser.parse_args() - # Make our blather object - blather = Blather(options) - # Init gobject threads - GObject.threads_init() - # We want a main loop - main_loop = GObject.MainLoop() - # Handle sigint - signal.signal(signal.SIGINT, signal.SIG_DFL) - # Run the blather - blather.run() - # Start the main loop - try: - main_loop.run() - except: - print "time to quit" - main_loop.quit() - sys.exit() - diff --git a/GtkTrayUI.py b/GtkTrayUI.py deleted file mode 100644 index dda153d..0000000 --- a/GtkTrayUI.py +++ /dev/null @@ -1,97 +0,0 @@ -import sys -from gi.repository import GObject -# Gtk -from gi.repository import Gtk, Gdk - -class UI(GObject.GObject): - __gsignals__ = { - 'command' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)) - } - - def __init__(self, args, continuous): - GObject.GObject.__init__(self) - self.continuous = continuous - - self.statusicon = Gtk.StatusIcon() - self.statusicon.set_title("Blather") - self.statusicon.set_name("Blather") - self.statusicon.set_tooltip_text("Blather - Idle") - self.statusicon.set_has_tooltip(True) - self.statusicon.connect("activate", self.continuous_toggle) - self.statusicon.connect("popup-menu", self.popup_menu) - - self.menu = Gtk.Menu() - self.menu_listen = Gtk.MenuItem('Listen') - self.menu_continuous = Gtk.CheckMenuItem('Continuous') - self.menu_quit = Gtk.MenuItem('Quit') - self.menu.append(self.menu_listen) - self.menu.append(self.menu_continuous) - self.menu.append(self.menu_quit) - self.menu_listen.connect("activate", self.toggle_listen) - self.menu_continuous.connect("toggled", self.toggle_continuous) - self.menu_quit.connect("activate", self.quit) - self.menu.show_all() - - def continuous_toggle(self, item): - checked = self.menu_continuous.get_active() - self.menu_continuous.set_active(not checked) - - def toggle_continuous(self, item): - checked = self.menu_continuous.get_active() - self.menu_listen.set_sensitive(not checked) - if checked: - self.menu_listen.set_label("Listen") - self.emit('command', "continuous_listen") - self.statusicon.set_tooltip_text("Blather - Listening") - self.set_icon_active() - else: - self.set_icon_inactive() - self.statusicon.set_tooltip_text("Blather - Idle") - self.emit('command', "continuous_stop") - - def toggle_listen(self, item): - val = self.menu_listen.get_label() - if val == "Listen": - self.emit("command", "listen") - self.menu_listen.set_label("Stop") - self.statusicon.set_tooltip_text("Blather - Listening") - else: - self.icon_inactive() - self.menu_listen.set_label("Listen") - self.emit("command", "stop") - self.statusicon.set_tooltip_text("Blather - Idle") - - def popup_menu(self, item, button, time): - self.menu.popup(None, None, Gtk.StatusIcon.position_menu, item, button, time) - - def run(self): - # Set the icon - self.set_icon_inactive() - if self.continuous: - self.menu_continuous.set_active(True) - self.set_icon_active() - else: - self.menu_continuous.set_active(False) - self.statusicon.set_visible(True) - - def quit(self, item): - self.statusicon.set_visible(False) - self.emit("command", "quit") - - def finished(self, text): - if not self.menu_continuous.get_active(): - self.menu_listen.set_label("Listen") - self.statusicon.set_from_icon_name("blather_stopped") - self.statusicon.set_tooltip_text("Blather - Idle") - - def set_icon_active_asset(self, i): - self.icon_active = i - - def set_icon_inactive_asset(self, i): - self.icon_inactive = i - - def set_icon_active(self): - self.statusicon.set_from_file(self.icon_active) - - def set_icon_inactive(self): - self.statusicon.set_from_file(self.icon_inactive) diff --git a/GtkUI.py b/GtkUI.py deleted file mode 100644 index 2e0fc17..0000000 --- a/GtkUI.py +++ /dev/null @@ -1,109 +0,0 @@ -# This is part of Blather -# -- this code is licensed GPLv3 -# Copyright 2013 Jezra -import sys -from gi.repository import GObject -# Gtk -from gi.repository import Gtk, Gdk - -class UI(GObject.GObject): - __gsignals__ = { - 'command' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)) - } - - def __init__(self,args, continuous): - GObject.GObject.__init__(self) - self.continuous = continuous - # Make a window - self.window = Gtk.Window(Gtk.WindowType.TOPLEVEL) - self.window.connect("delete_event", self.delete_event) - # Give the window a name - self.window.set_title("BlatherGtk") - self.window.set_resizable(False) - - layout = Gtk.VBox() - self.window.add(layout) - # Make a listen/stop button - self.lsbutton = Gtk.Button("Listen") - layout.add(self.lsbutton) - # Make a continuous button - self.ccheckbox = Gtk.CheckButton("Continuous Listen") - layout.add(self.ccheckbox) - - # Connect the buttons - self.lsbutton.connect("clicked",self.lsbutton_clicked) - self.ccheckbox.connect("clicked",self.ccheckbox_clicked) - - # Add a label to the UI to display the last command - self.label = Gtk.Label() - layout.add(self.label) - - # Create an accellerator group for this window - accel = Gtk.AccelGroup() - # Add the ctrl+q to quit - accel.connect(Gdk.keyval_from_name('q'), Gdk.ModifierType.CONTROL_MASK, - Gtk.AccelFlags.VISIBLE, self.accel_quit) - # Lock the group - accel.lock() - # Add the group to the window - self.window.add_accel_group(accel) - - def ccheckbox_clicked(self, widget): - checked = self.ccheckbox.get_active() - self.lsbutton.set_sensitive(not checked) - if checked: - self.lsbutton_stopped() - self.emit('command', "continuous_listen") - self.set_icon_active() - else: - self.emit('command', "continuous_stop") - self.set_icon_inactive() - - def lsbutton_stopped(self): - self.lsbutton.set_label("Listen") - - def lsbutton_clicked(self, button): - val = self.lsbutton.get_label() - if val == "Listen": - self.emit("command", "listen") - self.lsbutton.set_label("Stop") - # Clear the label - self.label.set_text("") - self.set_icon_active() - else: - self.lsbutton_stopped() - self.emit("command", "stop") - self.set_icon_inactive() - - def run(self): - # Set the default icon - self.set_icon_inactive() - self.window.show_all() - if self.continuous: - self.set_icon_active() - self.ccheckbox.set_active(True) - - def accel_quit(self, accel_group, acceleratable, keyval, modifier): - self.emit("command", "quit") - - def delete_event(self, x, y): - self.emit("command", "quit") - - def finished(self, text): - # If the continuous isn't pressed - if not self.ccheckbox.get_active(): - self.lsbutton_stopped() - self.set_icon_inactive() - self.label.set_text(text) - - def set_icon_active_asset(self, i): - self.icon_active = i - - def set_icon_inactive_asset(self, i): - self.icon_inactive = i - - def set_icon_active(self): - Gtk.Window.set_default_icon_from_file(self.icon_active) - - def set_icon_inactive(self): - Gtk.Window.set_default_icon_from_file(self.icon_inactive) diff --git a/QtUI.py b/QtUI.py deleted file mode 100644 index 728ff80..0000000 --- a/QtUI.py +++ /dev/null @@ -1,115 +0,0 @@ -#This is part of Blather -# -- this code is licensed GPLv3 -# Copyright 2013 Jezra -import sys -import gobject -# Qt stuff -from PySide.QtCore import Signal, Qt -from PySide.QtGui import QApplication, QWidget, QMainWindow, QVBoxLayout -from PySide.QtGui import QLabel, QPushButton, QCheckBox, QIcon, QAction - -class UI(gobject.GObject): - __gsignals__ = { - 'command' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING,)) - } - - def __init__(self,args,continuous): - self.continuous = continuous - gobject.GObject.__init__(self) - #start by making our app - self.app = QApplication(args) - #make a window - self.window = QMainWindow() - #give the window a name - self.window.setWindowTitle("BlatherQt") - self.window.setMaximumSize(400,200) - center = QWidget() - self.window.setCentralWidget(center) - - layout = QVBoxLayout() - center.setLayout(layout) - #make a listen/stop button - self.lsbutton = QPushButton("Listen") - layout.addWidget(self.lsbutton) - #make a continuous button - self.ccheckbox = QCheckBox("Continuous Listen") - layout.addWidget(self.ccheckbox) - - #connect the buttons - self.lsbutton.clicked.connect(self.lsbutton_clicked) - self.ccheckbox.clicked.connect(self.ccheckbox_clicked) - - #add a label to the UI to display the last command - self.label = QLabel() - layout.addWidget(self.label) - - #add the actions for quiting - quit_action = QAction(self.window) - quit_action.setShortcut('Ctrl+Q') - quit_action.triggered.connect(self.accel_quit) - self.window.addAction(quit_action) - - def accel_quit(self): - #emit the quit - self.emit("command", "quit") - - def ccheckbox_clicked(self): - checked = self.ccheckbox.isChecked() - if checked: - #disable lsbutton - self.lsbutton.setEnabled(False) - self.lsbutton_stopped() - self.emit('command', "continuous_listen") - self.set_icon_active() - else: - self.lsbutton.setEnabled(True) - self.emit('command', "continuous_stop") - self.set_icon_inactive() - - def lsbutton_stopped(self): - self.lsbutton.setText("Listen") - - def lsbutton_clicked(self): - val = self.lsbutton.text() - if val == "Listen": - self.emit("command", "listen") - self.lsbutton.setText("Stop") - #clear the label - self.label.setText("") - self.set_icon_active() - else: - self.lsbutton_stopped() - self.emit("command", "stop") - self.set_icon_inactive() - - def run(self): - self.set_icon_inactive() - self.window.show() - if self.continuous: - self.set_icon_active() - self.ccheckbox.setCheckState(Qt.Checked) - self.ccheckbox_clicked() - self.app.exec_() - self.emit("command", "quit") - - def finished(self, text): - #if the continuous isn't pressed - if not self.ccheckbox.isChecked(): - self.lsbutton_stopped() - self.label.setText(text) - - def set_icon(self, icon): - self.window.setWindowIcon(QIcon(icon)) - - def set_icon_active_asset(self, i): - self.icon_active = i - - def set_icon_inactive_asset(self, i): - self.icon_inactive = i - - def set_icon_active(self): - self.window.setWindowIcon(QIcon(self.icon_active)) - - def set_icon_inactive(self): - self.window.setWindowIcon(QIcon(self.icon_inactive)) - diff --git a/Recognizer.py b/Recognizer.py deleted file mode 100755 index e962ea3..0000000 --- a/Recognizer.py +++ /dev/null @@ -1,65 +0,0 @@ -# This is part of Blather -# -- this code is licensed GPLv3 -# Copyright 2013 Jezra - -import gi -gi.require_version('Gst', '1.0') -from gi.repository import GObject, Gst -GObject.threads_init() -Gst.init(None) -import os.path -import sys - -# Define some global variables -this_dir = os.path.dirname( os.path.abspath(__file__) ) - - -class Recognizer(GObject.GObject): - __gsignals__ = { - 'finished' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)) - } - - def __init__(self, language_file, dictionary_file, src = None): - GObject.GObject.__init__(self) - self.commands = {} - if src: - audio_src = 'alsasrc device="hw:%d,0"' % (src) - else: - audio_src = 'autoaudiosrc' - - # Build the pipeline - cmd = audio_src+' ! audioconvert ! audioresample ! pocketsphinx name=asr ! appsink sync=false' - try: - self.pipeline=Gst.parse_launch( cmd ) - except Exception, e: - print e.message - print "You may need to install gstreamer1.0-pocketsphinx" - raise e - - bus = self.pipeline.get_bus() - bus.add_signal_watch() - - # Get the Auto Speech Recognition piece - asr=self.pipeline.get_by_name('asr') - bus.connect('message::element', self.result) - asr.set_property('lm', language_file) - asr.set_property('dict', dictionary_file) - asr.set_property('configured', True) - - def listen(self): - self.pipeline.set_state(Gst.State.PLAYING) - - def pause(self): - self.pipeline.set_state(Gst.State.PAUSED) - - def result(self, bus, msg): - msg_struct = msg.get_structure() - # Ignore messages that aren't from pocketsphinx - msgtype = msg_struct.get_name() - if msgtype != 'pocketsphinx': - return - - # If we have a final command, send it for processing - command = msg_struct.get_string('hypothesis') - if command != '' and msg_struct.get_boolean('final')[1]: - self.emit("finished", command) diff --git a/blather.py b/blather.py new file mode 100755 index 0000000..3853314 --- /dev/null +++ b/blather.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python2 + +# This is part of Kaylee +# -- this code is licensed GPLv3 +# Copyright 2013 Jezra +# Copyright 2015 Clayton G. Hobbs + +import sys +import signal +from gi.repository import GObject +import os.path +import subprocess +from optparse import OptionParser +try: + import yaml +except: + print "YAML is not supported. ~/.config/blather/options.yaml will not function" + +from recognizer import Recognizer + +# Where are the files? +conf_dir = os.path.expanduser("~/.config/blather") +lang_dir = os.path.join(conf_dir, "language") +command_file = os.path.join(conf_dir, "commands.conf") +strings_file = os.path.join(conf_dir, "sentences.corpus") +history_file = os.path.join(conf_dir, "blather.history") +opt_file = os.path.join(conf_dir, "options.yaml") +lang_file = os.path.join(lang_dir,'lm') +dic_file = os.path.join(lang_dir,'dic') +# Make the lang_dir if it doesn't exist +if not os.path.exists(lang_dir): + os.makedirs(lang_dir) + +class Blather: + + def __init__(self, opts): + # Import the recognizer so Gst doesn't clobber our -h + self.ui = None + self.options = {} + ui_continuous_listen = False + self.continuous_listen = False + + self.commands = {} + + # Read the commands + self.read_commands() + + # Load the options file + self.load_options() + + # Merge the opts + for k,v in opts.__dict__.items(): + if (not k in self.options) or opts.override: + self.options[k] = v + + if self.options['interface'] != None: + if self.options['interface'] == "g": + from gtkui import UI + elif self.options['interface'] == "gt": + from gtktrayui import UI + else: + print "no GUI defined" + sys.exit() + + self.ui = UI(args, self.options['continuous']) + self.ui.connect("command", self.process_command) + # Can we load the icon resource? + icon = self.load_resource("icon.png") + if icon: + self.ui.set_icon_active_asset(icon) + # Can we load the icon_inactive resource? + icon_inactive = self.load_resource("icon_inactive.png") + if icon_inactive: + self.ui.set_icon_inactive_asset(icon_inactive) + + if self.options['history']: + self.history = [] + + # Create the recognizer + try: + self.recognizer = Recognizer(lang_file, dic_file, self.options['microphone']) + except Exception, e: + #no recognizer? bummer + print 'error making recognizer' + sys.exit() + + self.recognizer.connect('finished', self.recognizer_finished) + + print "Using Options: ", self.options + + def read_commands(self): + # Read the commands file + file_lines = open(command_file) + strings = open(strings_file, "w") + for line in file_lines: + print line + # Trim the white spaces + line = line.strip() + # If the line has length and the first char isn't a hash + if len(line) and line[0]!="#": + # This is a parsible line + (key,value) = line.split(":",1) + print key, value + self.commands[key.strip().lower()] = value.strip() + strings.write( key.strip()+"\n") + # Close the strings file + strings.close() + + def load_options(self): + # Is there an opt file? + try: + opt_fh = open(opt_file) + text = opt_fh.read() + self.options = yaml.load(text) + except: + pass + + + def log_history(self,text): + if self.options['history']: + self.history.append(text) + if len(self.history) > self.options['history']: + # Pop off the first item + self.history.pop(0) + + # Open and truncate the blather history file + hfile = open(history_file, "w") + for line in self.history: + hfile.write( line+"\n") + # Close the file + hfile.close() + + def run_command(self, cmd): + '''Print the command, then run it''' + print cmd + subprocess.call(cmd, shell=True) + + def recognizer_finished(self, recognizer, text): + t = text.lower() + # Is there a matching command? + if self.commands.has_key( t ): + # Run the valid_sentence_command if there is a valid sentence command + if self.options['valid_sentence_command']: + subprocess.call(self.options['valid_sentence_command'], shell=True) + cmd = self.commands[t] + # Should we be passing words? + if self.options['pass_words']: + cmd += " " + t + self.run_command(cmd) + else: + self.run_command(cmd) + self.log_history(text) + else: + # Run the invalid_sentence_command if there is an invalid sentence command + if self.options['invalid_sentence_command']: + subprocess.call(self.options['invalid_sentence_command'], shell=True) + print "no matching command %s" % t + # If there is a UI and we are not continuous listen + if self.ui: + if not self.continuous_listen: + # Stop listening + self.recognizer.pause() + # Let the UI know that there is a finish + self.ui.finished(t) + + def run(self): + if self.ui: + self.ui.run() + else: + blather.recognizer.listen() + + def quit(self): + sys.exit() + + def process_command(self, UI, command): + print command + if command == "listen": + self.recognizer.listen() + elif command == "stop": + self.recognizer.pause() + elif command == "continuous_listen": + self.continuous_listen = True + self.recognizer.listen() + elif command == "continuous_stop": + self.continuous_listen = False + self.recognizer.pause() + elif command == "quit": + self.quit() + + def load_resource(self,string): + local_data = os.path.join(os.path.dirname(__file__), 'data') + paths = ["/usr/share/blather/","/usr/local/share/blather", local_data] + for path in paths: + resource = os.path.join(path, string) + if os.path.exists( resource ): + return resource + # If we get this far, no resource was found + return False + + +if __name__ == "__main__": + parser = OptionParser() + parser.add_option("-i", "--interface", type="string", dest="interface", + action='store', + help="Interface to use (if any). 'g' for GTK or 'gt' for GTK system tray icon") + + parser.add_option("-c", "--continuous", + action="store_true", dest="continuous", default=False, + help="starts interface with 'continuous' listen enabled") + + parser.add_option("-p", "--pass-words", + action="store_true", dest="pass_words", default=False, + help="passes the recognized words as arguments to the shell command") + + parser.add_option("-o", "--override", + action="store_true", dest="override", default=False, + help="override config file with command line options") + + parser.add_option("-H", "--history", type="int", + action="store", dest="history", + help="number of commands to store in history file") + + parser.add_option("-m", "--microphone", type="int", + action="store", dest="microphone", default=None, + help="Audio input card to use (if other than system default)") + + parser.add_option("--valid-sentence-command", type="string", dest="valid_sentence_command", + action='store', + help="command to run when a valid sentence is detected") + + parser.add_option( "--invalid-sentence-command", type="string", dest="invalid_sentence_command", + action='store', + help="command to run when an invalid sentence is detected") + + (options, args) = parser.parse_args() + # Make our blather object + blather = Blather(options) + # Init gobject threads + GObject.threads_init() + # We want a main loop + main_loop = GObject.MainLoop() + # Handle sigint + signal.signal(signal.SIGINT, signal.SIG_DFL) + # Run the blather + blather.run() + # Start the main loop + try: + main_loop.run() + except: + print "time to quit" + main_loop.quit() + sys.exit() + diff --git a/gtktrayui.py b/gtktrayui.py new file mode 100644 index 0000000..cb66259 --- /dev/null +++ b/gtktrayui.py @@ -0,0 +1,105 @@ +# This is part of Kaylee +# -- this code is licensed GPLv3 +# Copyright 2013 Jezra +# Copyright 2015 Clayton G. Hobbs + +import sys +from gi.repository import GObject +# Gtk +from gi.repository import Gtk, Gdk + +class UI(GObject.GObject): + __gsignals__ = { + 'command' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)) + } + idle_text = "Kaylee - Idle" + listening_text = "Kaylee - Listening" + + def __init__(self, args, continuous): + GObject.GObject.__init__(self) + self.continuous = continuous + + self.statusicon = Gtk.StatusIcon() + self.statusicon.set_title("Kaylee") + self.statusicon.set_name("Kaylee") + self.statusicon.set_tooltip_text(self.idle_text) + self.statusicon.set_has_tooltip(True) + self.statusicon.connect("activate", self.continuous_toggle) + self.statusicon.connect("popup-menu", self.popup_menu) + + self.menu = Gtk.Menu() + self.menu_listen = Gtk.MenuItem('Listen') + self.menu_continuous = Gtk.CheckMenuItem('Continuous') + self.menu_quit = Gtk.MenuItem('Quit') + self.menu.append(self.menu_listen) + self.menu.append(self.menu_continuous) + self.menu.append(self.menu_quit) + self.menu_listen.connect("activate", self.toggle_listen) + self.menu_continuous.connect("toggled", self.toggle_continuous) + self.menu_quit.connect("activate", self.quit) + self.menu.show_all() + + def continuous_toggle(self, item): + checked = self.menu_continuous.get_active() + self.menu_continuous.set_active(not checked) + + def toggle_continuous(self, item): + checked = self.menu_continuous.get_active() + self.menu_listen.set_sensitive(not checked) + if checked: + self.menu_listen.set_label("Listen") + self.emit('command', "continuous_listen") + self.statusicon.set_tooltip_text(self.listening_text) + self.set_icon_active() + else: + self.set_icon_inactive() + self.statusicon.set_tooltip_text(self.idle_text) + self.emit('command', "continuous_stop") + + def toggle_listen(self, item): + val = self.menu_listen.get_label() + if val == "Listen": + self.set_icon_active() + self.emit("command", "listen") + self.menu_listen.set_label("Stop") + self.statusicon.set_tooltip_text(self.listening_text) + else: + self.set_icon_inactive() + self.menu_listen.set_label("Listen") + self.emit("command", "stop") + self.statusicon.set_tooltip_text(self.idle_text) + + def popup_menu(self, item, button, time): + self.menu.popup(None, None, Gtk.StatusIcon.position_menu, item, button, time) + + def run(self): + # Set the icon + self.set_icon_inactive() + if self.continuous: + self.menu_continuous.set_active(True) + self.set_icon_active() + else: + self.menu_continuous.set_active(False) + self.statusicon.set_visible(True) + + def quit(self, item): + self.statusicon.set_visible(False) + self.emit("command", "quit") + + def finished(self, text): + if not self.menu_continuous.get_active(): + self.menu_listen.set_label("Listen") + self.set_icon_inactive() + self.statusicon.set_tooltip_text(self.idle_text) + + def set_icon_active_asset(self, i): + self.icon_active = i + + def set_icon_inactive_asset(self, i): + self.icon_inactive = i + + def set_icon_active(self): + self.statusicon.set_from_file(self.icon_active) + + def set_icon_inactive(self): + self.statusicon.set_from_file(self.icon_inactive) diff --git a/gtkui.py b/gtkui.py new file mode 100644 index 0000000..7c3600d --- /dev/null +++ b/gtkui.py @@ -0,0 +1,111 @@ +# This is part of Kaylee +# -- this code is licensed GPLv3 +# Copyright 2013 Jezra +# Copyright 2015 Clayton G. Hobbs + +import sys +from gi.repository import GObject +# Gtk +from gi.repository import Gtk, Gdk + +class UI(GObject.GObject): + __gsignals__ = { + 'command' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)) + } + + def __init__(self,args, continuous): + GObject.GObject.__init__(self) + self.continuous = continuous + # Make a window + self.window = Gtk.Window(Gtk.WindowType.TOPLEVEL) + self.window.connect("delete_event", self.delete_event) + # Give the window a name + self.window.set_title("Kaylee") + self.window.set_resizable(False) + + layout = Gtk.VBox() + self.window.add(layout) + # Make a listen/stop button + self.lsbutton = Gtk.Button("Listen") + layout.add(self.lsbutton) + # Make a continuous button + self.ccheckbox = Gtk.CheckButton("Continuous Listen") + layout.add(self.ccheckbox) + + # Connect the buttons + self.lsbutton.connect("clicked",self.lsbutton_clicked) + self.ccheckbox.connect("clicked",self.ccheckbox_clicked) + + # Add a label to the UI to display the last command + self.label = Gtk.Label() + layout.add(self.label) + + # Create an accellerator group for this window + accel = Gtk.AccelGroup() + # Add the ctrl+q to quit + accel.connect(Gdk.keyval_from_name('q'), Gdk.ModifierType.CONTROL_MASK, + Gtk.AccelFlags.VISIBLE, self.accel_quit) + # Lock the group + accel.lock() + # Add the group to the window + self.window.add_accel_group(accel) + + def ccheckbox_clicked(self, widget): + checked = self.ccheckbox.get_active() + self.lsbutton.set_sensitive(not checked) + if checked: + self.lsbutton_stopped() + self.emit('command', "continuous_listen") + self.set_icon_active() + else: + self.emit('command', "continuous_stop") + self.set_icon_inactive() + + def lsbutton_stopped(self): + self.lsbutton.set_label("Listen") + + def lsbutton_clicked(self, button): + val = self.lsbutton.get_label() + if val == "Listen": + self.emit("command", "listen") + self.lsbutton.set_label("Stop") + # Clear the label + self.label.set_text("") + self.set_icon_active() + else: + self.lsbutton_stopped() + self.emit("command", "stop") + self.set_icon_inactive() + + def run(self): + # Set the default icon + self.set_icon_inactive() + self.window.show_all() + if self.continuous: + self.set_icon_active() + self.ccheckbox.set_active(True) + + def accel_quit(self, accel_group, acceleratable, keyval, modifier): + self.emit("command", "quit") + + def delete_event(self, x, y): + self.emit("command", "quit") + + def finished(self, text): + # If the continuous isn't pressed + if not self.ccheckbox.get_active(): + self.lsbutton_stopped() + self.set_icon_inactive() + self.label.set_text(text) + + def set_icon_active_asset(self, i): + self.icon_active = i + + def set_icon_inactive_asset(self, i): + self.icon_inactive = i + + def set_icon_active(self): + Gtk.Window.set_default_icon_from_file(self.icon_active) + + def set_icon_inactive(self): + Gtk.Window.set_default_icon_from_file(self.icon_inactive) diff --git a/recognizer.py b/recognizer.py new file mode 100755 index 0000000..038d4cf --- /dev/null +++ b/recognizer.py @@ -0,0 +1,66 @@ +# This is part of Kaylee +# -- this code is licensed GPLv3 +# Copyright 2013 Jezra +# Copyright 2015 Clayton G. Hobbs + +import gi +gi.require_version('Gst', '1.0') +from gi.repository import GObject, Gst +GObject.threads_init() +Gst.init(None) +import os.path +import sys + +# Define some global variables +this_dir = os.path.dirname( os.path.abspath(__file__) ) + + +class Recognizer(GObject.GObject): + __gsignals__ = { + 'finished' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)) + } + + def __init__(self, language_file, dictionary_file, src = None): + GObject.GObject.__init__(self) + self.commands = {} + if src: + audio_src = 'alsasrc device="hw:%d,0"' % (src) + else: + audio_src = 'autoaudiosrc' + + # Build the pipeline + cmd = audio_src+' ! audioconvert ! audioresample ! pocketsphinx name=asr ! appsink sync=false' + try: + self.pipeline=Gst.parse_launch( cmd ) + except Exception, e: + print e.message + print "You may need to install gstreamer1.0-pocketsphinx" + raise e + + bus = self.pipeline.get_bus() + bus.add_signal_watch() + + # Get the Auto Speech Recognition piece + asr=self.pipeline.get_by_name('asr') + bus.connect('message::element', self.result) + asr.set_property('lm', language_file) + asr.set_property('dict', dictionary_file) + asr.set_property('configured', True) + + def listen(self): + self.pipeline.set_state(Gst.State.PLAYING) + + def pause(self): + self.pipeline.set_state(Gst.State.PAUSED) + + def result(self, bus, msg): + msg_struct = msg.get_structure() + # Ignore messages that aren't from pocketsphinx + msgtype = msg_struct.get_name() + if msgtype != 'pocketsphinx': + return + + # If we have a final command, send it for processing + command = msg_struct.get_string('hypothesis') + if command != '' and msg_struct.get_boolean('final')[1]: + self.emit("finished", command) -- cgit 1.4.1 From f176a2a9e307a77cdb0ce605a7fa946e4985aba1 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sat, 26 Dec 2015 22:46:39 -0500 Subject: Update README to reflect the missing Qt UI --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index feb0b46..cecd1e9 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ but adds a lot of features that go beyond the original purpose of Blather. 1. pocketsphinx 2. gstreamer-1.0 (and what ever plugin has pocket sphinx support) 3. gstreamer-1.0 base plugins (required for alsa) -4. pyside (only required for the Qt based UI) 5. pygtk (only required for the Gtk based UI) 6. pyyaml (only required for reading the options file) @@ -26,7 +25,6 @@ but adds a lot of features that go beyond the original purpose of Blather. 4. Download the resulting XXXX.lm file to the ~/.config/blather/language directory and rename to file to 'lm' 5. Download the resulting XXXX.dic file to the ~/.config/blather/language directory and rename to file to 'dic' 6. Run Blather.py - * For Qt GUI, run Blather.py -i q * For Gtk GUI, run Blather.py -i g * To start a UI in 'continuous' listen mode, use the -c flag * To use a microphone other than the system default, use the -m flag -- cgit 1.4.1 From 4c20e7d7ca1b9392e3b5a303eef3b5fa71f2b833 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sat, 26 Dec 2015 22:51:45 -0500 Subject: More changes to README This fork is called Kaylee, not Blather. Let's at least be consistent with referring to the program as Kaylee, even if the code is still in blather.py. --- README.md | 54 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index cecd1e9..f0bca34 100644 --- a/README.md +++ b/README.md @@ -18,45 +18,53 @@ but adds a lot of features that go beyond the original purpose of Blather. ## Usage -0. Move commands.tmp to ~/.config/blather/commands.conf and fill the file with sentences and command to run -1. Run Blather.py, this will generate ~/.config/blather/sentences.corpus based on sentences in the 'commands' file -2. Quit blather (there is a good chance it will just segfault) -3. Go to and upload the sentences.corpus file -4. Download the resulting XXXX.lm file to the ~/.config/blather/language directory and rename to file to 'lm' -5. Download the resulting XXXX.dic file to the ~/.config/blather/language directory and rename to file to 'dic' -6. Run Blather.py - * For Gtk GUI, run Blather.py -i g +1. Move commands.tmp to ~/.config/blather/commands.conf and fill the file with +sentences and command to run +2. Run blather.py, this will generate ~/.config/blather/sentences.corpus based +on sentences in the 'commands' file +3. Quit Kaylee (there is a good chance it will just segfault) +4. Go to and upload the +sentences.corpus file +5. Download the resulting XXXX.lm file to the ~/.config/blather/language +directory and rename to file to 'lm' +6. Download the resulting XXXX.dic file to the ~/.config/blather/language +directory and rename to file to 'dic' +7. Run blather.py + * For GTK UI, run blather.py -i g * To start a UI in 'continuous' listen mode, use the -c flag * To use a microphone other than the system default, use the -m flag -7. Start talking +8. Start talking -**Note:** to start Blather without needing to enter command line options all the time, copy options.yaml.tmp to ~/.config/blather/options.yaml and edit accordingly. +**Note:** to start Kaylee without needing to enter command line options all the +time, copy options.yaml.tmp to ~/.config/blather/options.yaml and edit +accordingly. ### Bonus -Once the sentences.corpus file has been created, run the language_updater.sh script to automate the process of creating and downloading language files. +Once the sentences.corpus file has been created, run the language_updater.sh +script to automate the process of creating and downloading language files. ### Examples -* To run blather with the GTK UI and start in continuous listen mode: -`./Blather.py -i g -c` +* To run Kaylee with the GTK UI and start in continuous listen mode: +`./blather.py -i g -c` -* To run blather with no UI and using a USB microphone recognized and device 2: -`./Blather.py -m 2` +* To run Kaylee with no UI and using a USB microphone recognized and device 2: +`./blather.py -m 2` -* To have blather pass the matched sentence to the executing command: - `./Blather.py -p` +* To have Kaylee pass the matched sentence to the executed command: +`./blather.py -p` - **explanation:** if the commands.conf contains: - **good morning world : example_command.sh** - then 3 arguments, 'good', 'morning', and 'world' would get passed to example_command.sh as - `example_command.sh good morning world` +**explanation:** if the commands.conf contains: +`good morning world: example_command.sh` +then 3 arguments, 'good', 'morning', and 'world' would get passed to +example_command.sh as `example_command.sh good morning world` * To run a command when a valid sentence has been detected: - `./Blather.py --valid-sentence-command=/path/to/command` + `./blather.py --valid-sentence-command=/path/to/command` **note:** this can be set in the options.yml file * To run a command when a invalid sentence has been detected: - `./Blather.py --invalid-sentence-command=/path/to/command` + `./blather.py --invalid-sentence-command=/path/to/command` **note:** this can be set in the options.yml file ### Finding the Device Number of a USB microphone -- cgit 1.4.1 From 900a3d8d2f8e96b5cdd20c95c076b42bfdec9d75 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sun, 27 Dec 2015 00:06:48 -0500 Subject: Update language automatically on startup We now have a hash.yaml file which contains a SHA256 hash of sentences.corpus. If this differs from the hash the file calculated when Kaylee sarts, the language is updated and the new hash is stored in hash.yaml. --- blather.py | 90 +++++++++++++++++++++++++++++++++++++++++++---------------- recognizer.py | 8 +++--- 2 files changed, 70 insertions(+), 28 deletions(-) diff --git a/blather.py b/blather.py index 3853314..2a2c452 100755 --- a/blather.py +++ b/blather.py @@ -5,16 +5,18 @@ # Copyright 2013 Jezra # Copyright 2015 Clayton G. Hobbs +from __future__ import print_function import sys import signal -from gi.repository import GObject +import hashlib import os.path import subprocess from optparse import OptionParser +from gi.repository import GObject try: import yaml except: - print "YAML is not supported. ~/.config/blather/options.yaml will not function" + print("YAML is not supported; unable to use config file") from recognizer import Recognizer @@ -25,8 +27,9 @@ command_file = os.path.join(conf_dir, "commands.conf") strings_file = os.path.join(conf_dir, "sentences.corpus") history_file = os.path.join(conf_dir, "blather.history") opt_file = os.path.join(conf_dir, "options.yaml") -lang_file = os.path.join(lang_dir,'lm') -dic_file = os.path.join(lang_dir,'dic') +hash_file = os.path.join(conf_dir, "hash.yaml") +lang_file = os.path.join(lang_dir, 'lm') +dic_file = os.path.join(lang_dir, 'dic') # Make the lang_dir if it doesn't exist if not os.path.exists(lang_dir): os.makedirs(lang_dir) @@ -34,7 +37,6 @@ if not os.path.exists(lang_dir): class Blather: def __init__(self, opts): - # Import the recognizer so Gst doesn't clobber our -h self.ui = None self.options = {} ui_continuous_listen = False @@ -48,8 +50,8 @@ class Blather: # Load the options file self.load_options() - # Merge the opts - for k,v in opts.__dict__.items(): + # Merge the options with the ones provided by command-line arguments + for k, v in opts.__dict__.items(): if (not k in self.options) or opts.override: self.options[k] = v @@ -59,7 +61,7 @@ class Blather: elif self.options['interface'] == "gt": from gtktrayui import UI else: - print "no GUI defined" + print("no GUI defined") sys.exit() self.ui = UI(args, self.options['continuous']) @@ -76,47 +78,51 @@ class Blather: if self.options['history']: self.history = [] + # Update the language if necessary + self.update_language() + # Create the recognizer try: self.recognizer = Recognizer(lang_file, dic_file, self.options['microphone']) - except Exception, e: - #no recognizer? bummer - print 'error making recognizer' + except Exception as e: + # No recognizer? bummer + print('error making recognizer') sys.exit() self.recognizer.connect('finished', self.recognizer_finished) - print "Using Options: ", self.options + print("Using Options: ", self.options) def read_commands(self): # Read the commands file file_lines = open(command_file) strings = open(strings_file, "w") for line in file_lines: - print line + print(line) # Trim the white spaces line = line.strip() # If the line has length and the first char isn't a hash if len(line) and line[0]!="#": # This is a parsible line - (key,value) = line.split(":",1) - print key, value + (key, value) = line.split(":", 1) + print(key, value) self.commands[key.strip().lower()] = value.strip() strings.write( key.strip()+"\n") # Close the strings file strings.close() def load_options(self): + """If possible, load options from the options.yaml file""" # Is there an opt file? try: opt_fh = open(opt_file) text = opt_fh.read() self.options = yaml.load(text) except: + # Do nothing if the options file cannot be loaded pass - - def log_history(self,text): + def log_history(self, text): if self.options['history']: self.history.append(text) if len(self.history) > self.options['history']: @@ -130,9 +136,45 @@ class Blather: # Close the file hfile.close() + def update_language(self): + """Update the language if its hash has changed""" + try: + # Load the stored hash from the hash file + try: + with open(hash_file, 'r') as f: + text = f.read() + hashes = yaml.load(text) + stored_hash = hashes['language'] + except (IOError, KeyError, TypeError): + # No stored hash + stored_hash = '' + + # Calculate the hash the language file has right now + hasher = hashlib.sha256() + with open(strings_file, 'rb') as sfile: + buf = sfile.read() + hasher.update(buf) + new_hash = hasher.hexdigest() + + # If the hashes differ + if stored_hash != new_hash: + # Update the language + # FIXME: Do this with Python, not Bash + self.run_command('./language_updater.sh') + # Store the new hash + new_hashes = {'language': new_hash} + with open(hash_file, 'w') as f: + f.write(yaml.dump(new_hashes)) + except Exception as e: + # Do nothing if the hash file cannot be loaded + # FIXME: This is kind of bad; maybe YAML should be mandatory. + print('error updating language') + print(e) + pass + def run_command(self, cmd): - '''Print the command, then run it''' - print cmd + """Print the command, then run it""" + print(cmd) subprocess.call(cmd, shell=True) def recognizer_finished(self, recognizer, text): @@ -154,7 +196,7 @@ class Blather: # Run the invalid_sentence_command if there is an invalid sentence command if self.options['invalid_sentence_command']: subprocess.call(self.options['invalid_sentence_command'], shell=True) - print "no matching command %s" % t + print("no matching command {0}".format(t)) # If there is a UI and we are not continuous listen if self.ui: if not self.continuous_listen: @@ -173,7 +215,7 @@ class Blather: sys.exit() def process_command(self, UI, command): - print command + print(command) if command == "listen": self.recognizer.listen() elif command == "stop": @@ -187,9 +229,9 @@ class Blather: elif command == "quit": self.quit() - def load_resource(self,string): + def load_resource(self, string): local_data = os.path.join(os.path.dirname(__file__), 'data') - paths = ["/usr/share/blather/","/usr/local/share/blather", local_data] + paths = ["/usr/share/blather/", "/usr/local/share/blather", local_data] for path in paths: resource = os.path.join(path, string) if os.path.exists( resource ): @@ -247,7 +289,7 @@ if __name__ == "__main__": try: main_loop.run() except: - print "time to quit" + print("time to quit") main_loop.quit() sys.exit() diff --git a/recognizer.py b/recognizer.py index 038d4cf..bb35480 100755 --- a/recognizer.py +++ b/recognizer.py @@ -32,9 +32,9 @@ class Recognizer(GObject.GObject): cmd = audio_src+' ! audioconvert ! audioresample ! pocketsphinx name=asr ! appsink sync=false' try: self.pipeline=Gst.parse_launch( cmd ) - except Exception, e: - print e.message - print "You may need to install gstreamer1.0-pocketsphinx" + except Exception as e: + print(e.message) + print("You may need to install gstreamer1.0-pocketsphinx") raise e bus = self.pipeline.get_bus() @@ -63,4 +63,4 @@ class Recognizer(GObject.GObject): # If we have a final command, send it for processing command = msg_struct.get_string('hypothesis') if command != '' and msg_struct.get_boolean('final')[1]: - self.emit("finished", command) + self.emit("finished", command) -- cgit 1.4.1 From 649927fb06ac5f4dced9141e5fb8250c5ffa5ff8 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sun, 27 Dec 2015 00:10:29 -0500 Subject: Mention self-updating feature in README --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f0bca34..b8200a1 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,11 @@ accordingly. ### Bonus -Once the sentences.corpus file has been created, run the language_updater.sh -script to automate the process of creating and downloading language files. +~Once the sentences.corpus file has been created, run the language_updater.sh +script to automate the process of creating and downloading language files.~ + +Kaylee now updates the language automatically. You should never need to run +language_updater.sh manually. ### Examples -- cgit 1.4.1 From 2099a52933697b336dbd8f4201441a875444084c Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sun, 27 Dec 2015 00:11:36 -0500 Subject: Whoops, two ~ on either side for strikethrough --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b8200a1..bb0b409 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,8 @@ accordingly. ### Bonus -~Once the sentences.corpus file has been created, run the language_updater.sh -script to automate the process of creating and downloading language files.~ +~~Once the sentences.corpus file has been created, run the language_updater.sh +script to automate the process of creating and downloading language files.~~ Kaylee now updates the language automatically. You should never need to run language_updater.sh manually. -- cgit 1.4.1 From 980c6c4c0554faa7162373a0b6caf8c495f212bd Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sun, 27 Dec 2015 00:17:36 -0500 Subject: Work towards Python 3 support --- blather.py | 2 +- gtkui.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/blather.py b/blather.py index 2a2c452..5406583 100755 --- a/blather.py +++ b/blather.py @@ -180,7 +180,7 @@ class Blather: def recognizer_finished(self, recognizer, text): t = text.lower() # Is there a matching command? - if self.commands.has_key( t ): + if t in self.commands: # Run the valid_sentence_command if there is a valid sentence command if self.options['valid_sentence_command']: subprocess.call(self.options['valid_sentence_command'], shell=True) diff --git a/gtkui.py b/gtkui.py index 7c3600d..db5174a 100644 --- a/gtkui.py +++ b/gtkui.py @@ -13,7 +13,7 @@ class UI(GObject.GObject): 'command' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)) } - def __init__(self,args, continuous): + def __init__(self, args, continuous): GObject.GObject.__init__(self) self.continuous = continuous # Make a window @@ -33,8 +33,8 @@ class UI(GObject.GObject): layout.add(self.ccheckbox) # Connect the buttons - self.lsbutton.connect("clicked",self.lsbutton_clicked) - self.ccheckbox.connect("clicked",self.ccheckbox_clicked) + self.lsbutton.connect("clicked", self.lsbutton_clicked) + self.ccheckbox.connect("clicked", self.ccheckbox_clicked) # Add a label to the UI to display the last command self.label = Gtk.Label() -- cgit 1.4.1 From d722e33a351e65077c636f7a92011a0e82a59973 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sun, 27 Dec 2015 02:13:24 -0500 Subject: Removed override command line option Personally, I don't see a point to a switch that must be turned on to make all other switches work at all. It's a confusing behaviour that made me think I somehow broke the GUIs. --- blather.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/blather.py b/blather.py index 5406583..6abd9fb 100755 --- a/blather.py +++ b/blather.py @@ -52,8 +52,7 @@ class Blather: # Merge the options with the ones provided by command-line arguments for k, v in opts.__dict__.items(): - if (not k in self.options) or opts.override: - self.options[k] = v + self.options[k] = v if self.options['interface'] != None: if self.options['interface'] == "g": @@ -254,10 +253,6 @@ if __name__ == "__main__": action="store_true", dest="pass_words", default=False, help="passes the recognized words as arguments to the shell command") - parser.add_option("-o", "--override", - action="store_true", dest="override", default=False, - help="override config file with command line options") - parser.add_option("-H", "--history", type="int", action="store", dest="history", help="number of commands to store in history file") -- cgit 1.4.1 From 5a944237bdc6bdbdb3d630be8819373c3b9508dd Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sun, 27 Dec 2015 11:56:08 -0500 Subject: Beginning work moving from YAML to JSON Mostly working for the options file now. Still some difficulty with command-line arguments, though; they're overriding the config file even when not specified. If I made them simply not get stored at all when not specified, there would be further problems when a configuration file is not present. Maybe I should make a whole new class to handle this. --- blather.py | 45 ++++++++++++++++++++++----------------------- gtktrayui.py | 2 ++ gtkui.py | 2 ++ options.json.tmp | 8 ++++++++ options.yaml.tmp | 8 -------- recognizer.py | 4 ++-- 6 files changed, 36 insertions(+), 33 deletions(-) create mode 100644 options.json.tmp delete mode 100644 options.yaml.tmp diff --git a/blather.py b/blather.py index 6abd9fb..a31f1ec 100755 --- a/blather.py +++ b/blather.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 # This is part of Kaylee # -- this code is licensed GPLv3 @@ -11,8 +11,9 @@ import signal import hashlib import os.path import subprocess -from optparse import OptionParser +from argparse import ArgumentParser from gi.repository import GObject +import json try: import yaml except: @@ -26,7 +27,7 @@ lang_dir = os.path.join(conf_dir, "language") command_file = os.path.join(conf_dir, "commands.conf") strings_file = os.path.join(conf_dir, "sentences.corpus") history_file = os.path.join(conf_dir, "blather.history") -opt_file = os.path.join(conf_dir, "options.yaml") +opt_file = os.path.join(conf_dir, "options.json") hash_file = os.path.join(conf_dir, "hash.yaml") lang_file = os.path.join(lang_dir, 'lm') dic_file = os.path.join(lang_dir, 'dic') @@ -50,9 +51,11 @@ class Blather: # Load the options file self.load_options() + print(opts) # Merge the options with the ones provided by command-line arguments - for k, v in opts.__dict__.items(): - self.options[k] = v + for k, v in vars(opts).items(): + if v is not None: + self.options[k] = v if self.options['interface'] != None: if self.options['interface'] == "g": @@ -111,15 +114,11 @@ class Blather: strings.close() def load_options(self): - """If possible, load options from the options.yaml file""" + """Load options from the options.json file""" # Is there an opt file? - try: - opt_fh = open(opt_file) - text = opt_fh.read() - self.options = yaml.load(text) - except: - # Do nothing if the options file cannot be loaded - pass + with open(opt_file, 'r') as f: + self.options = json.load(f) + print(self.options) def log_history(self, text): if self.options['history']: @@ -240,38 +239,38 @@ class Blather: if __name__ == "__main__": - parser = OptionParser() - parser.add_option("-i", "--interface", type="string", dest="interface", + parser = ArgumentParser() + parser.add_argument("-i", "--interface", type=str, dest="interface", action='store', help="Interface to use (if any). 'g' for GTK or 'gt' for GTK system tray icon") - parser.add_option("-c", "--continuous", + parser.add_argument("-c", "--continuous", action="store_true", dest="continuous", default=False, help="starts interface with 'continuous' listen enabled") - parser.add_option("-p", "--pass-words", + parser.add_argument("-p", "--pass-words", action="store_true", dest="pass_words", default=False, help="passes the recognized words as arguments to the shell command") - parser.add_option("-H", "--history", type="int", + parser.add_argument("-H", "--history", type=int, action="store", dest="history", help="number of commands to store in history file") - parser.add_option("-m", "--microphone", type="int", + parser.add_argument("-m", "--microphone", type=int, action="store", dest="microphone", default=None, help="Audio input card to use (if other than system default)") - parser.add_option("--valid-sentence-command", type="string", dest="valid_sentence_command", + parser.add_argument("--valid-sentence-command", type=str, dest="valid_sentence_command", action='store', help="command to run when a valid sentence is detected") - parser.add_option( "--invalid-sentence-command", type="string", dest="invalid_sentence_command", + parser.add_argument( "--invalid-sentence-command", type=str, dest="invalid_sentence_command", action='store', help="command to run when an invalid sentence is detected") - (options, args) = parser.parse_args() + args = parser.parse_args() # Make our blather object - blather = Blather(options) + blather = Blather(args) # Init gobject threads GObject.threads_init() # We want a main loop diff --git a/gtktrayui.py b/gtktrayui.py index cb66259..8c6c47c 100644 --- a/gtktrayui.py +++ b/gtktrayui.py @@ -4,8 +4,10 @@ # Copyright 2015 Clayton G. Hobbs import sys +import gi from gi.repository import GObject # Gtk +gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk class UI(GObject.GObject): diff --git a/gtkui.py b/gtkui.py index db5174a..b1e25ef 100644 --- a/gtkui.py +++ b/gtkui.py @@ -4,8 +4,10 @@ # Copyright 2015 Clayton G. Hobbs import sys +import gi from gi.repository import GObject # Gtk +gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk class UI(GObject.GObject): diff --git a/options.json.tmp b/options.json.tmp new file mode 100644 index 0000000..a3eba6f --- /dev/null +++ b/options.json.tmp @@ -0,0 +1,8 @@ +{ + "continuous": false, + "history": null, + "microphone": null, + "interface": null, + "valid_sentence_command": null, + "invalid_sentence_command": null +} diff --git a/options.yaml.tmp b/options.yaml.tmp deleted file mode 100644 index fd53a97..0000000 --- a/options.yaml.tmp +++ /dev/null @@ -1,8 +0,0 @@ -#This is a YAML file -#these options can be over-ridden by commandline arguments -continuous: false -history: null -microphone: null -interface: null -valid_sentence_command: null -invalid_sentence_command: null diff --git a/recognizer.py b/recognizer.py index bb35480..0a65b7a 100755 --- a/recognizer.py +++ b/recognizer.py @@ -29,9 +29,9 @@ class Recognizer(GObject.GObject): audio_src = 'autoaudiosrc' # Build the pipeline - cmd = audio_src+' ! audioconvert ! audioresample ! pocketsphinx name=asr ! appsink sync=false' + cmd = audio_src + ' ! audioconvert ! audioresample ! pocketsphinx name=asr ! appsink sync=false' try: - self.pipeline=Gst.parse_launch( cmd ) + self.pipeline = Gst.parse_launch( cmd ) except Exception as e: print(e.message) print("You may need to install gstreamer1.0-pocketsphinx") -- cgit 1.4.1 From 6347c5fdbb9b963dbd6c2cb5541ef047468806b4 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sun, 27 Dec 2015 12:12:19 -0500 Subject: Replaced last YAML bit with JSON Hashes are now stored in hash.json, not hash.yaml. Also, we use the XDG configuration directory rather than assuming we should use ~/.config/. Maybe I should also make use of XDG data and/or cache directories. --- README.md | 9 ++++----- blather.py | 67 ++++++++++++++++++++++++++------------------------------------ 2 files changed, 32 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index bb0b409..b2949b6 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,9 @@ but adds a lot of features that go beyond the original purpose of Blather. ## Requirements 1. pocketsphinx -2. gstreamer-1.0 (and what ever plugin has pocket sphinx support) -3. gstreamer-1.0 base plugins (required for alsa) -5. pygtk (only required for the Gtk based UI) -6. pyyaml (only required for reading the options file) +2. gstreamer-1.0 (and what ever plugin has pocketsphinx support) +3. gstreamer-1.0 base plugins (required for ALSA) +4. python-gobject (required for GStreamer and the GTK-based UI) **Note:** it may also be required to install `pocketsphinx-hmm-en-hub4wsj` @@ -36,7 +35,7 @@ directory and rename to file to 'dic' 8. Start talking **Note:** to start Kaylee without needing to enter command line options all the -time, copy options.yaml.tmp to ~/.config/blather/options.yaml and edit +time, copy options.json.tmp to ~/.config/blather/options.json and edit accordingly. ### Bonus diff --git a/blather.py b/blather.py index a31f1ec..b1c3777 100755 --- a/blather.py +++ b/blather.py @@ -12,23 +12,20 @@ import hashlib import os.path import subprocess from argparse import ArgumentParser -from gi.repository import GObject +from gi.repository import GObject, GLib import json -try: - import yaml -except: - print("YAML is not supported; unable to use config file") from recognizer import Recognizer # Where are the files? -conf_dir = os.path.expanduser("~/.config/blather") +conf_dir = os.path.expanduser(os.path.join(GLib.get_user_config_dir(), + "blather")) lang_dir = os.path.join(conf_dir, "language") command_file = os.path.join(conf_dir, "commands.conf") strings_file = os.path.join(conf_dir, "sentences.corpus") history_file = os.path.join(conf_dir, "blather.history") opt_file = os.path.join(conf_dir, "options.json") -hash_file = os.path.join(conf_dir, "hash.yaml") +hash_file = os.path.join(conf_dir, "hash.json") lang_file = os.path.join(lang_dir, 'lm') dic_file = os.path.join(lang_dir, 'dic') # Make the lang_dir if it doesn't exist @@ -136,39 +133,31 @@ class Blather: def update_language(self): """Update the language if its hash has changed""" + # Load the stored hash from the hash file try: - # Load the stored hash from the hash file - try: - with open(hash_file, 'r') as f: - text = f.read() - hashes = yaml.load(text) - stored_hash = hashes['language'] - except (IOError, KeyError, TypeError): - # No stored hash - stored_hash = '' - - # Calculate the hash the language file has right now - hasher = hashlib.sha256() - with open(strings_file, 'rb') as sfile: - buf = sfile.read() - hasher.update(buf) - new_hash = hasher.hexdigest() - - # If the hashes differ - if stored_hash != new_hash: - # Update the language - # FIXME: Do this with Python, not Bash - self.run_command('./language_updater.sh') - # Store the new hash - new_hashes = {'language': new_hash} - with open(hash_file, 'w') as f: - f.write(yaml.dump(new_hashes)) - except Exception as e: - # Do nothing if the hash file cannot be loaded - # FIXME: This is kind of bad; maybe YAML should be mandatory. - print('error updating language') - print(e) - pass + with open(hash_file, 'r') as f: + hashes = json.load(f) + stored_hash = hashes['language'] + except (IOError, KeyError, TypeError): + # No stored hash + stored_hash = '' + + # Calculate the hash the language file has right now + hasher = hashlib.sha256() + with open(strings_file, 'rb') as sfile: + buf = sfile.read() + hasher.update(buf) + new_hash = hasher.hexdigest() + + # If the hashes differ + if stored_hash != new_hash: + # Update the language + # FIXME: Do this with Python, not Bash + self.run_command('./language_updater.sh') + # Store the new hash + new_hashes = {'language': new_hash} + with open(hash_file, 'w') as f: + json.dump(new_hashes, f) def run_command(self, cmd): """Print the command, then run it""" -- cgit 1.4.1 From 443883b6898f2a75e64d8d4797dd448ef3aeda70 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sun, 27 Dec 2015 12:23:14 -0500 Subject: Minor formatting improvements --- blather.py | 8 ++++---- recognizer.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/blather.py b/blather.py index b1c3777..00d0c49 100755 --- a/blather.py +++ b/blather.py @@ -106,7 +106,7 @@ class Blather: (key, value) = line.split(":", 1) print(key, value) self.commands[key.strip().lower()] = value.strip() - strings.write( key.strip()+"\n") + strings.write(key.strip() + "\n") # Close the strings file strings.close() @@ -127,7 +127,7 @@ class Blather: # Open and truncate the blather history file hfile = open(history_file, "w") for line in self.history: - hfile.write( line+"\n") + hfile.write(line + "\n") # Close the file hfile.close() @@ -221,7 +221,7 @@ class Blather: paths = ["/usr/share/blather/", "/usr/local/share/blather", local_data] for path in paths: resource = os.path.join(path, string) - if os.path.exists( resource ): + if os.path.exists(resource): return resource # If we get this far, no resource was found return False @@ -253,7 +253,7 @@ if __name__ == "__main__": action='store', help="command to run when a valid sentence is detected") - parser.add_argument( "--invalid-sentence-command", type=str, dest="invalid_sentence_command", + parser.add_argument("--invalid-sentence-command", type=str, dest="invalid_sentence_command", action='store', help="command to run when an invalid sentence is detected") diff --git a/recognizer.py b/recognizer.py index 0a65b7a..06a2b87 100755 --- a/recognizer.py +++ b/recognizer.py @@ -12,7 +12,7 @@ import os.path import sys # Define some global variables -this_dir = os.path.dirname( os.path.abspath(__file__) ) +this_dir = os.path.dirname(os.path.abspath(__file__)) class Recognizer(GObject.GObject): @@ -31,7 +31,7 @@ class Recognizer(GObject.GObject): # Build the pipeline cmd = audio_src + ' ! audioconvert ! audioresample ! pocketsphinx name=asr ! appsink sync=false' try: - self.pipeline = Gst.parse_launch( cmd ) + self.pipeline = Gst.parse_launch(cmd) except Exception as e: print(e.message) print("You may need to install gstreamer1.0-pocketsphinx") -- cgit 1.4.1 From 87c0a1bfe93ffbe7eb67c41ea2b46fd5a7c1ab28 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sun, 27 Dec 2015 12:56:57 -0500 Subject: Improved config/argument behaviour The configuration file is now properly overridden by argument parsing. This was accomplished by loading the config file, then treating the specified options as a namespace for the ArgumentParser. This makes things from the config file get overridden iff they were specified on the command line (not simply from defaults set in the ArgumentParser). --- blather.py | 55 ++++++------------------------------------------------- config.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ recognizer.py | 12 +++++------- 3 files changed, 67 insertions(+), 56 deletions(-) create mode 100644 config.py diff --git a/blather.py b/blather.py index 00d0c49..d7fa14b 100755 --- a/blather.py +++ b/blather.py @@ -11,11 +11,11 @@ import signal import hashlib import os.path import subprocess -from argparse import ArgumentParser from gi.repository import GObject, GLib import json from recognizer import Recognizer +from config import Config # Where are the files? conf_dir = os.path.expanduser(os.path.join(GLib.get_user_config_dir(), @@ -24,7 +24,6 @@ lang_dir = os.path.join(conf_dir, "language") command_file = os.path.join(conf_dir, "commands.conf") strings_file = os.path.join(conf_dir, "sentences.corpus") history_file = os.path.join(conf_dir, "blather.history") -opt_file = os.path.join(conf_dir, "options.json") hash_file = os.path.join(conf_dir, "hash.json") lang_file = os.path.join(lang_dir, 'lm') dic_file = os.path.join(lang_dir, 'dic') @@ -34,7 +33,7 @@ if not os.path.exists(lang_dir): class Blather: - def __init__(self, opts): + def __init__(self): self.ui = None self.options = {} ui_continuous_listen = False @@ -46,13 +45,8 @@ class Blather: self.read_commands() # Load the options file - self.load_options() - - print(opts) - # Merge the options with the ones provided by command-line arguments - for k, v in vars(opts).items(): - if v is not None: - self.options[k] = v + self.config = Config() + self.options = vars(self.config.options) if self.options['interface'] != None: if self.options['interface'] == "g": @@ -63,7 +57,7 @@ class Blather: print("no GUI defined") sys.exit() - self.ui = UI(args, self.options['continuous']) + self.ui = UI(self.options, self.options['continuous']) self.ui.connect("command", self.process_command) # Can we load the icon resource? icon = self.load_resource("icon.png") @@ -110,13 +104,6 @@ class Blather: # Close the strings file strings.close() - def load_options(self): - """Load options from the options.json file""" - # Is there an opt file? - with open(opt_file, 'r') as f: - self.options = json.load(f) - print(self.options) - def log_history(self, text): if self.options['history']: self.history.append(text) @@ -228,38 +215,8 @@ class Blather: if __name__ == "__main__": - parser = ArgumentParser() - parser.add_argument("-i", "--interface", type=str, dest="interface", - action='store', - help="Interface to use (if any). 'g' for GTK or 'gt' for GTK system tray icon") - - parser.add_argument("-c", "--continuous", - action="store_true", dest="continuous", default=False, - help="starts interface with 'continuous' listen enabled") - - parser.add_argument("-p", "--pass-words", - action="store_true", dest="pass_words", default=False, - help="passes the recognized words as arguments to the shell command") - - parser.add_argument("-H", "--history", type=int, - action="store", dest="history", - help="number of commands to store in history file") - - parser.add_argument("-m", "--microphone", type=int, - action="store", dest="microphone", default=None, - help="Audio input card to use (if other than system default)") - - parser.add_argument("--valid-sentence-command", type=str, dest="valid_sentence_command", - action='store', - help="command to run when a valid sentence is detected") - - parser.add_argument("--invalid-sentence-command", type=str, dest="invalid_sentence_command", - action='store', - help="command to run when an invalid sentence is detected") - - args = parser.parse_args() # Make our blather object - blather = Blather(args) + blather = Blather() # Init gobject threads GObject.threads_init() # We want a main loop diff --git a/config.py b/config.py new file mode 100644 index 0000000..21b4ec2 --- /dev/null +++ b/config.py @@ -0,0 +1,56 @@ +# This is part of Kaylee +# -- this code is licensed GPLv3 +# Copyright 2013 Jezra +# Copyright 2015 Clayton G. Hobbs + +import json +import os +from argparse import ArgumentParser, Namespace + +from gi.repository import GLib + +class Config: + conf_dir = os.path.expanduser(os.path.join(GLib.get_user_config_dir(), + "blather")) + opt_file = os.path.join(conf_dir, "options.json") + + def __init__(self): + # Set up the argument parser + self.parser = ArgumentParser() + self.parser.add_argument("-i", "--interface", type=str, + dest="interface", action='store', + help="Interface to use (if any). 'g' for GTK or 'gt' for GTK" + + " system tray icon") + + self.parser.add_argument("-c", "--continuous", + action="store_true", dest="continuous", default=False, + help="starts interface with 'continuous' listen enabled") + + self.parser.add_argument("-p", "--pass-words", + action="store_true", dest="pass_words", default=False, + help="passes the recognized words as arguments to the shell" + + " command") + + self.parser.add_argument("-H", "--history", type=int, + action="store", dest="history", + help="number of commands to store in history file") + + self.parser.add_argument("-m", "--microphone", type=int, + action="store", dest="microphone", default=None, + help="Audio input card to use (if other than system default)") + + self.parser.add_argument("--valid-sentence-command", type=str, + dest="valid_sentence_command", action='store', + help="command to run when a valid sentence is detected") + + self.parser.add_argument("--invalid-sentence-command", type=str, + dest="invalid_sentence_command", action='store', + help="command to run when an invalid sentence is detected") + + # Read the configuration file + with open(self.opt_file, 'r') as f: + self.options = json.load(f) + self.options = Namespace(**self.options) + + # Parse command-line arguments, overriding config file as appropriate + self.args = self.parser.parse_args(namespace=self.options) diff --git a/recognizer.py b/recognizer.py index 06a2b87..02424a8 100755 --- a/recognizer.py +++ b/recognizer.py @@ -3,16 +3,14 @@ # Copyright 2013 Jezra # Copyright 2015 Clayton G. Hobbs +import os.path +import sys + import gi gi.require_version('Gst', '1.0') from gi.repository import GObject, Gst GObject.threads_init() Gst.init(None) -import os.path -import sys - -# Define some global variables -this_dir = os.path.dirname(os.path.abspath(__file__)) class Recognizer(GObject.GObject): @@ -20,7 +18,7 @@ class Recognizer(GObject.GObject): 'finished' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)) } - def __init__(self, language_file, dictionary_file, src = None): + def __init__(self, language_file, dictionary_file, src=None): GObject.GObject.__init__(self) self.commands = {} if src: @@ -41,7 +39,7 @@ class Recognizer(GObject.GObject): bus.add_signal_watch() # Get the Auto Speech Recognition piece - asr=self.pipeline.get_by_name('asr') + asr = self.pipeline.get_by_name('asr') bus.connect('message::element', self.result) asr.set_property('lm', language_file) asr.set_property('dict', dictionary_file) -- cgit 1.4.1 From 0d67d50252849eca6e659f2daf20616542ec2a05 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sun, 27 Dec 2015 13:57:33 -0500 Subject: Quieter; reorganize gst pipeline code Removed a bunch of print statements that looked like they were added for debugging, then never removed. Reorganized GStreamer pipeline code so that all configuration is done when the pipeline is created. --- blather.py | 4 ---- recognizer.py | 16 +++++++++------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/blather.py b/blather.py index d7fa14b..7cfe276 100755 --- a/blather.py +++ b/blather.py @@ -84,21 +84,17 @@ class Blather: self.recognizer.connect('finished', self.recognizer_finished) - print("Using Options: ", self.options) - def read_commands(self): # Read the commands file file_lines = open(command_file) strings = open(strings_file, "w") for line in file_lines: - print(line) # Trim the white spaces line = line.strip() # If the line has length and the first char isn't a hash if len(line) and line[0]!="#": # This is a parsible line (key, value) = line.split(":", 1) - print(key, value) self.commands[key.strip().lower()] = value.strip() strings.write(key.strip() + "\n") # Close the strings file diff --git a/recognizer.py b/recognizer.py index 02424a8..ac970a0 100755 --- a/recognizer.py +++ b/recognizer.py @@ -27,7 +27,14 @@ class Recognizer(GObject.GObject): audio_src = 'autoaudiosrc' # Build the pipeline - cmd = audio_src + ' ! audioconvert ! audioresample ! pocketsphinx name=asr ! appsink sync=false' + cmd = ( + audio_src + + ' ! audioconvert' + + ' ! audioresample' + + ' ! pocketsphinx lm=' + language_file + ' dict=' + + dictionary_file + ' configured=true' + + ' ! appsink sync=false' + ) try: self.pipeline = Gst.parse_launch(cmd) except Exception as e: @@ -35,15 +42,10 @@ class Recognizer(GObject.GObject): print("You may need to install gstreamer1.0-pocketsphinx") raise e + # Process results from the pipeline with self.result() bus = self.pipeline.get_bus() bus.add_signal_watch() - - # Get the Auto Speech Recognition piece - asr = self.pipeline.get_by_name('asr') bus.connect('message::element', self.result) - asr.set_property('lm', language_file) - asr.set_property('dict', dictionary_file) - asr.set_property('configured', True) def listen(self): self.pipeline.set_state(Gst.State.PLAYING) -- cgit 1.4.1 From 25ebc8fe8b34939507d741636a11816cb4f11db2 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sun, 27 Dec 2015 14:40:53 -0500 Subject: Moved paths to Config class Paths of important files and directories are part of the program's configuration, no? We're making better use of XDG paths now. Only configuration-y things go in $XDG_CONFIG_HOME now, with cache-y and data-y things going in the appropriate places instead of just being crammed in with configuration. --- blather.py | 43 ++++++++++++---------------------------- config.py | 56 +++++++++++++++++++++++++++++++++++++++++++---------- language_updater.sh | 13 +++++++------ recognizer.py | 10 ++++++---- 4 files changed, 71 insertions(+), 51 deletions(-) diff --git a/blather.py b/blather.py index 7cfe276..a90afe3 100755 --- a/blather.py +++ b/blather.py @@ -17,19 +17,6 @@ import json from recognizer import Recognizer from config import Config -# Where are the files? -conf_dir = os.path.expanduser(os.path.join(GLib.get_user_config_dir(), - "blather")) -lang_dir = os.path.join(conf_dir, "language") -command_file = os.path.join(conf_dir, "commands.conf") -strings_file = os.path.join(conf_dir, "sentences.corpus") -history_file = os.path.join(conf_dir, "blather.history") -hash_file = os.path.join(conf_dir, "hash.json") -lang_file = os.path.join(lang_dir, 'lm') -dic_file = os.path.join(lang_dir, 'dic') -# Make the lang_dir if it doesn't exist -if not os.path.exists(lang_dir): - os.makedirs(lang_dir) class Blather: @@ -41,13 +28,13 @@ class Blather: self.commands = {} - # Read the commands - self.read_commands() - - # Load the options file + # Load configuration self.config = Config() self.options = vars(self.config.options) + # Read the commands + self.read_commands() + if self.options['interface'] != None: if self.options['interface'] == "g": from gtkui import UI @@ -75,24 +62,18 @@ class Blather: self.update_language() # Create the recognizer - try: - self.recognizer = Recognizer(lang_file, dic_file, self.options['microphone']) - except Exception as e: - # No recognizer? bummer - print('error making recognizer') - sys.exit() - + self.recognizer = Recognizer(self.config) self.recognizer.connect('finished', self.recognizer_finished) def read_commands(self): # Read the commands file - file_lines = open(command_file) - strings = open(strings_file, "w") + file_lines = open(self.config.command_file) + strings = open(self.config.strings_file, "w") for line in file_lines: # Trim the white spaces line = line.strip() # If the line has length and the first char isn't a hash - if len(line) and line[0]!="#": + if len(line) and line[0] != "#": # This is a parsible line (key, value) = line.split(":", 1) self.commands[key.strip().lower()] = value.strip() @@ -108,7 +89,7 @@ class Blather: self.history.pop(0) # Open and truncate the blather history file - hfile = open(history_file, "w") + hfile = open(self.config.history_file, "w") for line in self.history: hfile.write(line + "\n") # Close the file @@ -118,7 +99,7 @@ class Blather: """Update the language if its hash has changed""" # Load the stored hash from the hash file try: - with open(hash_file, 'r') as f: + with open(self.config.hash_file, 'r') as f: hashes = json.load(f) stored_hash = hashes['language'] except (IOError, KeyError, TypeError): @@ -127,7 +108,7 @@ class Blather: # Calculate the hash the language file has right now hasher = hashlib.sha256() - with open(strings_file, 'rb') as sfile: + with open(self.config.strings_file, 'rb') as sfile: buf = sfile.read() hasher.update(buf) new_hash = hasher.hexdigest() @@ -139,7 +120,7 @@ class Blather: self.run_command('./language_updater.sh') # Store the new hash new_hashes = {'language': new_hash} - with open(hash_file, 'w') as f: + with open(self.config.hash_file, 'w') as f: json.dump(new_hashes, f) def run_command(self, cmd): diff --git a/config.py b/config.py index 21b4ec2..482e929 100644 --- a/config.py +++ b/config.py @@ -10,11 +10,34 @@ from argparse import ArgumentParser, Namespace from gi.repository import GLib class Config: - conf_dir = os.path.expanduser(os.path.join(GLib.get_user_config_dir(), - "blather")) + """Keep track of the configuration of Kaylee""" + # Name of the program, for later use + program_name = "kaylee" + + # Directories + conf_dir = os.path.join(GLib.get_user_config_dir(), program_name) + cache_dir = os.path.join(GLib.get_user_cache_dir(), program_name) + data_dir = os.path.join(GLib.get_user_data_dir(), program_name) + + # Configuration files + command_file = os.path.join(conf_dir, "commands.conf") opt_file = os.path.join(conf_dir, "options.json") + # Cache files + history_file = os.path.join(cache_dir, program_name + "history") + hash_file = os.path.join(cache_dir, "hash.json") + + # Data files + strings_file = os.path.join(data_dir, "sentences.corpus") + lang_file = os.path.join(data_dir, 'lm') + dic_file = os.path.join(data_dir, 'dic') + def __init__(self): + # Ensure necessary directories exist + self._make_dir(self.conf_dir) + self._make_dir(self.cache_dir) + self._make_dir(self.data_dir) + # Set up the argument parser self.parser = ArgumentParser() self.parser.add_argument("-i", "--interface", type=str, @@ -24,16 +47,16 @@ class Config: self.parser.add_argument("-c", "--continuous", action="store_true", dest="continuous", default=False, - help="starts interface with 'continuous' listen enabled") + help="Start interface with 'continuous' listen enabled") self.parser.add_argument("-p", "--pass-words", action="store_true", dest="pass_words", default=False, - help="passes the recognized words as arguments to the shell" + + help="Pass the recognized words as arguments to the shell" + " command") self.parser.add_argument("-H", "--history", type=int, action="store", dest="history", - help="number of commands to store in history file") + help="Number of commands to store in history file") self.parser.add_argument("-m", "--microphone", type=int, action="store", dest="microphone", default=None, @@ -41,16 +64,29 @@ class Config: self.parser.add_argument("--valid-sentence-command", type=str, dest="valid_sentence_command", action='store', - help="command to run when a valid sentence is detected") + help="Command to run when a valid sentence is detected") self.parser.add_argument("--invalid-sentence-command", type=str, dest="invalid_sentence_command", action='store', - help="command to run when an invalid sentence is detected") + help="Command to run when an invalid sentence is detected") # Read the configuration file - with open(self.opt_file, 'r') as f: - self.options = json.load(f) - self.options = Namespace(**self.options) + self._read_options_file() # Parse command-line arguments, overriding config file as appropriate self.args = self.parser.parse_args(namespace=self.options) + print(self.args) + print(self.options) + + def _make_dir(self, directory): + if not os.path.exists(directory): + os.makedirs(directory) + + def _read_options_file(self): + try: + with open(self.opt_file, 'r') as f: + self.options = json.load(f) + self.options = Namespace(**self.options) + except FileNotFoundError: + # Make an empty options namespace + self.options = Namespace() diff --git a/language_updater.sh b/language_updater.sh index ec5c868..5a2c232 100755 --- a/language_updater.sh +++ b/language_updater.sh @@ -1,10 +1,11 @@ #!/bin/bash -blatherdir=~/.config/blather -sentences=$blatherdir/sentences.corpus +blatherdir=~/.config/kaylee +blatherdatadir=~/.local/share/kaylee +blathercachedir=~/.cache/kaylee +sentences=$blatherdatadir/sentences.corpus sourcefile=$blatherdir/commands.conf -langdir=$blatherdir/language -tempfile=$blatherdir/url.txt +tempfile=$blathercachedir/url.txt lmtoolurl=http://www.speech.cs.cmu.edu/cgi-bin/tools/lmtool/run cd $blatherdir @@ -25,7 +26,7 @@ curl -C - -O $(cat $tempfile).dic curl -C - -O $(cat $tempfile).lm # mv em to the right name/place -mv *.dic $langdir/dic -mv *.lm $langdir/lm +mv *.dic $blatherdatadir/dic +mv *.lm $blatherdatadir/lm rm $tempfile diff --git a/recognizer.py b/recognizer.py index ac970a0..3d6f4bf 100755 --- a/recognizer.py +++ b/recognizer.py @@ -18,11 +18,13 @@ class Recognizer(GObject.GObject): 'finished' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)) } - def __init__(self, language_file, dictionary_file, src=None): + def __init__(self, config): GObject.GObject.__init__(self) self.commands = {} + + src = config.options.microphone if src: - audio_src = 'alsasrc device="hw:%d,0"' % (src) + audio_src = 'alsasrc device="hw:{0},0"'.format(src) else: audio_src = 'autoaudiosrc' @@ -31,8 +33,8 @@ class Recognizer(GObject.GObject): audio_src + ' ! audioconvert' + ' ! audioresample' + - ' ! pocketsphinx lm=' + language_file + ' dict=' + - dictionary_file + ' configured=true' + + ' ! pocketsphinx lm=' + config.lang_file + ' dict=' + + config.dic_file + ' configured=true' + ' ! appsink sync=false' ) try: -- cgit 1.4.1 From c5578954ed54a8569014105fd75aa5fe07ba1c89 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sun, 27 Dec 2015 15:17:04 -0500 Subject: Removed prints from config.py --- config.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/config.py b/config.py index 482e929..48db1d6 100644 --- a/config.py +++ b/config.py @@ -74,9 +74,7 @@ class Config: self._read_options_file() # Parse command-line arguments, overriding config file as appropriate - self.args = self.parser.parse_args(namespace=self.options) - print(self.args) - print(self.options) + self.parser.parse_args(namespace=self.options) def _make_dir(self, directory): if not os.path.exists(directory): -- cgit 1.4.1 From e4b693b2061a0e3d93feba4fa570df7424bbe0d4 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sun, 27 Dec 2015 17:00:45 -0500 Subject: Rewrote language_updater.sh in Python At the same time, I moved the logic to check if the language should be updated into the new LanguageUpdater class. The README has been updated to reflect the fact that you no longer need to do any of this manually ever. --- README.md | 28 +++++------------- blather.py | 34 +++------------------- language_updater.sh | 32 --------------------- languageupdater.py | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 83 deletions(-) delete mode 100755 language_updater.sh create mode 100644 languageupdater.py diff --git a/README.md b/README.md index b2949b6..22729a0 100644 --- a/README.md +++ b/README.md @@ -17,35 +17,21 @@ but adds a lot of features that go beyond the original purpose of Blather. ## Usage -1. Move commands.tmp to ~/.config/blather/commands.conf and fill the file with +1. Move commands.tmp to ~/.config/kaylee/commands.conf and fill the file with sentences and command to run -2. Run blather.py, this will generate ~/.config/blather/sentences.corpus based -on sentences in the 'commands' file -3. Quit Kaylee (there is a good chance it will just segfault) -4. Go to and upload the -sentences.corpus file -5. Download the resulting XXXX.lm file to the ~/.config/blather/language -directory and rename to file to 'lm' -6. Download the resulting XXXX.dic file to the ~/.config/blather/language -directory and rename to file to 'dic' -7. Run blather.py +2. Run blather.py. This will generate ~/.local/share/kaylee/sentences.corpus +based on sentences in the 'commands' file, then use + to create and save a new +language model and dictionary. * For GTK UI, run blather.py -i g * To start a UI in 'continuous' listen mode, use the -c flag * To use a microphone other than the system default, use the -m flag -8. Start talking +3. Start talking **Note:** to start Kaylee without needing to enter command line options all the -time, copy options.json.tmp to ~/.config/blather/options.json and edit +time, copy options.json.tmp to ~/.config/kaylee/options.json and edit accordingly. -### Bonus - -~~Once the sentences.corpus file has been created, run the language_updater.sh -script to automate the process of creating and downloading language files.~~ - -Kaylee now updates the language automatically. You should never need to run -language_updater.sh manually. - ### Examples * To run Kaylee with the GTK UI and start in continuous listen mode: diff --git a/blather.py b/blather.py index a90afe3..23802e8 100755 --- a/blather.py +++ b/blather.py @@ -16,6 +16,7 @@ import json from recognizer import Recognizer from config import Config +from languageupdater import LanguageUpdater class Blather: @@ -35,7 +36,7 @@ class Blather: # Read the commands self.read_commands() - if self.options['interface'] != None: + if self.options['interface']: if self.options['interface'] == "g": from gtkui import UI elif self.options['interface'] == "gt": @@ -59,7 +60,8 @@ class Blather: self.history = [] # Update the language if necessary - self.update_language() + self.language_updater = LanguageUpdater(self.config) + self.language_updater.update_language_if_changed() # Create the recognizer self.recognizer = Recognizer(self.config) @@ -95,34 +97,6 @@ class Blather: # Close the file hfile.close() - def update_language(self): - """Update the language if its hash has changed""" - # Load the stored hash from the hash file - try: - with open(self.config.hash_file, 'r') as f: - hashes = json.load(f) - stored_hash = hashes['language'] - except (IOError, KeyError, TypeError): - # No stored hash - stored_hash = '' - - # Calculate the hash the language file has right now - hasher = hashlib.sha256() - with open(self.config.strings_file, 'rb') as sfile: - buf = sfile.read() - hasher.update(buf) - new_hash = hasher.hexdigest() - - # If the hashes differ - if stored_hash != new_hash: - # Update the language - # FIXME: Do this with Python, not Bash - self.run_command('./language_updater.sh') - # Store the new hash - new_hashes = {'language': new_hash} - with open(self.config.hash_file, 'w') as f: - json.dump(new_hashes, f) - def run_command(self, cmd): """Print the command, then run it""" print(cmd) diff --git a/language_updater.sh b/language_updater.sh deleted file mode 100755 index 5a2c232..0000000 --- a/language_updater.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash - -blatherdir=~/.config/kaylee -blatherdatadir=~/.local/share/kaylee -blathercachedir=~/.cache/kaylee -sentences=$blatherdatadir/sentences.corpus -sourcefile=$blatherdir/commands.conf -tempfile=$blathercachedir/url.txt -lmtoolurl=http://www.speech.cs.cmu.edu/cgi-bin/tools/lmtool/run - -cd $blatherdir - -sed -f - $sourcefile > $sentences <Index of (.*?).*', line): + path = host + re.sub(r'.*Index of (.*?).*', r'\1', line) + # If we found the number, keep it and break + elif re.search(r'.*TAR[0-9]*?\.tgz.*', line): + number = re.sub(r'.*TAR([0-9]*?)\.tgz.*', r'\1', line) + break + + lm_url = path + '/' + number + '.lm' + dic_url = path + '/' + number + '.dic' + + self._download_file(lm_url, self.config.lang_file) + self._download_file(dic_url, self.config.dic_file) + + def save_language_hash(self): + new_hashes = {'language': self.new_hash} + with open(self.config.hash_file, 'w') as f: + json.dump(new_hashes, f) + + def _download_file(self, url, path): + r = requests.get(url, stream=True) + if r.status_code == 200: + with open(path, 'wb') as f: + for chunk in r: + f.write(chunk) -- cgit 1.4.1 From 22f27d2806c50b09b13450a975f85142fc9c4e69 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sun, 27 Dec 2015 17:02:57 -0500 Subject: Mention the new dependency in README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 22729a0..1900197 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ but adds a lot of features that go beyond the original purpose of Blather. 2. gstreamer-1.0 (and what ever plugin has pocketsphinx support) 3. gstreamer-1.0 base plugins (required for ALSA) 4. python-gobject (required for GStreamer and the GTK-based UI) +5. python-requests (required for automatic language updating) **Note:** it may also be required to install `pocketsphinx-hmm-en-hub4wsj` -- cgit 1.4.1 From 9bffb8d16698f12bbb5332d511d9287d53966373 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sun, 27 Dec 2015 17:08:17 -0500 Subject: Rename license file to COPYING --- COPYING | 674 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ gpl-3.0.txt | 674 ------------------------------------------------------------ 2 files changed, 674 insertions(+), 674 deletions(-) create mode 100644 COPYING delete mode 100644 gpl-3.0.txt diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/gpl-3.0.txt b/gpl-3.0.txt deleted file mode 100644 index 94a9ed0..0000000 --- a/gpl-3.0.txt +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. -- cgit 1.4.1 From a40bbe25e236c85d3a7e3e86576395c2e4b65b0b Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sun, 27 Dec 2015 17:19:38 -0500 Subject: Rename blather.py to kaylee.py --- README.md | 14 ++--- blather.py | 186 ------------------------------------------------------------- kaylee.py | 186 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 193 insertions(+), 193 deletions(-) delete mode 100755 blather.py create mode 100755 kaylee.py diff --git a/README.md b/README.md index 1900197..0b6c2d0 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,11 @@ but adds a lot of features that go beyond the original purpose of Blather. 1. Move commands.tmp to ~/.config/kaylee/commands.conf and fill the file with sentences and command to run -2. Run blather.py. This will generate ~/.local/share/kaylee/sentences.corpus +2. Run kaylee.py. This will generate ~/.local/share/kaylee/sentences.corpus based on sentences in the 'commands' file, then use to create and save a new language model and dictionary. - * For GTK UI, run blather.py -i g + * For GTK UI, run kaylee.py -i g * To start a UI in 'continuous' listen mode, use the -c flag * To use a microphone other than the system default, use the -m flag 3. Start talking @@ -36,13 +36,13 @@ accordingly. ### Examples * To run Kaylee with the GTK UI and start in continuous listen mode: -`./blather.py -i g -c` +`./kaylee.py -i g -c` * To run Kaylee with no UI and using a USB microphone recognized and device 2: -`./blather.py -m 2` +`./kaylee.py -m 2` * To have Kaylee pass the matched sentence to the executed command: -`./blather.py -p` +`./kaylee.py -p` **explanation:** if the commands.conf contains: `good morning world: example_command.sh` @@ -50,10 +50,10 @@ then 3 arguments, 'good', 'morning', and 'world' would get passed to example_command.sh as `example_command.sh good morning world` * To run a command when a valid sentence has been detected: - `./blather.py --valid-sentence-command=/path/to/command` + `./kaylee.py --valid-sentence-command=/path/to/command` **note:** this can be set in the options.yml file * To run a command when a invalid sentence has been detected: - `./blather.py --invalid-sentence-command=/path/to/command` + `./kaylee.py --invalid-sentence-command=/path/to/command` **note:** this can be set in the options.yml file ### Finding the Device Number of a USB microphone diff --git a/blather.py b/blather.py deleted file mode 100755 index 23802e8..0000000 --- a/blather.py +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/env python3 - -# This is part of Kaylee -# -- this code is licensed GPLv3 -# Copyright 2013 Jezra -# Copyright 2015 Clayton G. Hobbs - -from __future__ import print_function -import sys -import signal -import hashlib -import os.path -import subprocess -from gi.repository import GObject, GLib -import json - -from recognizer import Recognizer -from config import Config -from languageupdater import LanguageUpdater - - -class Blather: - - def __init__(self): - self.ui = None - self.options = {} - ui_continuous_listen = False - self.continuous_listen = False - - self.commands = {} - - # Load configuration - self.config = Config() - self.options = vars(self.config.options) - - # Read the commands - self.read_commands() - - if self.options['interface']: - if self.options['interface'] == "g": - from gtkui import UI - elif self.options['interface'] == "gt": - from gtktrayui import UI - else: - print("no GUI defined") - sys.exit() - - self.ui = UI(self.options, self.options['continuous']) - self.ui.connect("command", self.process_command) - # Can we load the icon resource? - icon = self.load_resource("icon.png") - if icon: - self.ui.set_icon_active_asset(icon) - # Can we load the icon_inactive resource? - icon_inactive = self.load_resource("icon_inactive.png") - if icon_inactive: - self.ui.set_icon_inactive_asset(icon_inactive) - - if self.options['history']: - self.history = [] - - # Update the language if necessary - self.language_updater = LanguageUpdater(self.config) - self.language_updater.update_language_if_changed() - - # Create the recognizer - self.recognizer = Recognizer(self.config) - self.recognizer.connect('finished', self.recognizer_finished) - - def read_commands(self): - # Read the commands file - file_lines = open(self.config.command_file) - strings = open(self.config.strings_file, "w") - for line in file_lines: - # Trim the white spaces - line = line.strip() - # If the line has length and the first char isn't a hash - if len(line) and line[0] != "#": - # This is a parsible line - (key, value) = line.split(":", 1) - self.commands[key.strip().lower()] = value.strip() - strings.write(key.strip() + "\n") - # Close the strings file - strings.close() - - def log_history(self, text): - if self.options['history']: - self.history.append(text) - if len(self.history) > self.options['history']: - # Pop off the first item - self.history.pop(0) - - # Open and truncate the blather history file - hfile = open(self.config.history_file, "w") - for line in self.history: - hfile.write(line + "\n") - # Close the file - hfile.close() - - def run_command(self, cmd): - """Print the command, then run it""" - print(cmd) - subprocess.call(cmd, shell=True) - - def recognizer_finished(self, recognizer, text): - t = text.lower() - # Is there a matching command? - if t in self.commands: - # Run the valid_sentence_command if there is a valid sentence command - if self.options['valid_sentence_command']: - subprocess.call(self.options['valid_sentence_command'], shell=True) - cmd = self.commands[t] - # Should we be passing words? - if self.options['pass_words']: - cmd += " " + t - self.run_command(cmd) - else: - self.run_command(cmd) - self.log_history(text) - else: - # Run the invalid_sentence_command if there is an invalid sentence command - if self.options['invalid_sentence_command']: - subprocess.call(self.options['invalid_sentence_command'], shell=True) - print("no matching command {0}".format(t)) - # If there is a UI and we are not continuous listen - if self.ui: - if not self.continuous_listen: - # Stop listening - self.recognizer.pause() - # Let the UI know that there is a finish - self.ui.finished(t) - - def run(self): - if self.ui: - self.ui.run() - else: - blather.recognizer.listen() - - def quit(self): - sys.exit() - - def process_command(self, UI, command): - print(command) - if command == "listen": - self.recognizer.listen() - elif command == "stop": - self.recognizer.pause() - elif command == "continuous_listen": - self.continuous_listen = True - self.recognizer.listen() - elif command == "continuous_stop": - self.continuous_listen = False - self.recognizer.pause() - elif command == "quit": - self.quit() - - def load_resource(self, string): - local_data = os.path.join(os.path.dirname(__file__), 'data') - paths = ["/usr/share/blather/", "/usr/local/share/blather", local_data] - for path in paths: - resource = os.path.join(path, string) - if os.path.exists(resource): - return resource - # If we get this far, no resource was found - return False - - -if __name__ == "__main__": - # Make our blather object - blather = Blather() - # Init gobject threads - GObject.threads_init() - # We want a main loop - main_loop = GObject.MainLoop() - # Handle sigint - signal.signal(signal.SIGINT, signal.SIG_DFL) - # Run the blather - blather.run() - # Start the main loop - try: - main_loop.run() - except: - print("time to quit") - main_loop.quit() - sys.exit() - diff --git a/kaylee.py b/kaylee.py new file mode 100755 index 0000000..7aedb22 --- /dev/null +++ b/kaylee.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 + +# This is part of Kaylee +# -- this code is licensed GPLv3 +# Copyright 2013 Jezra +# Copyright 2015 Clayton G. Hobbs + +from __future__ import print_function +import sys +import signal +import hashlib +import os.path +import subprocess +from gi.repository import GObject, GLib +import json + +from recognizer import Recognizer +from config import Config +from languageupdater import LanguageUpdater + + +class Kaylee: + + def __init__(self): + self.ui = None + self.options = {} + ui_continuous_listen = False + self.continuous_listen = False + + self.commands = {} + + # Load configuration + self.config = Config() + self.options = vars(self.config.options) + + # Read the commands + self.read_commands() + + if self.options['interface']: + if self.options['interface'] == "g": + from gtkui import UI + elif self.options['interface'] == "gt": + from gtktrayui import UI + else: + print("no GUI defined") + sys.exit() + + self.ui = UI(self.options, self.options['continuous']) + self.ui.connect("command", self.process_command) + # Can we load the icon resource? + icon = self.load_resource("icon.png") + if icon: + self.ui.set_icon_active_asset(icon) + # Can we load the icon_inactive resource? + icon_inactive = self.load_resource("icon_inactive.png") + if icon_inactive: + self.ui.set_icon_inactive_asset(icon_inactive) + + if self.options['history']: + self.history = [] + + # Update the language if necessary + self.language_updater = LanguageUpdater(self.config) + self.language_updater.update_language_if_changed() + + # Create the recognizer + self.recognizer = Recognizer(self.config) + self.recognizer.connect('finished', self.recognizer_finished) + + def read_commands(self): + # Read the commands file + file_lines = open(self.config.command_file) + strings = open(self.config.strings_file, "w") + for line in file_lines: + # Trim the white spaces + line = line.strip() + # If the line has length and the first char isn't a hash + if len(line) and line[0] != "#": + # This is a parsible line + (key, value) = line.split(":", 1) + self.commands[key.strip().lower()] = value.strip() + strings.write(key.strip() + "\n") + # Close the strings file + strings.close() + + def log_history(self, text): + if self.options['history']: + self.history.append(text) + if len(self.history) > self.options['history']: + # Pop off the first item + self.history.pop(0) + + # Open and truncate the history file + hfile = open(self.config.history_file, "w") + for line in self.history: + hfile.write(line + "\n") + # Close the file + hfile.close() + + def run_command(self, cmd): + """Print the command, then run it""" + print(cmd) + subprocess.call(cmd, shell=True) + + def recognizer_finished(self, recognizer, text): + t = text.lower() + # Is there a matching command? + if t in self.commands: + # Run the valid_sentence_command if there is a valid sentence command + if self.options['valid_sentence_command']: + subprocess.call(self.options['valid_sentence_command'], shell=True) + cmd = self.commands[t] + # Should we be passing words? + if self.options['pass_words']: + cmd += " " + t + self.run_command(cmd) + else: + self.run_command(cmd) + self.log_history(text) + else: + # Run the invalid_sentence_command if there is an invalid sentence command + if self.options['invalid_sentence_command']: + subprocess.call(self.options['invalid_sentence_command'], shell=True) + print("no matching command {0}".format(t)) + # If there is a UI and we are not continuous listen + if self.ui: + if not self.continuous_listen: + # Stop listening + self.recognizer.pause() + # Let the UI know that there is a finish + self.ui.finished(t) + + def run(self): + if self.ui: + self.ui.run() + else: + self.recognizer.listen() + + def quit(self): + sys.exit() + + def process_command(self, UI, command): + print(command) + if command == "listen": + self.recognizer.listen() + elif command == "stop": + self.recognizer.pause() + elif command == "continuous_listen": + self.continuous_listen = True + self.recognizer.listen() + elif command == "continuous_stop": + self.continuous_listen = False + self.recognizer.pause() + elif command == "quit": + self.quit() + + def load_resource(self, string): + local_data = os.path.join(os.path.dirname(__file__), 'data') + paths = ["/usr/share/kaylee/", "/usr/local/share/kaylee", local_data] + for path in paths: + resource = os.path.join(path, string) + if os.path.exists(resource): + return resource + # If we get this far, no resource was found + return False + + +if __name__ == "__main__": + # Make our kaylee object + kaylee = Kaylee() + # Init gobject threads + GObject.threads_init() + # We want a main loop + main_loop = GObject.MainLoop() + # Handle sigint + signal.signal(signal.SIGINT, signal.SIG_DFL) + # Run the kaylee + kaylee.run() + # Start the main loop + try: + main_loop.run() + except: + print("time to quit") + main_loop.quit() + sys.exit() + -- cgit 1.4.1 From ed077b414463b6be2542ed8f821cdd3c4a194ede Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sun, 27 Dec 2015 17:40:38 -0500 Subject: Changed icons for Kaylee This really is not Blather anymore. --- assets/blather.png | Bin 13571 -> 0 bytes assets/blather.svg | 116 --------------------------------------- assets/blather.xcf | Bin 23830 -> 0 bytes assets/blather_inactive.png | Bin 13379 -> 0 bytes assets/blathersrc.png | Bin 27324 -> 0 bytes assets/kaylee.svg | 129 ++++++++++++++++++++++++++++++++++++++++++++ data/icon.png | Bin 13571 -> 27147 bytes data/icon_inactive.png | Bin 13379 -> 27997 bytes 8 files changed, 129 insertions(+), 116 deletions(-) delete mode 100644 assets/blather.png delete mode 100644 assets/blather.svg delete mode 100644 assets/blather.xcf delete mode 100644 assets/blather_inactive.png delete mode 100644 assets/blathersrc.png create mode 100644 assets/kaylee.svg diff --git a/assets/blather.png b/assets/blather.png deleted file mode 100644 index e1a83cb..0000000 Binary files a/assets/blather.png and /dev/null differ diff --git a/assets/blather.svg b/assets/blather.svg deleted file mode 100644 index e7e3446..0000000 --- a/assets/blather.svg +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - B - - - - - B - - - diff --git a/assets/blather.xcf b/assets/blather.xcf deleted file mode 100644 index 9a11666..0000000 Binary files a/assets/blather.xcf and /dev/null differ diff --git a/assets/blather_inactive.png b/assets/blather_inactive.png deleted file mode 100644 index 63d1572..0000000 Binary files a/assets/blather_inactive.png and /dev/null differ diff --git a/assets/blathersrc.png b/assets/blathersrc.png deleted file mode 100644 index fb187e4..0000000 Binary files a/assets/blathersrc.png and /dev/null differ diff --git a/assets/kaylee.svg b/assets/kaylee.svg new file mode 100644 index 0000000..f5e3627 --- /dev/null +++ b/assets/kaylee.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + K + + + + + K + + + diff --git a/data/icon.png b/data/icon.png index e1a83cb..e9b3902 100644 Binary files a/data/icon.png and b/data/icon.png differ diff --git a/data/icon_inactive.png b/data/icon_inactive.png index 63d1572..8ac72d5 100644 Binary files a/data/icon_inactive.png and b/data/icon_inactive.png differ -- cgit 1.4.1 From 5c8cee5bb093b25f117b825b207bcf91c0808778 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sun, 27 Dec 2015 20:27:30 -0500 Subject: Removed executable permissions from recognizer.py --- recognizer.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 recognizer.py diff --git a/recognizer.py b/recognizer.py old mode 100755 new mode 100644 -- cgit 1.4.1 From 57f58295a48dfa4d893eb1546c5f2f64133c0e7f Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sun, 27 Dec 2015 20:47:05 -0500 Subject: Refactored a couple regular expressions --- languageupdater.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/languageupdater.py b/languageupdater.py index a82e023..662a988 100644 --- a/languageupdater.py +++ b/languageupdater.py @@ -55,13 +55,15 @@ class LanguageUpdater: r = requests.post(url, files=files, data=values) # Parse response to get URLs of the files we need + path_re = r'.*Index of (.*?).*' + number_re = r'.*TAR[0-9]*?\.tgz.*' for line in r.text.split('\n'): # If we found the directory, keep it and don't break - if re.search(r'.*Index of (.*?).*', line): - path = host + re.sub(r'.*Index of (.*?).*', r'\1', line) + if re.search(path_re, line): + path = host + re.sub(path_re, r'\1', line) # If we found the number, keep it and break - elif re.search(r'.*TAR[0-9]*?\.tgz.*', line): - number = re.sub(r'.*TAR([0-9]*?)\.tgz.*', r'\1', line) + elif re.search(number_re, line): + number = re.sub(number_re, r'\1', line) break lm_url = path + '/' + number + '.lm' -- cgit 1.4.1 From e19d76f0515b291f9c6994bfd0faccccf5b894aa Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Wed, 30 Dec 2015 23:33:29 -0500 Subject: Added number parsing capabilities See commands.tmp for an example. It's pretty neat, but it could still use some work. I thought of a really clever way to parse numbers, better than the one I came up with last night, but since I have a working implementation now I figure I'd better commit it. We have a new bug which causes the dictionary to be updated every time the program starts. I hope I didn't force that to happen last night or something, but I have a vague feeling I did. --- commands.tmp | 10 +-- config.py | 2 +- gtktrayui.py | 2 +- gtkui.py | 2 +- kaylee.py | 27 ++++++-- languageupdater.py | 4 +- numberparser.py | 182 +++++++++++++++++++++++++++++++++++++++++++++++++++++ recognizer.py | 2 +- 8 files changed, 216 insertions(+), 15 deletions(-) create mode 100644 numberparser.py diff --git a/commands.tmp b/commands.tmp index 9e41147..10fa2c5 100644 --- a/commands.tmp +++ b/commands.tmp @@ -1,5 +1,7 @@ -# commands are key:value pairs -# key is the sentence to listen for -# value is the command to run when the key is spoken +# commands are pars of the form: +# KEY: VALUE +# KEY is the sentence to listen for +# VALUE is the command to run when the key is spoken -hello world:echo "hello world" +hello world: echo "hello world" +start a %d minute timer: (echo {0} minute timer started && sleep {0}m && echo {0} minute timer ended) & diff --git a/config.py b/config.py index 48db1d6..6bd8c9e 100644 --- a/config.py +++ b/config.py @@ -1,7 +1,7 @@ # This is part of Kaylee # -- this code is licensed GPLv3 -# Copyright 2013 Jezra # Copyright 2015 Clayton G. Hobbs +# Portions Copyright 2013 Jezra import json import os diff --git a/gtktrayui.py b/gtktrayui.py index 8c6c47c..f18c449 100644 --- a/gtktrayui.py +++ b/gtktrayui.py @@ -1,7 +1,7 @@ # This is part of Kaylee # -- this code is licensed GPLv3 -# Copyright 2013 Jezra # Copyright 2015 Clayton G. Hobbs +# Portions Copyright 2013 Jezra import sys import gi diff --git a/gtkui.py b/gtkui.py index b1e25ef..ffb39c2 100644 --- a/gtkui.py +++ b/gtkui.py @@ -1,7 +1,7 @@ # This is part of Kaylee # -- this code is licensed GPLv3 -# Copyright 2013 Jezra # Copyright 2015 Clayton G. Hobbs +# Portions Copyright 2013 Jezra import sys import gi diff --git a/kaylee.py b/kaylee.py index 7aedb22..0ea9a16 100755 --- a/kaylee.py +++ b/kaylee.py @@ -2,8 +2,8 @@ # This is part of Kaylee # -- this code is licensed GPLv3 -# Copyright 2013 Jezra # Copyright 2015 Clayton G. Hobbs +# Portions Copyright 2013 Jezra from __future__ import print_function import sys @@ -17,6 +17,7 @@ import json from recognizer import Recognizer from config import Config from languageupdater import LanguageUpdater +from numberparser import NumberParser class Kaylee: @@ -33,6 +34,9 @@ class Kaylee: self.config = Config() self.options = vars(self.config.options) + # Create number parser for later use + self.number_parser = NumberParser() + # Read the commands self.read_commands() @@ -79,7 +83,10 @@ class Kaylee: # This is a parsible line (key, value) = line.split(":", 1) self.commands[key.strip().lower()] = value.strip() - strings.write(key.strip() + "\n") + strings.write(key.strip().replace('%d', '') + "\n") + # Add number words to the corpus + for word in self.number_parser.number_words: + strings.write(word + "\n") # Close the strings file strings.close() @@ -104,6 +111,7 @@ class Kaylee: def recognizer_finished(self, recognizer, text): t = text.lower() + numt, nums = self.number_parser.parse_all_numbers(t) # Is there a matching command? if t in self.commands: # Run the valid_sentence_command if there is a valid sentence command @@ -113,9 +121,18 @@ class Kaylee: # Should we be passing words? if self.options['pass_words']: cmd += " " + t - self.run_command(cmd) - else: - self.run_command(cmd) + self.run_command(cmd) + self.log_history(text) + elif numt in self.commands: + # Run the valid_sentence_command if there is a valid sentence command + if self.options['valid_sentence_command']: + subprocess.call(self.options['valid_sentence_command'], shell=True) + cmd = self.commands[numt] + cmd = cmd.format(*nums) + # Should we be passing words? + if self.options['pass_words']: + cmd += " " + t + self.run_command(cmd) self.log_history(text) else: # Run the invalid_sentence_command if there is an invalid sentence command diff --git a/languageupdater.py b/languageupdater.py index 662a988..afdfc21 100644 --- a/languageupdater.py +++ b/languageupdater.py @@ -1,7 +1,7 @@ # This is part of Kaylee # -- this code is licensed GPLv3 -# Copyright 2013 Jezra # Copyright 2015 Clayton G. Hobbs +# Portions Copyright 2013 Jezra import hashlib import json @@ -56,7 +56,7 @@ class LanguageUpdater: # Parse response to get URLs of the files we need path_re = r'.*Index of (.*?).*' - number_re = r'.*TAR[0-9]*?\.tgz.*' + number_re = r'.*TAR([0-9]*?)\.tgz.*' for line in r.text.split('\n'): # If we found the directory, keep it and don't break if re.search(path_re, line): diff --git a/numberparser.py b/numberparser.py new file mode 100644 index 0000000..fb04027 --- /dev/null +++ b/numberparser.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python +# numberparser.py - Translate words to decimal + +# This is part of Kaylee +# -- this code is licensed GPLv3 +# Copyright 2015 Clayton G. Hobbs +# Portions Copyright 2013 Jezra +import re + +# Define the mappings from words to numbers +class NumberParser: + zero = { + 'zero': 0 + } + + ones = { + 'one': 1, + 'two': 2, + 'three': 3, + 'four': 4, + 'five': 5, + 'six': 6, + 'seven': 7, + 'eight': 8, + 'nine': 9 + } + + special_ones = { + 'ten': 10, + 'eleven': 11, + 'twelve': 12, + 'thirteen': 13, + 'fourteen': 14, + 'fifteen': 15, + 'sixteen': 16, + 'seventeen': 17, + 'eighteen': 18, + 'ninteen': 19 + } + + tens = { + 'twenty': 20, + 'thirty': 30, + 'fourty': 40, + 'fifty': 50, + 'sixty': 60, + 'seventy': 70, + 'eighty': 80, + 'ninty': 90 + } + + hundred = { + 'hundred': 100 + } + + exp = { + 'thousand': 1000, + 'million': 1000000, + 'billion': 1000000000 + } + + allowed = [ + 'and' + ] + + def __init__(self): + self.number_words = [] + for word in self.zero: + self.number_words.append(word) + for word in self.ones: + self.number_words.append(word) + for word in self.special_ones: + self.number_words.append(word) + for word in self.tens: + self.number_words.append(word) + for word in self.hundred: + self.number_words.append(word) + for word in self.exp: + self.number_words.append(word) + self.mandatory_number_words = self.number_words.copy() + for word in self.allowed: + self.number_words.append(word) + + def parse_number(self, text_line): + """ + Parse numbers from natural language into ints + + TODO: Throw more exceptions when invalid numbers are detected. Only + allow certian valueless words within numbers. Support zero. + """ + value = 0 + partial_value = 0 + last_list = None + + # Split text_line by commas, whitespace, and hyphens + text_line = text_line.strip() + text_words = re.split(r'[,\s-]+', text_line) + # Parse the number + for word in text_words: + if word in self.zero: + if last_list is not None: + raise ValueError('Invalid number') + value = 0 + last_list = self.zero + elif word in self.ones: + if last_list in (self.zero, self.ones, self.special_ones): + raise ValueError('Invalid number') + value += self.ones[word] + last_list = self.ones + elif word in self.special_ones: + if last_list in (self.zero, self.ones, self.special_ones, self.tens): + raise ValueError('Invalid number') + value += self.special_ones[word] + last_list = self.special_ones + elif word in self.tens: + if last_list in (self.zero, self.ones, self.special_ones, self.tens): + raise ValueError('Invalid number') + value += self.tens[word] + last_list = self.tens + elif word in self.hundred: + if last_list not in (self.ones, self.special_ones, self.tens): + raise ValueError('Invalid number') + value *= self.hundred[word] + last_list = self.hundred + elif word in self.exp: + if last_list in (self.zero, self.exp): + raise ValueError('Invalid number') + partial_value += value * self.exp[word] + value = 0 + last_list = self.exp + elif word not in self.allowed: + raise ValueError('Invalid number') + # Debugging information + #print(word, value, partial_value) + # Finish parsing the number + value += partial_value + return value + + def parse_all_numbers(self, text_line): + nums = [] + t_numless = '' + current_num = '' + + # Split text_line by commas, whitespace, and hyphens + text_line = text_line.strip() + text_words = re.split(r'[,\s-]+', text_line) + for word in text_words: + # If we aren't starting a number, add the word to the result string + if word not in self.mandatory_number_words: + if current_num: + if word in self.number_words: + current_num += word + ' ' + else: + try: + nums.append(self.parse_number(current_num)) + except ValueError: + nums.append(-1) + current_num = '' + t_numless += '%d' + ' ' + if not current_num: + t_numless += word + ' ' + else: + # We're parsing a number now + current_num += word + ' ' + if current_num: + try: + nums.append(self.parse_number(current_num)) + except ValueError: + nums.append(-1) + current_num = '' + t_numless += '%d' + ' ' + + return (t_numless.strip(), nums) + +if __name__ == '__main__': + np = NumberParser() + # Get the words to translate + text_line = input('Enter a number: ') + # Parse it to an integer + value = np.parse_all_numbers(text_line) + # Print the result + print('I claim that you meant the decimal number', value) diff --git a/recognizer.py b/recognizer.py index 3d6f4bf..4d60695 100644 --- a/recognizer.py +++ b/recognizer.py @@ -1,7 +1,7 @@ # This is part of Kaylee # -- this code is licensed GPLv3 -# Copyright 2013 Jezra # Copyright 2015 Clayton G. Hobbs +# Portions Copyright 2013 Jezra import os.path import sys -- cgit 1.4.1 From 557f0b60cb19b22fd79eb1f81a58139c4942f2c4 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Wed, 30 Dec 2015 23:40:31 -0500 Subject: Fixed the guaranteed-dictionary-update bug It wasn't actually guaranteed, it turns out. I was iterating over dictionary keys, which is done in arbitrary order. The result was that in different executions of the program, the corpus was generated differently, so the hashes differed, and the language had to be updated. Sorting the keys before adding them to the list of number-words fixed the problem. --- numberparser.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/numberparser.py b/numberparser.py index fb04027..a87698d 100644 --- a/numberparser.py +++ b/numberparser.py @@ -65,20 +65,20 @@ class NumberParser: def __init__(self): self.number_words = [] - for word in self.zero: + for word in sorted(self.zero.keys()): self.number_words.append(word) - for word in self.ones: + for word in sorted(self.ones.keys()): self.number_words.append(word) - for word in self.special_ones: + for word in sorted(self.special_ones.keys()): self.number_words.append(word) - for word in self.tens: + for word in sorted(self.tens.keys()): self.number_words.append(word) - for word in self.hundred: + for word in sorted(self.hundred.keys()): self.number_words.append(word) - for word in self.exp: + for word in sorted(self.exp.keys()): self.number_words.append(word) self.mandatory_number_words = self.number_words.copy() - for word in self.allowed: + for word in sorted(self.allowed): self.number_words.append(word) def parse_number(self, text_line): -- cgit 1.4.1 From 93f09d0d9631bb1c90afd8744ca16bf4b7b79e3b Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sat, 2 Jan 2016 13:31:42 -0500 Subject: Rewrote NumberParser.parse_all_numbers() Its control flow was confusing before; now it's much more straightforward. We make a string representing classes of words, split that by a regular expression for number words, then parse each number and build up our return string and list. It works just as well as the previous method, is a bit shorter, and I feel that it's clearer as well. --- numberparser.py | 47 ++++++++++++++++++++++------------------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/numberparser.py b/numberparser.py index a87698d..f02bb6e 100644 --- a/numberparser.py +++ b/numberparser.py @@ -139,44 +139,41 @@ class NumberParser: def parse_all_numbers(self, text_line): nums = [] t_numless = '' - current_num = '' # Split text_line by commas, whitespace, and hyphens - text_line = text_line.strip() - text_words = re.split(r'[,\s-]+', text_line) + text_words = re.split(r'[,\s-]+', text_line.strip()) + # Get a string of word classes + tw_classes = '' for word in text_words: - # If we aren't starting a number, add the word to the result string - if word not in self.mandatory_number_words: - if current_num: - if word in self.number_words: - current_num += word + ' ' - else: - try: - nums.append(self.parse_number(current_num)) - except ValueError: - nums.append(-1) - current_num = '' - t_numless += '%d' + ' ' - if not current_num: - t_numless += word + ' ' + if word in self.mandatory_number_words: + tw_classes += 'm' + elif word in self.allowed: + tw_classes += 'a' else: - # We're parsing a number now - current_num += word + ' ' - if current_num: + tw_classes += 'w' + + # For each string of number words: + last_end = 0 + for m in re.finditer('m[am]*m|m', tw_classes): + # Get the number words + num_words = ' '.join(text_words[m.start():m.end()]) + # Parse the number and store the value try: - nums.append(self.parse_number(current_num)) + nums.append(self.parse_number(num_words)) except ValueError: nums.append(-1) - current_num = '' - t_numless += '%d' + ' ' + # Add words to t_numless + t_numless += ' '.join(text_words[last_end:m.start()]) + ' %d ' + last_end = m.end() + t_numless += ' '.join(text_words[last_end:]) return (t_numless.strip(), nums) if __name__ == '__main__': np = NumberParser() # Get the words to translate - text_line = input('Enter a number: ') + text_line = input('Enter a string: ') # Parse it to an integer value = np.parse_all_numbers(text_line) # Print the result - print('I claim that you meant the decimal number', value) + print(value) -- cgit 1.4.1 From 703d2e9fb3033d4d8eeeb30976c2b4fff399f324 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sat, 2 Jan 2016 13:34:50 -0500 Subject: Update copyright for 2016 --- config.py | 2 +- gtktrayui.py | 2 +- gtkui.py | 2 +- kaylee.py | 2 +- languageupdater.py | 2 +- numberparser.py | 2 +- recognizer.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/config.py b/config.py index 6bd8c9e..30f8df0 100644 --- a/config.py +++ b/config.py @@ -1,6 +1,6 @@ # This is part of Kaylee # -- this code is licensed GPLv3 -# Copyright 2015 Clayton G. Hobbs +# Copyright 2015-2016 Clayton G. Hobbs # Portions Copyright 2013 Jezra import json diff --git a/gtktrayui.py b/gtktrayui.py index f18c449..f595f26 100644 --- a/gtktrayui.py +++ b/gtktrayui.py @@ -1,6 +1,6 @@ # This is part of Kaylee # -- this code is licensed GPLv3 -# Copyright 2015 Clayton G. Hobbs +# Copyright 2015-2016 Clayton G. Hobbs # Portions Copyright 2013 Jezra import sys diff --git a/gtkui.py b/gtkui.py index ffb39c2..1268ccf 100644 --- a/gtkui.py +++ b/gtkui.py @@ -1,6 +1,6 @@ # This is part of Kaylee # -- this code is licensed GPLv3 -# Copyright 2015 Clayton G. Hobbs +# Copyright 2015-2016 Clayton G. Hobbs # Portions Copyright 2013 Jezra import sys diff --git a/kaylee.py b/kaylee.py index 0ea9a16..4293777 100755 --- a/kaylee.py +++ b/kaylee.py @@ -2,7 +2,7 @@ # This is part of Kaylee # -- this code is licensed GPLv3 -# Copyright 2015 Clayton G. Hobbs +# Copyright 2015-2016 Clayton G. Hobbs # Portions Copyright 2013 Jezra from __future__ import print_function diff --git a/languageupdater.py b/languageupdater.py index afdfc21..035bee4 100644 --- a/languageupdater.py +++ b/languageupdater.py @@ -1,6 +1,6 @@ # This is part of Kaylee # -- this code is licensed GPLv3 -# Copyright 2015 Clayton G. Hobbs +# Copyright 2015-2016 Clayton G. Hobbs # Portions Copyright 2013 Jezra import hashlib diff --git a/numberparser.py b/numberparser.py index f02bb6e..fec07f2 100644 --- a/numberparser.py +++ b/numberparser.py @@ -3,7 +3,7 @@ # This is part of Kaylee # -- this code is licensed GPLv3 -# Copyright 2015 Clayton G. Hobbs +# Copyright 2015-2016 Clayton G. Hobbs # Portions Copyright 2013 Jezra import re diff --git a/recognizer.py b/recognizer.py index 4d60695..2ab1945 100644 --- a/recognizer.py +++ b/recognizer.py @@ -1,6 +1,6 @@ # This is part of Kaylee # -- this code is licensed GPLv3 -# Copyright 2015 Clayton G. Hobbs +# Copyright 2015-2016 Clayton G. Hobbs # Portions Copyright 2013 Jezra import os.path -- cgit 1.4.1 From 31731113b6a5000a4e0ce78da013b79cd8c26c80 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Wed, 3 Feb 2016 23:31:29 -0500 Subject: A few corrections and formatting changes to README Man, nvim in nvim is trippy. What's the right way to do this? --- README.md | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 0b6c2d0..840af58 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,10 @@ but adds a lot of features that go beyond the original purpose of Blather. ## Usage 1. Move commands.tmp to ~/.config/kaylee/commands.conf and fill the file with -sentences and command to run +sentences and commands to run 2. Run kaylee.py. This will generate ~/.local/share/kaylee/sentences.corpus -based on sentences in the 'commands' file, then use - to create and save a new +based on sentences in the 'commands' file, then use the [Sphinx Knowledge Base +Tool](http://www.speech.cs.cmu.edu/tools/lmtool.html) to create and save a new language model and dictionary. * For GTK UI, run kaylee.py -i g * To start a UI in 'continuous' listen mode, use the -c flag @@ -35,26 +35,27 @@ accordingly. ### Examples -* To run Kaylee with the GTK UI and start in continuous listen mode: -`./kaylee.py -i g -c` +* To run Kaylee with the GTK UI, starting in continuous listen mode: + `./kaylee.py -i g -c` -* To run Kaylee with no UI and using a USB microphone recognized and device 2: -`./kaylee.py -m 2` +* To run Kaylee with no UI and using a USB microphone recognized as device 2: + `./kaylee.py -m 2` * To have Kaylee pass the matched sentence to the executed command: -`./kaylee.py -p` + `./kaylee.py -p` -**explanation:** if the commands.conf contains: -`good morning world: example_command.sh` -then 3 arguments, 'good', 'morning', and 'world' would get passed to -example_command.sh as `example_command.sh good morning world` + **Explanation:** if the commands.conf contains the line: + + good morning world: example_command.sh + + Then three arguments, 'good', 'morning', and 'world', would get passed to + example_command.sh as `example_command.sh good morning world`. * To run a command when a valid sentence has been detected: - `./kaylee.py --valid-sentence-command=/path/to/command` - **note:** this can be set in the options.yml file + `./kaylee.py --valid-sentence-command=/path/to/command` + * To run a command when a invalid sentence has been detected: - `./kaylee.py --invalid-sentence-command=/path/to/command` - **note:** this can be set in the options.yml file + `./kaylee.py --invalid-sentence-command=/path/to/command` ### Finding the Device Number of a USB microphone There are a few ways to find the device number of a USB microphone. -- cgit 1.4.1 From b04dbd12cd00b495065606f40aa14c150df48b98 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Wed, 3 Feb 2016 23:43:42 -0500 Subject: Wrap long lines in kaylee.py --- kaylee.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/kaylee.py b/kaylee.py index 4293777..5f5b611 100755 --- a/kaylee.py +++ b/kaylee.py @@ -114,9 +114,10 @@ class Kaylee: numt, nums = self.number_parser.parse_all_numbers(t) # Is there a matching command? if t in self.commands: - # Run the valid_sentence_command if there is a valid sentence command + # Run the valid_sentence_command if it's set if self.options['valid_sentence_command']: - subprocess.call(self.options['valid_sentence_command'], shell=True) + subprocess.call(self.options['valid_sentence_command'], + shell=True) cmd = self.commands[t] # Should we be passing words? if self.options['pass_words']: @@ -124,9 +125,10 @@ class Kaylee: self.run_command(cmd) self.log_history(text) elif numt in self.commands: - # Run the valid_sentence_command if there is a valid sentence command + # Run the valid_sentence_command if it's set if self.options['valid_sentence_command']: - subprocess.call(self.options['valid_sentence_command'], shell=True) + subprocess.call(self.options['valid_sentence_command'], + shell=True) cmd = self.commands[numt] cmd = cmd.format(*nums) # Should we be passing words? @@ -135,9 +137,10 @@ class Kaylee: self.run_command(cmd) self.log_history(text) else: - # Run the invalid_sentence_command if there is an invalid sentence command + # Run the invalid_sentence_command if it's set if self.options['invalid_sentence_command']: - subprocess.call(self.options['invalid_sentence_command'], shell=True) + subprocess.call(self.options['invalid_sentence_command'], + shell=True) print("no matching command {0}".format(t)) # If there is a UI and we are not continuous listen if self.ui: -- cgit 1.4.1 From 8d4e46d5b3d8950db6105b3e83825a81a3e5c8fb Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Thu, 4 Feb 2016 00:13:29 -0500 Subject: Moved command configuration to options.json It makes sense to put all the configuration in one place, no? --- README.md | 28 ++++++++++------------------ commands.tmp | 7 ------- kaylee.py | 22 ++++++++-------------- options.json.tmp | 4 ++++ 4 files changed, 22 insertions(+), 39 deletions(-) delete mode 100644 commands.tmp diff --git a/README.md b/README.md index 840af58..7d59f01 100644 --- a/README.md +++ b/README.md @@ -18,20 +18,19 @@ but adds a lot of features that go beyond the original purpose of Blather. ## Usage -1. Move commands.tmp to ~/.config/kaylee/commands.conf and fill the file with -sentences and commands to run +1. Move options.json.tmp to ~/.config/kaylee/options.json and fill the + "commands" section of the file with sentences to speak and commands to run. 2. Run kaylee.py. This will generate ~/.local/share/kaylee/sentences.corpus -based on sentences in the 'commands' file, then use the [Sphinx Knowledge Base -Tool](http://www.speech.cs.cmu.edu/tools/lmtool.html) to create and save a new -language model and dictionary. + based on sentences in the "commands" section of options.json, then use the + [Sphinx Knowledge Base Tool](http://www.speech.cs.cmu.edu/tools/lmtool.html) + to create and save a new language model and dictionary. * For GTK UI, run kaylee.py -i g * To start a UI in 'continuous' listen mode, use the -c flag * To use a microphone other than the system default, use the -m flag -3. Start talking +3. Start talking! -**Note:** to start Kaylee without needing to enter command line options all the -time, copy options.json.tmp to ~/.config/kaylee/options.json and edit -accordingly. +**Note:** default values for command-line arguments may be specified in the +options.json file. ### Examples @@ -41,15 +40,8 @@ accordingly. * To run Kaylee with no UI and using a USB microphone recognized as device 2: `./kaylee.py -m 2` -* To have Kaylee pass the matched sentence to the executed command: - `./kaylee.py -p` - - **Explanation:** if the commands.conf contains the line: - - good morning world: example_command.sh - - Then three arguments, 'good', 'morning', and 'world', would get passed to - example_command.sh as `example_command.sh good morning world`. +* To have Kaylee pass each word of the matched sentence as a separate argument + to the executed command: `./kaylee.py -p` * To run a command when a valid sentence has been detected: `./kaylee.py --valid-sentence-command=/path/to/command` diff --git a/commands.tmp b/commands.tmp deleted file mode 100644 index 10fa2c5..0000000 --- a/commands.tmp +++ /dev/null @@ -1,7 +0,0 @@ -# commands are pars of the form: -# KEY: VALUE -# KEY is the sentence to listen for -# VALUE is the command to run when the key is spoken - -hello world: echo "hello world" -start a %d minute timer: (echo {0} minute timer started && sleep {0}m && echo {0} minute timer ended) & diff --git a/kaylee.py b/kaylee.py index 5f5b611..4317c36 100755 --- a/kaylee.py +++ b/kaylee.py @@ -33,12 +33,13 @@ class Kaylee: # Load configuration self.config = Config() self.options = vars(self.config.options) + self.commands = self.options['commands'] # Create number parser for later use self.number_parser = NumberParser() - # Read the commands - self.read_commands() + # Create the strings file + self.create_strings_file() if self.options['interface']: if self.options['interface'] == "g": @@ -71,19 +72,12 @@ class Kaylee: self.recognizer = Recognizer(self.config) self.recognizer.connect('finished', self.recognizer_finished) - def read_commands(self): - # Read the commands file - file_lines = open(self.config.command_file) + def create_strings_file(self): + # Open the strings file strings = open(self.config.strings_file, "w") - for line in file_lines: - # Trim the white spaces - line = line.strip() - # If the line has length and the first char isn't a hash - if len(line) and line[0] != "#": - # This is a parsible line - (key, value) = line.split(":", 1) - self.commands[key.strip().lower()] = value.strip() - strings.write(key.strip().replace('%d', '') + "\n") + # Add command words to the corpus + for voice_cmd in sorted(self.commands.keys()): + strings.write(voice_cmd.strip().replace('%d', '') + "\n") # Add number words to the corpus for word in self.number_parser.number_words: strings.write(word + "\n") diff --git a/options.json.tmp b/options.json.tmp index a3eba6f..dfde1e5 100644 --- a/options.json.tmp +++ b/options.json.tmp @@ -1,4 +1,8 @@ { + "commands": { + "hello world": "echo \"hello world\"", + "start a %d minute timer": "(echo {0} minute timer started && sleep {0}m && echo {0} minute timer ended) &" + }, "continuous": false, "history": null, "microphone": null, -- cgit 1.4.1 From d36b65890a925c3de87dfc103b4e2279d9b33eb1 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Thu, 4 Feb 2016 12:44:43 -0500 Subject: Removed unused import in kaylee.py --- kaylee.py | 1 - 1 file changed, 1 deletion(-) diff --git a/kaylee.py b/kaylee.py index 4317c36..de5c7c4 100755 --- a/kaylee.py +++ b/kaylee.py @@ -8,7 +8,6 @@ from __future__ import print_function import sys import signal -import hashlib import os.path import subprocess from gi.repository import GObject, GLib -- cgit 1.4.1 From 9b9cc013ab324b98fcb3ec883c97d57df232a808 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Thu, 4 Feb 2016 12:58:18 -0500 Subject: Open all files using with blocks I truly am a modern Python man. --- kaylee.py | 24 ++++++++++-------------- languageupdater.py | 14 ++++++++------ 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/kaylee.py b/kaylee.py index de5c7c4..4774137 100755 --- a/kaylee.py +++ b/kaylee.py @@ -73,15 +73,13 @@ class Kaylee: def create_strings_file(self): # Open the strings file - strings = open(self.config.strings_file, "w") - # Add command words to the corpus - for voice_cmd in sorted(self.commands.keys()): - strings.write(voice_cmd.strip().replace('%d', '') + "\n") - # Add number words to the corpus - for word in self.number_parser.number_words: - strings.write(word + "\n") - # Close the strings file - strings.close() + with open(self.config.strings_file, 'w') as strings: + # Add command words to the corpus + for voice_cmd in sorted(self.commands.keys()): + strings.write(voice_cmd.strip().replace('%d', '') + "\n") + # Add number words to the corpus + for word in self.number_parser.number_words: + strings.write(word + "\n") def log_history(self, text): if self.options['history']: @@ -91,11 +89,9 @@ class Kaylee: self.history.pop(0) # Open and truncate the history file - hfile = open(self.config.history_file, "w") - for line in self.history: - hfile.write(line + "\n") - # Close the file - hfile.close() + with open(self.config.history_file, 'w') as hfile: + for line in self.history: + hfile.write(line + '\n') def run_command(self, cmd): """Print the command, then run it""" diff --git a/languageupdater.py b/languageupdater.py index 035bee4..98397c7 100644 --- a/languageupdater.py +++ b/languageupdater.py @@ -47,17 +47,19 @@ class LanguageUpdater: host = 'http://www.speech.cs.cmu.edu' url = host + '/cgi-bin/tools/lmtool/run' - # Prepare request - files = {'corpus': open(self.config.strings_file, 'rb')} - values = {'formtype': 'simple'} + # Submit the corpus to the lmtool + response_text = "" + with open(self.config.strings_file, 'rb') as corpus: + files = {'corpus': corpus} + values = {'formtype': 'simple'} - # Send corpus to the server - r = requests.post(url, files=files, data=values) + r = requests.post(url, files=files, data=values) + response_text = r.text # Parse response to get URLs of the files we need path_re = r'.*Index of (.*?).*' number_re = r'.*TAR([0-9]*?)\.tgz.*' - for line in r.text.split('\n'): + for line in response_text.split('\n'): # If we found the directory, keep it and don't break if re.search(path_re, line): path = host + re.sub(path_re, r'\1', line) -- cgit 1.4.1 From bc07966df1831a73df715bec4ef7014631c9dadb Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Fri, 12 Feb 2016 19:27:32 -0500 Subject: Only write the strings file if necessary Now we check a hash of the voice commands before writing the strings file to reduce how much we write to the hard disk. In implementing this, I realized that some code was being duplicated in an easily fixable way, so I created a Hasher object that keeps track of the hash.json file. Resolves #6 --- hasher.py | 37 +++++++++++++++++++++++++++++++++++++ kaylee.py | 26 ++++++++++++++++++++++---- languageupdater.py | 23 ++++++++--------------- recognizer.py | 3 ++- 4 files changed, 69 insertions(+), 20 deletions(-) create mode 100644 hasher.py diff --git a/hasher.py b/hasher.py new file mode 100644 index 0000000..4aebd51 --- /dev/null +++ b/hasher.py @@ -0,0 +1,37 @@ +# This is part of Kaylee +# -- this code is licensed GPLv3 +# Copyright 2015-2016 Clayton G. Hobbs +# Portions Copyright 2013 Jezra + +import json +import hashlib + +class Hasher: + """Keep track of hashes for Kaylee""" + + def __init__(self, config): + self.config = config + try: + with open(self.config.hash_file, 'r') as f: + self.hashes = json.load(f) + except IOError: + # No stored hash + self.hashes = {} + + def __getitem__(self, hashname): + try: + return self.hashes[hashname] + except (KeyError, TypeError): + return None + + def __setitem__(self, hashname, value): + self.hashes[hashname] = value + + def get_hash_object(self): + """Returns an object to compute a new hash""" + return hashlib.sha256() + + def store(self): + """Store the current hashes into a the hash file""" + with open(self.config.hash_file, 'w') as f: + json.dump(self.hashes, f) diff --git a/kaylee.py b/kaylee.py index 4774137..3f25a5b 100755 --- a/kaylee.py +++ b/kaylee.py @@ -11,12 +11,12 @@ import signal import os.path import subprocess from gi.repository import GObject, GLib -import json from recognizer import Recognizer from config import Config from languageupdater import LanguageUpdater from numberparser import NumberParser +from hasher import Hasher class Kaylee: @@ -27,8 +27,6 @@ class Kaylee: ui_continuous_listen = False self.continuous_listen = False - self.commands = {} - # Load configuration self.config = Config() self.options = vars(self.config.options) @@ -37,8 +35,11 @@ class Kaylee: # Create number parser for later use self.number_parser = NumberParser() + # Create a hasher + self.hasher = Hasher(self.config) + # Create the strings file - self.create_strings_file() + self.update_strings_file_if_changed() if self.options['interface']: if self.options['interface'] == "g": @@ -71,6 +72,23 @@ class Kaylee: self.recognizer = Recognizer(self.config) self.recognizer.connect('finished', self.recognizer_finished) + def update_voice_commands_if_changed(self): + """Use hashes to test if the voice commands have changed""" + stored_hash = self.hasher['voice_commands'] + + # Calculate the hash the voice commands have right now + hasher = self.hasher.get_hash_object() + for voice_cmd in self.commands.keys(): + hasher.update(voice_cmd.encode('utf-8')) + # Add a separator to avoid odd behavior + hasher.update('\n'.encode('utf-8')) + new_hash = hasher.hexdigest() + + if new_hash != stored_hash: + self.create_strings_file() + self.hasher['voice_commands'] = new_hash + self.hasher.store() + def create_strings_file(self): # Open the strings file with open(self.config.strings_file, 'w') as strings: diff --git a/languageupdater.py b/languageupdater.py index 98397c7..3f36d06 100644 --- a/languageupdater.py +++ b/languageupdater.py @@ -3,16 +3,17 @@ # Copyright 2015-2016 Clayton G. Hobbs # Portions Copyright 2013 Jezra -import hashlib -import json import re import requests +from hasher import Hasher + class LanguageUpdater: def __init__(self, config): self.config = config + self.hasher = Hasher(config) def update_language_if_changed(self): """Test if the language has changed, and if it has, update it""" @@ -21,18 +22,11 @@ class LanguageUpdater: self.save_language_hash() def language_has_changed(self): - """Use SHA256 hashes to test if the language has changed""" - # Load the stored hash from the hash file - try: - with open(self.config.hash_file, 'r') as f: - hashes = json.load(f) - self.stored_hash = hashes['language'] - except (IOError, KeyError, TypeError): - # No stored hash - self.stored_hash = '' + """Use hashes to test if the language has changed""" + self.stored_hash = self.hasher['language'] # Calculate the hash the language file has right now - hasher = hashlib.sha256() + hasher = self.hasher.get_hash_object() with open(self.config.strings_file, 'rb') as sfile: buf = sfile.read() hasher.update(buf) @@ -75,9 +69,8 @@ class LanguageUpdater: self._download_file(dic_url, self.config.dic_file) def save_language_hash(self): - new_hashes = {'language': self.new_hash} - with open(self.config.hash_file, 'w') as f: - json.dump(new_hashes, f) + self.hasher['language'] = self.new_hash + self.hasher.store() def _download_file(self, url, path): r = requests.get(url, stream=True) diff --git a/recognizer.py b/recognizer.py index 2ab1945..b54c055 100644 --- a/recognizer.py +++ b/recognizer.py @@ -15,7 +15,8 @@ Gst.init(None) class Recognizer(GObject.GObject): __gsignals__ = { - 'finished' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)) + 'finished' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, + (GObject.TYPE_STRING,)) } def __init__(self, config): -- cgit 1.4.1 From b95f1154291f6af9e95193b442abc61e9d457fcc Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Fri, 12 Feb 2016 19:36:14 -0500 Subject: Remove __future__ print_function It turns out I'm using Python-3-specific features elsewhere in the code, so there's no reason to continue partially trying to support Python 2. --- kaylee.py | 1 - 1 file changed, 1 deletion(-) diff --git a/kaylee.py b/kaylee.py index 3f25a5b..298a2c6 100755 --- a/kaylee.py +++ b/kaylee.py @@ -5,7 +5,6 @@ # Copyright 2015-2016 Clayton G. Hobbs # Portions Copyright 2013 Jezra -from __future__ import print_function import sys import signal import os.path -- cgit 1.4.1 From 1c2928ff1a68db1f0408e02138121e8ac1253239 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Tue, 16 Feb 2016 13:14:28 -0500 Subject: Reorganize classes into a "kayleevc" package That's "Kaylee Voice Command" if you didn't figure it out. I think everything still works properly, but I'll do more testing later to verify. --- README.md | 3 +- config.py | 90 --------------------- gtktrayui.py | 107 ------------------------- gtkui.py | 113 -------------------------- hasher.py | 37 --------- kaylee.py | 206 +----------------------------------------------- kayleevc/__init__.py | 0 kayleevc/gui.py | 209 +++++++++++++++++++++++++++++++++++++++++++++++++ kayleevc/kaylee.py | 207 ++++++++++++++++++++++++++++++++++++++++++++++++ kayleevc/numbers.py | 178 +++++++++++++++++++++++++++++++++++++++++ kayleevc/recognizer.py | 69 ++++++++++++++++ kayleevc/util.py | 202 +++++++++++++++++++++++++++++++++++++++++++++++ languageupdater.py | 80 ------------------- numberparser.py | 179 ------------------------------------------ recognizer.py | 69 ---------------- 15 files changed, 869 insertions(+), 880 deletions(-) delete mode 100644 config.py delete mode 100644 gtktrayui.py delete mode 100644 gtkui.py delete mode 100644 hasher.py create mode 100644 kayleevc/__init__.py create mode 100644 kayleevc/gui.py create mode 100644 kayleevc/kaylee.py create mode 100644 kayleevc/numbers.py create mode 100644 kayleevc/recognizer.py create mode 100644 kayleevc/util.py delete mode 100644 languageupdater.py delete mode 100644 numberparser.py delete mode 100644 recognizer.py diff --git a/README.md b/README.md index 7d59f01..5715733 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ but adds a lot of features that go beyond the original purpose of Blather. ## Requirements -1. pocketsphinx +1. Python 3 (tested with 3.5, may work with older versions) +1. pocketsphinx 5prealpha 2. gstreamer-1.0 (and what ever plugin has pocketsphinx support) 3. gstreamer-1.0 base plugins (required for ALSA) 4. python-gobject (required for GStreamer and the GTK-based UI) diff --git a/config.py b/config.py deleted file mode 100644 index 30f8df0..0000000 --- a/config.py +++ /dev/null @@ -1,90 +0,0 @@ -# This is part of Kaylee -# -- this code is licensed GPLv3 -# Copyright 2015-2016 Clayton G. Hobbs -# Portions Copyright 2013 Jezra - -import json -import os -from argparse import ArgumentParser, Namespace - -from gi.repository import GLib - -class Config: - """Keep track of the configuration of Kaylee""" - # Name of the program, for later use - program_name = "kaylee" - - # Directories - conf_dir = os.path.join(GLib.get_user_config_dir(), program_name) - cache_dir = os.path.join(GLib.get_user_cache_dir(), program_name) - data_dir = os.path.join(GLib.get_user_data_dir(), program_name) - - # Configuration files - command_file = os.path.join(conf_dir, "commands.conf") - opt_file = os.path.join(conf_dir, "options.json") - - # Cache files - history_file = os.path.join(cache_dir, program_name + "history") - hash_file = os.path.join(cache_dir, "hash.json") - - # Data files - strings_file = os.path.join(data_dir, "sentences.corpus") - lang_file = os.path.join(data_dir, 'lm') - dic_file = os.path.join(data_dir, 'dic') - - def __init__(self): - # Ensure necessary directories exist - self._make_dir(self.conf_dir) - self._make_dir(self.cache_dir) - self._make_dir(self.data_dir) - - # Set up the argument parser - self.parser = ArgumentParser() - self.parser.add_argument("-i", "--interface", type=str, - dest="interface", action='store', - help="Interface to use (if any). 'g' for GTK or 'gt' for GTK" + - " system tray icon") - - self.parser.add_argument("-c", "--continuous", - action="store_true", dest="continuous", default=False, - help="Start interface with 'continuous' listen enabled") - - self.parser.add_argument("-p", "--pass-words", - action="store_true", dest="pass_words", default=False, - help="Pass the recognized words as arguments to the shell" + - " command") - - self.parser.add_argument("-H", "--history", type=int, - action="store", dest="history", - help="Number of commands to store in history file") - - self.parser.add_argument("-m", "--microphone", type=int, - action="store", dest="microphone", default=None, - help="Audio input card to use (if other than system default)") - - self.parser.add_argument("--valid-sentence-command", type=str, - dest="valid_sentence_command", action='store', - help="Command to run when a valid sentence is detected") - - self.parser.add_argument("--invalid-sentence-command", type=str, - dest="invalid_sentence_command", action='store', - help="Command to run when an invalid sentence is detected") - - # Read the configuration file - self._read_options_file() - - # Parse command-line arguments, overriding config file as appropriate - self.parser.parse_args(namespace=self.options) - - def _make_dir(self, directory): - if not os.path.exists(directory): - os.makedirs(directory) - - def _read_options_file(self): - try: - with open(self.opt_file, 'r') as f: - self.options = json.load(f) - self.options = Namespace(**self.options) - except FileNotFoundError: - # Make an empty options namespace - self.options = Namespace() diff --git a/gtktrayui.py b/gtktrayui.py deleted file mode 100644 index f595f26..0000000 --- a/gtktrayui.py +++ /dev/null @@ -1,107 +0,0 @@ -# This is part of Kaylee -# -- this code is licensed GPLv3 -# Copyright 2015-2016 Clayton G. Hobbs -# Portions Copyright 2013 Jezra - -import sys -import gi -from gi.repository import GObject -# Gtk -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, Gdk - -class UI(GObject.GObject): - __gsignals__ = { - 'command' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)) - } - idle_text = "Kaylee - Idle" - listening_text = "Kaylee - Listening" - - def __init__(self, args, continuous): - GObject.GObject.__init__(self) - self.continuous = continuous - - self.statusicon = Gtk.StatusIcon() - self.statusicon.set_title("Kaylee") - self.statusicon.set_name("Kaylee") - self.statusicon.set_tooltip_text(self.idle_text) - self.statusicon.set_has_tooltip(True) - self.statusicon.connect("activate", self.continuous_toggle) - self.statusicon.connect("popup-menu", self.popup_menu) - - self.menu = Gtk.Menu() - self.menu_listen = Gtk.MenuItem('Listen') - self.menu_continuous = Gtk.CheckMenuItem('Continuous') - self.menu_quit = Gtk.MenuItem('Quit') - self.menu.append(self.menu_listen) - self.menu.append(self.menu_continuous) - self.menu.append(self.menu_quit) - self.menu_listen.connect("activate", self.toggle_listen) - self.menu_continuous.connect("toggled", self.toggle_continuous) - self.menu_quit.connect("activate", self.quit) - self.menu.show_all() - - def continuous_toggle(self, item): - checked = self.menu_continuous.get_active() - self.menu_continuous.set_active(not checked) - - def toggle_continuous(self, item): - checked = self.menu_continuous.get_active() - self.menu_listen.set_sensitive(not checked) - if checked: - self.menu_listen.set_label("Listen") - self.emit('command', "continuous_listen") - self.statusicon.set_tooltip_text(self.listening_text) - self.set_icon_active() - else: - self.set_icon_inactive() - self.statusicon.set_tooltip_text(self.idle_text) - self.emit('command', "continuous_stop") - - def toggle_listen(self, item): - val = self.menu_listen.get_label() - if val == "Listen": - self.set_icon_active() - self.emit("command", "listen") - self.menu_listen.set_label("Stop") - self.statusicon.set_tooltip_text(self.listening_text) - else: - self.set_icon_inactive() - self.menu_listen.set_label("Listen") - self.emit("command", "stop") - self.statusicon.set_tooltip_text(self.idle_text) - - def popup_menu(self, item, button, time): - self.menu.popup(None, None, Gtk.StatusIcon.position_menu, item, button, time) - - def run(self): - # Set the icon - self.set_icon_inactive() - if self.continuous: - self.menu_continuous.set_active(True) - self.set_icon_active() - else: - self.menu_continuous.set_active(False) - self.statusicon.set_visible(True) - - def quit(self, item): - self.statusicon.set_visible(False) - self.emit("command", "quit") - - def finished(self, text): - if not self.menu_continuous.get_active(): - self.menu_listen.set_label("Listen") - self.set_icon_inactive() - self.statusicon.set_tooltip_text(self.idle_text) - - def set_icon_active_asset(self, i): - self.icon_active = i - - def set_icon_inactive_asset(self, i): - self.icon_inactive = i - - def set_icon_active(self): - self.statusicon.set_from_file(self.icon_active) - - def set_icon_inactive(self): - self.statusicon.set_from_file(self.icon_inactive) diff --git a/gtkui.py b/gtkui.py deleted file mode 100644 index 1268ccf..0000000 --- a/gtkui.py +++ /dev/null @@ -1,113 +0,0 @@ -# This is part of Kaylee -# -- this code is licensed GPLv3 -# Copyright 2015-2016 Clayton G. Hobbs -# Portions Copyright 2013 Jezra - -import sys -import gi -from gi.repository import GObject -# Gtk -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, Gdk - -class UI(GObject.GObject): - __gsignals__ = { - 'command' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)) - } - - def __init__(self, args, continuous): - GObject.GObject.__init__(self) - self.continuous = continuous - # Make a window - self.window = Gtk.Window(Gtk.WindowType.TOPLEVEL) - self.window.connect("delete_event", self.delete_event) - # Give the window a name - self.window.set_title("Kaylee") - self.window.set_resizable(False) - - layout = Gtk.VBox() - self.window.add(layout) - # Make a listen/stop button - self.lsbutton = Gtk.Button("Listen") - layout.add(self.lsbutton) - # Make a continuous button - self.ccheckbox = Gtk.CheckButton("Continuous Listen") - layout.add(self.ccheckbox) - - # Connect the buttons - self.lsbutton.connect("clicked", self.lsbutton_clicked) - self.ccheckbox.connect("clicked", self.ccheckbox_clicked) - - # Add a label to the UI to display the last command - self.label = Gtk.Label() - layout.add(self.label) - - # Create an accellerator group for this window - accel = Gtk.AccelGroup() - # Add the ctrl+q to quit - accel.connect(Gdk.keyval_from_name('q'), Gdk.ModifierType.CONTROL_MASK, - Gtk.AccelFlags.VISIBLE, self.accel_quit) - # Lock the group - accel.lock() - # Add the group to the window - self.window.add_accel_group(accel) - - def ccheckbox_clicked(self, widget): - checked = self.ccheckbox.get_active() - self.lsbutton.set_sensitive(not checked) - if checked: - self.lsbutton_stopped() - self.emit('command', "continuous_listen") - self.set_icon_active() - else: - self.emit('command', "continuous_stop") - self.set_icon_inactive() - - def lsbutton_stopped(self): - self.lsbutton.set_label("Listen") - - def lsbutton_clicked(self, button): - val = self.lsbutton.get_label() - if val == "Listen": - self.emit("command", "listen") - self.lsbutton.set_label("Stop") - # Clear the label - self.label.set_text("") - self.set_icon_active() - else: - self.lsbutton_stopped() - self.emit("command", "stop") - self.set_icon_inactive() - - def run(self): - # Set the default icon - self.set_icon_inactive() - self.window.show_all() - if self.continuous: - self.set_icon_active() - self.ccheckbox.set_active(True) - - def accel_quit(self, accel_group, acceleratable, keyval, modifier): - self.emit("command", "quit") - - def delete_event(self, x, y): - self.emit("command", "quit") - - def finished(self, text): - # If the continuous isn't pressed - if not self.ccheckbox.get_active(): - self.lsbutton_stopped() - self.set_icon_inactive() - self.label.set_text(text) - - def set_icon_active_asset(self, i): - self.icon_active = i - - def set_icon_inactive_asset(self, i): - self.icon_inactive = i - - def set_icon_active(self): - Gtk.Window.set_default_icon_from_file(self.icon_active) - - def set_icon_inactive(self): - Gtk.Window.set_default_icon_from_file(self.icon_inactive) diff --git a/hasher.py b/hasher.py deleted file mode 100644 index 4aebd51..0000000 --- a/hasher.py +++ /dev/null @@ -1,37 +0,0 @@ -# This is part of Kaylee -# -- this code is licensed GPLv3 -# Copyright 2015-2016 Clayton G. Hobbs -# Portions Copyright 2013 Jezra - -import json -import hashlib - -class Hasher: - """Keep track of hashes for Kaylee""" - - def __init__(self, config): - self.config = config - try: - with open(self.config.hash_file, 'r') as f: - self.hashes = json.load(f) - except IOError: - # No stored hash - self.hashes = {} - - def __getitem__(self, hashname): - try: - return self.hashes[hashname] - except (KeyError, TypeError): - return None - - def __setitem__(self, hashname, value): - self.hashes[hashname] = value - - def get_hash_object(self): - """Returns an object to compute a new hash""" - return hashlib.sha256() - - def store(self): - """Store the current hashes into a the hash file""" - with open(self.config.hash_file, 'w') as f: - json.dump(self.hashes, f) diff --git a/kaylee.py b/kaylee.py index 298a2c6..082d6b6 100755 --- a/kaylee.py +++ b/kaylee.py @@ -1,212 +1,10 @@ #!/usr/bin/env python3 - # This is part of Kaylee # -- this code is licensed GPLv3 # Copyright 2015-2016 Clayton G. Hobbs # Portions Copyright 2013 Jezra -import sys -import signal -import os.path -import subprocess -from gi.repository import GObject, GLib - -from recognizer import Recognizer -from config import Config -from languageupdater import LanguageUpdater -from numberparser import NumberParser -from hasher import Hasher - - -class Kaylee: - - def __init__(self): - self.ui = None - self.options = {} - ui_continuous_listen = False - self.continuous_listen = False - - # Load configuration - self.config = Config() - self.options = vars(self.config.options) - self.commands = self.options['commands'] - - # Create number parser for later use - self.number_parser = NumberParser() - - # Create a hasher - self.hasher = Hasher(self.config) - - # Create the strings file - self.update_strings_file_if_changed() - - if self.options['interface']: - if self.options['interface'] == "g": - from gtkui import UI - elif self.options['interface'] == "gt": - from gtktrayui import UI - else: - print("no GUI defined") - sys.exit() - - self.ui = UI(self.options, self.options['continuous']) - self.ui.connect("command", self.process_command) - # Can we load the icon resource? - icon = self.load_resource("icon.png") - if icon: - self.ui.set_icon_active_asset(icon) - # Can we load the icon_inactive resource? - icon_inactive = self.load_resource("icon_inactive.png") - if icon_inactive: - self.ui.set_icon_inactive_asset(icon_inactive) - - if self.options['history']: - self.history = [] - - # Update the language if necessary - self.language_updater = LanguageUpdater(self.config) - self.language_updater.update_language_if_changed() - - # Create the recognizer - self.recognizer = Recognizer(self.config) - self.recognizer.connect('finished', self.recognizer_finished) +from kayleevc import kaylee - def update_voice_commands_if_changed(self): - """Use hashes to test if the voice commands have changed""" - stored_hash = self.hasher['voice_commands'] - - # Calculate the hash the voice commands have right now - hasher = self.hasher.get_hash_object() - for voice_cmd in self.commands.keys(): - hasher.update(voice_cmd.encode('utf-8')) - # Add a separator to avoid odd behavior - hasher.update('\n'.encode('utf-8')) - new_hash = hasher.hexdigest() - - if new_hash != stored_hash: - self.create_strings_file() - self.hasher['voice_commands'] = new_hash - self.hasher.store() - - def create_strings_file(self): - # Open the strings file - with open(self.config.strings_file, 'w') as strings: - # Add command words to the corpus - for voice_cmd in sorted(self.commands.keys()): - strings.write(voice_cmd.strip().replace('%d', '') + "\n") - # Add number words to the corpus - for word in self.number_parser.number_words: - strings.write(word + "\n") - - def log_history(self, text): - if self.options['history']: - self.history.append(text) - if len(self.history) > self.options['history']: - # Pop off the first item - self.history.pop(0) - - # Open and truncate the history file - with open(self.config.history_file, 'w') as hfile: - for line in self.history: - hfile.write(line + '\n') - - def run_command(self, cmd): - """Print the command, then run it""" - print(cmd) - subprocess.call(cmd, shell=True) - - def recognizer_finished(self, recognizer, text): - t = text.lower() - numt, nums = self.number_parser.parse_all_numbers(t) - # Is there a matching command? - if t in self.commands: - # Run the valid_sentence_command if it's set - if self.options['valid_sentence_command']: - subprocess.call(self.options['valid_sentence_command'], - shell=True) - cmd = self.commands[t] - # Should we be passing words? - if self.options['pass_words']: - cmd += " " + t - self.run_command(cmd) - self.log_history(text) - elif numt in self.commands: - # Run the valid_sentence_command if it's set - if self.options['valid_sentence_command']: - subprocess.call(self.options['valid_sentence_command'], - shell=True) - cmd = self.commands[numt] - cmd = cmd.format(*nums) - # Should we be passing words? - if self.options['pass_words']: - cmd += " " + t - self.run_command(cmd) - self.log_history(text) - else: - # Run the invalid_sentence_command if it's set - if self.options['invalid_sentence_command']: - subprocess.call(self.options['invalid_sentence_command'], - shell=True) - print("no matching command {0}".format(t)) - # If there is a UI and we are not continuous listen - if self.ui: - if not self.continuous_listen: - # Stop listening - self.recognizer.pause() - # Let the UI know that there is a finish - self.ui.finished(t) - - def run(self): - if self.ui: - self.ui.run() - else: - self.recognizer.listen() - - def quit(self): - sys.exit() - - def process_command(self, UI, command): - print(command) - if command == "listen": - self.recognizer.listen() - elif command == "stop": - self.recognizer.pause() - elif command == "continuous_listen": - self.continuous_listen = True - self.recognizer.listen() - elif command == "continuous_stop": - self.continuous_listen = False - self.recognizer.pause() - elif command == "quit": - self.quit() - - def load_resource(self, string): - local_data = os.path.join(os.path.dirname(__file__), 'data') - paths = ["/usr/share/kaylee/", "/usr/local/share/kaylee", local_data] - for path in paths: - resource = os.path.join(path, string) - if os.path.exists(resource): - return resource - # If we get this far, no resource was found - return False - - -if __name__ == "__main__": - # Make our kaylee object - kaylee = Kaylee() - # Init gobject threads - GObject.threads_init() - # We want a main loop - main_loop = GObject.MainLoop() - # Handle sigint - signal.signal(signal.SIGINT, signal.SIG_DFL) - # Run the kaylee +if __name__ == '__main__': kaylee.run() - # Start the main loop - try: - main_loop.run() - except: - print("time to quit") - main_loop.quit() - sys.exit() - diff --git a/kayleevc/__init__.py b/kayleevc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kayleevc/gui.py b/kayleevc/gui.py new file mode 100644 index 0000000..3369e33 --- /dev/null +++ b/kayleevc/gui.py @@ -0,0 +1,209 @@ +# This is part of Kaylee +# -- this code is licensed GPLv3 +# Copyright 2015-2016 Clayton G. Hobbs +# Portions Copyright 2013 Jezra + +import sys +import gi +from gi.repository import GObject +# Gtk +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, Gdk + +class GTKTrayInterface(GObject.GObject): + __gsignals__ = { + 'command' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)) + } + idle_text = "Kaylee - Idle" + listening_text = "Kaylee - Listening" + + def __init__(self, args, continuous): + GObject.GObject.__init__(self) + self.continuous = continuous + + self.statusicon = Gtk.StatusIcon() + self.statusicon.set_title("Kaylee") + self.statusicon.set_name("Kaylee") + self.statusicon.set_tooltip_text(self.idle_text) + self.statusicon.set_has_tooltip(True) + self.statusicon.connect("activate", self.continuous_toggle) + self.statusicon.connect("popup-menu", self.popup_menu) + + self.menu = Gtk.Menu() + self.menu_listen = Gtk.MenuItem('Listen') + self.menu_continuous = Gtk.CheckMenuItem('Continuous') + self.menu_quit = Gtk.MenuItem('Quit') + self.menu.append(self.menu_listen) + self.menu.append(self.menu_continuous) + self.menu.append(self.menu_quit) + self.menu_listen.connect("activate", self.toggle_listen) + self.menu_continuous.connect("toggled", self.toggle_continuous) + self.menu_quit.connect("activate", self.quit) + self.menu.show_all() + + def continuous_toggle(self, item): + checked = self.menu_continuous.get_active() + self.menu_continuous.set_active(not checked) + + def toggle_continuous(self, item): + checked = self.menu_continuous.get_active() + self.menu_listen.set_sensitive(not checked) + if checked: + self.menu_listen.set_label("Listen") + self.emit('command', "continuous_listen") + self.statusicon.set_tooltip_text(self.listening_text) + self.set_icon_active() + else: + self.set_icon_inactive() + self.statusicon.set_tooltip_text(self.idle_text) + self.emit('command', "continuous_stop") + + def toggle_listen(self, item): + val = self.menu_listen.get_label() + if val == "Listen": + self.set_icon_active() + self.emit("command", "listen") + self.menu_listen.set_label("Stop") + self.statusicon.set_tooltip_text(self.listening_text) + else: + self.set_icon_inactive() + self.menu_listen.set_label("Listen") + self.emit("command", "stop") + self.statusicon.set_tooltip_text(self.idle_text) + + def popup_menu(self, item, button, time): + self.menu.popup(None, None, Gtk.StatusIcon.position_menu, item, button, time) + + def run(self): + # Set the icon + self.set_icon_inactive() + if self.continuous: + self.menu_continuous.set_active(True) + self.set_icon_active() + else: + self.menu_continuous.set_active(False) + self.statusicon.set_visible(True) + + def quit(self, item): + self.statusicon.set_visible(False) + self.emit("command", "quit") + + def finished(self, text): + if not self.menu_continuous.get_active(): + self.menu_listen.set_label("Listen") + self.set_icon_inactive() + self.statusicon.set_tooltip_text(self.idle_text) + + def set_icon_active_asset(self, i): + self.icon_active = i + + def set_icon_inactive_asset(self, i): + self.icon_inactive = i + + def set_icon_active(self): + self.statusicon.set_from_file(self.icon_active) + + def set_icon_inactive(self): + self.statusicon.set_from_file(self.icon_inactive) + +class GTKInterface(GObject.GObject): + __gsignals__ = { + 'command' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)) + } + + def __init__(self, args, continuous): + GObject.GObject.__init__(self) + self.continuous = continuous + # Make a window + self.window = Gtk.Window(Gtk.WindowType.TOPLEVEL) + self.window.connect("delete_event", self.delete_event) + # Give the window a name + self.window.set_title("Kaylee") + self.window.set_resizable(False) + + layout = Gtk.VBox() + self.window.add(layout) + # Make a listen/stop button + self.lsbutton = Gtk.Button("Listen") + layout.add(self.lsbutton) + # Make a continuous button + self.ccheckbox = Gtk.CheckButton("Continuous Listen") + layout.add(self.ccheckbox) + + # Connect the buttons + self.lsbutton.connect("clicked", self.lsbutton_clicked) + self.ccheckbox.connect("clicked", self.ccheckbox_clicked) + + # Add a label to the UI to display the last command + self.label = Gtk.Label() + layout.add(self.label) + + # Create an accellerator group for this window + accel = Gtk.AccelGroup() + # Add the ctrl+q to quit + accel.connect(Gdk.keyval_from_name('q'), Gdk.ModifierType.CONTROL_MASK, + Gtk.AccelFlags.VISIBLE, self.accel_quit) + # Lock the group + accel.lock() + # Add the group to the window + self.window.add_accel_group(accel) + + def ccheckbox_clicked(self, widget): + checked = self.ccheckbox.get_active() + self.lsbutton.set_sensitive(not checked) + if checked: + self.lsbutton_stopped() + self.emit('command', "continuous_listen") + self.set_icon_active() + else: + self.emit('command', "continuous_stop") + self.set_icon_inactive() + + def lsbutton_stopped(self): + self.lsbutton.set_label("Listen") + + def lsbutton_clicked(self, button): + val = self.lsbutton.get_label() + if val == "Listen": + self.emit("command", "listen") + self.lsbutton.set_label("Stop") + # Clear the label + self.label.set_text("") + self.set_icon_active() + else: + self.lsbutton_stopped() + self.emit("command", "stop") + self.set_icon_inactive() + + def run(self): + # Set the default icon + self.set_icon_inactive() + self.window.show_all() + if self.continuous: + self.set_icon_active() + self.ccheckbox.set_active(True) + + def accel_quit(self, accel_group, acceleratable, keyval, modifier): + self.emit("command", "quit") + + def delete_event(self, x, y): + self.emit("command", "quit") + + def finished(self, text): + # If the continuous isn't pressed + if not self.ccheckbox.get_active(): + self.lsbutton_stopped() + self.set_icon_inactive() + self.label.set_text(text) + + def set_icon_active_asset(self, i): + self.icon_active = i + + def set_icon_inactive_asset(self, i): + self.icon_inactive = i + + def set_icon_active(self): + Gtk.Window.set_default_icon_from_file(self.icon_active) + + def set_icon_inactive(self): + Gtk.Window.set_default_icon_from_file(self.icon_inactive) diff --git a/kayleevc/kaylee.py b/kayleevc/kaylee.py new file mode 100644 index 0000000..a99f9d4 --- /dev/null +++ b/kayleevc/kaylee.py @@ -0,0 +1,207 @@ +# This is part of Kaylee +# -- this code is licensed GPLv3 +# Copyright 2015-2016 Clayton G. Hobbs +# Portions Copyright 2013 Jezra + +import sys +import signal +import os.path +import subprocess +from gi.repository import GObject, GLib + +from kayleevc.recognizer import Recognizer +from kayleevc.util import * +from kayleevc.numbers import NumberParser + + +class Kaylee: + + def __init__(self): + self.ui = None + self.options = {} + ui_continuous_listen = False + self.continuous_listen = False + + # Load configuration + self.config = Config() + self.options = vars(self.config.options) + self.commands = self.options['commands'] + + # Create number parser for later use + self.number_parser = NumberParser() + + # Create a hasher + self.hasher = Hasher(self.config) + + # Create the strings file + self.update_voice_commands_if_changed() + + if self.options['interface']: + if self.options['interface'] == "g": + from kayleevc.gui import GTKInterface as UI + elif self.options['interface'] == "gt": + from kayleevc.gui import GTKTrayInterface as UI + else: + print("no GUI defined") + sys.exit() + + self.ui = UI(self.options, self.options['continuous']) + self.ui.connect("command", self.process_command) + # Can we load the icon resource? + icon = self.load_resource("icon.png") + if icon: + self.ui.set_icon_active_asset(icon) + # Can we load the icon_inactive resource? + icon_inactive = self.load_resource("icon_inactive.png") + if icon_inactive: + self.ui.set_icon_inactive_asset(icon_inactive) + + if self.options['history']: + self.history = [] + + # Update the language if necessary + self.language_updater = LanguageUpdater(self.config) + self.language_updater.update_language_if_changed() + + # Create the recognizer + self.recognizer = Recognizer(self.config) + self.recognizer.connect('finished', self.recognizer_finished) + + def update_voice_commands_if_changed(self): + """Use hashes to test if the voice commands have changed""" + stored_hash = self.hasher['voice_commands'] + + # Calculate the hash the voice commands have right now + hasher = self.hasher.get_hash_object() + for voice_cmd in self.commands.keys(): + hasher.update(voice_cmd.encode('utf-8')) + # Add a separator to avoid odd behavior + hasher.update('\n'.encode('utf-8')) + new_hash = hasher.hexdigest() + + if new_hash != stored_hash: + self.create_strings_file() + self.hasher['voice_commands'] = new_hash + self.hasher.store() + + def create_strings_file(self): + # Open the strings file + with open(self.config.strings_file, 'w') as strings: + # Add command words to the corpus + for voice_cmd in sorted(self.commands.keys()): + strings.write(voice_cmd.strip().replace('%d', '') + "\n") + # Add number words to the corpus + for word in self.number_parser.number_words: + strings.write(word + "\n") + + def log_history(self, text): + if self.options['history']: + self.history.append(text) + if len(self.history) > self.options['history']: + # Pop off the first item + self.history.pop(0) + + # Open and truncate the history file + with open(self.config.history_file, 'w') as hfile: + for line in self.history: + hfile.write(line + '\n') + + def run_command(self, cmd): + """Print the command, then run it""" + print(cmd) + subprocess.call(cmd, shell=True) + + def recognizer_finished(self, recognizer, text): + t = text.lower() + numt, nums = self.number_parser.parse_all_numbers(t) + # Is there a matching command? + if t in self.commands: + # Run the valid_sentence_command if it's set + if self.options['valid_sentence_command']: + subprocess.call(self.options['valid_sentence_command'], + shell=True) + cmd = self.commands[t] + # Should we be passing words? + if self.options['pass_words']: + cmd += " " + t + self.run_command(cmd) + self.log_history(text) + elif numt in self.commands: + # Run the valid_sentence_command if it's set + if self.options['valid_sentence_command']: + subprocess.call(self.options['valid_sentence_command'], + shell=True) + cmd = self.commands[numt] + cmd = cmd.format(*nums) + # Should we be passing words? + if self.options['pass_words']: + cmd += " " + t + self.run_command(cmd) + self.log_history(text) + else: + # Run the invalid_sentence_command if it's set + if self.options['invalid_sentence_command']: + subprocess.call(self.options['invalid_sentence_command'], + shell=True) + print("no matching command {0}".format(t)) + # If there is a UI and we are not continuous listen + if self.ui: + if not self.continuous_listen: + # Stop listening + self.recognizer.pause() + # Let the UI know that there is a finish + self.ui.finished(t) + + def run(self): + if self.ui: + self.ui.run() + else: + self.recognizer.listen() + + def quit(self): + sys.exit() + + def process_command(self, UI, command): + print(command) + if command == "listen": + self.recognizer.listen() + elif command == "stop": + self.recognizer.pause() + elif command == "continuous_listen": + self.continuous_listen = True + self.recognizer.listen() + elif command == "continuous_stop": + self.continuous_listen = False + self.recognizer.pause() + elif command == "quit": + self.quit() + + def load_resource(self, string): + local_data = os.path.join(os.path.dirname(__file__), '..', 'data') + paths = ["/usr/share/kaylee/", "/usr/local/share/kaylee", local_data] + for path in paths: + resource = os.path.join(path, string) + if os.path.exists(resource): + return resource + # If we get this far, no resource was found + return False + + +def run(): + # Make our kaylee object + kaylee = Kaylee() + # Init gobject threads + GObject.threads_init() + # We want a main loop + main_loop = GObject.MainLoop() + # Handle sigint + signal.signal(signal.SIGINT, signal.SIG_DFL) + # Run the kaylee + kaylee.run() + # Start the main loop + try: + main_loop.run() + except: + print("time to quit") + main_loop.quit() + sys.exit() diff --git a/kayleevc/numbers.py b/kayleevc/numbers.py new file mode 100644 index 0000000..6d41b63 --- /dev/null +++ b/kayleevc/numbers.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python +# This is part of Kaylee +# -- this code is licensed GPLv3 +# Copyright 2015-2016 Clayton G. Hobbs +# Portions Copyright 2013 Jezra + +import re + +# Define the mappings from words to numbers +class NumberParser: + zero = { + 'zero': 0 + } + + ones = { + 'one': 1, + 'two': 2, + 'three': 3, + 'four': 4, + 'five': 5, + 'six': 6, + 'seven': 7, + 'eight': 8, + 'nine': 9 + } + + special_ones = { + 'ten': 10, + 'eleven': 11, + 'twelve': 12, + 'thirteen': 13, + 'fourteen': 14, + 'fifteen': 15, + 'sixteen': 16, + 'seventeen': 17, + 'eighteen': 18, + 'ninteen': 19 + } + + tens = { + 'twenty': 20, + 'thirty': 30, + 'fourty': 40, + 'fifty': 50, + 'sixty': 60, + 'seventy': 70, + 'eighty': 80, + 'ninty': 90 + } + + hundred = { + 'hundred': 100 + } + + exp = { + 'thousand': 1000, + 'million': 1000000, + 'billion': 1000000000 + } + + allowed = [ + 'and' + ] + + def __init__(self): + self.number_words = [] + for word in sorted(self.zero.keys()): + self.number_words.append(word) + for word in sorted(self.ones.keys()): + self.number_words.append(word) + for word in sorted(self.special_ones.keys()): + self.number_words.append(word) + for word in sorted(self.tens.keys()): + self.number_words.append(word) + for word in sorted(self.hundred.keys()): + self.number_words.append(word) + for word in sorted(self.exp.keys()): + self.number_words.append(word) + self.mandatory_number_words = self.number_words.copy() + for word in sorted(self.allowed): + self.number_words.append(word) + + def parse_number(self, text_line): + """ + Parse numbers from natural language into ints + + TODO: Throw more exceptions when invalid numbers are detected. Only + allow certian valueless words within numbers. Support zero. + """ + value = 0 + partial_value = 0 + last_list = None + + # Split text_line by commas, whitespace, and hyphens + text_line = text_line.strip() + text_words = re.split(r'[,\s-]+', text_line) + # Parse the number + for word in text_words: + if word in self.zero: + if last_list is not None: + raise ValueError('Invalid number') + value = 0 + last_list = self.zero + elif word in self.ones: + if last_list in (self.zero, self.ones, self.special_ones): + raise ValueError('Invalid number') + value += self.ones[word] + last_list = self.ones + elif word in self.special_ones: + if last_list in (self.zero, self.ones, self.special_ones, self.tens): + raise ValueError('Invalid number') + value += self.special_ones[word] + last_list = self.special_ones + elif word in self.tens: + if last_list in (self.zero, self.ones, self.special_ones, self.tens): + raise ValueError('Invalid number') + value += self.tens[word] + last_list = self.tens + elif word in self.hundred: + if last_list not in (self.ones, self.special_ones, self.tens): + raise ValueError('Invalid number') + value *= self.hundred[word] + last_list = self.hundred + elif word in self.exp: + if last_list in (self.zero, self.exp): + raise ValueError('Invalid number') + partial_value += value * self.exp[word] + value = 0 + last_list = self.exp + elif word not in self.allowed: + raise ValueError('Invalid number') + # Debugging information + #print(word, value, partial_value) + # Finish parsing the number + value += partial_value + return value + + def parse_all_numbers(self, text_line): + nums = [] + t_numless = '' + + # Split text_line by commas, whitespace, and hyphens + text_words = re.split(r'[,\s-]+', text_line.strip()) + # Get a string of word classes + tw_classes = '' + for word in text_words: + if word in self.mandatory_number_words: + tw_classes += 'm' + elif word in self.allowed: + tw_classes += 'a' + else: + tw_classes += 'w' + + # For each string of number words: + last_end = 0 + for m in re.finditer('m[am]*m|m', tw_classes): + # Get the number words + num_words = ' '.join(text_words[m.start():m.end()]) + # Parse the number and store the value + try: + nums.append(self.parse_number(num_words)) + except ValueError: + nums.append(-1) + # Add words to t_numless + t_numless += ' '.join(text_words[last_end:m.start()]) + ' %d ' + last_end = m.end() + t_numless += ' '.join(text_words[last_end:]) + + return (t_numless.strip(), nums) + +if __name__ == '__main__': + np = NumberParser() + # Get the words to translate + text_line = input('Enter a string: ') + # Parse it to an integer + value = np.parse_all_numbers(text_line) + # Print the result + print(value) diff --git a/kayleevc/recognizer.py b/kayleevc/recognizer.py new file mode 100644 index 0000000..b54c055 --- /dev/null +++ b/kayleevc/recognizer.py @@ -0,0 +1,69 @@ +# This is part of Kaylee +# -- this code is licensed GPLv3 +# Copyright 2015-2016 Clayton G. Hobbs +# Portions Copyright 2013 Jezra + +import os.path +import sys + +import gi +gi.require_version('Gst', '1.0') +from gi.repository import GObject, Gst +GObject.threads_init() +Gst.init(None) + + +class Recognizer(GObject.GObject): + __gsignals__ = { + 'finished' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, + (GObject.TYPE_STRING,)) + } + + def __init__(self, config): + GObject.GObject.__init__(self) + self.commands = {} + + src = config.options.microphone + if src: + audio_src = 'alsasrc device="hw:{0},0"'.format(src) + else: + audio_src = 'autoaudiosrc' + + # Build the pipeline + cmd = ( + audio_src + + ' ! audioconvert' + + ' ! audioresample' + + ' ! pocketsphinx lm=' + config.lang_file + ' dict=' + + config.dic_file + ' configured=true' + + ' ! appsink sync=false' + ) + try: + self.pipeline = Gst.parse_launch(cmd) + except Exception as e: + print(e.message) + print("You may need to install gstreamer1.0-pocketsphinx") + raise e + + # Process results from the pipeline with self.result() + bus = self.pipeline.get_bus() + bus.add_signal_watch() + bus.connect('message::element', self.result) + + def listen(self): + self.pipeline.set_state(Gst.State.PLAYING) + + def pause(self): + self.pipeline.set_state(Gst.State.PAUSED) + + def result(self, bus, msg): + msg_struct = msg.get_structure() + # Ignore messages that aren't from pocketsphinx + msgtype = msg_struct.get_name() + if msgtype != 'pocketsphinx': + return + + # If we have a final command, send it for processing + command = msg_struct.get_string('hypothesis') + if command != '' and msg_struct.get_boolean('final')[1]: + self.emit("finished", command) diff --git a/kayleevc/util.py b/kayleevc/util.py new file mode 100644 index 0000000..5c93b7f --- /dev/null +++ b/kayleevc/util.py @@ -0,0 +1,202 @@ +# This is part of Kaylee +# -- this code is licensed GPLv3 +# Copyright 2015-2016 Clayton G. Hobbs +# Portions Copyright 2013 Jezra + +import re +import json +import hashlib +import os +from argparse import ArgumentParser, Namespace + +import requests + +from gi.repository import GLib + +class Config: + """Keep track of the configuration of Kaylee""" + # Name of the program, for later use + program_name = "kaylee" + + # Directories + conf_dir = os.path.join(GLib.get_user_config_dir(), program_name) + cache_dir = os.path.join(GLib.get_user_cache_dir(), program_name) + data_dir = os.path.join(GLib.get_user_data_dir(), program_name) + + # Configuration files + command_file = os.path.join(conf_dir, "commands.conf") + opt_file = os.path.join(conf_dir, "options.json") + + # Cache files + history_file = os.path.join(cache_dir, program_name + "history") + hash_file = os.path.join(cache_dir, "hash.json") + + # Data files + strings_file = os.path.join(data_dir, "sentences.corpus") + lang_file = os.path.join(data_dir, 'lm') + dic_file = os.path.join(data_dir, 'dic') + + def __init__(self): + # Ensure necessary directories exist + self._make_dir(self.conf_dir) + self._make_dir(self.cache_dir) + self._make_dir(self.data_dir) + + # Set up the argument parser + self.parser = ArgumentParser() + self.parser.add_argument("-i", "--interface", type=str, + dest="interface", action='store', + help="Interface to use (if any). 'g' for GTK or 'gt' for GTK" + + " system tray icon") + + self.parser.add_argument("-c", "--continuous", + action="store_true", dest="continuous", default=False, + help="Start interface with 'continuous' listen enabled") + + self.parser.add_argument("-p", "--pass-words", + action="store_true", dest="pass_words", default=False, + help="Pass the recognized words as arguments to the shell" + + " command") + + self.parser.add_argument("-H", "--history", type=int, + action="store", dest="history", + help="Number of commands to store in history file") + + self.parser.add_argument("-m", "--microphone", type=int, + action="store", dest="microphone", default=None, + help="Audio input card to use (if other than system default)") + + self.parser.add_argument("--valid-sentence-command", type=str, + dest="valid_sentence_command", action='store', + help="Command to run when a valid sentence is detected") + + self.parser.add_argument("--invalid-sentence-command", type=str, + dest="invalid_sentence_command", action='store', + help="Command to run when an invalid sentence is detected") + + # Read the configuration file + self._read_options_file() + + # Parse command-line arguments, overriding config file as appropriate + self.parser.parse_args(namespace=self.options) + + def _make_dir(self, directory): + if not os.path.exists(directory): + os.makedirs(directory) + + def _read_options_file(self): + try: + with open(self.opt_file, 'r') as f: + self.options = json.load(f) + self.options = Namespace(**self.options) + except FileNotFoundError: + # Make an empty options namespace + self.options = Namespace() + +class Hasher: + """Keep track of hashes for Kaylee""" + + def __init__(self, config): + self.config = config + try: + with open(self.config.hash_file, 'r') as f: + self.hashes = json.load(f) + except IOError: + # No stored hash + self.hashes = {} + + def __getitem__(self, hashname): + try: + return self.hashes[hashname] + except (KeyError, TypeError): + return None + + def __setitem__(self, hashname, value): + self.hashes[hashname] = value + + def get_hash_object(self): + """Returns an object to compute a new hash""" + return hashlib.sha256() + + def store(self): + """Store the current hashes into a the hash file""" + with open(self.config.hash_file, 'w') as f: + json.dump(self.hashes, f) + +class LanguageUpdater: + """ + Handles updating the language using the online lmtool. + + This class provides methods to check if the corpus has changed, and to + update the language to match the new corpus using the lmtool. This allows + us to automatically update the language if the corpus has changed, saving + the user from having to do this manually. + """ + + def __init__(self, config): + self.config = config + self.hasher = Hasher(config) + + def update_language_if_changed(self): + """Test if the language has changed, and if it has, update it""" + if self.language_has_changed(): + self.update_language() + self.save_language_hash() + + def language_has_changed(self): + """Use hashes to test if the language has changed""" + self.stored_hash = self.hasher['language'] + + # Calculate the hash the language file has right now + hasher = self.hasher.get_hash_object() + with open(self.config.strings_file, 'rb') as sfile: + buf = sfile.read() + hasher.update(buf) + self.new_hash = hasher.hexdigest() + + return self.new_hash != self.stored_hash + + def update_language(self): + """Update the language using the online lmtool""" + print('Updating language using online lmtool') + + host = 'http://www.speech.cs.cmu.edu' + url = host + '/cgi-bin/tools/lmtool/run' + + # Submit the corpus to the lmtool + response_text = "" + with open(self.config.strings_file, 'rb') as corpus: + files = {'corpus': corpus} + values = {'formtype': 'simple'} + + r = requests.post(url, files=files, data=values) + response_text = r.text + + # Parse response to get URLs of the files we need + path_re = r'.*Index of (.*?).*' + number_re = r'.*TAR([0-9]*?)\.tgz.*' + for line in response_text.split('\n'): + # If we found the directory, keep it and don't break + if re.search(path_re, line): + path = host + re.sub(path_re, r'\1', line) + # If we found the number, keep it and break + elif re.search(number_re, line): + number = re.sub(number_re, r'\1', line) + break + + lm_url = path + '/' + number + '.lm' + dic_url = path + '/' + number + '.dic' + + self._download_file(lm_url, self.config.lang_file) + self._download_file(dic_url, self.config.dic_file) + + def save_language_hash(self): + self.hasher['language'] = self.new_hash + self.hasher.store() + + def _download_file(self, url, path): + r = requests.get(url, stream=True) + if r.status_code == 200: + with open(path, 'wb') as f: + for chunk in r: + f.write(chunk) diff --git a/languageupdater.py b/languageupdater.py deleted file mode 100644 index 3f36d06..0000000 --- a/languageupdater.py +++ /dev/null @@ -1,80 +0,0 @@ -# This is part of Kaylee -# -- this code is licensed GPLv3 -# Copyright 2015-2016 Clayton G. Hobbs -# Portions Copyright 2013 Jezra - -import re - -import requests - -from hasher import Hasher - -class LanguageUpdater: - - def __init__(self, config): - self.config = config - self.hasher = Hasher(config) - - def update_language_if_changed(self): - """Test if the language has changed, and if it has, update it""" - if self.language_has_changed(): - self.update_language() - self.save_language_hash() - - def language_has_changed(self): - """Use hashes to test if the language has changed""" - self.stored_hash = self.hasher['language'] - - # Calculate the hash the language file has right now - hasher = self.hasher.get_hash_object() - with open(self.config.strings_file, 'rb') as sfile: - buf = sfile.read() - hasher.update(buf) - self.new_hash = hasher.hexdigest() - - return self.new_hash != self.stored_hash - - def update_language(self): - """Update the language using the online lmtool""" - print('Updating language using online lmtool') - - host = 'http://www.speech.cs.cmu.edu' - url = host + '/cgi-bin/tools/lmtool/run' - - # Submit the corpus to the lmtool - response_text = "" - with open(self.config.strings_file, 'rb') as corpus: - files = {'corpus': corpus} - values = {'formtype': 'simple'} - - r = requests.post(url, files=files, data=values) - response_text = r.text - - # Parse response to get URLs of the files we need - path_re = r'.*Index of (.*?).*' - number_re = r'.*TAR([0-9]*?)\.tgz.*' - for line in response_text.split('\n'): - # If we found the directory, keep it and don't break - if re.search(path_re, line): - path = host + re.sub(path_re, r'\1', line) - # If we found the number, keep it and break - elif re.search(number_re, line): - number = re.sub(number_re, r'\1', line) - break - - lm_url = path + '/' + number + '.lm' - dic_url = path + '/' + number + '.dic' - - self._download_file(lm_url, self.config.lang_file) - self._download_file(dic_url, self.config.dic_file) - - def save_language_hash(self): - self.hasher['language'] = self.new_hash - self.hasher.store() - - def _download_file(self, url, path): - r = requests.get(url, stream=True) - if r.status_code == 200: - with open(path, 'wb') as f: - for chunk in r: - f.write(chunk) diff --git a/numberparser.py b/numberparser.py deleted file mode 100644 index fec07f2..0000000 --- a/numberparser.py +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/env python -# numberparser.py - Translate words to decimal - -# This is part of Kaylee -# -- this code is licensed GPLv3 -# Copyright 2015-2016 Clayton G. Hobbs -# Portions Copyright 2013 Jezra -import re - -# Define the mappings from words to numbers -class NumberParser: - zero = { - 'zero': 0 - } - - ones = { - 'one': 1, - 'two': 2, - 'three': 3, - 'four': 4, - 'five': 5, - 'six': 6, - 'seven': 7, - 'eight': 8, - 'nine': 9 - } - - special_ones = { - 'ten': 10, - 'eleven': 11, - 'twelve': 12, - 'thirteen': 13, - 'fourteen': 14, - 'fifteen': 15, - 'sixteen': 16, - 'seventeen': 17, - 'eighteen': 18, - 'ninteen': 19 - } - - tens = { - 'twenty': 20, - 'thirty': 30, - 'fourty': 40, - 'fifty': 50, - 'sixty': 60, - 'seventy': 70, - 'eighty': 80, - 'ninty': 90 - } - - hundred = { - 'hundred': 100 - } - - exp = { - 'thousand': 1000, - 'million': 1000000, - 'billion': 1000000000 - } - - allowed = [ - 'and' - ] - - def __init__(self): - self.number_words = [] - for word in sorted(self.zero.keys()): - self.number_words.append(word) - for word in sorted(self.ones.keys()): - self.number_words.append(word) - for word in sorted(self.special_ones.keys()): - self.number_words.append(word) - for word in sorted(self.tens.keys()): - self.number_words.append(word) - for word in sorted(self.hundred.keys()): - self.number_words.append(word) - for word in sorted(self.exp.keys()): - self.number_words.append(word) - self.mandatory_number_words = self.number_words.copy() - for word in sorted(self.allowed): - self.number_words.append(word) - - def parse_number(self, text_line): - """ - Parse numbers from natural language into ints - - TODO: Throw more exceptions when invalid numbers are detected. Only - allow certian valueless words within numbers. Support zero. - """ - value = 0 - partial_value = 0 - last_list = None - - # Split text_line by commas, whitespace, and hyphens - text_line = text_line.strip() - text_words = re.split(r'[,\s-]+', text_line) - # Parse the number - for word in text_words: - if word in self.zero: - if last_list is not None: - raise ValueError('Invalid number') - value = 0 - last_list = self.zero - elif word in self.ones: - if last_list in (self.zero, self.ones, self.special_ones): - raise ValueError('Invalid number') - value += self.ones[word] - last_list = self.ones - elif word in self.special_ones: - if last_list in (self.zero, self.ones, self.special_ones, self.tens): - raise ValueError('Invalid number') - value += self.special_ones[word] - last_list = self.special_ones - elif word in self.tens: - if last_list in (self.zero, self.ones, self.special_ones, self.tens): - raise ValueError('Invalid number') - value += self.tens[word] - last_list = self.tens - elif word in self.hundred: - if last_list not in (self.ones, self.special_ones, self.tens): - raise ValueError('Invalid number') - value *= self.hundred[word] - last_list = self.hundred - elif word in self.exp: - if last_list in (self.zero, self.exp): - raise ValueError('Invalid number') - partial_value += value * self.exp[word] - value = 0 - last_list = self.exp - elif word not in self.allowed: - raise ValueError('Invalid number') - # Debugging information - #print(word, value, partial_value) - # Finish parsing the number - value += partial_value - return value - - def parse_all_numbers(self, text_line): - nums = [] - t_numless = '' - - # Split text_line by commas, whitespace, and hyphens - text_words = re.split(r'[,\s-]+', text_line.strip()) - # Get a string of word classes - tw_classes = '' - for word in text_words: - if word in self.mandatory_number_words: - tw_classes += 'm' - elif word in self.allowed: - tw_classes += 'a' - else: - tw_classes += 'w' - - # For each string of number words: - last_end = 0 - for m in re.finditer('m[am]*m|m', tw_classes): - # Get the number words - num_words = ' '.join(text_words[m.start():m.end()]) - # Parse the number and store the value - try: - nums.append(self.parse_number(num_words)) - except ValueError: - nums.append(-1) - # Add words to t_numless - t_numless += ' '.join(text_words[last_end:m.start()]) + ' %d ' - last_end = m.end() - t_numless += ' '.join(text_words[last_end:]) - - return (t_numless.strip(), nums) - -if __name__ == '__main__': - np = NumberParser() - # Get the words to translate - text_line = input('Enter a string: ') - # Parse it to an integer - value = np.parse_all_numbers(text_line) - # Print the result - print(value) diff --git a/recognizer.py b/recognizer.py deleted file mode 100644 index b54c055..0000000 --- a/recognizer.py +++ /dev/null @@ -1,69 +0,0 @@ -# This is part of Kaylee -# -- this code is licensed GPLv3 -# Copyright 2015-2016 Clayton G. Hobbs -# Portions Copyright 2013 Jezra - -import os.path -import sys - -import gi -gi.require_version('Gst', '1.0') -from gi.repository import GObject, Gst -GObject.threads_init() -Gst.init(None) - - -class Recognizer(GObject.GObject): - __gsignals__ = { - 'finished' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, - (GObject.TYPE_STRING,)) - } - - def __init__(self, config): - GObject.GObject.__init__(self) - self.commands = {} - - src = config.options.microphone - if src: - audio_src = 'alsasrc device="hw:{0},0"'.format(src) - else: - audio_src = 'autoaudiosrc' - - # Build the pipeline - cmd = ( - audio_src + - ' ! audioconvert' + - ' ! audioresample' + - ' ! pocketsphinx lm=' + config.lang_file + ' dict=' + - config.dic_file + ' configured=true' + - ' ! appsink sync=false' - ) - try: - self.pipeline = Gst.parse_launch(cmd) - except Exception as e: - print(e.message) - print("You may need to install gstreamer1.0-pocketsphinx") - raise e - - # Process results from the pipeline with self.result() - bus = self.pipeline.get_bus() - bus.add_signal_watch() - bus.connect('message::element', self.result) - - def listen(self): - self.pipeline.set_state(Gst.State.PLAYING) - - def pause(self): - self.pipeline.set_state(Gst.State.PAUSED) - - def result(self, bus, msg): - msg_struct = msg.get_structure() - # Ignore messages that aren't from pocketsphinx - msgtype = msg_struct.get_name() - if msgtype != 'pocketsphinx': - return - - # If we have a final command, send it for processing - command = msg_struct.get_string('hypothesis') - if command != '' and msg_struct.get_boolean('final')[1]: - self.emit("finished", command) -- cgit 1.4.1 From a3f63727bf69a8338301d53f92fbb776d7ad3b0e Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sat, 30 Apr 2016 17:36:37 -0400 Subject: Tell users to copy config, not move it --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5715733..d39067b 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ but adds a lot of features that go beyond the original purpose of Blather. ## Usage -1. Move options.json.tmp to ~/.config/kaylee/options.json and fill the +1. Copy options.json.tmp to ~/.config/kaylee/options.json and fill the "commands" section of the file with sentences to speak and commands to run. 2. Run kaylee.py. This will generate ~/.local/share/kaylee/sentences.corpus based on sentences in the "commands" section of options.json, then use the -- cgit 1.4.1 From c4e5600c5eb38b542899bba197e917afb95041e8 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sat, 30 Apr 2016 17:39:26 -0400 Subject: Convert README to reStructuredText More Python-friendly, yada yada yada. --- README.md | 57 ---------------------------------------------------- README.rst | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 57 deletions(-) delete mode 100644 README.md create mode 100644 README.rst diff --git a/README.md b/README.md deleted file mode 100644 index d39067b..0000000 --- a/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# Kaylee - -Kaylee is a somewhat fancy speech recognizer that will run commands and perform -other functions when a user speaks loosely preset sentences. It is based on -[Blather](https://gitlab.com/jezra/blather) by [Jezra](http://www.jezra.net/), -but adds a lot of features that go beyond the original purpose of Blather. - -## Requirements - -1. Python 3 (tested with 3.5, may work with older versions) -1. pocketsphinx 5prealpha -2. gstreamer-1.0 (and what ever plugin has pocketsphinx support) -3. gstreamer-1.0 base plugins (required for ALSA) -4. python-gobject (required for GStreamer and the GTK-based UI) -5. python-requests (required for automatic language updating) - -**Note:** it may also be required to install `pocketsphinx-hmm-en-hub4wsj` - - -## Usage - -1. Copy options.json.tmp to ~/.config/kaylee/options.json and fill the - "commands" section of the file with sentences to speak and commands to run. -2. Run kaylee.py. This will generate ~/.local/share/kaylee/sentences.corpus - based on sentences in the "commands" section of options.json, then use the - [Sphinx Knowledge Base Tool](http://www.speech.cs.cmu.edu/tools/lmtool.html) - to create and save a new language model and dictionary. - * For GTK UI, run kaylee.py -i g - * To start a UI in 'continuous' listen mode, use the -c flag - * To use a microphone other than the system default, use the -m flag -3. Start talking! - -**Note:** default values for command-line arguments may be specified in the -options.json file. - -### Examples - -* To run Kaylee with the GTK UI, starting in continuous listen mode: - `./kaylee.py -i g -c` - -* To run Kaylee with no UI and using a USB microphone recognized as device 2: - `./kaylee.py -m 2` - -* To have Kaylee pass each word of the matched sentence as a separate argument - to the executed command: `./kaylee.py -p` - -* To run a command when a valid sentence has been detected: - `./kaylee.py --valid-sentence-command=/path/to/command` - -* To run a command when a invalid sentence has been detected: - `./kaylee.py --invalid-sentence-command=/path/to/command` - -### Finding the Device Number of a USB microphone -There are a few ways to find the device number of a USB microphone. - -* `cat /proc/asound/cards` -* `arecord -l` diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..c07c630 --- /dev/null +++ b/README.rst @@ -0,0 +1,68 @@ +Kaylee +====== + +Kaylee is a somewhat fancy speech recognizer that will run commands and +perform other functions when a user speaks loosely preset sentences. It +is based on `Blather `__ by +`Jezra `__, but adds a lot of features that go +beyond the original purpose of Blather. + +Requirements +------------ + +1. Python 3 (tested with 3.5, may work with older versions) +2. pocketsphinx 5prealpha +3. gstreamer-1.0 (and what ever plugin has pocketsphinx support) +4. gstreamer-1.0 base plugins (required for ALSA) +5. python-gobject (required for GStreamer and the GTK-based UI) +6. python-requests (required for automatic language updating) + +**Note:** it may also be required to install +``pocketsphinx-hmm-en-hub4wsj`` + +Usage +----- + +1. Copy options.json.tmp to ~/.config/kaylee/options.json and fill the + "commands" section of the file with sentences to speak and commands + to run. +2. Run kaylee.py. This will generate + ~/.local/share/kaylee/sentences.corpus based on sentences in the + "commands" section of options.json, then use the `Sphinx Knowledge + Base Tool `__ to + create and save a new language model and dictionary. + + - For GTK UI, run kaylee.py -i g + - To start a UI in 'continuous' listen mode, use the -c flag + - To use a microphone other than the system default, use the -m flag + +3. Start talking! + +**Note:** default values for command-line arguments may be specified in +the options.json file. + +Examples +~~~~~~~~ + +- To run Kaylee with the GTK UI, starting in continuous listen mode: + ``./kaylee.py -i g -c`` + +- To run Kaylee with no UI and using a USB microphone recognized as + device 2: ``./kaylee.py -m 2`` + +- To have Kaylee pass each word of the matched sentence as a separate + argument to the executed command: ``./kaylee.py -p`` + +- To run a command when a valid sentence has been detected: + ``./kaylee.py --valid-sentence-command=/path/to/command`` + +- To run a command when a invalid sentence has been detected: + ``./kaylee.py --invalid-sentence-command=/path/to/command`` + +Finding the Device Number of a USB microphone +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There are a few ways to find the device number of a USB microphone. + +- ``cat /proc/asound/cards`` +- ``arecord -l`` -- cgit 1.4.1 From 06132ae7a6b6c61289ff5fe60deae19184d8d374 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sat, 30 Apr 2016 18:28:23 -0400 Subject: Add packaging files to .gitignore --- .gitignore | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.gitignore b/.gitignore index 0d20b64..fa96d32 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,20 @@ +__pycache__/ *.pyc + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg -- cgit 1.4.1 From a1f694572a87a697d190f71c6577f44f2d607a9a Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sat, 30 Apr 2016 19:06:45 -0400 Subject: Add a setup.py script for installation I finally got around to learning how to make a setup.py script. I have finally blossomed into a mature adult. --- setup.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 setup.py diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..dc32d65 --- /dev/null +++ b/setup.py @@ -0,0 +1,38 @@ +from setuptools import setup + +with open("README.rst") as file: + long_description = file.read() + +setup( + name = "KayleeVC", + version = "0.1.0", + author = "Clayton G. Hobbs", + author_email = "clay@lakeserv.net", + description = ("Somewhat fancy voice command recognition software"), + license = "GPLv3+", + keywords = "voice speech command control", + url = "https://github.com/Ratfink/kaylee", + packages = ['kayleevc'], + long_description = long_description, + classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: X11 Applications :: GTK", + "Intended Audience :: End Users/Desktop", + "License :: OSI Approved :: GNU General Public License v3 or later " + "(GPLv3+)", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.5", + "Topic :: Home Automation" + ], + install_requires=["requests"], + data_files = [ + ("/usr/share/kaylee", ["data/icon_inactive.png", "data/icon.png", + "options.json.tmp"]) + ], + entry_points = { + "console_scripts": [ + "kaylee=kayleevc.kaylee:run" + ] + } +) -- cgit 1.4.1 From 7e62fce409fee102610762a05a27c851237fe479 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sat, 30 Apr 2016 22:03:45 -0400 Subject: Update to work with the latest pocketsphinx Apparently CMU thinks it's a good idea to release a new version of a piece of software that breaks existing APIs without even changing the version number. I find this idea to be highly dubious at best. Nevertheless, I have updated Kaylee to support the latest version of pocketsphinx. --- kayleevc/recognizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kayleevc/recognizer.py b/kayleevc/recognizer.py index b54c055..09e14e4 100644 --- a/kayleevc/recognizer.py +++ b/kayleevc/recognizer.py @@ -35,7 +35,7 @@ class Recognizer(GObject.GObject): ' ! audioconvert' + ' ! audioresample' + ' ! pocketsphinx lm=' + config.lang_file + ' dict=' + - config.dic_file + ' configured=true' + + config.dic_file + ' ! appsink sync=false' ) try: -- cgit 1.4.1 From 55f3c7b52c7bf42eed0b34651ea9b80cf61b72bc Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sat, 30 Apr 2016 22:07:42 -0400 Subject: Update version number to 0.1.1 Since I fixed a critical bug, I think a new version number is in order. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index dc32d65..444eaf0 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ with open("README.rst") as file: setup( name = "KayleeVC", - version = "0.1.0", + version = "0.1.1", author = "Clayton G. Hobbs", author_email = "clay@lakeserv.net", description = ("Somewhat fancy voice command recognition software"), -- cgit 1.4.1 From fc0196f697314307ab4ebb00bf7f5de43ca672fe Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sat, 14 May 2016 23:41:23 -0400 Subject: Add a systemd service file for starting Kaylee Next I need to make the setup.py script install it. It's meant to be a user service (installed to /usr/lib/systemd/user/), since Kaylee currently does not support being run outside of a normal user account. Also, it would be really nice to make it possible to reload the service once it has been started. This will require some changes to Kaylee to support synchronously reloading configuration. I understand that you aren't supposed to reload units asynchronously, and I *really* can't trust Kaylee to reload succinctly since reloading configuration may require contacting a remote server until #10 is implemented. Configuration reloading may be a separate issue from the systemd unit file, but I'd like to see it by version 0.2. --- systemd/kaylee.service | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 systemd/kaylee.service diff --git a/systemd/kaylee.service b/systemd/kaylee.service new file mode 100644 index 0000000..51c5a04 --- /dev/null +++ b/systemd/kaylee.service @@ -0,0 +1,8 @@ +[Unit] +Description=Somewhat fancy voice command recognition software + +[Service] +ExecStart=/usr/bin/kaylee + +[Install] +WantedBy=default.target -- cgit 1.4.1 From 2611c6dcb64efabbd36c4925175c1533aeddb51e Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sun, 15 May 2016 00:03:29 -0400 Subject: Install systemd unit file from setup.py Now distributions created from setup.py put the systemd unit file in the correct location for units provided by packages. It may then be enabled and started by the user with systemctl. Closes #11. --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 444eaf0..34ba60c 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,8 @@ setup( install_requires=["requests"], data_files = [ ("/usr/share/kaylee", ["data/icon_inactive.png", "data/icon.png", - "options.json.tmp"]) + "options.json.tmp"]), + ("/usr/lib/systemd/user", ["systemd/kaylee.service"]) ], entry_points = { "console_scripts": [ -- cgit 1.4.1 From bce8ef1763c0a068e30d6302eb7c0568e22ea525 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sun, 15 May 2016 12:18:44 -0400 Subject: Formatting and documentation Added some blank lines and wrapped some long lines to be closer to PEP 8 compliance. Improved docstrings in NumberParser class. Added docstrings for undocumented code and removed a TODO message that no longer applies. --- kayleevc/gui.py | 8 ++++++-- kayleevc/numbers.py | 19 ++++++++++++------- kayleevc/util.py | 3 +++ 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/kayleevc/gui.py b/kayleevc/gui.py index 3369e33..27085a8 100644 --- a/kayleevc/gui.py +++ b/kayleevc/gui.py @@ -10,9 +10,11 @@ from gi.repository import GObject gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk + class GTKTrayInterface(GObject.GObject): __gsignals__ = { - 'command' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)) + 'command' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, + (GObject.TYPE_STRING,)) } idle_text = "Kaylee - Idle" listening_text = "Kaylee - Listening" @@ -106,9 +108,11 @@ class GTKTrayInterface(GObject.GObject): def set_icon_inactive(self): self.statusicon.set_from_file(self.icon_inactive) + class GTKInterface(GObject.GObject): __gsignals__ = { - 'command' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)) + 'command': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, + (GObject.TYPE_STRING,)) } def __init__(self, args, continuous): diff --git a/kayleevc/numbers.py b/kayleevc/numbers.py index 6d41b63..cb52728 100644 --- a/kayleevc/numbers.py +++ b/kayleevc/numbers.py @@ -6,8 +6,10 @@ import re -# Define the mappings from words to numbers + class NumberParser: + """Parses integers from English strings""" + zero = { 'zero': 0 } @@ -81,12 +83,7 @@ class NumberParser: self.number_words.append(word) def parse_number(self, text_line): - """ - Parse numbers from natural language into ints - - TODO: Throw more exceptions when invalid numbers are detected. Only - allow certian valueless words within numbers. Support zero. - """ + """Parse a number from English into an int""" value = 0 partial_value = 0 last_list = None @@ -136,6 +133,13 @@ class NumberParser: return value def parse_all_numbers(self, text_line): + """ + Parse all numbers from English to ints + + Returns a tuple whose first element is text_line with all English + numbers replaced with "%d", and whose second element is a list + containing all the parsed numbers as ints. + """ nums = [] t_numless = '' @@ -168,6 +172,7 @@ class NumberParser: return (t_numless.strip(), nums) + if __name__ == '__main__': np = NumberParser() # Get the words to translate diff --git a/kayleevc/util.py b/kayleevc/util.py index 5c93b7f..8e33629 100644 --- a/kayleevc/util.py +++ b/kayleevc/util.py @@ -13,6 +13,7 @@ import requests from gi.repository import GLib + class Config: """Keep track of the configuration of Kaylee""" # Name of the program, for later use @@ -93,6 +94,7 @@ class Config: # Make an empty options namespace self.options = Namespace() + class Hasher: """Keep track of hashes for Kaylee""" @@ -123,6 +125,7 @@ class Hasher: with open(self.config.hash_file, 'w') as f: json.dump(self.hashes, f) + class LanguageUpdater: """ Handles updating the language using the online lmtool. -- cgit 1.4.1 From d314f9480d57483dbab912dd9d60fc30131810b3 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sun, 15 May 2016 17:38:27 -0400 Subject: Remove reference to the old commands file Commands aren't stored in a special file anymore, and Kaylee never tries to look at that file. Therefore, there's no need for the Config object to keep that file's path, and so the path has been removed. --- kayleevc/util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/kayleevc/util.py b/kayleevc/util.py index 8e33629..5155d06 100644 --- a/kayleevc/util.py +++ b/kayleevc/util.py @@ -25,7 +25,6 @@ class Config: data_dir = os.path.join(GLib.get_user_data_dir(), program_name) # Configuration files - command_file = os.path.join(conf_dir, "commands.conf") opt_file = os.path.join(conf_dir, "options.json") # Cache files -- cgit 1.4.1 From e3fd6a44b9824e1100b86a4f0278dc312bfb8cb0 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Mon, 16 May 2016 13:06:33 -0400 Subject: Renamed Config.parser -> Config._parser The argument parser isn't part of the API of the Config class, so now its name reflects that. --- kayleevc/util.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/kayleevc/util.py b/kayleevc/util.py index 5155d06..7984dc3 100644 --- a/kayleevc/util.py +++ b/kayleevc/util.py @@ -43,34 +43,34 @@ class Config: self._make_dir(self.data_dir) # Set up the argument parser - self.parser = ArgumentParser() - self.parser.add_argument("-i", "--interface", type=str, + self._parser = ArgumentParser() + self._parser.add_argument("-i", "--interface", type=str, dest="interface", action='store', help="Interface to use (if any). 'g' for GTK or 'gt' for GTK" + " system tray icon") - self.parser.add_argument("-c", "--continuous", + self._parser.add_argument("-c", "--continuous", action="store_true", dest="continuous", default=False, help="Start interface with 'continuous' listen enabled") - self.parser.add_argument("-p", "--pass-words", + self._parser.add_argument("-p", "--pass-words", action="store_true", dest="pass_words", default=False, help="Pass the recognized words as arguments to the shell" + " command") - self.parser.add_argument("-H", "--history", type=int, + self._parser.add_argument("-H", "--history", type=int, action="store", dest="history", help="Number of commands to store in history file") - self.parser.add_argument("-m", "--microphone", type=int, + self._parser.add_argument("-m", "--microphone", type=int, action="store", dest="microphone", default=None, help="Audio input card to use (if other than system default)") - self.parser.add_argument("--valid-sentence-command", type=str, + self._parser.add_argument("--valid-sentence-command", type=str, dest="valid_sentence_command", action='store', help="Command to run when a valid sentence is detected") - self.parser.add_argument("--invalid-sentence-command", type=str, + self._parser.add_argument("--invalid-sentence-command", type=str, dest="invalid_sentence_command", action='store', help="Command to run when an invalid sentence is detected") @@ -78,7 +78,7 @@ class Config: self._read_options_file() # Parse command-line arguments, overriding config file as appropriate - self.parser.parse_args(namespace=self.options) + self._parser.parse_args(namespace=self.options) def _make_dir(self, directory): if not os.path.exists(directory): -- cgit 1.4.1 From 7db43a846bbed39cdc9f134a65fe3251ee633fe1 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Mon, 16 May 2016 14:00:00 -0400 Subject: Remove unused code --- kayleevc/kaylee.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/kayleevc/kaylee.py b/kayleevc/kaylee.py index a99f9d4..f278444 100644 --- a/kayleevc/kaylee.py +++ b/kayleevc/kaylee.py @@ -19,7 +19,6 @@ class Kaylee: def __init__(self): self.ui = None self.options = {} - ui_continuous_listen = False self.continuous_listen = False # Load configuration @@ -177,6 +176,7 @@ class Kaylee: self.quit() def load_resource(self, string): + # TODO: Use the Config object for this path management local_data = os.path.join(os.path.dirname(__file__), '..', 'data') paths = ["/usr/share/kaylee/", "/usr/local/share/kaylee", local_data] for path in paths: @@ -202,6 +202,5 @@ def run(): try: main_loop.run() except: - print("time to quit") main_loop.quit() sys.exit() -- cgit 1.4.1 From 3128e3a172bb7189549d4d1b77f1b900bb3a26b2 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Tue, 17 May 2016 15:08:42 -0400 Subject: New icons! They still need work of course, since I'm a programmer and not an artist. The new icons of course are Kaylee's parasol, and anyone who doesn't know that needs to watch Firefly. --- assets/kaylee.svg | 272 ++++++++++++++++++++++++++++--------------- assets/kaylee_gray.svg | 219 ++++++++++++++++++++++++++++++++++ assets/kaylee_small.svg | 111 ++++++++++++++++++ assets/kaylee_small_gray.svg | 111 ++++++++++++++++++ data/icon.png | Bin 27147 -> 58750 bytes data/icon_inactive.png | Bin 27997 -> 57969 bytes data/icon_inactive_small.png | Bin 0 -> 2912 bytes data/icon_small.png | Bin 0 -> 2699 bytes kayleevc/kaylee.py | 4 +- 9 files changed, 624 insertions(+), 93 deletions(-) create mode 100644 assets/kaylee_gray.svg create mode 100644 assets/kaylee_small.svg create mode 100644 assets/kaylee_small_gray.svg create mode 100644 data/icon_inactive_small.png create mode 100644 data/icon_small.png diff --git a/assets/kaylee.svg b/assets/kaylee.svg index f5e3627..2ae00d5 100644 --- a/assets/kaylee.svg +++ b/assets/kaylee.svg @@ -9,43 +9,44 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="512" - height="1024" - id="svg2" - version="1.1" inkscape:version="0.91 r13725" + version="1.1" + id="svg2" + viewBox="0 0 256 256" + height="256" + width="256" sodipodi:docname="kaylee.svg" - inkscape:export-filename="/storage/projects/blather/assets/blathersrc.png" + inkscape:export-filename="/home/clay/kaylee/data/icon.png" inkscape:export-xdpi="90" inkscape:export-ydpi="90"> - + inkscape:guide-bbox="true" + units="px"> + @@ -55,75 +56,164 @@ image/svg+xml - + - - - - - K - - - - - K - + transform="translate(0,-796.36216)"> + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/kaylee_gray.svg b/assets/kaylee_gray.svg new file mode 100644 index 0000000..75bce7d --- /dev/null +++ b/assets/kaylee_gray.svg @@ -0,0 +1,219 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/kaylee_small.svg b/assets/kaylee_small.svg new file mode 100644 index 0000000..6a0268d --- /dev/null +++ b/assets/kaylee_small.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/assets/kaylee_small_gray.svg b/assets/kaylee_small_gray.svg new file mode 100644 index 0000000..36b3bd8 --- /dev/null +++ b/assets/kaylee_small_gray.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/data/icon.png b/data/icon.png index e9b3902..1569cb3 100644 Binary files a/data/icon.png and b/data/icon.png differ diff --git a/data/icon_inactive.png b/data/icon_inactive.png index 8ac72d5..0f67bb4 100644 Binary files a/data/icon_inactive.png and b/data/icon_inactive.png differ diff --git a/data/icon_inactive_small.png b/data/icon_inactive_small.png new file mode 100644 index 0000000..1d4e7ff Binary files /dev/null and b/data/icon_inactive_small.png differ diff --git a/data/icon_small.png b/data/icon_small.png new file mode 100644 index 0000000..3caa172 Binary files /dev/null and b/data/icon_small.png differ diff --git a/kayleevc/kaylee.py b/kayleevc/kaylee.py index f278444..18e21ef 100644 --- a/kayleevc/kaylee.py +++ b/kayleevc/kaylee.py @@ -47,11 +47,11 @@ class Kaylee: self.ui = UI(self.options, self.options['continuous']) self.ui.connect("command", self.process_command) # Can we load the icon resource? - icon = self.load_resource("icon.png") + icon = self.load_resource("icon_small.png") if icon: self.ui.set_icon_active_asset(icon) # Can we load the icon_inactive resource? - icon_inactive = self.load_resource("icon_inactive.png") + icon_inactive = self.load_resource("icon_inactive_small.png") if icon_inactive: self.ui.set_icon_inactive_asset(icon_inactive) -- cgit 1.4.1 From a4c186b826c93765f0281ad28ff753a0d75fcc88 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Tue, 31 May 2016 15:33:32 -0400 Subject: A bit of cleanup in the README It actually does do the things claimed in the first sentence, so don't use the future tense. There's no need to tell the user the details of intermediate steps in generating the language. All the user needs to know is that it uses the network to update the language model when it starts. --- README.rst | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index c07c630..7674312 100644 --- a/README.rst +++ b/README.rst @@ -1,8 +1,8 @@ Kaylee ====== -Kaylee is a somewhat fancy speech recognizer that will run commands and -perform other functions when a user speaks loosely preset sentences. It +Kaylee is a somewhat fancy speech recognizer that runs commands and +performs other functions when a user speaks loosely preset sentences. It is based on `Blather `__ by `Jezra `__, but adds a lot of features that go beyond the original purpose of Blather. @@ -26,15 +26,15 @@ Usage 1. Copy options.json.tmp to ~/.config/kaylee/options.json and fill the "commands" section of the file with sentences to speak and commands to run. -2. Run kaylee.py. This will generate - ~/.local/share/kaylee/sentences.corpus based on sentences in the - "commands" section of options.json, then use the `Sphinx Knowledge - Base Tool `__ to - create and save a new language model and dictionary. - - - For GTK UI, run kaylee.py -i g - - To start a UI in 'continuous' listen mode, use the -c flag - - To use a microphone other than the system default, use the -m flag +2. Run Kaylee with ``./kaylee.py``. This generates a language model and + dictionary using the `Sphinx Knowledge Base Tool + `__, then listens for + commands with the system default microphone. + + - For the GTK UI, run ``./kaylee.py -i g``. + - To start a UI in 'continuous' listen mode, use the ``-c`` flag. + - To use a microphone other than the system default, use the ``-m`` + flag. 3. Start talking! -- cgit 1.4.1 From cb4822025f2bd3e34ab39ca4a0cbef82a05d8f0d Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Wed, 29 Jun 2016 13:14:58 -0400 Subject: Put all number-words on one line in the corpus This makes pocketsphinx pick up way fewer false-positives of single number-words as recognised sentences. It doesn't seem to make any difference in anything else, but fewer false-positives is always nice. --- kayleevc/kaylee.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kayleevc/kaylee.py b/kayleevc/kaylee.py index 18e21ef..4e99d1a 100644 --- a/kayleevc/kaylee.py +++ b/kayleevc/kaylee.py @@ -91,7 +91,8 @@ class Kaylee: strings.write(voice_cmd.strip().replace('%d', '') + "\n") # Add number words to the corpus for word in self.number_parser.number_words: - strings.write(word + "\n") + strings.write(word + " ") + strings.write("\n") def log_history(self, text): if self.options['history']: -- cgit 1.4.1 From baeb0e1fa08fb6b21e4a22458de16b4948d1f2d0 Mon Sep 17 00:00:00 2001 From: "Clayton G. Hobbs" Date: Sun, 3 Jul 2016 21:18:23 -0400 Subject: Fixed recognition of "forty" I spelled it "fourty", which tricks the lmtool into making it much harder to recognize the word. Now it's spelled "forty", which makes it much easier to understand. --- kayleevc/numbers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kayleevc/numbers.py b/kayleevc/numbers.py index cb52728..be0036f 100644 --- a/kayleevc/numbers.py +++ b/kayleevc/numbers.py @@ -42,7 +42,7 @@ class NumberParser: tens = { 'twenty': 20, 'thirty': 30, - 'fourty': 40, + 'forty': 40, 'fifty': 50, 'sixty': 60, 'seventy': 70, -- cgit 1.4.1 From cf6e693773101b5ac6aea7a4186e7e15ce4508d5 Mon Sep 17 00:00:00 2001 From: Irene Knapp Date: Sat, 6 Sep 2025 15:42:27 -0700 Subject: move all the Kaylee code into a subdirectory this is preparation for merging it into the Pollyana repo Force-Push: yes Change-Id: I155624ba39a0d212c999f38aaf2de2cd62b7aa49 --- COPYING | 674 --------------------------------------- README.rst | 68 ---- assets/kaylee.svg | 219 ------------- assets/kaylee_gray.svg | 219 ------------- assets/kaylee_small.svg | 111 ------- assets/kaylee_small_gray.svg | 111 ------- data/icon.png | Bin 58750 -> 0 bytes data/icon_inactive.png | Bin 57969 -> 0 bytes data/icon_inactive_small.png | Bin 2912 -> 0 bytes data/icon_small.png | Bin 2699 -> 0 bytes kaylee.py | 10 - kayleevc/__init__.py | 0 kayleevc/gui.py | 213 ------------- kayleevc/kaylee.py | 207 ------------ kayleevc/numbers.py | 183 ----------- kayleevc/recognizer.py | 69 ---- kayleevc/util.py | 204 ------------ options.json.tmp | 12 - setup.py | 39 --- src/COPYING | 674 +++++++++++++++++++++++++++++++++++++++ src/README.rst | 68 ++++ src/assets/kaylee.svg | 219 +++++++++++++ src/assets/kaylee_gray.svg | 219 +++++++++++++ src/assets/kaylee_small.svg | 111 +++++++ src/assets/kaylee_small_gray.svg | 111 +++++++ src/data/icon.png | Bin 0 -> 58750 bytes src/data/icon_inactive.png | Bin 0 -> 57969 bytes src/data/icon_inactive_small.png | Bin 0 -> 2912 bytes src/data/icon_small.png | Bin 0 -> 2699 bytes src/kaylee.py | 10 + src/kayleevc/__init__.py | 0 src/kayleevc/gui.py | 213 +++++++++++++ src/kayleevc/kaylee.py | 207 ++++++++++++ src/kayleevc/numbers.py | 183 +++++++++++ src/kayleevc/recognizer.py | 69 ++++ src/kayleevc/util.py | 204 ++++++++++++ src/options.json.tmp | 12 + src/setup.py | 39 +++ src/systemd/kaylee.service | 8 + systemd/kaylee.service | 8 - 40 files changed, 2347 insertions(+), 2347 deletions(-) delete mode 100644 COPYING delete mode 100644 README.rst delete mode 100644 assets/kaylee.svg delete mode 100644 assets/kaylee_gray.svg delete mode 100644 assets/kaylee_small.svg delete mode 100644 assets/kaylee_small_gray.svg delete mode 100644 data/icon.png delete mode 100644 data/icon_inactive.png delete mode 100644 data/icon_inactive_small.png delete mode 100644 data/icon_small.png delete mode 100755 kaylee.py delete mode 100644 kayleevc/__init__.py delete mode 100644 kayleevc/gui.py delete mode 100644 kayleevc/kaylee.py delete mode 100644 kayleevc/numbers.py delete mode 100644 kayleevc/recognizer.py delete mode 100644 kayleevc/util.py delete mode 100644 options.json.tmp delete mode 100644 setup.py create mode 100644 src/COPYING create mode 100644 src/README.rst create mode 100644 src/assets/kaylee.svg create mode 100644 src/assets/kaylee_gray.svg create mode 100644 src/assets/kaylee_small.svg create mode 100644 src/assets/kaylee_small_gray.svg create mode 100644 src/data/icon.png create mode 100644 src/data/icon_inactive.png create mode 100644 src/data/icon_inactive_small.png create mode 100644 src/data/icon_small.png create mode 100755 src/kaylee.py create mode 100644 src/kayleevc/__init__.py create mode 100644 src/kayleevc/gui.py create mode 100644 src/kayleevc/kaylee.py create mode 100644 src/kayleevc/numbers.py create mode 100644 src/kayleevc/recognizer.py create mode 100644 src/kayleevc/util.py create mode 100644 src/options.json.tmp create mode 100644 src/setup.py create mode 100644 src/systemd/kaylee.service delete mode 100644 systemd/kaylee.service diff --git a/COPYING b/COPYING deleted file mode 100644 index 94a9ed0..0000000 --- a/COPYING +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/README.rst b/README.rst deleted file mode 100644 index 7674312..0000000 --- a/README.rst +++ /dev/null @@ -1,68 +0,0 @@ -Kaylee -====== - -Kaylee is a somewhat fancy speech recognizer that runs commands and -performs other functions when a user speaks loosely preset sentences. It -is based on `Blather `__ by -`Jezra `__, but adds a lot of features that go -beyond the original purpose of Blather. - -Requirements ------------- - -1. Python 3 (tested with 3.5, may work with older versions) -2. pocketsphinx 5prealpha -3. gstreamer-1.0 (and what ever plugin has pocketsphinx support) -4. gstreamer-1.0 base plugins (required for ALSA) -5. python-gobject (required for GStreamer and the GTK-based UI) -6. python-requests (required for automatic language updating) - -**Note:** it may also be required to install -``pocketsphinx-hmm-en-hub4wsj`` - -Usage ------ - -1. Copy options.json.tmp to ~/.config/kaylee/options.json and fill the - "commands" section of the file with sentences to speak and commands - to run. -2. Run Kaylee with ``./kaylee.py``. This generates a language model and - dictionary using the `Sphinx Knowledge Base Tool - `__, then listens for - commands with the system default microphone. - - - For the GTK UI, run ``./kaylee.py -i g``. - - To start a UI in 'continuous' listen mode, use the ``-c`` flag. - - To use a microphone other than the system default, use the ``-m`` - flag. - -3. Start talking! - -**Note:** default values for command-line arguments may be specified in -the options.json file. - -Examples -~~~~~~~~ - -- To run Kaylee with the GTK UI, starting in continuous listen mode: - ``./kaylee.py -i g -c`` - -- To run Kaylee with no UI and using a USB microphone recognized as - device 2: ``./kaylee.py -m 2`` - -- To have Kaylee pass each word of the matched sentence as a separate - argument to the executed command: ``./kaylee.py -p`` - -- To run a command when a valid sentence has been detected: - ``./kaylee.py --valid-sentence-command=/path/to/command`` - -- To run a command when a invalid sentence has been detected: - ``./kaylee.py --invalid-sentence-command=/path/to/command`` - -Finding the Device Number of a USB microphone -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -There are a few ways to find the device number of a USB microphone. - -- ``cat /proc/asound/cards`` -- ``arecord -l`` diff --git a/assets/kaylee.svg b/assets/kaylee.svg deleted file mode 100644 index 2ae00d5..0000000 --- a/assets/kaylee.svg +++ /dev/null @@ -1,219 +0,0 @@ - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/kaylee_gray.svg b/assets/kaylee_gray.svg deleted file mode 100644 index 75bce7d..0000000 --- a/assets/kaylee_gray.svg +++ /dev/null @@ -1,219 +0,0 @@ - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/kaylee_small.svg b/assets/kaylee_small.svg deleted file mode 100644 index 6a0268d..0000000 --- a/assets/kaylee_small.svg +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - diff --git a/assets/kaylee_small_gray.svg b/assets/kaylee_small_gray.svg deleted file mode 100644 index 36b3bd8..0000000 --- a/assets/kaylee_small_gray.svg +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - diff --git a/data/icon.png b/data/icon.png deleted file mode 100644 index 1569cb3..0000000 Binary files a/data/icon.png and /dev/null differ diff --git a/data/icon_inactive.png b/data/icon_inactive.png deleted file mode 100644 index 0f67bb4..0000000 Binary files a/data/icon_inactive.png and /dev/null differ diff --git a/data/icon_inactive_small.png b/data/icon_inactive_small.png deleted file mode 100644 index 1d4e7ff..0000000 Binary files a/data/icon_inactive_small.png and /dev/null differ diff --git a/data/icon_small.png b/data/icon_small.png deleted file mode 100644 index 3caa172..0000000 Binary files a/data/icon_small.png and /dev/null differ diff --git a/kaylee.py b/kaylee.py deleted file mode 100755 index 082d6b6..0000000 --- a/kaylee.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env python3 -# This is part of Kaylee -# -- this code is licensed GPLv3 -# Copyright 2015-2016 Clayton G. Hobbs -# Portions Copyright 2013 Jezra - -from kayleevc import kaylee - -if __name__ == '__main__': - kaylee.run() diff --git a/kayleevc/__init__.py b/kayleevc/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/kayleevc/gui.py b/kayleevc/gui.py deleted file mode 100644 index 27085a8..0000000 --- a/kayleevc/gui.py +++ /dev/null @@ -1,213 +0,0 @@ -# This is part of Kaylee -# -- this code is licensed GPLv3 -# Copyright 2015-2016 Clayton G. Hobbs -# Portions Copyright 2013 Jezra - -import sys -import gi -from gi.repository import GObject -# Gtk -gi.require_version('Gtk', '3.0') -from gi.repository import Gtk, Gdk - - -class GTKTrayInterface(GObject.GObject): - __gsignals__ = { - 'command' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, - (GObject.TYPE_STRING,)) - } - idle_text = "Kaylee - Idle" - listening_text = "Kaylee - Listening" - - def __init__(self, args, continuous): - GObject.GObject.__init__(self) - self.continuous = continuous - - self.statusicon = Gtk.StatusIcon() - self.statusicon.set_title("Kaylee") - self.statusicon.set_name("Kaylee") - self.statusicon.set_tooltip_text(self.idle_text) - self.statusicon.set_has_tooltip(True) - self.statusicon.connect("activate", self.continuous_toggle) - self.statusicon.connect("popup-menu", self.popup_menu) - - self.menu = Gtk.Menu() - self.menu_listen = Gtk.MenuItem('Listen') - self.menu_continuous = Gtk.CheckMenuItem('Continuous') - self.menu_quit = Gtk.MenuItem('Quit') - self.menu.append(self.menu_listen) - self.menu.append(self.menu_continuous) - self.menu.append(self.menu_quit) - self.menu_listen.connect("activate", self.toggle_listen) - self.menu_continuous.connect("toggled", self.toggle_continuous) - self.menu_quit.connect("activate", self.quit) - self.menu.show_all() - - def continuous_toggle(self, item): - checked = self.menu_continuous.get_active() - self.menu_continuous.set_active(not checked) - - def toggle_continuous(self, item): - checked = self.menu_continuous.get_active() - self.menu_listen.set_sensitive(not checked) - if checked: - self.menu_listen.set_label("Listen") - self.emit('command', "continuous_listen") - self.statusicon.set_tooltip_text(self.listening_text) - self.set_icon_active() - else: - self.set_icon_inactive() - self.statusicon.set_tooltip_text(self.idle_text) - self.emit('command', "continuous_stop") - - def toggle_listen(self, item): - val = self.menu_listen.get_label() - if val == "Listen": - self.set_icon_active() - self.emit("command", "listen") - self.menu_listen.set_label("Stop") - self.statusicon.set_tooltip_text(self.listening_text) - else: - self.set_icon_inactive() - self.menu_listen.set_label("Listen") - self.emit("command", "stop") - self.statusicon.set_tooltip_text(self.idle_text) - - def popup_menu(self, item, button, time): - self.menu.popup(None, None, Gtk.StatusIcon.position_menu, item, button, time) - - def run(self): - # Set the icon - self.set_icon_inactive() - if self.continuous: - self.menu_continuous.set_active(True) - self.set_icon_active() - else: - self.menu_continuous.set_active(False) - self.statusicon.set_visible(True) - - def quit(self, item): - self.statusicon.set_visible(False) - self.emit("command", "quit") - - def finished(self, text): - if not self.menu_continuous.get_active(): - self.menu_listen.set_label("Listen") - self.set_icon_inactive() - self.statusicon.set_tooltip_text(self.idle_text) - - def set_icon_active_asset(self, i): - self.icon_active = i - - def set_icon_inactive_asset(self, i): - self.icon_inactive = i - - def set_icon_active(self): - self.statusicon.set_from_file(self.icon_active) - - def set_icon_inactive(self): - self.statusicon.set_from_file(self.icon_inactive) - - -class GTKInterface(GObject.GObject): - __gsignals__ = { - 'command': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, - (GObject.TYPE_STRING,)) - } - - def __init__(self, args, continuous): - GObject.GObject.__init__(self) - self.continuous = continuous - # Make a window - self.window = Gtk.Window(Gtk.WindowType.TOPLEVEL) - self.window.connect("delete_event", self.delete_event) - # Give the window a name - self.window.set_title("Kaylee") - self.window.set_resizable(False) - - layout = Gtk.VBox() - self.window.add(layout) - # Make a listen/stop button - self.lsbutton = Gtk.Button("Listen") - layout.add(self.lsbutton) - # Make a continuous button - self.ccheckbox = Gtk.CheckButton("Continuous Listen") - layout.add(self.ccheckbox) - - # Connect the buttons - self.lsbutton.connect("clicked", self.lsbutton_clicked) - self.ccheckbox.connect("clicked", self.ccheckbox_clicked) - - # Add a label to the UI to display the last command - self.label = Gtk.Label() - layout.add(self.label) - - # Create an accellerator group for this window - accel = Gtk.AccelGroup() - # Add the ctrl+q to quit - accel.connect(Gdk.keyval_from_name('q'), Gdk.ModifierType.CONTROL_MASK, - Gtk.AccelFlags.VISIBLE, self.accel_quit) - # Lock the group - accel.lock() - # Add the group to the window - self.window.add_accel_group(accel) - - def ccheckbox_clicked(self, widget): - checked = self.ccheckbox.get_active() - self.lsbutton.set_sensitive(not checked) - if checked: - self.lsbutton_stopped() - self.emit('command', "continuous_listen") - self.set_icon_active() - else: - self.emit('command', "continuous_stop") - self.set_icon_inactive() - - def lsbutton_stopped(self): - self.lsbutton.set_label("Listen") - - def lsbutton_clicked(self, button): - val = self.lsbutton.get_label() - if val == "Listen": - self.emit("command", "listen") - self.lsbutton.set_label("Stop") - # Clear the label - self.label.set_text("") - self.set_icon_active() - else: - self.lsbutton_stopped() - self.emit("command", "stop") - self.set_icon_inactive() - - def run(self): - # Set the default icon - self.set_icon_inactive() - self.window.show_all() - if self.continuous: - self.set_icon_active() - self.ccheckbox.set_active(True) - - def accel_quit(self, accel_group, acceleratable, keyval, modifier): - self.emit("command", "quit") - - def delete_event(self, x, y): - self.emit("command", "quit") - - def finished(self, text): - # If the continuous isn't pressed - if not self.ccheckbox.get_active(): - self.lsbutton_stopped() - self.set_icon_inactive() - self.label.set_text(text) - - def set_icon_active_asset(self, i): - self.icon_active = i - - def set_icon_inactive_asset(self, i): - self.icon_inactive = i - - def set_icon_active(self): - Gtk.Window.set_default_icon_from_file(self.icon_active) - - def set_icon_inactive(self): - Gtk.Window.set_default_icon_from_file(self.icon_inactive) diff --git a/kayleevc/kaylee.py b/kayleevc/kaylee.py deleted file mode 100644 index 4e99d1a..0000000 --- a/kayleevc/kaylee.py +++ /dev/null @@ -1,207 +0,0 @@ -# This is part of Kaylee -# -- this code is licensed GPLv3 -# Copyright 2015-2016 Clayton G. Hobbs -# Portions Copyright 2013 Jezra - -import sys -import signal -import os.path -import subprocess -from gi.repository import GObject, GLib - -from kayleevc.recognizer import Recognizer -from kayleevc.util import * -from kayleevc.numbers import NumberParser - - -class Kaylee: - - def __init__(self): - self.ui = None - self.options = {} - self.continuous_listen = False - - # Load configuration - self.config = Config() - self.options = vars(self.config.options) - self.commands = self.options['commands'] - - # Create number parser for later use - self.number_parser = NumberParser() - - # Create a hasher - self.hasher = Hasher(self.config) - - # Create the strings file - self.update_voice_commands_if_changed() - - if self.options['interface']: - if self.options['interface'] == "g": - from kayleevc.gui import GTKInterface as UI - elif self.options['interface'] == "gt": - from kayleevc.gui import GTKTrayInterface as UI - else: - print("no GUI defined") - sys.exit() - - self.ui = UI(self.options, self.options['continuous']) - self.ui.connect("command", self.process_command) - # Can we load the icon resource? - icon = self.load_resource("icon_small.png") - if icon: - self.ui.set_icon_active_asset(icon) - # Can we load the icon_inactive resource? - icon_inactive = self.load_resource("icon_inactive_small.png") - if icon_inactive: - self.ui.set_icon_inactive_asset(icon_inactive) - - if self.options['history']: - self.history = [] - - # Update the language if necessary - self.language_updater = LanguageUpdater(self.config) - self.language_updater.update_language_if_changed() - - # Create the recognizer - self.recognizer = Recognizer(self.config) - self.recognizer.connect('finished', self.recognizer_finished) - - def update_voice_commands_if_changed(self): - """Use hashes to test if the voice commands have changed""" - stored_hash = self.hasher['voice_commands'] - - # Calculate the hash the voice commands have right now - hasher = self.hasher.get_hash_object() - for voice_cmd in self.commands.keys(): - hasher.update(voice_cmd.encode('utf-8')) - # Add a separator to avoid odd behavior - hasher.update('\n'.encode('utf-8')) - new_hash = hasher.hexdigest() - - if new_hash != stored_hash: - self.create_strings_file() - self.hasher['voice_commands'] = new_hash - self.hasher.store() - - def create_strings_file(self): - # Open the strings file - with open(self.config.strings_file, 'w') as strings: - # Add command words to the corpus - for voice_cmd in sorted(self.commands.keys()): - strings.write(voice_cmd.strip().replace('%d', '') + "\n") - # Add number words to the corpus - for word in self.number_parser.number_words: - strings.write(word + " ") - strings.write("\n") - - def log_history(self, text): - if self.options['history']: - self.history.append(text) - if len(self.history) > self.options['history']: - # Pop off the first item - self.history.pop(0) - - # Open and truncate the history file - with open(self.config.history_file, 'w') as hfile: - for line in self.history: - hfile.write(line + '\n') - - def run_command(self, cmd): - """Print the command, then run it""" - print(cmd) - subprocess.call(cmd, shell=True) - - def recognizer_finished(self, recognizer, text): - t = text.lower() - numt, nums = self.number_parser.parse_all_numbers(t) - # Is there a matching command? - if t in self.commands: - # Run the valid_sentence_command if it's set - if self.options['valid_sentence_command']: - subprocess.call(self.options['valid_sentence_command'], - shell=True) - cmd = self.commands[t] - # Should we be passing words? - if self.options['pass_words']: - cmd += " " + t - self.run_command(cmd) - self.log_history(text) - elif numt in self.commands: - # Run the valid_sentence_command if it's set - if self.options['valid_sentence_command']: - subprocess.call(self.options['valid_sentence_command'], - shell=True) - cmd = self.commands[numt] - cmd = cmd.format(*nums) - # Should we be passing words? - if self.options['pass_words']: - cmd += " " + t - self.run_command(cmd) - self.log_history(text) - else: - # Run the invalid_sentence_command if it's set - if self.options['invalid_sentence_command']: - subprocess.call(self.options['invalid_sentence_command'], - shell=True) - print("no matching command {0}".format(t)) - # If there is a UI and we are not continuous listen - if self.ui: - if not self.continuous_listen: - # Stop listening - self.recognizer.pause() - # Let the UI know that there is a finish - self.ui.finished(t) - - def run(self): - if self.ui: - self.ui.run() - else: - self.recognizer.listen() - - def quit(self): - sys.exit() - - def process_command(self, UI, command): - print(command) - if command == "listen": - self.recognizer.listen() - elif command == "stop": - self.recognizer.pause() - elif command == "continuous_listen": - self.continuous_listen = True - self.recognizer.listen() - elif command == "continuous_stop": - self.continuous_listen = False - self.recognizer.pause() - elif command == "quit": - self.quit() - - def load_resource(self, string): - # TODO: Use the Config object for this path management - local_data = os.path.join(os.path.dirname(__file__), '..', 'data') - paths = ["/usr/share/kaylee/", "/usr/local/share/kaylee", local_data] - for path in paths: - resource = os.path.join(path, string) - if os.path.exists(resource): - return resource - # If we get this far, no resource was found - return False - - -def run(): - # Make our kaylee object - kaylee = Kaylee() - # Init gobject threads - GObject.threads_init() - # We want a main loop - main_loop = GObject.MainLoop() - # Handle sigint - signal.signal(signal.SIGINT, signal.SIG_DFL) - # Run the kaylee - kaylee.run() - # Start the main loop - try: - main_loop.run() - except: - main_loop.quit() - sys.exit() diff --git a/kayleevc/numbers.py b/kayleevc/numbers.py deleted file mode 100644 index be0036f..0000000 --- a/kayleevc/numbers.py +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env python -# This is part of Kaylee -# -- this code is licensed GPLv3 -# Copyright 2015-2016 Clayton G. Hobbs -# Portions Copyright 2013 Jezra - -import re - - -class NumberParser: - """Parses integers from English strings""" - - zero = { - 'zero': 0 - } - - ones = { - 'one': 1, - 'two': 2, - 'three': 3, - 'four': 4, - 'five': 5, - 'six': 6, - 'seven': 7, - 'eight': 8, - 'nine': 9 - } - - special_ones = { - 'ten': 10, - 'eleven': 11, - 'twelve': 12, - 'thirteen': 13, - 'fourteen': 14, - 'fifteen': 15, - 'sixteen': 16, - 'seventeen': 17, - 'eighteen': 18, - 'ninteen': 19 - } - - tens = { - 'twenty': 20, - 'thirty': 30, - 'forty': 40, - 'fifty': 50, - 'sixty': 60, - 'seventy': 70, - 'eighty': 80, - 'ninty': 90 - } - - hundred = { - 'hundred': 100 - } - - exp = { - 'thousand': 1000, - 'million': 1000000, - 'billion': 1000000000 - } - - allowed = [ - 'and' - ] - - def __init__(self): - self.number_words = [] - for word in sorted(self.zero.keys()): - self.number_words.append(word) - for word in sorted(self.ones.keys()): - self.number_words.append(word) - for word in sorted(self.special_ones.keys()): - self.number_words.append(word) - for word in sorted(self.tens.keys()): - self.number_words.append(word) - for word in sorted(self.hundred.keys()): - self.number_words.append(word) - for word in sorted(self.exp.keys()): - self.number_words.append(word) - self.mandatory_number_words = self.number_words.copy() - for word in sorted(self.allowed): - self.number_words.append(word) - - def parse_number(self, text_line): - """Parse a number from English into an int""" - value = 0 - partial_value = 0 - last_list = None - - # Split text_line by commas, whitespace, and hyphens - text_line = text_line.strip() - text_words = re.split(r'[,\s-]+', text_line) - # Parse the number - for word in text_words: - if word in self.zero: - if last_list is not None: - raise ValueError('Invalid number') - value = 0 - last_list = self.zero - elif word in self.ones: - if last_list in (self.zero, self.ones, self.special_ones): - raise ValueError('Invalid number') - value += self.ones[word] - last_list = self.ones - elif word in self.special_ones: - if last_list in (self.zero, self.ones, self.special_ones, self.tens): - raise ValueError('Invalid number') - value += self.special_ones[word] - last_list = self.special_ones - elif word in self.tens: - if last_list in (self.zero, self.ones, self.special_ones, self.tens): - raise ValueError('Invalid number') - value += self.tens[word] - last_list = self.tens - elif word in self.hundred: - if last_list not in (self.ones, self.special_ones, self.tens): - raise ValueError('Invalid number') - value *= self.hundred[word] - last_list = self.hundred - elif word in self.exp: - if last_list in (self.zero, self.exp): - raise ValueError('Invalid number') - partial_value += value * self.exp[word] - value = 0 - last_list = self.exp - elif word not in self.allowed: - raise ValueError('Invalid number') - # Debugging information - #print(word, value, partial_value) - # Finish parsing the number - value += partial_value - return value - - def parse_all_numbers(self, text_line): - """ - Parse all numbers from English to ints - - Returns a tuple whose first element is text_line with all English - numbers replaced with "%d", and whose second element is a list - containing all the parsed numbers as ints. - """ - nums = [] - t_numless = '' - - # Split text_line by commas, whitespace, and hyphens - text_words = re.split(r'[,\s-]+', text_line.strip()) - # Get a string of word classes - tw_classes = '' - for word in text_words: - if word in self.mandatory_number_words: - tw_classes += 'm' - elif word in self.allowed: - tw_classes += 'a' - else: - tw_classes += 'w' - - # For each string of number words: - last_end = 0 - for m in re.finditer('m[am]*m|m', tw_classes): - # Get the number words - num_words = ' '.join(text_words[m.start():m.end()]) - # Parse the number and store the value - try: - nums.append(self.parse_number(num_words)) - except ValueError: - nums.append(-1) - # Add words to t_numless - t_numless += ' '.join(text_words[last_end:m.start()]) + ' %d ' - last_end = m.end() - t_numless += ' '.join(text_words[last_end:]) - - return (t_numless.strip(), nums) - - -if __name__ == '__main__': - np = NumberParser() - # Get the words to translate - text_line = input('Enter a string: ') - # Parse it to an integer - value = np.parse_all_numbers(text_line) - # Print the result - print(value) diff --git a/kayleevc/recognizer.py b/kayleevc/recognizer.py deleted file mode 100644 index 09e14e4..0000000 --- a/kayleevc/recognizer.py +++ /dev/null @@ -1,69 +0,0 @@ -# This is part of Kaylee -# -- this code is licensed GPLv3 -# Copyright 2015-2016 Clayton G. Hobbs -# Portions Copyright 2013 Jezra - -import os.path -import sys - -import gi -gi.require_version('Gst', '1.0') -from gi.repository import GObject, Gst -GObject.threads_init() -Gst.init(None) - - -class Recognizer(GObject.GObject): - __gsignals__ = { - 'finished' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, - (GObject.TYPE_STRING,)) - } - - def __init__(self, config): - GObject.GObject.__init__(self) - self.commands = {} - - src = config.options.microphone - if src: - audio_src = 'alsasrc device="hw:{0},0"'.format(src) - else: - audio_src = 'autoaudiosrc' - - # Build the pipeline - cmd = ( - audio_src + - ' ! audioconvert' + - ' ! audioresample' + - ' ! pocketsphinx lm=' + config.lang_file + ' dict=' + - config.dic_file + - ' ! appsink sync=false' - ) - try: - self.pipeline = Gst.parse_launch(cmd) - except Exception as e: - print(e.message) - print("You may need to install gstreamer1.0-pocketsphinx") - raise e - - # Process results from the pipeline with self.result() - bus = self.pipeline.get_bus() - bus.add_signal_watch() - bus.connect('message::element', self.result) - - def listen(self): - self.pipeline.set_state(Gst.State.PLAYING) - - def pause(self): - self.pipeline.set_state(Gst.State.PAUSED) - - def result(self, bus, msg): - msg_struct = msg.get_structure() - # Ignore messages that aren't from pocketsphinx - msgtype = msg_struct.get_name() - if msgtype != 'pocketsphinx': - return - - # If we have a final command, send it for processing - command = msg_struct.get_string('hypothesis') - if command != '' and msg_struct.get_boolean('final')[1]: - self.emit("finished", command) diff --git a/kayleevc/util.py b/kayleevc/util.py deleted file mode 100644 index 7984dc3..0000000 --- a/kayleevc/util.py +++ /dev/null @@ -1,204 +0,0 @@ -# This is part of Kaylee -# -- this code is licensed GPLv3 -# Copyright 2015-2016 Clayton G. Hobbs -# Portions Copyright 2013 Jezra - -import re -import json -import hashlib -import os -from argparse import ArgumentParser, Namespace - -import requests - -from gi.repository import GLib - - -class Config: - """Keep track of the configuration of Kaylee""" - # Name of the program, for later use - program_name = "kaylee" - - # Directories - conf_dir = os.path.join(GLib.get_user_config_dir(), program_name) - cache_dir = os.path.join(GLib.get_user_cache_dir(), program_name) - data_dir = os.path.join(GLib.get_user_data_dir(), program_name) - - # Configuration files - opt_file = os.path.join(conf_dir, "options.json") - - # Cache files - history_file = os.path.join(cache_dir, program_name + "history") - hash_file = os.path.join(cache_dir, "hash.json") - - # Data files - strings_file = os.path.join(data_dir, "sentences.corpus") - lang_file = os.path.join(data_dir, 'lm') - dic_file = os.path.join(data_dir, 'dic') - - def __init__(self): - # Ensure necessary directories exist - self._make_dir(self.conf_dir) - self._make_dir(self.cache_dir) - self._make_dir(self.data_dir) - - # Set up the argument parser - self._parser = ArgumentParser() - self._parser.add_argument("-i", "--interface", type=str, - dest="interface", action='store', - help="Interface to use (if any). 'g' for GTK or 'gt' for GTK" + - " system tray icon") - - self._parser.add_argument("-c", "--continuous", - action="store_true", dest="continuous", default=False, - help="Start interface with 'continuous' listen enabled") - - self._parser.add_argument("-p", "--pass-words", - action="store_true", dest="pass_words", default=False, - help="Pass the recognized words as arguments to the shell" + - " command") - - self._parser.add_argument("-H", "--history", type=int, - action="store", dest="history", - help="Number of commands to store in history file") - - self._parser.add_argument("-m", "--microphone", type=int, - action="store", dest="microphone", default=None, - help="Audio input card to use (if other than system default)") - - self._parser.add_argument("--valid-sentence-command", type=str, - dest="valid_sentence_command", action='store', - help="Command to run when a valid sentence is detected") - - self._parser.add_argument("--invalid-sentence-command", type=str, - dest="invalid_sentence_command", action='store', - help="Command to run when an invalid sentence is detected") - - # Read the configuration file - self._read_options_file() - - # Parse command-line arguments, overriding config file as appropriate - self._parser.parse_args(namespace=self.options) - - def _make_dir(self, directory): - if not os.path.exists(directory): - os.makedirs(directory) - - def _read_options_file(self): - try: - with open(self.opt_file, 'r') as f: - self.options = json.load(f) - self.options = Namespace(**self.options) - except FileNotFoundError: - # Make an empty options namespace - self.options = Namespace() - - -class Hasher: - """Keep track of hashes for Kaylee""" - - def __init__(self, config): - self.config = config - try: - with open(self.config.hash_file, 'r') as f: - self.hashes = json.load(f) - except IOError: - # No stored hash - self.hashes = {} - - def __getitem__(self, hashname): - try: - return self.hashes[hashname] - except (KeyError, TypeError): - return None - - def __setitem__(self, hashname, value): - self.hashes[hashname] = value - - def get_hash_object(self): - """Returns an object to compute a new hash""" - return hashlib.sha256() - - def store(self): - """Store the current hashes into a the hash file""" - with open(self.config.hash_file, 'w') as f: - json.dump(self.hashes, f) - - -class LanguageUpdater: - """ - Handles updating the language using the online lmtool. - - This class provides methods to check if the corpus has changed, and to - update the language to match the new corpus using the lmtool. This allows - us to automatically update the language if the corpus has changed, saving - the user from having to do this manually. - """ - - def __init__(self, config): - self.config = config - self.hasher = Hasher(config) - - def update_language_if_changed(self): - """Test if the language has changed, and if it has, update it""" - if self.language_has_changed(): - self.update_language() - self.save_language_hash() - - def language_has_changed(self): - """Use hashes to test if the language has changed""" - self.stored_hash = self.hasher['language'] - - # Calculate the hash the language file has right now - hasher = self.hasher.get_hash_object() - with open(self.config.strings_file, 'rb') as sfile: - buf = sfile.read() - hasher.update(buf) - self.new_hash = hasher.hexdigest() - - return self.new_hash != self.stored_hash - - def update_language(self): - """Update the language using the online lmtool""" - print('Updating language using online lmtool') - - host = 'http://www.speech.cs.cmu.edu' - url = host + '/cgi-bin/tools/lmtool/run' - - # Submit the corpus to the lmtool - response_text = "" - with open(self.config.strings_file, 'rb') as corpus: - files = {'corpus': corpus} - values = {'formtype': 'simple'} - - r = requests.post(url, files=files, data=values) - response_text = r.text - - # Parse response to get URLs of the files we need - path_re = r'.*Index of (.*?).*' - number_re = r'.*TAR([0-9]*?)\.tgz.*' - for line in response_text.split('\n'): - # If we found the directory, keep it and don't break - if re.search(path_re, line): - path = host + re.sub(path_re, r'\1', line) - # If we found the number, keep it and break - elif re.search(number_re, line): - number = re.sub(number_re, r'\1', line) - break - - lm_url = path + '/' + number + '.lm' - dic_url = path + '/' + number + '.dic' - - self._download_file(lm_url, self.config.lang_file) - self._download_file(dic_url, self.config.dic_file) - - def save_language_hash(self): - self.hasher['language'] = self.new_hash - self.hasher.store() - - def _download_file(self, url, path): - r = requests.get(url, stream=True) - if r.status_code == 200: - with open(path, 'wb') as f: - for chunk in r: - f.write(chunk) diff --git a/options.json.tmp b/options.json.tmp deleted file mode 100644 index dfde1e5..0000000 --- a/options.json.tmp +++ /dev/null @@ -1,12 +0,0 @@ -{ - "commands": { - "hello world": "echo \"hello world\"", - "start a %d minute timer": "(echo {0} minute timer started && sleep {0}m && echo {0} minute timer ended) &" - }, - "continuous": false, - "history": null, - "microphone": null, - "interface": null, - "valid_sentence_command": null, - "invalid_sentence_command": null -} diff --git a/setup.py b/setup.py deleted file mode 100644 index 34ba60c..0000000 --- a/setup.py +++ /dev/null @@ -1,39 +0,0 @@ -from setuptools import setup - -with open("README.rst") as file: - long_description = file.read() - -setup( - name = "KayleeVC", - version = "0.1.1", - author = "Clayton G. Hobbs", - author_email = "clay@lakeserv.net", - description = ("Somewhat fancy voice command recognition software"), - license = "GPLv3+", - keywords = "voice speech command control", - url = "https://github.com/Ratfink/kaylee", - packages = ['kayleevc'], - long_description = long_description, - classifiers = [ - "Development Status :: 3 - Alpha", - "Environment :: X11 Applications :: GTK", - "Intended Audience :: End Users/Desktop", - "License :: OSI Approved :: GNU General Public License v3 or later " - "(GPLv3+)", - "Operating System :: POSIX :: Linux", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.5", - "Topic :: Home Automation" - ], - install_requires=["requests"], - data_files = [ - ("/usr/share/kaylee", ["data/icon_inactive.png", "data/icon.png", - "options.json.tmp"]), - ("/usr/lib/systemd/user", ["systemd/kaylee.service"]) - ], - entry_points = { - "console_scripts": [ - "kaylee=kayleevc.kaylee:run" - ] - } -) diff --git a/src/COPYING b/src/COPYING new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/src/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/src/README.rst b/src/README.rst new file mode 100644 index 0000000..7674312 --- /dev/null +++ b/src/README.rst @@ -0,0 +1,68 @@ +Kaylee +====== + +Kaylee is a somewhat fancy speech recognizer that runs commands and +performs other functions when a user speaks loosely preset sentences. It +is based on `Blather `__ by +`Jezra `__, but adds a lot of features that go +beyond the original purpose of Blather. + +Requirements +------------ + +1. Python 3 (tested with 3.5, may work with older versions) +2. pocketsphinx 5prealpha +3. gstreamer-1.0 (and what ever plugin has pocketsphinx support) +4. gstreamer-1.0 base plugins (required for ALSA) +5. python-gobject (required for GStreamer and the GTK-based UI) +6. python-requests (required for automatic language updating) + +**Note:** it may also be required to install +``pocketsphinx-hmm-en-hub4wsj`` + +Usage +----- + +1. Copy options.json.tmp to ~/.config/kaylee/options.json and fill the + "commands" section of the file with sentences to speak and commands + to run. +2. Run Kaylee with ``./kaylee.py``. This generates a language model and + dictionary using the `Sphinx Knowledge Base Tool + `__, then listens for + commands with the system default microphone. + + - For the GTK UI, run ``./kaylee.py -i g``. + - To start a UI in 'continuous' listen mode, use the ``-c`` flag. + - To use a microphone other than the system default, use the ``-m`` + flag. + +3. Start talking! + +**Note:** default values for command-line arguments may be specified in +the options.json file. + +Examples +~~~~~~~~ + +- To run Kaylee with the GTK UI, starting in continuous listen mode: + ``./kaylee.py -i g -c`` + +- To run Kaylee with no UI and using a USB microphone recognized as + device 2: ``./kaylee.py -m 2`` + +- To have Kaylee pass each word of the matched sentence as a separate + argument to the executed command: ``./kaylee.py -p`` + +- To run a command when a valid sentence has been detected: + ``./kaylee.py --valid-sentence-command=/path/to/command`` + +- To run a command when a invalid sentence has been detected: + ``./kaylee.py --invalid-sentence-command=/path/to/command`` + +Finding the Device Number of a USB microphone +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There are a few ways to find the device number of a USB microphone. + +- ``cat /proc/asound/cards`` +- ``arecord -l`` diff --git a/src/assets/kaylee.svg b/src/assets/kaylee.svg new file mode 100644 index 0000000..2ae00d5 --- /dev/null +++ b/src/assets/kaylee.svg @@ -0,0 +1,219 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/kaylee_gray.svg b/src/assets/kaylee_gray.svg new file mode 100644 index 0000000..75bce7d --- /dev/null +++ b/src/assets/kaylee_gray.svg @@ -0,0 +1,219 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/kaylee_small.svg b/src/assets/kaylee_small.svg new file mode 100644 index 0000000..6a0268d --- /dev/null +++ b/src/assets/kaylee_small.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/src/assets/kaylee_small_gray.svg b/src/assets/kaylee_small_gray.svg new file mode 100644 index 0000000..36b3bd8 --- /dev/null +++ b/src/assets/kaylee_small_gray.svg @@ -0,0 +1,111 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/src/data/icon.png b/src/data/icon.png new file mode 100644 index 0000000..1569cb3 Binary files /dev/null and b/src/data/icon.png differ diff --git a/src/data/icon_inactive.png b/src/data/icon_inactive.png new file mode 100644 index 0000000..0f67bb4 Binary files /dev/null and b/src/data/icon_inactive.png differ diff --git a/src/data/icon_inactive_small.png b/src/data/icon_inactive_small.png new file mode 100644 index 0000000..1d4e7ff Binary files /dev/null and b/src/data/icon_inactive_small.png differ diff --git a/src/data/icon_small.png b/src/data/icon_small.png new file mode 100644 index 0000000..3caa172 Binary files /dev/null and b/src/data/icon_small.png differ diff --git a/src/kaylee.py b/src/kaylee.py new file mode 100755 index 0000000..082d6b6 --- /dev/null +++ b/src/kaylee.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +# This is part of Kaylee +# -- this code is licensed GPLv3 +# Copyright 2015-2016 Clayton G. Hobbs +# Portions Copyright 2013 Jezra + +from kayleevc import kaylee + +if __name__ == '__main__': + kaylee.run() diff --git a/src/kayleevc/__init__.py b/src/kayleevc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/kayleevc/gui.py b/src/kayleevc/gui.py new file mode 100644 index 0000000..27085a8 --- /dev/null +++ b/src/kayleevc/gui.py @@ -0,0 +1,213 @@ +# This is part of Kaylee +# -- this code is licensed GPLv3 +# Copyright 2015-2016 Clayton G. Hobbs +# Portions Copyright 2013 Jezra + +import sys +import gi +from gi.repository import GObject +# Gtk +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk, Gdk + + +class GTKTrayInterface(GObject.GObject): + __gsignals__ = { + 'command' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, + (GObject.TYPE_STRING,)) + } + idle_text = "Kaylee - Idle" + listening_text = "Kaylee - Listening" + + def __init__(self, args, continuous): + GObject.GObject.__init__(self) + self.continuous = continuous + + self.statusicon = Gtk.StatusIcon() + self.statusicon.set_title("Kaylee") + self.statusicon.set_name("Kaylee") + self.statusicon.set_tooltip_text(self.idle_text) + self.statusicon.set_has_tooltip(True) + self.statusicon.connect("activate", self.continuous_toggle) + self.statusicon.connect("popup-menu", self.popup_menu) + + self.menu = Gtk.Menu() + self.menu_listen = Gtk.MenuItem('Listen') + self.menu_continuous = Gtk.CheckMenuItem('Continuous') + self.menu_quit = Gtk.MenuItem('Quit') + self.menu.append(self.menu_listen) + self.menu.append(self.menu_continuous) + self.menu.append(self.menu_quit) + self.menu_listen.connect("activate", self.toggle_listen) + self.menu_continuous.connect("toggled", self.toggle_continuous) + self.menu_quit.connect("activate", self.quit) + self.menu.show_all() + + def continuous_toggle(self, item): + checked = self.menu_continuous.get_active() + self.menu_continuous.set_active(not checked) + + def toggle_continuous(self, item): + checked = self.menu_continuous.get_active() + self.menu_listen.set_sensitive(not checked) + if checked: + self.menu_listen.set_label("Listen") + self.emit('command', "continuous_listen") + self.statusicon.set_tooltip_text(self.listening_text) + self.set_icon_active() + else: + self.set_icon_inactive() + self.statusicon.set_tooltip_text(self.idle_text) + self.emit('command', "continuous_stop") + + def toggle_listen(self, item): + val = self.menu_listen.get_label() + if val == "Listen": + self.set_icon_active() + self.emit("command", "listen") + self.menu_listen.set_label("Stop") + self.statusicon.set_tooltip_text(self.listening_text) + else: + self.set_icon_inactive() + self.menu_listen.set_label("Listen") + self.emit("command", "stop") + self.statusicon.set_tooltip_text(self.idle_text) + + def popup_menu(self, item, button, time): + self.menu.popup(None, None, Gtk.StatusIcon.position_menu, item, button, time) + + def run(self): + # Set the icon + self.set_icon_inactive() + if self.continuous: + self.menu_continuous.set_active(True) + self.set_icon_active() + else: + self.menu_continuous.set_active(False) + self.statusicon.set_visible(True) + + def quit(self, item): + self.statusicon.set_visible(False) + self.emit("command", "quit") + + def finished(self, text): + if not self.menu_continuous.get_active(): + self.menu_listen.set_label("Listen") + self.set_icon_inactive() + self.statusicon.set_tooltip_text(self.idle_text) + + def set_icon_active_asset(self, i): + self.icon_active = i + + def set_icon_inactive_asset(self, i): + self.icon_inactive = i + + def set_icon_active(self): + self.statusicon.set_from_file(self.icon_active) + + def set_icon_inactive(self): + self.statusicon.set_from_file(self.icon_inactive) + + +class GTKInterface(GObject.GObject): + __gsignals__ = { + 'command': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, + (GObject.TYPE_STRING,)) + } + + def __init__(self, args, continuous): + GObject.GObject.__init__(self) + self.continuous = continuous + # Make a window + self.window = Gtk.Window(Gtk.WindowType.TOPLEVEL) + self.window.connect("delete_event", self.delete_event) + # Give the window a name + self.window.set_title("Kaylee") + self.window.set_resizable(False) + + layout = Gtk.VBox() + self.window.add(layout) + # Make a listen/stop button + self.lsbutton = Gtk.Button("Listen") + layout.add(self.lsbutton) + # Make a continuous button + self.ccheckbox = Gtk.CheckButton("Continuous Listen") + layout.add(self.ccheckbox) + + # Connect the buttons + self.lsbutton.connect("clicked", self.lsbutton_clicked) + self.ccheckbox.connect("clicked", self.ccheckbox_clicked) + + # Add a label to the UI to display the last command + self.label = Gtk.Label() + layout.add(self.label) + + # Create an accellerator group for this window + accel = Gtk.AccelGroup() + # Add the ctrl+q to quit + accel.connect(Gdk.keyval_from_name('q'), Gdk.ModifierType.CONTROL_MASK, + Gtk.AccelFlags.VISIBLE, self.accel_quit) + # Lock the group + accel.lock() + # Add the group to the window + self.window.add_accel_group(accel) + + def ccheckbox_clicked(self, widget): + checked = self.ccheckbox.get_active() + self.lsbutton.set_sensitive(not checked) + if checked: + self.lsbutton_stopped() + self.emit('command', "continuous_listen") + self.set_icon_active() + else: + self.emit('command', "continuous_stop") + self.set_icon_inactive() + + def lsbutton_stopped(self): + self.lsbutton.set_label("Listen") + + def lsbutton_clicked(self, button): + val = self.lsbutton.get_label() + if val == "Listen": + self.emit("command", "listen") + self.lsbutton.set_label("Stop") + # Clear the label + self.label.set_text("") + self.set_icon_active() + else: + self.lsbutton_stopped() + self.emit("command", "stop") + self.set_icon_inactive() + + def run(self): + # Set the default icon + self.set_icon_inactive() + self.window.show_all() + if self.continuous: + self.set_icon_active() + self.ccheckbox.set_active(True) + + def accel_quit(self, accel_group, acceleratable, keyval, modifier): + self.emit("command", "quit") + + def delete_event(self, x, y): + self.emit("command", "quit") + + def finished(self, text): + # If the continuous isn't pressed + if not self.ccheckbox.get_active(): + self.lsbutton_stopped() + self.set_icon_inactive() + self.label.set_text(text) + + def set_icon_active_asset(self, i): + self.icon_active = i + + def set_icon_inactive_asset(self, i): + self.icon_inactive = i + + def set_icon_active(self): + Gtk.Window.set_default_icon_from_file(self.icon_active) + + def set_icon_inactive(self): + Gtk.Window.set_default_icon_from_file(self.icon_inactive) diff --git a/src/kayleevc/kaylee.py b/src/kayleevc/kaylee.py new file mode 100644 index 0000000..4e99d1a --- /dev/null +++ b/src/kayleevc/kaylee.py @@ -0,0 +1,207 @@ +# This is part of Kaylee +# -- this code is licensed GPLv3 +# Copyright 2015-2016 Clayton G. Hobbs +# Portions Copyright 2013 Jezra + +import sys +import signal +import os.path +import subprocess +from gi.repository import GObject, GLib + +from kayleevc.recognizer import Recognizer +from kayleevc.util import * +from kayleevc.numbers import NumberParser + + +class Kaylee: + + def __init__(self): + self.ui = None + self.options = {} + self.continuous_listen = False + + # Load configuration + self.config = Config() + self.options = vars(self.config.options) + self.commands = self.options['commands'] + + # Create number parser for later use + self.number_parser = NumberParser() + + # Create a hasher + self.hasher = Hasher(self.config) + + # Create the strings file + self.update_voice_commands_if_changed() + + if self.options['interface']: + if self.options['interface'] == "g": + from kayleevc.gui import GTKInterface as UI + elif self.options['interface'] == "gt": + from kayleevc.gui import GTKTrayInterface as UI + else: + print("no GUI defined") + sys.exit() + + self.ui = UI(self.options, self.options['continuous']) + self.ui.connect("command", self.process_command) + # Can we load the icon resource? + icon = self.load_resource("icon_small.png") + if icon: + self.ui.set_icon_active_asset(icon) + # Can we load the icon_inactive resource? + icon_inactive = self.load_resource("icon_inactive_small.png") + if icon_inactive: + self.ui.set_icon_inactive_asset(icon_inactive) + + if self.options['history']: + self.history = [] + + # Update the language if necessary + self.language_updater = LanguageUpdater(self.config) + self.language_updater.update_language_if_changed() + + # Create the recognizer + self.recognizer = Recognizer(self.config) + self.recognizer.connect('finished', self.recognizer_finished) + + def update_voice_commands_if_changed(self): + """Use hashes to test if the voice commands have changed""" + stored_hash = self.hasher['voice_commands'] + + # Calculate the hash the voice commands have right now + hasher = self.hasher.get_hash_object() + for voice_cmd in self.commands.keys(): + hasher.update(voice_cmd.encode('utf-8')) + # Add a separator to avoid odd behavior + hasher.update('\n'.encode('utf-8')) + new_hash = hasher.hexdigest() + + if new_hash != stored_hash: + self.create_strings_file() + self.hasher['voice_commands'] = new_hash + self.hasher.store() + + def create_strings_file(self): + # Open the strings file + with open(self.config.strings_file, 'w') as strings: + # Add command words to the corpus + for voice_cmd in sorted(self.commands.keys()): + strings.write(voice_cmd.strip().replace('%d', '') + "\n") + # Add number words to the corpus + for word in self.number_parser.number_words: + strings.write(word + " ") + strings.write("\n") + + def log_history(self, text): + if self.options['history']: + self.history.append(text) + if len(self.history) > self.options['history']: + # Pop off the first item + self.history.pop(0) + + # Open and truncate the history file + with open(self.config.history_file, 'w') as hfile: + for line in self.history: + hfile.write(line + '\n') + + def run_command(self, cmd): + """Print the command, then run it""" + print(cmd) + subprocess.call(cmd, shell=True) + + def recognizer_finished(self, recognizer, text): + t = text.lower() + numt, nums = self.number_parser.parse_all_numbers(t) + # Is there a matching command? + if t in self.commands: + # Run the valid_sentence_command if it's set + if self.options['valid_sentence_command']: + subprocess.call(self.options['valid_sentence_command'], + shell=True) + cmd = self.commands[t] + # Should we be passing words? + if self.options['pass_words']: + cmd += " " + t + self.run_command(cmd) + self.log_history(text) + elif numt in self.commands: + # Run the valid_sentence_command if it's set + if self.options['valid_sentence_command']: + subprocess.call(self.options['valid_sentence_command'], + shell=True) + cmd = self.commands[numt] + cmd = cmd.format(*nums) + # Should we be passing words? + if self.options['pass_words']: + cmd += " " + t + self.run_command(cmd) + self.log_history(text) + else: + # Run the invalid_sentence_command if it's set + if self.options['invalid_sentence_command']: + subprocess.call(self.options['invalid_sentence_command'], + shell=True) + print("no matching command {0}".format(t)) + # If there is a UI and we are not continuous listen + if self.ui: + if not self.continuous_listen: + # Stop listening + self.recognizer.pause() + # Let the UI know that there is a finish + self.ui.finished(t) + + def run(self): + if self.ui: + self.ui.run() + else: + self.recognizer.listen() + + def quit(self): + sys.exit() + + def process_command(self, UI, command): + print(command) + if command == "listen": + self.recognizer.listen() + elif command == "stop": + self.recognizer.pause() + elif command == "continuous_listen": + self.continuous_listen = True + self.recognizer.listen() + elif command == "continuous_stop": + self.continuous_listen = False + self.recognizer.pause() + elif command == "quit": + self.quit() + + def load_resource(self, string): + # TODO: Use the Config object for this path management + local_data = os.path.join(os.path.dirname(__file__), '..', 'data') + paths = ["/usr/share/kaylee/", "/usr/local/share/kaylee", local_data] + for path in paths: + resource = os.path.join(path, string) + if os.path.exists(resource): + return resource + # If we get this far, no resource was found + return False + + +def run(): + # Make our kaylee object + kaylee = Kaylee() + # Init gobject threads + GObject.threads_init() + # We want a main loop + main_loop = GObject.MainLoop() + # Handle sigint + signal.signal(signal.SIGINT, signal.SIG_DFL) + # Run the kaylee + kaylee.run() + # Start the main loop + try: + main_loop.run() + except: + main_loop.quit() + sys.exit() diff --git a/src/kayleevc/numbers.py b/src/kayleevc/numbers.py new file mode 100644 index 0000000..be0036f --- /dev/null +++ b/src/kayleevc/numbers.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python +# This is part of Kaylee +# -- this code is licensed GPLv3 +# Copyright 2015-2016 Clayton G. Hobbs +# Portions Copyright 2013 Jezra + +import re + + +class NumberParser: + """Parses integers from English strings""" + + zero = { + 'zero': 0 + } + + ones = { + 'one': 1, + 'two': 2, + 'three': 3, + 'four': 4, + 'five': 5, + 'six': 6, + 'seven': 7, + 'eight': 8, + 'nine': 9 + } + + special_ones = { + 'ten': 10, + 'eleven': 11, + 'twelve': 12, + 'thirteen': 13, + 'fourteen': 14, + 'fifteen': 15, + 'sixteen': 16, + 'seventeen': 17, + 'eighteen': 18, + 'ninteen': 19 + } + + tens = { + 'twenty': 20, + 'thirty': 30, + 'forty': 40, + 'fifty': 50, + 'sixty': 60, + 'seventy': 70, + 'eighty': 80, + 'ninty': 90 + } + + hundred = { + 'hundred': 100 + } + + exp = { + 'thousand': 1000, + 'million': 1000000, + 'billion': 1000000000 + } + + allowed = [ + 'and' + ] + + def __init__(self): + self.number_words = [] + for word in sorted(self.zero.keys()): + self.number_words.append(word) + for word in sorted(self.ones.keys()): + self.number_words.append(word) + for word in sorted(self.special_ones.keys()): + self.number_words.append(word) + for word in sorted(self.tens.keys()): + self.number_words.append(word) + for word in sorted(self.hundred.keys()): + self.number_words.append(word) + for word in sorted(self.exp.keys()): + self.number_words.append(word) + self.mandatory_number_words = self.number_words.copy() + for word in sorted(self.allowed): + self.number_words.append(word) + + def parse_number(self, text_line): + """Parse a number from English into an int""" + value = 0 + partial_value = 0 + last_list = None + + # Split text_line by commas, whitespace, and hyphens + text_line = text_line.strip() + text_words = re.split(r'[,\s-]+', text_line) + # Parse the number + for word in text_words: + if word in self.zero: + if last_list is not None: + raise ValueError('Invalid number') + value = 0 + last_list = self.zero + elif word in self.ones: + if last_list in (self.zero, self.ones, self.special_ones): + raise ValueError('Invalid number') + value += self.ones[word] + last_list = self.ones + elif word in self.special_ones: + if last_list in (self.zero, self.ones, self.special_ones, self.tens): + raise ValueError('Invalid number') + value += self.special_ones[word] + last_list = self.special_ones + elif word in self.tens: + if last_list in (self.zero, self.ones, self.special_ones, self.tens): + raise ValueError('Invalid number') + value += self.tens[word] + last_list = self.tens + elif word in self.hundred: + if last_list not in (self.ones, self.special_ones, self.tens): + raise ValueError('Invalid number') + value *= self.hundred[word] + last_list = self.hundred + elif word in self.exp: + if last_list in (self.zero, self.exp): + raise ValueError('Invalid number') + partial_value += value * self.exp[word] + value = 0 + last_list = self.exp + elif word not in self.allowed: + raise ValueError('Invalid number') + # Debugging information + #print(word, value, partial_value) + # Finish parsing the number + value += partial_value + return value + + def parse_all_numbers(self, text_line): + """ + Parse all numbers from English to ints + + Returns a tuple whose first element is text_line with all English + numbers replaced with "%d", and whose second element is a list + containing all the parsed numbers as ints. + """ + nums = [] + t_numless = '' + + # Split text_line by commas, whitespace, and hyphens + text_words = re.split(r'[,\s-]+', text_line.strip()) + # Get a string of word classes + tw_classes = '' + for word in text_words: + if word in self.mandatory_number_words: + tw_classes += 'm' + elif word in self.allowed: + tw_classes += 'a' + else: + tw_classes += 'w' + + # For each string of number words: + last_end = 0 + for m in re.finditer('m[am]*m|m', tw_classes): + # Get the number words + num_words = ' '.join(text_words[m.start():m.end()]) + # Parse the number and store the value + try: + nums.append(self.parse_number(num_words)) + except ValueError: + nums.append(-1) + # Add words to t_numless + t_numless += ' '.join(text_words[last_end:m.start()]) + ' %d ' + last_end = m.end() + t_numless += ' '.join(text_words[last_end:]) + + return (t_numless.strip(), nums) + + +if __name__ == '__main__': + np = NumberParser() + # Get the words to translate + text_line = input('Enter a string: ') + # Parse it to an integer + value = np.parse_all_numbers(text_line) + # Print the result + print(value) diff --git a/src/kayleevc/recognizer.py b/src/kayleevc/recognizer.py new file mode 100644 index 0000000..09e14e4 --- /dev/null +++ b/src/kayleevc/recognizer.py @@ -0,0 +1,69 @@ +# This is part of Kaylee +# -- this code is licensed GPLv3 +# Copyright 2015-2016 Clayton G. Hobbs +# Portions Copyright 2013 Jezra + +import os.path +import sys + +import gi +gi.require_version('Gst', '1.0') +from gi.repository import GObject, Gst +GObject.threads_init() +Gst.init(None) + + +class Recognizer(GObject.GObject): + __gsignals__ = { + 'finished' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, + (GObject.TYPE_STRING,)) + } + + def __init__(self, config): + GObject.GObject.__init__(self) + self.commands = {} + + src = config.options.microphone + if src: + audio_src = 'alsasrc device="hw:{0},0"'.format(src) + else: + audio_src = 'autoaudiosrc' + + # Build the pipeline + cmd = ( + audio_src + + ' ! audioconvert' + + ' ! audioresample' + + ' ! pocketsphinx lm=' + config.lang_file + ' dict=' + + config.dic_file + + ' ! appsink sync=false' + ) + try: + self.pipeline = Gst.parse_launch(cmd) + except Exception as e: + print(e.message) + print("You may need to install gstreamer1.0-pocketsphinx") + raise e + + # Process results from the pipeline with self.result() + bus = self.pipeline.get_bus() + bus.add_signal_watch() + bus.connect('message::element', self.result) + + def listen(self): + self.pipeline.set_state(Gst.State.PLAYING) + + def pause(self): + self.pipeline.set_state(Gst.State.PAUSED) + + def result(self, bus, msg): + msg_struct = msg.get_structure() + # Ignore messages that aren't from pocketsphinx + msgtype = msg_struct.get_name() + if msgtype != 'pocketsphinx': + return + + # If we have a final command, send it for processing + command = msg_struct.get_string('hypothesis') + if command != '' and msg_struct.get_boolean('final')[1]: + self.emit("finished", command) diff --git a/src/kayleevc/util.py b/src/kayleevc/util.py new file mode 100644 index 0000000..7984dc3 --- /dev/null +++ b/src/kayleevc/util.py @@ -0,0 +1,204 @@ +# This is part of Kaylee +# -- this code is licensed GPLv3 +# Copyright 2015-2016 Clayton G. Hobbs +# Portions Copyright 2013 Jezra + +import re +import json +import hashlib +import os +from argparse import ArgumentParser, Namespace + +import requests + +from gi.repository import GLib + + +class Config: + """Keep track of the configuration of Kaylee""" + # Name of the program, for later use + program_name = "kaylee" + + # Directories + conf_dir = os.path.join(GLib.get_user_config_dir(), program_name) + cache_dir = os.path.join(GLib.get_user_cache_dir(), program_name) + data_dir = os.path.join(GLib.get_user_data_dir(), program_name) + + # Configuration files + opt_file = os.path.join(conf_dir, "options.json") + + # Cache files + history_file = os.path.join(cache_dir, program_name + "history") + hash_file = os.path.join(cache_dir, "hash.json") + + # Data files + strings_file = os.path.join(data_dir, "sentences.corpus") + lang_file = os.path.join(data_dir, 'lm') + dic_file = os.path.join(data_dir, 'dic') + + def __init__(self): + # Ensure necessary directories exist + self._make_dir(self.conf_dir) + self._make_dir(self.cache_dir) + self._make_dir(self.data_dir) + + # Set up the argument parser + self._parser = ArgumentParser() + self._parser.add_argument("-i", "--interface", type=str, + dest="interface", action='store', + help="Interface to use (if any). 'g' for GTK or 'gt' for GTK" + + " system tray icon") + + self._parser.add_argument("-c", "--continuous", + action="store_true", dest="continuous", default=False, + help="Start interface with 'continuous' listen enabled") + + self._parser.add_argument("-p", "--pass-words", + action="store_true", dest="pass_words", default=False, + help="Pass the recognized words as arguments to the shell" + + " command") + + self._parser.add_argument("-H", "--history", type=int, + action="store", dest="history", + help="Number of commands to store in history file") + + self._parser.add_argument("-m", "--microphone", type=int, + action="store", dest="microphone", default=None, + help="Audio input card to use (if other than system default)") + + self._parser.add_argument("--valid-sentence-command", type=str, + dest="valid_sentence_command", action='store', + help="Command to run when a valid sentence is detected") + + self._parser.add_argument("--invalid-sentence-command", type=str, + dest="invalid_sentence_command", action='store', + help="Command to run when an invalid sentence is detected") + + # Read the configuration file + self._read_options_file() + + # Parse command-line arguments, overriding config file as appropriate + self._parser.parse_args(namespace=self.options) + + def _make_dir(self, directory): + if not os.path.exists(directory): + os.makedirs(directory) + + def _read_options_file(self): + try: + with open(self.opt_file, 'r') as f: + self.options = json.load(f) + self.options = Namespace(**self.options) + except FileNotFoundError: + # Make an empty options namespace + self.options = Namespace() + + +class Hasher: + """Keep track of hashes for Kaylee""" + + def __init__(self, config): + self.config = config + try: + with open(self.config.hash_file, 'r') as f: + self.hashes = json.load(f) + except IOError: + # No stored hash + self.hashes = {} + + def __getitem__(self, hashname): + try: + return self.hashes[hashname] + except (KeyError, TypeError): + return None + + def __setitem__(self, hashname, value): + self.hashes[hashname] = value + + def get_hash_object(self): + """Returns an object to compute a new hash""" + return hashlib.sha256() + + def store(self): + """Store the current hashes into a the hash file""" + with open(self.config.hash_file, 'w') as f: + json.dump(self.hashes, f) + + +class LanguageUpdater: + """ + Handles updating the language using the online lmtool. + + This class provides methods to check if the corpus has changed, and to + update the language to match the new corpus using the lmtool. This allows + us to automatically update the language if the corpus has changed, saving + the user from having to do this manually. + """ + + def __init__(self, config): + self.config = config + self.hasher = Hasher(config) + + def update_language_if_changed(self): + """Test if the language has changed, and if it has, update it""" + if self.language_has_changed(): + self.update_language() + self.save_language_hash() + + def language_has_changed(self): + """Use hashes to test if the language has changed""" + self.stored_hash = self.hasher['language'] + + # Calculate the hash the language file has right now + hasher = self.hasher.get_hash_object() + with open(self.config.strings_file, 'rb') as sfile: + buf = sfile.read() + hasher.update(buf) + self.new_hash = hasher.hexdigest() + + return self.new_hash != self.stored_hash + + def update_language(self): + """Update the language using the online lmtool""" + print('Updating language using online lmtool') + + host = 'http://www.speech.cs.cmu.edu' + url = host + '/cgi-bin/tools/lmtool/run' + + # Submit the corpus to the lmtool + response_text = "" + with open(self.config.strings_file, 'rb') as corpus: + files = {'corpus': corpus} + values = {'formtype': 'simple'} + + r = requests.post(url, files=files, data=values) + response_text = r.text + + # Parse response to get URLs of the files we need + path_re = r'.*Index of (.*?).*' + number_re = r'.*TAR([0-9]*?)\.tgz.*' + for line in response_text.split('\n'): + # If we found the directory, keep it and don't break + if re.search(path_re, line): + path = host + re.sub(path_re, r'\1', line) + # If we found the number, keep it and break + elif re.search(number_re, line): + number = re.sub(number_re, r'\1', line) + break + + lm_url = path + '/' + number + '.lm' + dic_url = path + '/' + number + '.dic' + + self._download_file(lm_url, self.config.lang_file) + self._download_file(dic_url, self.config.dic_file) + + def save_language_hash(self): + self.hasher['language'] = self.new_hash + self.hasher.store() + + def _download_file(self, url, path): + r = requests.get(url, stream=True) + if r.status_code == 200: + with open(path, 'wb') as f: + for chunk in r: + f.write(chunk) diff --git a/src/options.json.tmp b/src/options.json.tmp new file mode 100644 index 0000000..dfde1e5 --- /dev/null +++ b/src/options.json.tmp @@ -0,0 +1,12 @@ +{ + "commands": { + "hello world": "echo \"hello world\"", + "start a %d minute timer": "(echo {0} minute timer started && sleep {0}m && echo {0} minute timer ended) &" + }, + "continuous": false, + "history": null, + "microphone": null, + "interface": null, + "valid_sentence_command": null, + "invalid_sentence_command": null +} diff --git a/src/setup.py b/src/setup.py new file mode 100644 index 0000000..34ba60c --- /dev/null +++ b/src/setup.py @@ -0,0 +1,39 @@ +from setuptools import setup + +with open("README.rst") as file: + long_description = file.read() + +setup( + name = "KayleeVC", + version = "0.1.1", + author = "Clayton G. Hobbs", + author_email = "clay@lakeserv.net", + description = ("Somewhat fancy voice command recognition software"), + license = "GPLv3+", + keywords = "voice speech command control", + url = "https://github.com/Ratfink/kaylee", + packages = ['kayleevc'], + long_description = long_description, + classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: X11 Applications :: GTK", + "Intended Audience :: End Users/Desktop", + "License :: OSI Approved :: GNU General Public License v3 or later " + "(GPLv3+)", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.5", + "Topic :: Home Automation" + ], + install_requires=["requests"], + data_files = [ + ("/usr/share/kaylee", ["data/icon_inactive.png", "data/icon.png", + "options.json.tmp"]), + ("/usr/lib/systemd/user", ["systemd/kaylee.service"]) + ], + entry_points = { + "console_scripts": [ + "kaylee=kayleevc.kaylee:run" + ] + } +) diff --git a/src/systemd/kaylee.service b/src/systemd/kaylee.service new file mode 100644 index 0000000..51c5a04 --- /dev/null +++ b/src/systemd/kaylee.service @@ -0,0 +1,8 @@ +[Unit] +Description=Somewhat fancy voice command recognition software + +[Service] +ExecStart=/usr/bin/kaylee + +[Install] +WantedBy=default.target diff --git a/systemd/kaylee.service b/systemd/kaylee.service deleted file mode 100644 index 51c5a04..0000000 --- a/systemd/kaylee.service +++ /dev/null @@ -1,8 +0,0 @@ -[Unit] -Description=Somewhat fancy voice command recognition software - -[Service] -ExecStart=/usr/bin/kaylee - -[Install] -WantedBy=default.target -- cgit 1.4.1