diff --git a/helpers.py b/helpers.py index 8a7e2b9..674a66c 100644 --- a/helpers.py +++ b/helpers.py @@ -96,6 +96,12 @@ def playback(asterisk_agi, filepath, simulate_play=False, read=False, read_timeo logging.debug('User enter "%s"' % result) return result +def beep(asterisk_agi): + if check_simulate_mode(): + logging.debug('Beep') + else: + playback(asterisk_agi, 'beep') + def check_answered(asterisk_agi): if check_simulate_mode(): print('Simulate mode : Channel answered') @@ -111,3 +117,19 @@ def hangup(asterisk_agi): print('Simulate mode : Hangup') else: asterisk_agi.hangup() + +def record_file(asterisk_agi, filepath, fileformat='wav', escape_digits='#', max_duration=-1, simulate_record=False, simulate_record_duration=5): + if check_simulate_mode(): + if not simulate_record: + raw_input('Simulate mode : record %s file "%s" until you press on touch [enter]' % (fileformat, filepath)) + else: + assert fileformat == 'wav', "Only wav file format in support in simulate record mode" + logging.debug('Simulate mode : Record file %s', filepath) + raw_input('The record will start after you press [entre] key. In this mode, the record will stop after %d seconds.' % simulate_record_duration) + try: + arecord_path = get_path('arecord') + subprocess.check_output([arecord_path, '-vv', '-fdat', '-d %d' % simulate_record_duration, filepath]) + except Exception, e: + logging.warning('Fail to record %s file', filepath, exc_info=True) + else: + return asterisk_agi.record_file(filepath, format=fileformat, escape_digits=escape_digits, timeout=max_duration) diff --git a/record_message.py b/record_message.py new file mode 100755 index 0000000..b3ece26 --- /dev/null +++ b/record_message.py @@ -0,0 +1,273 @@ +#!/usr/bin/env python2 +# coding: utf8 + +import datetime +import logging +import os +import re +import sys +import tempfile +import traceback + +from asterisk import agi +from optparse import OptionParser +from picotts import PicoTTS + +from helpers import playback, enable_simulate_mode, get_var, set_var, hangup, check_answered, beep, record_file + +default_logfile = '/var/log/asterisk/record_message.agi.log' +default_lang = 'fr-FR' +default_intkey = "any" +default_speed = 1.1 +default_cachedir = '/var/cache/asterisk/picotts' +default_outputdir = '/var/lib/asterisk/sounds/records/' +default_fileformat = 'wav' +default_nameformat = '{datetime}-{CALLERID(num)}.{fileformat}' + +####### +# RUN # +####### + +# Options parser +parser = OptionParser() + +parser.add_option('-d', '--debug', + action="store_true", + dest="debug", + help="Enable debug mode") + +parser.add_option('-v', '--verbose', + action="store_true", + dest="verbose", + help="Enable verbose mode") + +parser.add_option('--simulate', + action="store_true", + dest="simulate", + help="Simulate AGI mode") + +parser.add_option('--simulate-play', + action="store_true", + dest="simulate_play", + help="Simulate mode : play file using mplayer") + +parser.add_option('--simulate-record', + action="store_true", + dest="simulate_record", + help="Simulate mode : record file using arecord") + +parser.add_option('-L', '--log-file', + action="store", type="string", + dest="logfile", default=default_logfile, + help="pico2wave path (Default : %s)" % default_logfile) + +parser.add_option('-l', '--lang', + action="store", type="string", + dest="lang", default=default_lang, + help="Language (Default : %s)" % default_lang) + +parser.add_option('-i', '--intkey', + action="store", type="string", + dest="intkey", default=default_intkey, + help="Interrupt key(s) (Default : Any)") + +parser.add_option('-s', '--speed', + action="store", type="float", + dest="speed", default=default_speed, + help="Speed factor (Default : %i)" % default_speed) + +parser.add_option('-S', '--sample-rate', + action="store", type="int", + dest="samplerate", + help="Sample rate (Default : auto-detect)") + +parser.add_option('-c', '--cache', + action="store_true", + dest="cache", + help="Enable cache") + +parser.add_option('-C', '--cache-dir', + action="store", type="string", + dest="cachedir", default=default_cachedir, + help="Cache directory path (Default : %s)" % default_cachedir) + +parser.add_option('--sox-path', + action="store", type="string", + dest="sox_path", + help="sox path (Default : auto-detec in PATH)") + +parser.add_option('--pico2wave-path', + action="store", type="string", + dest="pico2wave_path", + help="pico2wave path (Default : auto-detec in PATH)") + +parser.add_option('-f', '--format', + action="store", type="string", + dest="fileformat", default=default_fileformat, + help="Output file format (Default : %s)" % default_fileformat) + +parser.add_option('-O', '--output-dir', + action="store", type="string", + dest="outputdir", default=default_outputdir, + help="Output directory path (Default : %s)" % default_outputdir) + +parser.add_option('-n', '--name-format', + action="store", type="string", + dest="nameformat", default=default_nameformat, + help="Record file name format composed using channel variables and specials 'date' and 'datetime' and 'fileformat' variables (Default : %s)" % default_outputdir) + +parser.add_option('-m', '--max-duration', + action="store", type="int", + dest="maxduration", default=-1, + help="Max duration (in seconds, Default : no limit)") + +(options, args) = parser.parse_args() + +# Enable logs +logformat = '%(levelname)s - %(message)s' +if options.simulate: + logging.basicConfig(format=logformat, level=logging.DEBUG) + enable_simulate_mode() +else: + if options.debug: + loglevel = logging.DEBUG + elif options.verbose: + loglevel = logging.INFO + else: + loglevel = logging.WARNING + logging.basicConfig(filename=options.logfile, level=loglevel, + format=logformat) + +# Valid intkey parameter +if options.intkey != "any" and not re.match('^[0-9#*]*$', options.intkey): + logging.warning('Invalid interrupt key(s) provided ("%s"), use any.' % options.intkey) + options.intkey = "any" + +if options.speed <= 0: + logging.warning('Invalid speed provided, use default') + options.speed=default_speed + +logging.debug('Call parameters (lang = {lang}, intkey = {intkey}, speed = {speed} and output dir/format :\n{outputdir}/{nameformat}'.format( + lang=options.lang, intkey=options.intkey, + speed=options.speed, outputdir=options.outputdir, + nameformat=options.nameformat) +) + + +############# +# Functions # +############# + +def clean_tmp(): + if 'picotts' in globals(): + global picotts + picotts.clean_tmp() + global asterisk_agi + hangup(asterisk_agi) + +def play_msg(msg): + global asterisk_agi, options + filepath = picotts.getAudioFile(msg) + + # Playback message + logging.debug('Play file %s' % filepath) + playback(asterisk_agi, filepath, simulate_play=options.simulate_play, + intkey=options.intkey) + +def play_file(filepath): + global asterisk_agi, options + logging.debug('Play %s file', filepath) + playback(asterisk_agi, filepath, simulate_play=options.simulate_play, + intkey=options.intkey) + +def play_msg_and_hangup(msg=None): + global asterisk_agi + if not msg: + msg = u"Une erreur empêche ce service de fonctionner correctement. Si le problème persiste, merci de prendre contact avec le support." + play_msg(msg) + clean_tmp() + hangup(asterisk_agi) + sys.exit(0) + +try: + picotts_args = {} + # Start Asterisk AGI client + if not options.simulate: + asterisk_agi = agi.AGI() + picotts_args['asterisk_agi'] = asterisk_agi + else: + asterisk_agi = None + + if options.cache: + picotts_args['cachedir']=options.cachedir + + # Start PicoTTS engine + picotts = PicoTTS(lang = options.lang, speed=options.speed, **picotts_args) + + # Check output directory + if not os.path.isdir(options.outputdir): + logging.error("Ouput directory '%s' doesn't exists !", options.outputdir) + play_msg_and_hangup() + + # Compose output file name and path + variables = {} + for var in re.findall('\{([^\}]+)\}', options.nameformat): + if var == 'date': + variables['date'] = datetime.datetime.now().strftime('%Y%m%d') + elif var == 'datetime': + variables['datetime'] = datetime.datetime.now().strftime('%Y%m%d-%H%M%S') + elif var == 'fileformat': + variables['fileformat'] = options.fileformat + else: + variables[var] = get_var(asterisk_agi, var) + filename = options.nameformat.format(**variables) + logging.debug(u"Output file name : '%s'", filename) + + filepath = "{0}/{1}".format(options.outputdir, filename) + logging.debug(u"Output file path : '%s'", filename) + + # Check destination file does not already exist + if os.path.exists(filepath): + logging.warning("Output file '%s' already exists !", filepath) + play_msg_and_hangup() + + # Say hello :) + play_msg(u"Bonjour et bienvenue sur le service d'enregistrement de messages") + + # Check call is answered + check_answered(asterisk_agi) + + # Intro + play_msg(u"Vous allez pouvoir enregistrer un message après le bip sonore. Une fois votre message enregistré, appuyer sur la touche dièse pour terminer l'enregistrement.") + + beep(asterisk_agi) + + # Record file + record_file(asterisk_agi, filepath, options.fileformat, escape_digits='#', max_duration=options.maxduration, simulate_record=options.simulate_record) + + beep(asterisk_agi) + + # Check destination file now exist + if not os.path.exists(filepath): + logging.warning("Output file '%s' does not exists after the record !", filepath) + play_msg_and_hangup(u"Une erreur est survenue durant l'enregistrement de votre message. Si le problème persiste, merci de prendre contact avec le support.") + + # Replay message to the caller + play_msg(u"Voilà le message que vous avez enregistré :") + + beep(asterisk_agi) + + play_file(filepath) + + beep(asterisk_agi) + + play_msg_and_hangup(u"Au revoir") + sys.exit(0) +except agi.AGIAppError: + logging.info('An AGI error stop script', exc_info=True) + play_msg_and_hangup() +except Exception: + logging.error('Unexcepted error occured', exc_info=True) + play_msg_and_hangup() + sys.exit(1) +