Browse Source

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.
Clara Hobbs 8 years ago
parent
commit
1c2928ff1a
12 changed files with 517 additions and 528 deletions
  1. 2
    1
      README.md
  2. 0
    90
      config.py
  3. 0
    113
      gtkui.py
  4. 0
    37
      hasher.py
  5. 2
    204
      kaylee.py
  6. 0
    0
      kayleevc/__init__.py
  7. 103
    1
      kayleevc/gui.py
  8. 207
    0
      kayleevc/kaylee.py
  9. 1
    2
      kayleevc/numbers.py
  10. 0
    0
      kayleevc/recognizer.py
  11. 202
    0
      kayleevc/util.py
  12. 0
    80
      languageupdater.py

+ 2
- 1
README.md View File

@@ -7,7 +7,8 @@ but adds a lot of features that go beyond the original purpose of Blather.
7 7
 
8 8
 ## Requirements
9 9
 
10
-1. pocketsphinx
10
+1. Python 3 (tested with 3.5, may work with older versions)
11
+1. pocketsphinx 5prealpha
11 12
 2. gstreamer-1.0 (and what ever plugin has pocketsphinx support)
12 13
 3. gstreamer-1.0 base plugins (required for ALSA)
13 14
 4. python-gobject (required for GStreamer and the GTK-based UI)

+ 0
- 90
config.py View File

@@ -1,90 +0,0 @@
1
-# This is part of Kaylee
2
-# -- this code is licensed GPLv3
3
-# Copyright 2015-2016 Clayton G. Hobbs
4
-# Portions Copyright 2013 Jezra
5
-
6
-import json
7
-import os
8
-from argparse import ArgumentParser, Namespace
9
-
10
-from gi.repository import GLib
11
-
12
-class Config:
13
-    """Keep track of the configuration of Kaylee"""
14
-    # Name of the program, for later use
15
-    program_name = "kaylee"
16
-
17
-    # Directories
18
-    conf_dir = os.path.join(GLib.get_user_config_dir(), program_name)
19
-    cache_dir = os.path.join(GLib.get_user_cache_dir(), program_name)
20
-    data_dir = os.path.join(GLib.get_user_data_dir(), program_name)
21
-
22
-    # Configuration files
23
-    command_file = os.path.join(conf_dir, "commands.conf")
24
-    opt_file = os.path.join(conf_dir, "options.json")
25
-
26
-    # Cache files
27
-    history_file = os.path.join(cache_dir, program_name + "history")
28
-    hash_file = os.path.join(cache_dir, "hash.json")
29
-
30
-    # Data files
31
-    strings_file = os.path.join(data_dir, "sentences.corpus")
32
-    lang_file = os.path.join(data_dir, 'lm')
33
-    dic_file = os.path.join(data_dir, 'dic')
34
-
35
-    def __init__(self):
36
-        # Ensure necessary directories exist
37
-        self._make_dir(self.conf_dir)
38
-        self._make_dir(self.cache_dir)
39
-        self._make_dir(self.data_dir)
40
-
41
-        # Set up the argument parser
42
-        self.parser = ArgumentParser()
43
-        self.parser.add_argument("-i", "--interface", type=str,
44
-                dest="interface", action='store',
45
-                help="Interface to use (if any). 'g' for GTK or 'gt' for GTK" +
46
-                " system tray icon")
47
-
48
-        self.parser.add_argument("-c", "--continuous",
49
-                action="store_true", dest="continuous", default=False,
50
-                help="Start interface with 'continuous' listen enabled")
51
-
52
-        self.parser.add_argument("-p", "--pass-words",
53
-                action="store_true", dest="pass_words", default=False,
54
-                help="Pass the recognized words as arguments to the shell" +
55
-                " command")
56
-
57
-        self.parser.add_argument("-H", "--history", type=int,
58
-                action="store", dest="history",
59
-                help="Number of commands to store in history file")
60
-
61
-        self.parser.add_argument("-m", "--microphone", type=int,
62
-                action="store", dest="microphone", default=None,
63
-                help="Audio input card to use (if other than system default)")
64
-
65
-        self.parser.add_argument("--valid-sentence-command", type=str,
66
-                dest="valid_sentence_command", action='store',
67
-                help="Command to run when a valid sentence is detected")
68
-
69
-        self.parser.add_argument("--invalid-sentence-command", type=str,
70
-                dest="invalid_sentence_command", action='store',
71
-                help="Command to run when an invalid sentence is detected")
72
-
73
-        # Read the configuration file
74
-        self._read_options_file()
75
-
76
-        # Parse command-line arguments, overriding config file as appropriate
77
-        self.parser.parse_args(namespace=self.options)
78
-
79
-    def _make_dir(self, directory):
80
-        if not os.path.exists(directory):
81
-            os.makedirs(directory)
82
-
83
-    def _read_options_file(self):
84
-        try:
85
-            with open(self.opt_file, 'r') as f:
86
-                self.options = json.load(f)
87
-                self.options = Namespace(**self.options)
88
-        except FileNotFoundError:
89
-            # Make an empty options namespace
90
-            self.options = Namespace()

+ 0
- 113
gtkui.py View File

