Browse Source

Initial support for plugins

As part of the effort for resolving #12, I've started work on a plugin
API for Kaylee.  While very much a work in progress, it will allow
Python plugins to be written, loaded from user configuration, and
hooked in to events from necessary portions of Kaylee to handle voice
commands.

Currently there is only one plugin, a partial implementation of shell
command support as existed previously.  It works in that it executes
commands, but several old features are missing.  Also, the GUIs are
probably broken, but I'm not worried about that at the moment.
Clara Hobbs 7 years ago
parent
commit
1002fc436e

+ 25
- 57
kayleevc/kaylee.py View File

@@ -3,15 +3,16 @@
3 3
 # Copyright 2015-2016 Clayton G. Hobbs
4 4
 # Portions Copyright 2013 Jezra
5 5
 
6
+import importlib
6 7
 import sys
7 8
 import signal
8 9
 import os.path
9
-import subprocess
10 10
 from gi.repository import GObject, GLib
11 11
 
12 12
 from kayleevc.recognizer import Recognizer
13 13
 from kayleevc.util import *
14 14
 from kayleevc.numbers import NumberParser
15
+import kayleevc.plugins
15 16
 
16 17
 
17 18
 class Kaylee:
@@ -24,10 +25,13 @@ class Kaylee:
24 25
         # Load configuration
25 26
         self.config = Config()
26 27
         self.options = vars(self.config.options)
27
-        self.commands = self.options['commands']
28
+        self.commands = self.options['plugins']['.shell']
28 29
 
29
-        # Create number parser for later use
30
-        self.number_parser = NumberParser()
30
+        # Load plugins
31
+        self.plugins = []
32
+        for plugin in self.options['plugins'].keys():
33
+            pmod = importlib.import_module(plugin, 'kayleevc.plugins')
34
+            self.plugins.append(pmod.Plugin(self.config))
31 35
 
32 36
         # Create a hasher
33 37
         self.hasher = Hasher(self.config)
@@ -64,7 +68,10 @@ class Kaylee:
64 68
 
65 69
         # Create the recognizer
66 70
         self.recognizer = Recognizer(self.config)
67
-        self.recognizer.connect('finished', self.recognizer_finished)
71
+
72
+        # Connect the recognizer's finished signal to all the plugins
73
+        for plugin in self.plugins:
74
+            self.recognizer.connect('finished', plugin.recognizer_finished)
68 75
 
69 76
     def update_voice_commands_if_changed(self):
70 77
         """Use hashes to test if the voice commands have changed"""
@@ -72,10 +79,11 @@ class Kaylee:
72 79
 
73 80
         # Calculate the hash the voice commands have right now
74 81
         hasher = self.hasher.get_hash_object()
75
-        for voice_cmd in self.commands.keys():
76
-            hasher.update(voice_cmd.encode('utf-8'))
77
-            # Add a separator to avoid odd behavior
78
-            hasher.update('\n'.encode('utf-8'))
82
+        for plugin in self.plugins:
83
+            for string in sorted(plugin.corpus_strings):
84
+                hasher.update(string.encode('utf-8'))
85
+                # Add a separator to avoid odd behavior
86
+                hasher.update('\n'.encode('utf-8'))
79 87
         new_hash = hasher.hexdigest()
80 88
 
81 89
         if new_hash != stored_hash:
@@ -87,11 +95,14 @@ class Kaylee:
87 95
         # Open the strings file
88 96
         with open(self.config.strings_file, 'w') as strings:
89 97
             # Add command words to the corpus
90
-            for voice_cmd in sorted(self.commands.keys()):
91
-                strings.write(voice_cmd.strip().replace('%d', '') + "\n")
98
+            # FIXME: Doing this twice is a silly thing
99
+            for plugin in self.plugins:
100
+                for string in sorted(plugin.corpus_strings):
101
+                    strings.write(string + "\n")
92 102
             # Add number words to the corpus
93
-            for word in self.number_parser.number_words:
94
-                strings.write(word + " ")
103
+            if NumberParser.number_words is not None:
104
+                for word in NumberParser.number_words:
105
+                    strings.write(word + " ")
95 106
             strings.write("\n")
96 107
 
