Browse Source

Two stage command processing

As described in issue #16, commands are now processed in two stages.
First, all plugins get to give a confidence with which they are the
right plugin to handle the command.  Then, the one with the highest
confidence gets to run first, and if for some reason it can't handle the
command, other plugins try in turn.
Clara Hobbs 7 years ago
parent
commit
3e5291fcb6

+ 46
- 20
kayleevc/kaylee.py View File

@@ -4,6 +4,7 @@
4 4
 # Portions Copyright 2013 Jezra
5 5
 
6 6
 import importlib
7
+import queue
7 8
 import sys
8 9
 import subprocess
9 10
 import signal
@@ -36,7 +37,9 @@ class Kaylee:
36 37
         self.plugins = []
37 38
         for plugin in self.config.plugins.keys():
38 39
             pmod = importlib.import_module(plugin, 'kayleevc.plugins')
39
-            self.plugins.append(pmod.Plugin(self.config, plugin))
40
+            pobj = pmod.Plugin(self.config, plugin)
41
+            pobj.connect('tts', self.plugin_tts)
42
+            self.plugins.append(pobj)
40 43
 
41 44
         # Create a hasher
42 45
         self.hasher = Hasher(self.config)
@@ -74,11 +77,7 @@ class Kaylee:
74 77
         # Create the recognizer
75 78
         self.recognizer = Recognizer(self.config)
76 79
 
77
-        # Connect the recognizer's finished signal to all the plugins
78
-        for plugin in self.plugins:
79
-            self.recognizer.connect('finished', plugin.recognizer_finished)
80
-            plugin.connect('processed', self.plugin_processed)
81
-            plugin.connect('tts', self.plugin_tts)
80
+        # Connect the recognizer's finished signal to the appropriate method
82 81
         self.recognizer.connect('finished', self.recognizer_finished)
83 82
 
84 83
     def update_voice_commands_if_changed(self):
@@ -113,17 +112,8 @@ class Kaylee:
113 112
                     strings.write(word + " ")
114 113
             strings.write("\n")
115 114
 
116
-    def plugin_processed(self, plugin, text):
117
-        """Callback for ``processed`` signal from plugins
118
-
119
-        Runs the valid_sentence_command and logs the recognized sentence to the
120
-        history file.
121
-        """
122
-        # Run the valid_sentence_command if it's set
123
-        if self.options['valid_sentence_command']:
124
-            subprocess.call(self.options['valid_sentence_command'], shell=True)
125
-
126
-        # Log the command to the history file
115
+    def _log_history(self, plugin, text):
116
+        """Log the recognized sentence to the history file"""
127 117
         if self.options['history']:
128 118
             self.history.append("{}: {}".format(plugin.name, text))
129 119
             if len(self.history) > self.options['history']:
@@ -136,13 +126,49 @@ class Kaylee:
136 126
                     hfile.write(line + '\n')
137 127
         self._stop_ui(text)
138 128
 
139
-    def recognizer_finished(self, recognizer, text):
140
-        # No loaded plugin wanted the text, so run the invalid_sentence_command
141
-        # if it's set
129
+    def _valid_cmd(self):
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'], shell=True)
133
+
134
+    def _invalid_cmd(self, text):
135
+        """Run the invalid_sentence_command if it's set"""
142 136
         if self.options['invalid_sentence_command']:
143 137
             subprocess.call(self.options['invalid_sentence_command'],
144 138
                             shell=True)
145 139
         print("no matching command {0}".format(text))