@@ -1,113 +0,0 @@
1
-# This is part of Kaylee
2
-# -- this code is licensed GPLv3
3
-# Copyright 2015-2016 Clayton G. Hobbs
4
-# Portions Copyright 2013 Jezra
5
-
6
-import sys
7
-import gi
8
-from gi.repository import GObject
9
-# Gtk
10
-gi.require_version('Gtk', '3.0')
11
-from gi.repository import Gtk, Gdk
12
-
13
-class UI(GObject.GObject):
14
-    __gsignals__ = {
15
-        'command' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,))
16
-    }
17
-
18
-    def __init__(self, args, continuous):
19
-        GObject.GObject.__init__(self)
20
-        self.continuous = continuous
21
-        # Make a window
22
-        self.window = Gtk.Window(Gtk.WindowType.TOPLEVEL)
23
-        self.window.connect("delete_event", self.delete_event)
24
-        # Give the window a name
25
-        self.window.set_title("Kaylee")
26
-        self.window.set_resizable(False)
27
-
28
-        layout = Gtk.VBox()
29
-        self.window.add(layout)
30
-        # Make a listen/stop button
31
-        self.lsbutton = Gtk.Button("Listen")
32
-        layout.add(self.lsbutton)
33
-        # Make a continuous button
34
-        self.ccheckbox = Gtk.CheckButton("Continuous Listen")
35
-        layout.add(self.ccheckbox)
36
-
37
-        # Connect the buttons
38
-        self.lsbutton.connect("clicked", self.lsbutton_clicked)
39
-        self.ccheckbox.connect("clicked", self.ccheckbox_clicked)
40
-
41
-        # Add a label to the UI to display the last command
42
-        self.label = Gtk.Label()
43
-        layout.add(self.label)
44
-
45
-        # Create an accellerator group for this window
46
-        accel = Gtk.AccelGroup()
47
-        # Add the ctrl+q to quit
48
-        accel.connect(Gdk.keyval_from_name('q'), Gdk.ModifierType.CONTROL_MASK,
49
-                Gtk.AccelFlags.VISIBLE, self.accel_quit)
50
-        # Lock the group
51
-        accel.lock()
52
-        # Add the group to the window
53
-        self.window.add_accel_group(accel)
54
-
55
-    def ccheckbox_clicked(self, widget):
56
-        checked = self.ccheckbox.get_active()
57
-        self.lsbutton.set_sensitive(not checked)
58
-        if checked:
59
-            self.lsbutton_stopped()
60
-            self.emit('command', "continuous_listen")
61
-            self.set_icon_active()
62
-        else:
63
-            self.emit('command', "continuous_stop")
64
-            self.set_icon_inactive()
65
-
66
-    def lsbutton_stopped(self):
67
-        self.lsbutton.set_label("Listen")
68
-
69
-    def lsbutton_clicked(self, button):
70
-        val = self.lsbutton.get_label()
71
-        if val == "Listen":
72
-            self.emit("command", "listen")
73
-            self.lsbutton.set_label("Stop")
74
-            # Clear the label
75
-            self.label.set_text("")
76
-            self.set_icon_active()
77
-        else:
78
-            self.lsbutton_stopped()
79
-            self.emit("command", "stop")
80
-            self.set_icon_inactive()
81
-
82
-    def run(self):
83
-        # Set the default icon
84
-        self.set_icon_inactive()
85
-        self.window.show_all()
86
-        if self.continuous:
87
-            self.set_icon_active()
88
-            self.ccheckbox.set_active(True)
89
-
90
-    def accel_quit(self, accel_group, acceleratable, keyval, modifier):
91
-        self.emit("command", "quit")
92
-
93
-    def delete_event(self, x, y):
94
-        self.emit("command", "quit")
95
-
96
-    def finished(self, text):
97
-        # If the continuous isn't pressed
98
-        if not self.ccheckbox.get_active():
99
-            self.lsbutton_stopped()
100
-            self.set_icon_inactive()
101
-        self.label.set_text(text)
102
-
103
-    def set_icon_active_asset(self, i):
104
-        self.icon_active = i
105
-
106
-    def set_icon_inactive_asset(self, i):
107
-        self.icon_inactive = i
108
-
109
-    def set_icon_active(self):
110
-        Gtk.Window.set_default_icon_from_file(self.icon_active)
111
-
112
-    def set_icon_inactive(self):
113
-        Gtk.Window.set_default_icon_from_file(self.icon_inactive)

+ 0
- 37
hasher.py View File

@@ -1,37 +0,0 @@
1
-# This is part of Kaylee
2
-# -- this code is licensed GPLv3
3
-# Copyright 2015-2016 Clayton G. Hobbs
4
-# Portions Copyright 2013 Jezra
5
-
6
-import json
7
-import hashlib
8
-
9
-class Hasher:
10
-    """Keep track of hashes for Kaylee"""
11
-
12
-    def __init__(self, config):
13
-        self.config = config
14
-        try:
15
-            with open(self.config.hash_file, 'r') as f:
16
-                self.hashes = json.load(f)
17
-        except IOError:
18
-            # No stored hash
19
-            self.hashes = {}
20
-
21
-    def __getitem__(self, hashname):
22
-        try:
23
-            return self.hashes[hashname]
24
-        except (KeyError, TypeError):
25
-            return None
26
-
27
-    def __setitem__(self, hashname, value):
28
-        self.hashes[hashname] = value
29
-
30
-    def get_hash_object(self):
31
-        """Returns an object to compute a new hash"""
32
-        return hashlib.sha256()
33
-
34
-    def store(self):
35
-        """Store the current hashes into a the hash file"""
36
-        with open(self.config.hash_file, 'w') as f:
37
-            json.dump(self.hashes, f)