97 108
     def log_history(self, text):
@@ -106,51 +117,8 @@ class Kaylee:
106 117
                 for line in self.history:
107 118
                     hfile.write(line + '\n')
108 119
 
109
-    def run_command(self, cmd):
110
-        """Print the command, then run it"""
111
-        print(cmd)
112
-        subprocess.call(cmd, shell=True)
113
-
114 120
     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)
121
+        pass
154 122
 
155 123
     def run(self):
156 124
         if self.ui:

+ 20
- 16
kayleevc/numbers.py View File

@@ -64,23 +64,27 @@ class NumberParser:
64 64
         'and'
65 65
     ]
66 66
 
67
+    number_words = None
68
+    mandatory_number_words = None
69
+
67 70
     def __init__(self):
68
-        self.number_words = []
69
-        for word in sorted(self.zero.keys()):
70
-            self.number_words.append(word)
71
-        for word in sorted(self.ones.keys()):
72
-            self.number_words.append(word)
73
-        for word in sorted(self.special_ones.keys()):
74
-            self.number_words.append(word)
75
-        for word in sorted(self.tens.keys()):
76
-            self.number_words.append(word)
77
-        for word in sorted(self.hundred.keys()):
78
-            self.number_words.append(word)
79
-        for word in sorted(self.exp.keys()):
80
-            self.number_words.append(word)
81
-        self.mandatory_number_words = self.number_words.copy()
82
-        for word in sorted(self.allowed):
83
-            self.number_words.append(word)
71
+        if NumberParser.number_words is None:
72
+            NumberParser.number_words = []
73
+            for word in sorted(self.zero.keys()):
74
+                NumberParser.number_words.append(word)
75
+            for word in sorted(self.ones.keys()):
76
+                NumberParser.number_words.append(word)
77
+            for word in sorted(self.special_ones.keys()):
78
+                NumberParser.number_words.append(word)
79
+            for word in sorted(self.tens.keys()):
80
+                NumberParser.number_words.append(word)
81
+            for word in sorted(self.hundred.keys()):
82
+                NumberParser.number_words.append(word)
83
+            for word in sorted(self.exp.keys()):
84
+                NumberParser.number_words.append(word)
85
+            NumberParser.mandatory_number_words = self.number_words.copy()
86
+            for word in sorted(self.allowed):
87
+                NumberParser.number_words.append(word)
84 88
 
85 89
     def parse_number(self, text_line):
86 90
         """Parse a number from English into an int"""

+ 0
- 0
kayleevc/plugins/__init__.py View File


+ 35
- 0
kayleevc/plugins/pluginbase.py View File

@@ -0,0 +1,35 @@
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
+from abc import ABCMeta, abstractmethod
7
+
8
+
9
+class PluginBase(metaclass=ABCMeta):
10
+    """Base class for Kaylee plugins
11
+
12
+    Each Kaylee plugin module must define a subclass of this class, named
13
+    ``Plugin``.  These obviously must provide concrete implementations of all
14
+    abstract methods defined here.
15
+    """
16
+
17
+    def __init__(self, config):
18
+        """Initialize the plugin
19
+
20
+        All strings to be included in the corpus should be elements of
21
+        ``corpus_strings`` by the end of __init__'s execution.  Note however
22
+        that the words required for number support are automatically included,
23
+        so they need not be listed explicitly on a per-plugin basis.
24
+        """
25
+        self.config = config
26
+        self.corpus_strings = set()
27
+
28
+    @abstractmethod
29
+    def recognizer_finished(self, recognizer, text):
30
+        """Process a recognized voice command
31
+
32
+        This method must return True if the command was handled by the plugin,
33
+        and False otherwise.
34
+        """
35
+        pass

+ 80
- 0
kayleevc/plugins/shell.py View File