140
+
141
+    def recognizer_finished(self, recognizer, text):
142
+        confidence_heap = queue.PriorityQueue()
143
+        # Add plugins to the heap
144
+        for index, plugin in enumerate(self.plugins):
145
+            # Get plugin confidence
146
+            confidence = plugin.confidence(text)
147
+            # Clamp confidence to [0, 1]
148
+            confidence = min(max(confidence, 0), 1)
149
+            # TODO: Only add items if they meet minimum confidence
150
+            if confidence > 0:
151
+                # Add a triple to the heap, so plugins are sorted first by
152
+                # confidence, then by index to break ties
153
+                confidence_heap.put_nowait((1 - confidence, index, plugin))
154
+
155
+        # Run valid or invalid sentence command
156
+        if confidence_heap.empty():
157
+            self._invalid_cmd(text)
158
+        else:
159
+            self._valid_cmd()
160
+
161
+        # Give the command to the plugins that want it
162
+        while True:
163
+            try:
164
+                plugin = confidence_heap.get_nowait()[2]
165
+            except queue.Empty:
166
+                break
167
+            # If the plugin successfully handled the command
168
+            if plugin.handle(text):
169
+                self._log_history(plugin, text)
170
+                break
171
+
146 172
         self._stop_ui(text)
147 173
 
148 174
     def plugin_tts(self, plugin, text):

+ 6
- 3
kayleevc/plugins/darksky.py View File

@@ -137,11 +137,14 @@ class Plugin(PluginBase):
137 137
         with open(self._cache_filename, 'r') as f:
138 138
             return json.load(f)
139 139
 
140
-    def recognizer_finished(self, recognizer, text):
140
+    def confidence(self, text):
141
+        """Return whether or not the command can be handled"""
142
+        return 1 if text in self.corpus_strings else 0
143
+
144
+    def handle(self, text):
141 145
         """Speak reasonably up-to-date weather information"""
142 146
         # Is there a matching command?
143
-        if text in self.corpus_strings:
144
-            self.emit('processed', text)
147
+        if self.confidence(text):
145 148
             self._update_cache_if_stale()
146 149
             self.cache = self._read_cache()
147 150
             self.emit('tts', self.commands[text]())

+ 10
- 4
kayleevc/plugins/mpris.py View File

@@ -57,7 +57,6 @@ class Plugin(PluginBase):
57 57
             # Get the proxy
58 58
             return self._bus.get(bus_name, '/org/mpris/MediaPlayer2')
59 59
         except IndexError:
60
-            print('No media player found')
61 60
             return None
62 61
 
63 62
     def _next(self):
@@ -80,11 +79,18 @@ class Plugin(PluginBase):
80 79
         if p is not None:
81 80
             p.Previous()
82 81
 
83
-    def recognizer_finished(self, recognizer, text):
82
+    def confidence(self, text):
83
+        """Return whether or not the command can be handled"""
84
+        if text not in self.corpus_strings:
85
+            return 0
86
+        if self._mpris_proxy() is None:
87
+            return 0
88
+        return 1
89
+
90
+    def handle(self, text):
84 91
         """Print and speak the phrase when it is heard"""
85 92
         # Is there a matching command?
86
-        if text in self.corpus_strings:
87
-            self.emit('processed', text)
93
+        if self.confidence(text):
88 94
             self.commands[text]()
89 95
             return True
90 96
         else:

+ 18
- 8
kayleevc/plugins/pluginbase.py View File

@@ -14,8 +14,6 @@ class PluginBase(GObject.Object):
14 14
     """
15 15
 
16 16
     __gsignals__ = {
17
-        'processed' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
18
-                      (GObject.TYPE_STRING,)),
19 17
         'tts' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
20 18
                 (GObject.TYPE_STRING,))
21 19
     }
@@ -34,14 +32,26 @@ class PluginBase(GObject.Object):
34 32
         self.options = config.plugins[name]
35 33
         self.corpus_strings = set()
36 34
 
37
-    def recognizer_finished(self, recognizer, text):
35
+    def confidence(self, text):
36
+        """Return the confidence (0-1) with which this plugin can handle text
37
+
38
+        This method must return 1 if the command is definitely supposed to be
39
+        handled by this plugin (text is exactly a sentence configured to
40
+        perform some action) and must return 0 if the command is not recognized
41
+        at all.  The method must also return 0 if the command is recognized,
42
+        but for some reason would not perform any action currently, e.g. "pause
43
+        music" when no music player is running.  Intermediate values may be
44
+        used to indicate that text may be a command this plugin should handle,
45
+        but was misunderstood by the speech recognition system.
46
+        """
47
+        return 0
48
+
49
+    def handle(self, text):
38 50
         """Process a recognized voice command