+ 2
- 204
kaylee.py View File

@@ -1,212 +1,10 @@
1 1
 #!/usr/bin/env python3
2
-
3 2
 # This is part of Kaylee
4 3
 # -- this code is licensed GPLv3
5 4
 # Copyright 2015-2016 Clayton G. Hobbs
6 5
 # Portions Copyright 2013 Jezra
7 6
 
8
-import sys
9
-import signal
10
-import os.path
11
-import subprocess
12
-from gi.repository import GObject, GLib
13
-
14
-from recognizer import Recognizer
15
-from config import Config
16
-from languageupdater import LanguageUpdater
17
-from numberparser import NumberParser
18
-from hasher import Hasher
19
-
20
-
21
-class Kaylee:
22
-
23
-    def __init__(self):
24
-        self.ui = None
25
-        self.options = {}
26
-        ui_continuous_listen = False
27
-        self.continuous_listen = False
28
-
29
-        # Load configuration
30
-        self.config = Config()
31
-        self.options = vars(self.config.options)
32
-        self.commands = self.options['commands']
33
-
34
-        # Create number parser for later use
35
-        self.number_parser = NumberParser()
36
-
37
-        # Create a hasher
38
-        self.hasher = Hasher(self.config)
39
-
40
-        # Create the strings file
41
-        self.update_strings_file_if_changed()
42
-
43
-        if self.options['interface']:
44
-            if self.options['interface'] == "g":
45
-                from gtkui import UI
46
-            elif self.options['interface'] == "gt":
47
-                from gtktrayui import UI
48
-            else:
49
-                print("no GUI defined")
50
-                sys.exit()
51
-
52
-            self.ui = UI(self.options, self.options['continuous'])
53
-            self.ui.connect("command", self.process_command)
54
-            # Can we load the icon resource?
55
-            icon = self.load_resource("icon.png")
56
-            if icon:
57
-                self.ui.set_icon_active_asset(icon)
58
-            # Can we load the icon_inactive resource?
59
-            icon_inactive = self.load_resource("icon_inactive.png")
60
-            if icon_inactive:
61
-                self.ui.set_icon_inactive_asset(icon_inactive)
62
-
63
-        if self.options['history']:
64
-            self.history = []
65
-
66
-        # Update the language if necessary
67
-        self.language_updater = LanguageUpdater(self.config)
68
-        self.language_updater.update_language_if_changed()
69
-
70
-        # Create the recognizer
71
-        self.recognizer = Recognizer(self.config)
72
-        self.recognizer.connect('finished', self.recognizer_finished)
7
+from kayleevc import kaylee
73 8
 
74
-    def update_voice_commands_if_changed(self):
75
-        """Use hashes to test if the voice commands have changed"""
76
-        stored_hash = self.hasher['voice_commands']
77
-
78
-        # Calculate the hash the voice commands have right now
79
-        hasher = self.hasher.get_hash_object()
80
-        for voice_cmd in self.commands.keys():
81
-            hasher.update(voice_cmd.encode('utf-8'))
82
-            # Add a separator to avoid odd behavior
83
-            hasher.update('\n'.encode('utf-8'))
84
-        new_hash = hasher.hexdigest()
85
-
86
-        if new_hash != stored_hash:
87
-            self.create_strings_file()
88
-            self.hasher['voice_commands'] = new_hash
89
-            self.hasher.store()
90
-
91
-    def create_strings_file(self):
92
-        # Open the strings file
93
-        with open(self.config.strings_file, 'w') as strings:
94
-            # Add command words to the corpus
95
-            for voice_cmd in sorted(self.commands.keys()):
96
-                strings.write(voice_cmd.strip().replace('%d', '') + "\n")
97
-            # Add number words to the corpus
98
-            for word in self.number_parser.number_words:
99
-                strings.write(word + "\n")
100
-
101
-    def log_history(self, text):
102
-        if self.options['history']:
103
-            self.history.append(text)
104
-            if len(self.history) > self.options['history']:
105
-                # Pop off the first item
106
-                self.history.pop(0)
107
-
108
-            # Open and truncate the history file
109
-            with open(self.config.history_file, 'w') as hfile:
110
-                for line in self.history:
111
-                    hfile.write(line + '\n')
112
-
113
-    def run_command(self, cmd):
114
-        """Print the command, then run it"""
115
-        print(cmd)
116
-        subprocess.call(cmd, shell=True)
117
-
118
-    def recognizer_finished(self, recognizer, text):
119
-        t = text.lower()
120
-        numt, nums = self.number_parser.parse_all_numbers(t)
121
-        # Is there a matching command?
122
-        if t in self.commands:
123
-            # Run the valid_sentence_command if it's set
124
-            if self.options['valid_sentence_command']:
125
-                subprocess.call(self.options['valid_sentence_command'],
126
-                                shell=True)
127
-            cmd = self.commands[t]
128
-            # Should we be passing words?
129
-            if self.options['pass_words']:
130
-                cmd += " " + t
131
-            self.run_command(cmd)
132
-            self.log_history(text)
133
-        elif numt in self.commands:
134
-            # Run the valid_sentence_command if it's set
135
-            if self.options['valid_sentence_command']:
136
-                subprocess.call(self.options['valid_sentence_command'],
137
-                                shell=True)
138
-            cmd = self.commands[numt]
139
-            cmd = cmd.format(*nums)
140
-            # Should we be passing words?
141
-            if self.options['pass_words']:
142
-                cmd += " " + t
143
-            self.run_command(cmd)
144
-            self.log_history(text)
145
-        else:
146
-            # Run the invalid_sentence_command if it's set
147
-            if self.options['invalid_sentence_command']:
148
-                subprocess.call(self.options['invalid_sentence_command'],
149
-                                shell=True)
150
-            print("no matching command {0}".format(t))
151
-        # If there is a UI and we are not continuous listen
152
-        if self.ui:
153
-            if not self.continuous_listen:
154
-                # Stop listening
155
-                self.recognizer.pause()
156
-            # Let the UI know that there is a finish
157
-            self.ui.finished(t)
158
-
159
-    def run(self):
160
-        if self.ui:
161
-            self.ui.run()
162
-        else:
163
-            self.recognizer.listen()
164
-
165
-    def quit(self):
166
-        sys.exit()
167
-
168
-    def process_command(self, UI, command):
169
-        print(command)
170
-        if command == "listen":
171
-            self.recognizer.listen()
172
-        elif command == "stop":
173
-            self.recognizer.pause()
174
-        elif command == "continuous_listen":
175
-            self.continuous_listen = True
176
-            self.recognizer.listen()
177
-        elif command == "continuous_stop":
178
-            self.continuous_listen = False
179
-            self.recognizer.pause()
180
-        elif command == "quit":
181
-            self.quit()
182
-
183
-    def load_resource(self, string):
184
-        local_data = os.path.join(os.path.dirname(__file__), 'data')
185
-        paths = ["/usr/share/kaylee/", "/usr/local/share/kaylee", local_data]
186
-        for path in paths:
187
-            resource = os.path.join(path, string)
188
-            if os.path.exists(resource):
189
-                return resource
190
-        # If we get this far, no resource was found
191
-        return False
192
-
193
-
194
-if __name__ == "__main__":
195
-    # Make our kaylee object
196
-    kaylee = Kaylee()
197
-    # Init gobject threads
198
-    GObject.threads_init()
199
-    # We want a main loop
200
-    main_loop = GObject.MainLoop()
201
-    # Handle sigint
202
-    signal.signal(signal.SIGINT, signal.SIG_DFL)
203
-    # Run the kaylee
9
+if __name__ == '__main__':
204 10
     kaylee.run()
