diff --git a/setup.py b/setup.py index d27a2ce..56c250b 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup setup(name='winston', - version='0.1', + version='0.2', description='A voice-controlled virtual butler', url='http://github.com/nicbou/winston', author='Nicolas Bouliane', diff --git a/winston/__main__.py b/winston/__main__.py index 87c3427..28c0564 100644 --- a/winston/__main__.py +++ b/winston/__main__.py @@ -4,7 +4,9 @@ def main(): """ - Allows Winston to be installed as a package and to be run from the command line + Allows Winston to be installed as a package and to be run from the command line. + + This simply inits Winston using the config file. """ # Define and start a scheduler. These store tasks that are run at given times @@ -12,10 +14,13 @@ def main(): scheduler.start() # Load the commands in the interpreter. These dispatch commands. See the Interpreter's doc for details. - interpreter = Interpreter(commands=config.COMMANDS, scheduler=config.SCHEDULER) + interpreter = Interpreter(scheduler=config.SCHEDULER) + for command in config.COMMANDS: + interpreter.register(command) # Create a listener for pocketsphinx. It forwards recognized strings to Interpreters. See Listener's doc for details. - listener = Listener(interpreters=[interpreter], fsg_path=config.GRAMMAR_FILE, dict_path=config.DICT_FILE) + listener = Listener(fsg_path=config.GRAMMAR_FILE, dict_path=config.DICT_FILE) + listener.register(interpreter) # And wait... raw_input() diff --git a/winston/commands/__init__.py b/winston/commands/__init__.py index 9fdeef2..82e3aba 100644 --- a/winston/commands/__init__.py +++ b/winston/commands/__init__.py @@ -2,114 +2,12 @@ class Command(object): """ - Stores a command: - - name: an identifier for the regex and JSGF file generation. Should be unique. - - actions: a list of action variations: (say, tell us, tell me, tell) - - subjects: a list of action targets passed to the callback: (the time, the weather) - - callback: a function that executes the command with a subject string as the argument - - You can create a new command by extending it or by instanciating it. Extending the Command - object gives you the ability to keep application state and redefine how commands are - dispatched. + Stores a command that is executed by external events such as a voice command, + a change of state or a notification. """ - _interpreter = None - _callback = None - - def __init__(self, actions=[], subjects=[], callback=None, name='command', interpreter=None, always_active=False): - self.name = name # Used as a named group identifier in the regex - self.actions = actions - self.subjects = subjects - self.callback = callback - self.interpreter = interpreter # Reference to the interpreter that will run this command - self.always_active = always_active # This command will not run when the interpreter is inactive - - def dispatch(self, command, subject): - """ - Dispatches the command to the callback with the specified subject - as a callback. Easily overridden. - - command: The full matched command ('turn on the lights') - subject: The variable part of the command ('the lights') - """ - # Don't perform actions if the interpreter isn't active - if self.interpreter.active or (not self.interpreter) or self.always_active: - # Validate the existence of a subject, if any are specified - if not self.subjects: - self.callback(command) - elif isinstance(self.subjects, (tuple, list)) and subject in self.subjects: - # Match a subject list - self.callback(command, subject) - elif (not isinstance(self.subjects, (tuple, list))) and re.match(self.subjects, subject): - # Match a regex subject - self.callback(command, subject) - else: - print("Subject {0} does not exist for command {1}".format(subject, self.name)) - @property - def regex(self, group_name=None): + def on_event(self, event, sender): """ - Returns a regex string matching the command. + Handles events from the interpreter and other sources """ - # Build the command - # e.g. (open|turn on) (the lights|the television) - command = "" - if self.subjects: - regex_actions = self.actions - regex_subjects = self.subjects - - # If the actions is a list (and not a regex), join it into a single regex: - if isinstance(self.actions, (tuple, list)): - regex_actions = "|".join(regex_actions) - - # If the subjects is a list (and not a regex), join it into a single regex: - if isinstance(self.subjects, (tuple, list)): - regex_subjects = "|".join(regex_subjects) - - command = "({actions}) (?P<{name}Subject>({subject}))".format( - name = self.name, - actions = regex_actions, - subject = regex_subjects, - ) - else: - regex_actions = self.actions - - # If the actions is a list (and not a regex), join it into a single regex: - if isinstance(self.actions, (tuple, list)): - command = "({actions})".format( - actions = "|".join(self.actions), - ) - else: - command = regex_actions - - # Put the regex in a named group - named_group = "(?P<{name}>({command}))".format( - name = self.name, - command = command, - ) - - # Return a regex pattern string - return named_group - - @property - def interpreter(self): - return self._interpreter - - @interpreter.setter - def interpreter(self, value): - """ - Since this function is called when the interpreter is set, - this is the perfect place to attach initiation logic that - concerns the command's Interpreter. - - For example, this is the place to add events to the interpreter's - scheduler. - """ - self._interpreter = value - - @property - def callback(self): - return self._callback - - @callback.setter - def callback(self, value): - self._callback = value \ No newline at end of file + # Do something here. \ No newline at end of file diff --git a/winston/config.py b/winston/config.py index 3229178..87ce368 100644 --- a/winston/config.py +++ b/winston/config.py @@ -15,18 +15,8 @@ BALANCE_PATH = "/var/www/scripts/winston_balance.txt" # Define your own commands and add them here -from commands import say, activate, deactivate, account_balance, open_door, set_alarm, next_bus, dinner, Command -COMMANDS = [ # The list of commands passed to the interpreter - say.SayCommand(), # Simple command to get started - activate.ActivateCommand(), # Can activate winston - deactivate.DeactivateCommand(), # Can deactivate winston - account_balance.AccountBalanceCommand(), # Lots of variations, uses regex actions, and act proactively - open_door.OpenDoorCommand(), - set_alarm.AbsoluteAlarmCommand(), - set_alarm.RelativeAlarmCommand(), - next_bus.NextBusCommand(), - dinner.DinnerCommand(), - Command(name='whatTime', actions=('what time is it',), callback=say.sayTime), # A simple command +COMMANDS = [ + ] # Define a scheduler to store scheduled events diff --git a/winston/interpreter.py b/winston/interpreter.py index 47caf05..e819135 100644 --- a/winston/interpreter.py +++ b/winston/interpreter.py @@ -2,92 +2,48 @@ class Interpreter(object): """ - The interpreter turns matches commands with functions. It strongly relies - on the Command object, bundling them together into a bigger regex. + The interpreter turns matches command strings with the right Commands. It's + purpose is to notify all Commands of a new command string. - Strings come from the Listener, which is just a wrapper around pocketsphinx. - It only returns lowercase strings without punctuation. + The command strings come from the Listener, which is just a wrapper around pocketsphinx. """ - # The name with which all commands begin. Can be a word or a regex. - # Example: jenkins, alfred, robot. "Jenkins! Turn on the lights!" - signal = "winston" - - # Command prefixes and suffixes. Can be a tuple of words or a regex - prefixes = "( can you| could you)?( please)?" - suffixes = "( please)?" - - # The actual commands. Expects a list of Command objects - commands = () - - def __init__(self, commands, scheduler=None): + def __init__(self, scheduler=None): """ - Prepares the interpreter, compile the regex strings + Prepares the interpreter """ - # Keep a reference to the scheduler self.scheduler = scheduler - - # Keep a reference to the interpreter, give command a unique name - for index, command in enumerate(commands): - command.interpreter = self - command.name = 'cmd' + str(index) # Every command needs a unique name. Any valid regex group name will do. - self.commands = commands - self.regex = self.regex() # Build the command matcher + # The commands + self.commands = [] - # Commands can access self.interpreter.active and decide whether or not - # to perform an action. Commands can also "shut down" winston by setting - # active to false. - - # We still let the commands go through so a command can reactivate an - # interpreter. - self.active = True + def register(self, command): + """ + Registers new commands to the interpreter's events + """ + if not command in self.commands: + self.commands.append(command) - def regex(self): + def unregister(self, command): """ - Build a regex to match all possible commands + Unregisters a command from the interpreter's events """ - # Basic command structure - basic_command = "{signal}{prefix} {command}{suffix}" + if command in self.commands: + self.commands.remove(command) - # Build the command regex by joining individual regexes - # e.g. (command_1|command_2|command_3) - command_regexes = [] + def notify(self, event): + """ + Notifies all commands of a change in the state of + the interpreter. + """ for command in self.commands: - command_regexes.append(command.regex) - command_regex = "({0})".format("|".join(command_regexes)) - - # Wrap the command with the prefix and suffix regexes - final_regex = basic_command.format( - signal = self.signal, - command = command_regex, - prefix = self.prefixes, - suffix = self.suffixes, - ) - - # Return the compiled regex, ready to be used - return re.compile(final_regex) - - def match(self, command): - # Try matching the command to an action - result = self.regex.match(command) - if result: - groups = result.groupdict() # Get all the group matches from the regex + command.on_event(event, self) - print("Got '%s'" % command) - - if not self.active: - print("(interpreter is inactive)") - - for command in self.commands: - # Check if the command name matches a regex group - if command.name in groups and groups[command.name] is not None: - print('matched ' + command.__class__.__name__) - command_string = groups[command.name] # The command without "Winston, please ... thank you" - subject = None - if command.subjects: - subject = groups[command.name + 'Subject'] # The group of the subject ('the lights' in 'turn on the lights') - command.dispatch(command_string, subject) - else: - print("Could not match '{0}' to a command using regex".format(command)) + def on_event(self, event, sender): + """ + Handles events from the listener and other classes + it is registered to. + """ + if self.active: + self.notify(event) diff --git a/winston/listener.py b/winston/listener.py index f7d5e1a..93b59ef 100644 --- a/winston/listener.py +++ b/winston/listener.py @@ -7,12 +7,10 @@ class Listener(object): """ Listens, understands and processes speeches using the - python-gstreamer plugin. - - This class is loosely based on the example from the anemic - official pocketsphinx documentation. + python-gstreamer plugin. Notifies all attached interpreters + when input is received. """ - def __init__(self, interpreters=[], fsg_path=None, dict_path=None, start=True): + def __init__(self, fsg_path=None, dict_path=None, start=True): """ Initialize the listener """ @@ -25,14 +23,18 @@ def __init__(self, interpreters=[], fsg_path=None, dict_path=None, start=True): # Init gstreamer self.init_gstreamer() - # Set the command interpreters - self.interpreters = interpreters + # The command's interpreters + self.interpreters = [] # Start listening if start: self.start() def init_gstreamer(self): + """ + Init the gstreamer plugin that pipes its input to pocketsphinx to be recognized + """ + # Get the pipeline self.pipeline = gst.parse_launch('gconfaudiosrc ! audioconvert ! audioresample ' + '! vader name=vad auto_threshold=true ' @@ -40,13 +42,13 @@ def init_gstreamer(self): asr = self.pipeline.get_by_name('asr') # Bind the pipeline results - # asr.connect('partial_result', self.asr_partial_result) - asr.connect('result', self.asr_result) + asr.connect('result', self.voice_input) - # Load the grammar file unless it was deactivated + # Load the grammar file if self.fsg_path: asr.set_property("fsg", self.fsg_path) + # Load the dictionary if self.dict_path: asr.set_property("dict", self.dict_path) @@ -58,24 +60,47 @@ def init_gstreamer(self): self.pipeline.set_state(gst.STATE_PAUSED) - def asr_result(self, asr, parsed_text, utterance_id): + def voice_input(self, asr, parsed_text, utterance_id): """ - Receives a result from the pipeline, and forwards the parsed + Receive a result from the pipeline, and forward the parsed text to process_result. + + During the processing, voice recognition is paused so Winston + doesn't end up talking to himself. """ self.pause() - self.process_result(parsed_text) + self.notify(parsed_text) self.start() def start(self): + """ + Start listening + """ self.pipeline.set_state(gst.STATE_PLAYING) def pause(self): + """ + Stop listening + """ self.pipeline.set_state(gst.STATE_PAUSED) - def process_result(self, parsed_text): + def register(self, interpreter): + """ + Register interpreters to be notified of new input + """ + if not interpreter in self.interpreters: + self.interpreters.append(interpreter) + + def unregister(self, interpreter): + """ + Unregisters an interpreter + """ + if interpreter in self.interpreters: + self.interpreters.remove(interpreter) + + def notify(self, event): """ - Sends the command string to all interpreters for dispatching. + Notify all interpreters of a received input """ for interpreter in self.interpreters: - interpreter.match(parsed_text) \ No newline at end of file + interpreter.on_event(parsed_text, self) \ No newline at end of file