39 51
 
40 52
         This method must return True if the command was handled by the plugin,
41
-        and False otherwise.  As soon as it has been determined that the
42
-        command will be handled, this method must emit a ``processed`` signal
43
-        with ``text`` as its parameter.  If the plugin wants to speak some
44
-        words to the user, it can do so by emitting a ``tts`` signal with the
45
-        words to be spoken as its parameter.
53
+        and False otherwise.  If the plugin wants to speak some words to the
54
+        user, it can do so by emitting a ``tts`` signal with the words to be
55
+        spoken as its parameter.
46 56
         """
47 57
         return False

+ 11
- 3
kayleevc/plugins/shell.py View File

@@ -49,7 +49,17 @@ class Plugin(PluginBase):
49 49
         print(cmd)
50 50
         subprocess.call(cmd, shell=True)
51 51
 
52
-    def recognizer_finished(self, recognizer, text):
52
+    def confidence(self, text):
53
+        """Return whether or not the command can be handled"""
54
+        numt, nums = self.number_parser.parse_all_numbers(text)
55
+        if text in self.commands:
56
+            return 1
57
+        elif numt in self.commands:
58
+            return 1
59
+        else:
60
+            return 0
61
+
62
+    def handle(self, text):
53 63
         """Run the shell command corresponding to a recognized voice command
54 64
 
55 65
         Spoken number(s) may be substituted in the shell command.  The shell
@@ -58,13 +68,11 @@ class Plugin(PluginBase):
58 68
         numt, nums = self.number_parser.parse_all_numbers(text)
59 69
         # Is there a matching command?
60 70
         if text in self.commands:
61
-            self.emit('processed', text)
62 71
             cmd = self.commands[text]
63 72
             self._run_command(cmd)
64 73
             return True
65 74
         # Is there a matching command with numbers substituted?
66 75
         elif numt in self.commands:
67
-            self.emit('processed', text)
68 76
             cmd = self.commands[numt]
69 77
             cmd = cmd.format(*nums)
70 78
             self._run_command(cmd)

+ 7
- 4
kayleevc/plugins/simple.py View File

@@ -23,11 +23,14 @@ class Plugin(PluginBase):
23 23
 
24 24
         self.corpus_strings.add(self.options['phrase'])
25 25
 
26
-    def recognizer_finished(self, recognizer, text):
27
-        """Print and speak the phrase when it is heard"""
26
+    def confidence(self, text):
27
+        """Return 0 if text is not the phrase, 1 if it is"""
28
+        return 1 if text == self.options['phrase'] else 0
29
+
30
+    def handle(self, text):
31
+        """Print and speak the phrase"""
28 32
         # Is there a matching command?
29
-        if text == self.options['phrase']:
30
-            self.emit('processed', text)
33
+        if self.confidence(text):
31 34
             print("Simple plugin says:", text)
32 35
             self.emit('tts', text)
33 36
             return True

+ 1
- 2
kayleevc/recognizer.py View File

@@ -16,8 +16,7 @@ Gst.init(None)
16 16
 class Recognizer(GObject.GObject):
17 17
     __gsignals__ = {
18 18
         'finished' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_BOOLEAN,
19
-                      (GObject.TYPE_STRING,),
20
-                      GObject.signal_accumulator_true_handled)
19
+                      (GObject.TYPE_STRING,))
21 20
     }
22 21
 
23 22
     def __init__(self, config):

Loading…
Cancel
Save