Skip to content

Commit

Permalink
Code clean-up and improvements
Browse files Browse the repository at this point in the history
- Dobby can run as daemon
- Dobby shutdowns properly
- Use pyjulius 0.3
- More logging
- More documentation
- Fixed file logging
  • Loading branch information
Antoine Bertin committed Jan 14, 2012
1 parent 9e8c08b commit a26491c
Show file tree
Hide file tree
Showing 19 changed files with 305 additions and 219 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@ pip-log.txt
docs/_build

#Dobby
config.ini
data
249 changes: 234 additions & 15 deletions Dobby.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,20 @@
# along with Dobby. If not, see <http://www.gnu.org/licenses/>.

from Queue import Queue
from configobj import ConfigObj, flatten_errors
from dobby import infos
from dobby.app import initRecognizer, initTriggers, initSpeaker, initController, initLogging
from dobby.config import initConfig
from dobby.db import initDb
from dobby.controller import Controller
from dobby.db import initDb, Session
from dobby.recognizers.julius import Julius as JuliusRecognizer
from dobby.speakers.speechdispatcher import SpeechDispatcher
from dobby.triggers.clapper import Pattern, QuietPattern, NoisyPattern, Clapper
from dobby.triggers.julius import Julius as JuliusTrigger
from validate import Validator, VdtValueError, VdtTypeError
import argparse
import logging
import logging.handlers
import os
import signal
import sys


logger = logging.getLogger()
Expand All @@ -32,22 +40,52 @@ def main():
parser = argparse.ArgumentParser(description=infos.__description__)
parser.add_argument('-d', '--daemon', action='store_true', help='run as daemon', default=False)
parser.add_argument('-p', '--pid-file', action='store', dest='pid_file', help='create pid file')
parser.add_argument('-c', '--config-file', action='store', dest='config_file', help='config file to use', default='config.ini')
parser.add_argument('-c', '--config-file', action='store', dest='config_file', help='config file to use')
parser.add_argument('--list-devices', action='store_true', dest='list_devices', help='list available devices and exit')
parser.add_argument('--data-dir', action='store', dest='data_dir', help='data directory to store cache, config, logs and database', default='data')
group_verbosity = parser.add_mutually_exclusive_group()
group_verbosity.add_argument('-q', '--quiet', action='store_true', help='disable console output')
group_verbosity.add_argument('-v', '--verbose', action='store_true', help='verbose console output')
parser.add_argument('--version', action='version', version=infos.__version__)
args = parser.parse_args()

if args.list_devices:
import pyaudio
pa = pyaudio.PyAudio()
for i in range(pa.get_device_count()):
device_infos = pa.get_device_info_by_index(i)
print device_infos['name'] + "\t" + str(device_infos['index'])
sys.exit(0)

# Init paths
data_dir = os.path.abspath(args.data_dir)
config_file = args.config_file or os.path.join(data_dir, 'config.ini')

# Create the data directory
if not os.path.exists(data_dir):
os.makedirs(data_dir)

# Init config
config = initConfig(args.config_file)
config = initConfig(config_file)

# Init logging
#FIXME: args.verbose instead of True
initLogging(args.quiet, True, config['Logging'])
initLogging(args.quiet or args.daemon, args.verbose, os.path.join(data_dir, 'logs'))

# Init db
initDb()
initDb(os.path.join(data_dir, 'dobby.db'))

# Daemonize
if args.daemon:
daemonize()

# Write pid
pid_file = None
if args.pid_file:
pid_file = args.pid_file
elif args.daemon:
pid_file = os.path.join(data_dir, 'dobby.pid')
if pid_file:
file(pid_file, 'w+').write('%s\n' % str(os.getpid()))

# Init recognizer
recognizer = initRecognizer(config['Recognizer'])
Expand All @@ -66,13 +104,194 @@ def main():
# Welcome message
if config['General']['welcome_message']:
tts_queue.put(config['General']['welcome_message'])

# Handle termination
def handler(*args):
logger.info(u'Stop signal caught')
if config['General']['bye_message']:
tts_queue.put(config['General']['bye_message'])
controller.stop()
controller.join()
for trigger in triggers:
trigger.stop()
trigger.join()
recognizer.stop()
recognizer.join()
speaker.stop()
speaker.join()
config.write()
exit(0)

# Plug signals
signal.signal(signal.SIGINT, handler)
signal.signal(signal.SIGTERM, handler)

# Wait until it's time to exit
signal.pause()

def daemonize():
"""Daemonize"""
try:
pid = os.fork()
if pid > 0:
# exit first parent
sys.exit(0)
except OSError, e:
sys.stderr.write('Fork #1 failed: %d (%s)\n' % (e.errno, e.strerror))
sys.exit(1)

# decouple from parent environment
os.chdir('/')
os.setsid()
os.umask(0)

config.write()
# do second fork
try:
pid = os.fork()
if pid > 0:
# exit from second parent
sys.exit(0)
except OSError, e:
sys.stderr.write('Fork #2 failed: %d (%s)\n' % (e.errno, e.strerror))
sys.exit(1)

# redirect standard file descriptors
sys.stdout.flush()
sys.stderr.flush()
si = file('/dev/null', 'r')
so = file('/dev/null', 'a+')
se = file('/dev/null', 'a+', 0)
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())