205
-    # Start the main loop
206
-    try:
207
-        main_loop.run()
208
-    except:
209
-        print("time to quit")
210
-        main_loop.quit()
211
-        sys.exit()
212
-

+ 0
- 0
kayleevc/__init__.py View File


gtktrayui.py → kayleevc/gui.py View File

@@ -10,7 +10,7 @@ from gi.repository import GObject
10 10
 gi.require_version('Gtk', '3.0')
11 11
 from gi.repository import Gtk, Gdk
12 12
 
13
-class UI(GObject.GObject):
13
+class GTKTrayInterface(GObject.GObject):
14 14
     __gsignals__ = {
15 15
         'command' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,))
16 16
     }
@@ -105,3 +105,105 @@ class UI(GObject.GObject):
105 105
 
106 106
     def set_icon_inactive(self):
107 107
         self.statusicon.set_from_file(self.icon_inactive)
108
+
109
+class GTKInterface(GObject.GObject):
110
+    __gsignals__ = {
111
+        'command' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_STRING,))
112
+    }
113
+
114
+    def __init__(self, args, continuous):
115
+        GObject.GObject.__init__(self)
116
+        self.continuous = continuous
117
+        # Make a window
118
+        self.window = Gtk.Window(Gtk.WindowType.TOPLEVEL)
119
+        self.window.connect("delete_event", self.delete_event)
120
+        # Give the window a name
121
+        self.window.set_title("Kaylee")
122
+        self.window.set_resizable(False)
123
+
124
+        layout = Gtk.VBox()
125
+        self.window.add(layout)
126
+        # Make a listen/stop button
127
+        self.lsbutton = Gtk.Button("Listen")
128
+        layout.add(self.lsbutton)
129
+        # Make a continuous button
130
+        self.ccheckbox = Gtk.CheckButton("Continuous Listen")
131
+        layout.add(self.ccheckbox)
132
+
133
+        # Connect the buttons
134
+        self.lsbutton.connect("clicked", self.lsbutton_clicked)
135
+        self.ccheckbox.connect("clicked", self.ccheckbox_clicked)
136
+
137
+        # Add a label to the UI to display the last command
138
+        self.label = Gtk.Label()
139
+        layout.add(self.label)
140
+
141
+        # Create an accellerator group for this window
142
+        accel = Gtk.AccelGroup()
143
+        # Add the ctrl+q to quit
144
+        accel.connect(Gdk.keyval_from_name('q'), Gdk.ModifierType.CONTROL_MASK,
145
+                Gtk.AccelFlags.VISIBLE, self.accel_quit)
146
+        # Lock the group
147
+        accel.lock()
148
+        # Add the group to the window
149
+        self.window.add_accel_group(accel)
150
+
151
+    def ccheckbox_clicked(self, widget):
152
+        checked = self.ccheckbox.get_active()
153
+        self.lsbutton.set_sensitive(not checked)
154
+        if checked:
155
+            self.lsbutton_stopped()
156
+            self.emit('command', "continuous_listen")
157
+            self.set_icon_active()
158
+        else:
159
+            self.emit('command', "continuous_stop")
160
+            self.set_icon_inactive()
161
+
162
+    def lsbutton_stopped(self):
163
+        self.lsbutton.set_label("Listen")
164
+
165
+    def lsbutton_clicked(self, button):
166
+        val = self.lsbutton.get_label()
167
+        if val == "Listen":
168
+            self.emit("command", "listen")
169
+            self.lsbutton.set_label("Stop")
170
+            # Clear the label
171
+            self.label.set_text("")
172
+            self.set_icon_active()
173
+        else:
174
+            self.lsbutton_stopped()
175
+            self.emit("command", "stop")
176
+            self.set_icon_inactive()
177
+
178
+    def run(self):
179
+        # Set the default icon
180
+        self.set_icon_inactive()
181
+        self.window.show_all()
182
+        if self.continuous:
183
+            self.set_icon_active()
184
+            self.ccheckbox.set_active(True)
185
+
186
+    def accel_quit(self, accel_group, acceleratable, keyval, modifier):
187
+        self.emit("command", "quit")
188
+
189
+    def delete_event(self, x, y):
190
+        self.emit("command", "quit")
191
+
192
+    def finished(self, text):
193
+        # If the continuous isn't pressed
194
+        if not self.ccheckbox.get_active():
195
+            self.lsbutton_stopped()
196
+            self.set_icon_inactive()
197
+        self.label.set_text(text)
198
+
199
+    def set_icon_active_asset(self, i):
200
+        self.icon_active = i
201
+
202
+    def set_icon_inactive_asset(self, i):
203
+        self.icon_inactive = i
204
+
205
+    def set_icon_active(self):
206
+        Gtk.Window.set_default_icon_from_file(self.icon_active)
207
+
208
+    def set_icon_inactive(self):
209
+        Gtk.Window.set_default_icon_from_file(self.icon_inactive)

