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
 # Copyright 2015-2016 Clayton G. Hobbs
3
 # Copyright 2015-2016 Clayton G. Hobbs
4
 # Portions Copyright 2013 Jezra
4
 # Portions Copyright 2013 Jezra
5
 
5
 
6
+import importlib
6
 import sys
7
 import sys
7
 import signal
8
 import signal
8
 import os.path
9
 import os.path
9
-import subprocess
10
 from gi.repository import GObject, GLib
10
 from gi.repository import GObject, GLib
11
 
11
 
12
 from kayleevc.recognizer import Recognizer
12
 from kayleevc.recognizer import Recognizer
13
 from kayleevc.util import *
13
 from kayleevc.util import *
14
 from kayleevc.numbers import NumberParser
14
 from kayleevc.numbers import NumberParser
15
+import kayleevc.plugins
15
 
16
 
16
 
17
 
17
 class Kaylee:
18
 class Kaylee:
24
         # Load configuration
25
         # Load configuration
25
         self.config = Config()
26
         self.config = Config()
26
         self.options = vars(self.config.options)
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
         # Create a hasher
36
         # Create a hasher
33
         self.hasher = Hasher(self.config)
37
         self.hasher = Hasher(self.config)
64
 
68
 
65
         # Create the recognizer
69
         # Create the recognizer
66
         self.recognizer = Recognizer(self.config)
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
     def update_voice_commands_if_changed(self):
76
     def update_voice_commands_if_changed(self):
70
         """Use hashes to test if the voice commands have changed"""
77
         """Use hashes to test if the voice commands have changed"""
72
 
79
 
73
         # Calculate the hash the voice commands have right now
80
         # Calculate the hash the voice commands have right now
74
         hasher = self.hasher.get_hash_object()
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
         new_hash = hasher.hexdigest()
87
         new_hash = hasher.hexdigest()
80
 
88
 
81
         if new_hash != stored_hash:
89
         if new_hash != stored_hash:
87
         # Open the strings file
95
         # Open the strings file
88
         with open(self.config.strings_file, 'w') as strings:
96
         with open(self.config.strings_file, 'w') as strings:
89
             # Add command words to the corpus
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
             # Add number words to the corpus
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
             strings.write("\n")
106
             strings.write("\n")
96
 
107
 
97
     def log_history(self, text):
108
     def log_history(self, text):
106
                 for line in self.history:
117
                 for line in self.history:
107
                     hfile.write(line + '\n')
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
     def recognizer_finished(self, recognizer, text):
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
     def run(self):
123
     def run(self):
156
         if self.ui:
124
         if self.ui:

+ 20
- 16
kayleevc/numbers.py View File

64
         'and'
64
         'and'
65
     ]
65
     ]
66
 
66
 
67
+    number_words = None
68
+    mandatory_number_words = None
69
+
67
     def __init__(self):
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
     def parse_number(self, text_line):
89
     def parse_number(self, text_line):
86
         """Parse a number from English into an int"""
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

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

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
 
15
 
16
 class Recognizer(GObject.GObject):
16
 class Recognizer(GObject.GObject):
17
     __gsignals__ = {
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
     def __init__(self, config):
23
     def __init__(self, config):
65
         # If we have a final command, send it for processing
66
         # If we have a final command, send it for processing
66
         command = msg_struct.get_string('hypothesis')
67
         command = msg_struct.get_string('hypothesis')
67
         if command != '' and msg_struct.get_boolean('final')[1]:
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
 {
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
     "continuous": false,
8
     "continuous": false,
7
     "history": null,
9
     "history": null,

Loading…
Cancel
Save