Skip to content

Commit

Permalink
Implemented an observer pattern to handle commands
Browse files Browse the repository at this point in the history
  • Loading branch information
nicbou committed Dec 22, 2013
1 parent 653326c commit e6141c2
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 213 deletions.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
11 changes: 8 additions & 3 deletions winston/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@

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
scheduler = config.SCHEDULER
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()
Expand Down
112 changes: 5 additions & 107 deletions winston/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# Do something here.
14 changes: 2 additions & 12 deletions winston/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
104 changes: 30 additions & 74 deletions winston/interpreter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading

0 comments on commit e6141c2

Please sign in to comment.