+ 207
- 0
kayleevc/kaylee.py View File

@@ -0,0 +1,207 @@
1
+# This is part of Kaylee
2
+# -- this code is licensed GPLv3
3
+# Copyright 2015-2016 Clayton G. Hobbs
4
+# Portions Copyright 2013 Jezra
5
+
6
+import sys
7
+import signal
8
+import os.path
9
+import subprocess
10
+from gi.repository import GObject, GLib
11
+
12
+from kayleevc.recognizer import Recognizer
13
+from kayleevc.util import *
14
+from kayleevc.numbers import NumberParser
15
+
16
+
17
+class Kaylee:
18
+
19
+    def __init__(self):
20
+        self.ui = None
21
+        self.options = {}
22
+        ui_continuous_listen = False
23
+        self.continuous_listen = False
24
+
25
+        # Load configuration
26
+        self.config = Config()
27
+        self.options = vars(self.config.options)
28
+        self.commands = self.options['commands']
29
+
30
+        # Create number parser for later use
31
+        self.number_parser = NumberParser()
32
+
33
+        # Create a hasher
34
+        self.hasher = Hasher(self.config)
35
+
36
+        # Create the strings file
37
+        self.update_voice_commands_if_changed()
38
+
39
+        if self.options['interface']:
40
+            if self.options['interface'] == "g":
41
+                from kayleevc.gui import GTKInterface as UI
42
+            elif self.options['interface'] == "gt":
43
+                from kayleevc.gui import GTKTrayInterface as UI
44
+            else:
45
+                print("no GUI defined")
46
+                sys.exit()
47
+
48
+            self.ui = UI(self.options, self.options['continuous'])
49
+            self.ui.connect("command", self.process_command)
50
+            # Can we load the icon resource?
51
+            icon = self.load_resource("icon.png")
52
+            if icon:
53
+                self.ui.set_icon_active_asset(icon)
54
+            # Can we load the icon_inactive resource?
55
+            icon_inactive = self.load_resource("icon_inactive.png")
56
+            if icon_inactive:
57
+                self.ui.set_icon_inactive_asset(icon_inactive)
58
+
59
+        if self.options['history']:
60
+            self.history = []
61
+
62
+        # Update the language if necessary
63
+        self.language_updater = LanguageUpdater(self.config)
64
+        self.language_updater.update_language_if_changed()
65
+
66
+        # Create the recognizer
67
+        self.recognizer = Recognizer(self.config)
68
+        self.recognizer.connect('finished', self.recognizer_finished)
69
+
70
+    def update_voice_commands_if_changed(self):
71
+        """Use hashes to test if the voice commands have changed"""
72
+        stored_hash = self.hasher['voice_commands']
73
+
74
+        # Calculate the hash the voice commands have right now
75
+        hasher = self.hasher.get_hash_object()
76
+        for voice_cmd in self.commands.keys():
77
+            hasher.update(voice_cmd.encode('utf-8'))
78
+            # Add a separator to avoid odd behavior
79
+            hasher.update('\n'.encode('utf-8'))
80
+        new_hash = hasher.hexdigest()
81
+
82
+        if new_hash != stored_hash:
83
+            self.create_strings_file()
84
+            self.hasher['voice_commands'] = new_hash
85
+            self.hasher.store()
86
+
87
+    def create_strings_file(self):
88
+        # Open the strings file
89
+        with open(self.config.strings_file, 'w') as strings:
90
+            # Add command words to the corpus
91
+            for voice_cmd in sorted(self.commands.keys()):
92
+                strings.write(voice_cmd.strip().replace('%d', '') + "\n")
93
+            # Add number words to the corpus
94
+            for word in self.number_parser.number_words:
95
+                strings.write(word + "\n")
96
+
97
+    def log_history(self, text):
98
+        if self.options['history']:
99
+            self.history.append(text)
100
+            if len(self.history) > self.options['history']:
101
+                # Pop off the first item
102
+                self.history.pop(0)
103
+
104
+            # Open and truncate the history file
105
+            with open(self.config.history_file, 'w') as hfile:
106
+                for line in self.history:
107
+                    hfile.write(line + '\n')
108
+
109
+    def run_command(self, cmd):
110
+        """Print the command, then run it"""
111
+        print(cmd)
112
+        subprocess.call(cmd, shell=True)
113
+
114
+    def recognizer_finished(self, recognizer, text):
115
+        t = text.lower()
116
+        numt, nums = self.number_parser.parse_all_numbers(t)
117
+        # Is there a matching command?
118
+        if t in self.commands:
119
+            # Run the valid_sentence_command if it's set
120
+            if self.options['valid_sentence_command']:
121
+                subprocess.call(self.options['valid_sentence_command'],
122
+                                shell=True)
123
+            cmd = self.commands[t]
124
+            # Should we be passing words?
125
+            if self.options['pass_words']:
126
+                cmd += " " + t
127
+            self.run_command(cmd)
128
+            self.log_history(text)
129
+        elif numt in self.commands:
130
+            # Run the valid_sentence_command if it's set
131
+            if self.options['valid_sentence_command']:
132
+                subprocess.call(self.options['valid_sentence_command'],
133
+                                shell=True)
134
+            cmd = self.commands[numt]
135
+            cmd = cmd.format(*nums)
136
+            # Should we be passing words?
137
+            if self.options['pass_words']:
138
+                cmd += " " + t
139
+            self.run_command(cmd)
140
+            self.log_history(text)
141
+        else:
142
+            # Run the invalid_sentence_command if it's set
143
+            if self.options['invalid_sentence_command']:
144
+                subprocess.call(self.options['invalid_sentence_command'],
145
+                                shell=True)
146
+            print("no matching command {0}".format(t))
147
+        # If there is a UI and we are not continuous listen
148
+        if self.ui:
149
+            if not self.continuous_listen:
150
+                # Stop listening
151
+                self.recognizer.pause()
152
+            # Let the UI know that there is a finish
153
+            self.ui.finished(t)
154
+
155
+    def run(self):
156
+        if self.ui:
157
+            self.ui.run()
158
+        else:
159
+            self.recognizer.listen()
160
+
161
+    def quit(self):
162
+        sys.exit()
163
+
164
+    def process_command(self, UI, command):
165
+        print(command)
166
+        if command == "listen":
167
+            self.recognizer.listen()
168
+        elif command == "stop":
169
+            self.recognizer.pause()
170
+        elif command == "continuous_listen":
171
+            self.continuous_listen = True
172
+            self.recognizer.listen()
173
+        elif command == "continuous_stop":
174
+            self.continuous_listen = False
175
+            self.recognizer.pause()
176
+        elif command == "quit":
177
+            self.quit()
178
+
179
+    def load_resource(self, string):
180
+        local_data = os.path.join(os.path.dirname(__file__), '..', 'data')
181
+        paths = ["/usr/share/kaylee/", "/usr/local/share/kaylee", local_data]
182
+        for path in paths:
183
+            resource = os.path.join(path, string)
184
+            if os.path.exists(resource):
185
+                return resource
186
+        # If we get this far, no resource was found
187
+        return False
188
+
189
+
190
+def run():
191
+    # Make our kaylee object
192
+    kaylee = Kaylee()
193
+    # Init gobject threads
194
+    GObject.threads_init()
195
+    # We want a main loop
196
+    main_loop = GObject.MainLoop()
197
+    # Handle sigint
198
+    signal.signal(signal.SIGINT, signal.SIG_DFL)
199
+    # Run the kaylee
200
+    kaylee.run()
201
+    # Start the main loop
202
+    try:
203
+        main_loop.run()
204
+    except:
205
+        print("time to quit")
206
+        main_loop.quit()
207
+        sys.exit()