@@ -0,0 +1,80 @@
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 subprocess
7
+
8
+from .pluginbase import PluginBase
9
+from ..numbers import NumberParser
10
+
11
+
12
+class Plugin(PluginBase):
13
+
14
+    def __init__(self, config):
15
+        super().__init__(config)
16
+        self.options = vars(self.config.options)
17
+        self.number_parser = NumberParser()
18
+        self.commands = self.options['plugins']['.shell']
19
+
20
+        for voice_cmd in self.commands.keys():
21
+            self.corpus_strings.add(voice_cmd.strip().replace('%d', ''))
22
+
23
+    def _run_command(self, cmd):
24
+        """Print the command, then run it"""
25
+        print(cmd)
26
+        subprocess.call(cmd, shell=True)
27
+
28
+    def recognizer_finished(self, recognizer, text):
29
+        """Process a recognized voice command
30
+
31
+        Recognized commands have the corresponding shell commands executed,
32
+        possibly with number(s) substituted.
33
+        """
34
+        numt, nums = self.number_parser.parse_all_numbers(text)
35
+        # Is there a matching command?
36
+        if text in self.commands:
37
+            # Run the valid_sentence_command if it's set
38
+            # TODO: determine how to support this with plugins
39
+            if self.options['valid_sentence_command']:
40
+                subprocess.call(self.options['valid_sentence_command'],
41
+                                shell=True)
42
+            cmd = self.commands[text]
43
+            # Should we be passing words?
44
+            # TODO: configuration changes to make this not ridiculous
45
+            if self.options['pass_words']:
46
+                cmd += " " + text
47
+            self._run_command(cmd)
48
+            # TODO: Logging can be handled along with valid_sentence_command
49
+            #self.log_history(text)
50
+            return True
51
+        elif numt in self.commands:
52
+            # Run the valid_sentence_command if it's set
53
+            if self.options['valid_sentence_command']:
54
+                subprocess.call(self.options['valid_sentence_command'],
55
+                                shell=True)
56
+            cmd = self.commands[numt]
57
+            cmd = cmd.format(*nums)
58
+            # Should we be passing words?
59
+            if self.options['pass_words']:
60
+                cmd += " " + text
61
+            self._run_command(cmd)
62
+            #self.log_history(text)
63
+            return True
64
+        else:
65
+            # TODO: This could be implemented as a plugin, implicitly loaded
66
+            # last
67
+            # Run the invalid_sentence_command if it's set
68
+            if self.options['invalid_sentence_command']:
69
+                subprocess.call(self.options['invalid_sentence_command'],
70
+                                shell=True)
71
+            print("no matching command {0}".format(text))
72
+            return False
73
+        # TODO: Make the D-Bus interface so this can leave the main process
74
+        ## If there is a UI and we are not continuous listen
75
+        #if self.ui:
76
+        #    if not self.continuous_listen:
77
+        #        # Stop listening
78
+        #        self.recognizer.pause()
79
+        #    # Let the UI know that there is a finish
80
+        #    self.ui.finished(text)

+ 4
- 3
kayleevc/recognizer.py View File

@@ -15,8 +15,9 @@ Gst.init(None)
15 15
 
16 16
 class Recognizer(GObject.GObject):
17 17
     __gsignals__ = {
18
-        'finished' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
19
-                      (GObject.TYPE_STRING,))
18
+        'finished' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_BOOLEAN,
19
+                      (GObject.TYPE_STRING,),
20
+                      GObject.signal_accumulator_true_handled)
20 21
     }
21 22
 
22 23
     def __init__(self, config):
@@ -65,4 +66,4 @@ class Recognizer(GObject.GObject):
65 66
         # If we have a final command, send it for processing
66 67
         command = msg_struct.get_string('hypothesis')
67 68
         if command != '' and msg_struct.get_boolean('final')[1]:
68
-            self.emit("finished", command)
69
+            self.emit("finished", command.lower())

+ 5
- 3
options.json.tmp View File

@@ -1,7 +1,9 @@
1 1
 {
2
-    "commands": {
3
-        "hello world": "echo \"hello world\"",
4
-        "start a %d minute timer": "(echo {0} minute timer started && sleep {0}m && echo {0} minute timer ended) &"
2
+    "plugins": {
3
+        ".shell": {
4
+            "hello world": "echo \"hello world\"",
5
+            "start a %d minute timer": "(echo {0} minute timer started && sleep {0}m && echo {0} minute timer ended) &"
6
+        }
5 7
     },
6 8
     "continuous": false,
7 9
     "history": null,

Loading…
Cancel
Save