def initTriggers(event_queue, recognizer, config):
"""Initialize all triggers as defined in the config
:param Queue.Queue event_queue: where event will be raised into
:param Recognizer recognizer: recognizer instance (only :class:`~dobby.recognizers.julius.Julius` is supported now)
:param dict config: triggers-related settings
:return: started triggers
:rtype: list of Trigger
"""
logger.debug(u'Initializing triggers')
triggers = []
for trigger_name in config['triggers']:
if trigger_name == 'clapper':
pattern = Pattern([QuietPattern(1), NoisyPattern(1, 4), QuietPattern(1, 6), NoisyPattern(1, 4), QuietPattern(1)])
trigger = Clapper(event_queue, config['Clapper']['device_index'], pattern, config['Clapper']['threshold'],
config['Clapper']['channels'], config['Clapper']['rate'], config['Clapper']['block_time'])
trigger.start()
triggers.append(trigger)
logger.debug(u'Trigger clapper initialized')
elif trigger_name == 'julius':
trigger = JuliusTrigger(event_queue, config['Julius']['sentence'], recognizer, config['Julius']['action'])
trigger.start()
triggers.append(trigger)
logger.debug(u'Trigger julius initialized')
return triggers

def initRecognizer(config):
"""Initialize the recognizer as defined in the config
:param dict config: recognizer-related settings
:return: started recognizer
:rtype: Recognizer
"""
if config['recognizer'] == 'julius':
recognizer = JuliusRecognizer(config['Julius']['host'], config['Julius']['port'], config['Julius']['encoding'], config['Julius']['min_score'])
recognizer.start()
return recognizer

def initSpeaker(tts_queue, config):
"""Initialize the Speaker as defined in the config
:param Queue.Queue tts_queue: where actions are taken from
:param dict config: Speaker-related settings
:return: started Speaker
:rtype: Speaker
"""
if config['speaker'] == 'speechdispatcher':
speaker = SpeechDispatcher(tts_queue, 'Dobby', str(config['SpeechDispatcher']['engine']), str(config['SpeechDispatcher']['voice']),
str(config['SpeechDispatcher']['language']), config['SpeechDispatcher']['volume'],
config['SpeechDispatcher']['rate'], config['SpeechDispatcher']['pitch'])
speaker.start()
return speaker

def initController(event_queue, tts_queue, recognizer, config):
"""Initialize the Controller as defined in the config
:param Queue.Queue event_queue: where events are taken from
:param Queue.Queue tts_queue: where actions are put into
:param Recognizer recognizer: the recognizer instance
:param dict config: general settings
:return: controller
:rtype: Controller
"""
controller = Controller(event_queue, tts_queue, Session(), recognizer, config['recognition_timeout'], config['failed_message'], config['confirmation_messages'])
controller.start()
return controller

def initLogging(quiet, verbose, log_dir):
"""Initialize logging
:param boolean quiet: whether to log in console or not
:param boolean verbose: use DEBUG level for console logging
:param string log_dir: directory for log files
"""
root = logging.getLogger()
root.setLevel(logging.DEBUG)
handlers = []
if not os.path.exists(log_dir):
os.makedirs(log_dir)
handler = logging.handlers.RotatingFileHandler(os.path.join(log_dir, 'dobby.log'), maxBytes=2097152, backupCount=3, encoding='utf-8')
handler.setFormatter(logging.Formatter('%(asctime)s:%(levelname)s:%(name)s:%(message)s', datefmt='%m/%d/%Y %H:%M:%S'))
handlers.append(handler)
if not quiet:
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter('%(levelname)-8s:%(name)-32s:%(message)s'))
if verbose:
handler.setLevel(logging.DEBUG)
else:
handler.setLevel(logging.INFO)
handlers.append(handler)
root.handlers = handlers

def initConfig(path='config.ini'):
"""Initialize and validate the configuration file. If the validation test fails,
error messages are written to sys.stderr and we exit with status 1
:param string path: path to the configuration file
:return: the read configuration
:rtype: ConfigObj
"""
def is_option_list(value, *args):
"""Validator for a list of options"""
if not isinstance(value, list):
raise VdtTypeError(value)
for v in value:
if v not in args:
raise VdtValueError(v)
return value

config = ConfigObj(path, configspec='config.spec', encoding='utf-8')
vtor = Validator({'option_list': is_option_list})
results = config.validate(vtor, copy=True)
if results != True:
for (section_list, key, _) in flatten_errors(config, results):
if key is not None:
sys.stderr.write('The "%s" key in the section "%s" failed validation\n' % (key, ', '.join(section_list)))
else:
sys.stderr.write('The following section was missing: %s\n' % ', '.join(section_list))
sys.exit(1)
return config


if __name__ == '__main__':
# import pyaudio
# pa = pyaudio.PyAudio()
# for i in range(pa.get_device_count()):
# infos = pa.get_device_info_by_index(i)
# print infos['name'] + "\t" + str(infos['index'])
main()
6 changes: 1 addition & 5 deletions config.spec
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
[General]
welcome_message = string(default='I am ready to serve you, master.')
bye_message = string(default='Bye')
failed_message = string(default='I did not understand.')
confirmation_messages = string_list(default=list('Yes?', 'Yes, master?', 'What can I do for you?'))
recognition_timeout = integer(0, 60, default=5)

[Logging]
file = string(default='dobby.log')
max_bytes = integer(default=2097152)
backup_count = integer(0, 3, default=3)

[Speaker]
speaker = option('speechdispatcher', default='speechdispatcher')

Expand Down
Binary file removed dobby.db
Binary file not shown.
2 changes: 1 addition & 1 deletion dobby/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Dobby. If not, see <http://www.gnu.org/licenses/>.
# along with Dobby. If not, see <http://www.gnu.org/licenses/>.
Loading

0 comments on commit a26491c

Please sign in to comment.