numberparser.py → kayleevc/numbers.py View File

@@ -1,10 +1,9 @@
1 1
 #!/usr/bin/env python
2
-# numberparser.py - Translate words to decimal
3
-
4 2
 # This is part of Kaylee
5 3
 # -- this code is licensed GPLv3
6 4
 # Copyright 2015-2016 Clayton G. Hobbs
7 5
 # Portions Copyright 2013 Jezra
6
+
8 7
 import re
9 8
 
10 9
 # Define the mappings from words to numbers

recognizer.py → kayleevc/recognizer.py View File


+ 202
- 0
kayleevc/util.py View File

@@ -0,0 +1,202 @@
1
+# This is part of Kaylee
2
+# -- this code is licensed GPLv3
3
+# Copyright 2015-2016 Clayton G. Hobbs
4
+# Portions Copyright 2013 Jezra
5
+
6
+import re
7
+import json
8
+import hashlib
9
+import os
10
+from argparse import ArgumentParser, Namespace
11
+
12
+import requests
13
+
14
+from gi.repository import GLib
15
+
16
+class Config:
17
+    """Keep track of the configuration of Kaylee"""
18
+    # Name of the program, for later use
19
+    program_name = "kaylee"
20
+
21
+    # Directories
22
+    conf_dir = os.path.join(GLib.get_user_config_dir(), program_name)
23
+    cache_dir = os.path.join(GLib.get_user_cache_dir(), program_name)
24
+    data_dir = os.path.join(GLib.get_user_data_dir(), program_name)
25
+
26
+    # Configuration files
27
+    command_file = os.path.join(conf_dir, "commands.conf")
28
+    opt_file = os.path.join(conf_dir, "options.json")
29
+
30
+    # Cache files
31
+    history_file = os.path.join(cache_dir, program_name + "history")
32
+    hash_file = os.path.join(cache_dir, "hash.json")
33
+
34
+    # Data files
35
+    strings_file = os.path.join(data_dir, "sentences.corpus")
36
+    lang_file = os.path.join(data_dir, 'lm')
37
+    dic_file = os.path.join(data_dir, 'dic')
38
+
39
+    def __init__(self):
40
+        # Ensure necessary directories exist
41
+        self._make_dir(self.conf_dir)
42
+        self._make_dir(self.cache_dir)
43
+        self._make_dir(self.data_dir)
44
+
45
+        # Set up the argument parser
46
+        self.parser = ArgumentParser()
47
+        self.parser.add_argument("-i", "--interface", type=str,
48
+                dest="interface", action='store',
49
+                help="Interface to use (if any). 'g' for GTK or 'gt' for GTK" +
50
+                " system tray icon")
51
+
52
+        self.parser.add_argument("-c", "--continuous",
53
+                action="store_true", dest="continuous", default=False,
54
+                help="Start interface with 'continuous' listen enabled")
55
+
56
+        self.parser.add_argument("-p", "--pass-words",
57
+                action="store_true", dest="pass_words", default=False,
58
+                help="Pass the recognized words as arguments to the shell" +
59
+                " command")
60
+
61
+        self.parser.add_argument("-H", "--history", type=int,
62
+                action="store", dest="history",
63
+                help="Number of commands to store in history file")
64
+
65
+        self.parser.add_argument("-m", "--microphone", type=int,
66
+                action="store", dest="microphone", default=None,
67
+                help="Audio input card to use (if other than system default)")
68
+
69
+        self.parser.add_argument("--valid-sentence-command", type=str,
70
+                dest="valid_sentence_command", action='store',
71
+                help="Command to run when a valid sentence is detected")
72
+
73
+        self.parser.add_argument("--invalid-sentence-command", type=str,
74
+                dest="invalid_sentence_command", action='store',
75
+                help="Command to run when an invalid sentence is detected")
76
+
77
+        # Read the configuration file
78
+        self._read_options_file()
79
+
80
+        # Parse command-line arguments, overriding config file as appropriate
81
+        self.parser.parse_args(namespace=self.options)
82
+
83
+    def _make_dir(self, directory):
84
+        if not os.path.exists(directory):
85
+            os.makedirs(directory)
86
+
87
+    def _read_options_file(self):
88
+        try:
89
+            with open(self.opt_file, 'r') as f:
90
+                self.options = json.load(f)
91
+                self.options = Namespace(**self.options)
92
+        except FileNotFoundError:
93
+            # Make an empty options namespace
94
+            self.options = Namespace()
95
+
96
+class Hasher:
97
+    """Keep track of hashes for Kaylee"""
98
+
99
+    def __init__(self, config):
100
+        self.config = config
101
+        try:
102
+            with open(self.config.hash_file, 'r') as f:
103
+                self.hashes = json.load(f)
104
+        except IOError:
105
+            # No stored hash
106
+            self.hashes = {}
107
+
108
+    def __getitem__(self, hashname):
109
+        try:
110
+            return self.hashes[hashname]
111
+        except (KeyError, TypeError):
112
+            return None
113
+
114
+    def __setitem__(self, hashname, value):
115
+        self.hashes[hashname] = value
116
+
117
+    def get_hash_object(self):
118
+        """Returns an object to compute a new hash"""
119
+        return hashlib.sha256()
120
+
121
+    def store(self):
122
+        """Store the current hashes into a the hash file"""
123
+        with open(self.config.hash_file, 'w') as f:
124
+            json.dump(self.hashes, f)
125
+
126
+class LanguageUpdater:
127
+    """
128
+    Handles updating the language using the online lmtool.
129
+
130
+    This class provides methods to check if the corpus has changed, and to
131
+    update the language to match the new corpus using the lmtool.  This allows
132
+    us to automatically update the language if the corpus has changed, saving
133
+    the user from having to do this manually.
134
+    """
135
+
136
+    def __init__(self, config):
137
+        self.config = config
138
+        self.hasher = Hasher(config)
139
+
140
+    def update_language_if_changed(self):
141
+        """Test if the language has changed, and if it has, update it"""
142
+        if self.language_has_changed():
143
+            self.update_language()
144
+            self.save_language_hash()
145
+
146
+    def language_has_changed(self):
147
+        """Use hashes to test if the language has changed"""
148
+        self.stored_hash = self.hasher['language']
149
+
150
+        # Calculate the hash the language file has right now
151
+        hasher = self.hasher.get_hash_object()
152
+        with open(self.config.strings_file, 'rb') as sfile:
153
+            buf = sfile.read()
154
+            hasher.update(buf)
155
+        self.new_hash = hasher.hexdigest()
156
+
157
+        return self.new_hash != self.stored_hash
158
+
159
+    def update_language(self):
160
+        """Update the language using the online lmtool"""
161
+        print('Updating language using online lmtool')
162
+
163
+        host = 'http://www.speech.cs.cmu.edu'
164
+        url = host + '/cgi-bin/tools/lmtool/run'
165
+
166
+        # Submit the corpus to the lmtool
167
+        response_text = ""
168
+        with open(self.config.strings_file, 'rb') as corpus:
169
+            files = {'corpus': corpus}
170
+            values = {'formtype': 'simple'}
171
+
172
+            r = requests.post(url, files=files, data=values)
173
+            response_text = r.text
174
+
175
+        # Parse response to get URLs of the files we need
176
+        path_re = r'.*<title>Index of (.*?)</title>.*'
177
+        number_re = r'.*TAR([0-9]*?)\.tgz.*'
178
+        for line in response_text.split('\n'):
179
+            # If we found the directory, keep it and don't break
180
+            if re.search(path_re, line):
181
+                path = host + re.sub(path_re, r'\1', line)
182
+            # If we found the number, keep it and break
183
+            elif re.search(number_re, line):
184
+                number = re.sub(number_re, r'\1', line)
185
+                break
186
+
187
+        lm_url = path + '/' + number + '.lm'
188
+        dic_url = path + '/' + number + '.dic'
189
+
190
+        self._download_file(lm_url, self.config.lang_file)
191
+        self._download_file(dic_url, self.config.dic_file)
192
+
193
+    def save_language_hash(self):
194
+        self.hasher['language'] = self.new_hash
195
+        self.hasher.store()
196
+
197
+    def _download_file(self, url, path):
198
+        r = requests.get(url, stream=True)
199
+        if r.status_code == 200:
200
+            with open(path, 'wb') as f:
201
+                for chunk in r:
202
+                    f.write(chunk)

+ 0
- 80
languageupdater.py View File

@@ -1,80 +0,0 @@
1
-# This is part of Kaylee
2
-# -- this code is licensed GPLv3
3
-# Copyright 2015-2016 Clayton G. Hobbs
4
-# Portions Copyright 2013 Jezra
5
-
6
-import re
7
-
8
-import requests
9
-
10
-from hasher import Hasher
11
-
12
-class LanguageUpdater:
13
-
14
-    def __init__(self, config):
15
-        self.config = config
16
-        self.hasher = Hasher(config)
17
-
18
-    def update_language_if_changed(self):
19
-        """Test if the language has changed, and if it has, update it"""
20
-        if self.language_has_changed():
21
-            self.update_language()
22
-            self.save_language_hash()
23
-
24
-    def language_has_changed(self):
25
-        """Use hashes to test if the language has changed"""
26
-        self.stored_hash = self.hasher['language']
27
-
28
-        # Calculate the hash the language file has right now
29
-        hasher = self.hasher.get_hash_object()
30
-        with open(self.config.strings_file, 'rb') as sfile:
31
-            buf = sfile.read()
32
-            hasher.update(buf)
33
-        self.new_hash = hasher.hexdigest()
34
-
35
-        return self.new_hash != self.stored_hash
36
-
37
-    def update_language(self):
38
-        """Update the language using the online lmtool"""
39
-        print('Updating language using online lmtool')
40
-
41
-        host = 'http://www.speech.cs.cmu.edu'
42
-        url = host + '/cgi-bin/tools/lmtool/run'
43
-
44
-        # Submit the corpus to the lmtool
45
-        response_text = ""
46
-        with open(self.config.strings_file, 'rb') as corpus:
47
-            files = {'corpus': corpus}
48
-            values = {'formtype': 'simple'}
49
-
50
-            r = requests.post(url, files=files, data=values)
51
-            response_text = r.text
52
-
53
-        # Parse response to get URLs of the files we need
54
-        path_re = r'.*<title>Index of (.*?)</title>.*'
55
-        number_re = r'.*TAR([0-9]*?)\.tgz.*'
56
-        for line in response_text.split('\n'):
57
-            # If we found the directory, keep it and don't break
58
-            if re.search(path_re, line):
59
-                path = host + re.sub(path_re, r'\1', line)
60
-            # If we found the number, keep it and break
61
-            elif re.search(number_re, line):
62
-                number = re.sub(number_re, r'\1', line)
63
-                break
64
-
65
-        lm_url = path + '/' + number + '.lm'
66
-        dic_url = path + '/' + number + '.dic'
67
-
68
-        self._download_file(lm_url, self.config.lang_file)
69
-        self._download_file(dic_url, self.config.dic_file)
70
-
71
-    def save_language_hash(self):
72
-        self.hasher['language'] = self.new_hash
73
-        self.hasher.store()
74
-
75
-    def _download_file(self, url, path):
76
-        r = requests.get(url, stream=True)
77
-        if r.status_code == 200:
78
-            with open(path, 'wb') as f:
79
-                for chunk in r:
80
-                    f.write(chunk)

Loading…
Cancel
Save