First commit
This commit is contained in:
commit
1c20764e66
5 changed files with 845 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*.pyc
|
||||||
|
*~
|
323
conference.py
Executable file
323
conference.py
Executable file
|
@ -0,0 +1,323 @@
|
||||||
|
#!/usr/bin/env python2
|
||||||
|
# coding: utf8
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import re
|
||||||
|
from asterisk import agi
|
||||||
|
import logging
|
||||||
|
from optparse import OptionParser
|
||||||
|
from picotts import PicoTTS
|
||||||
|
from helpers import playback, enable_simulate_mode, get_var, set_var, hangup, check_answered
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
default_logfile = '/var/log/asterisk/conference.agi.log'
|
||||||
|
default_lang = 'fr-FR'
|
||||||
|
default_intkey = "any"
|
||||||
|
default_result_varname = "CONFID"
|
||||||
|
default_speed = 1.2
|
||||||
|
default_cachedir = '/var/cache/asterisk/picotts'
|
||||||
|
default_read_timeout = 3000
|
||||||
|
default_read_maxdigits = 20
|
||||||
|
default_read_maxtry = 3
|
||||||
|
|
||||||
|
#######
|
||||||
|
# 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('-t', '--read-timeout',
|
||||||
|
action="store", type="int",
|
||||||
|
dest="read_timeout", default=default_read_timeout,
|
||||||
|
help="Read timeout in ms (Default : %i)" % default_read_timeout)
|
||||||
|
|
||||||
|
parser.add_option('-m', '--read-max-digits',
|
||||||
|
action="store", type="int",
|
||||||
|
dest="read_maxdigits", default=default_read_maxdigits,
|
||||||
|
help="Read max digits (Default : %i)" % default_read_maxdigits)
|
||||||
|
|
||||||
|
parser.add_option('-T', '--read-max-try',
|
||||||
|
action="store", type="int",
|
||||||
|
dest="read_maxtry", default=default_read_maxtry,
|
||||||
|
help="Read max try (Default : %i)" % default_read_maxtry)
|
||||||
|
|
||||||
|
parser.add_option('--can-create',
|
||||||
|
action="store_true",
|
||||||
|
dest="can_create",
|
||||||
|
help="User can create a conference")
|
||||||
|
|
||||||
|
|
||||||
|
parser.add_option('-n', '--name',
|
||||||
|
action="store", type="string",
|
||||||
|
dest="varname", default=default_result_varname,
|
||||||
|
help="User input result variable name (Default : %s)" % default_result_varname)
|
||||||
|
|
||||||
|
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)")
|
||||||
|
|
||||||
|
(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 varname :\n{varname}'.format(
|
||||||
|
lang=options.lang, intkey=options.intkey,
|
||||||
|
speed=options.speed, varname=options.varname)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#############
|
||||||
|
# Functions #
|
||||||
|
#############
|
||||||
|
|
||||||
|
def check_confid(confid):
|
||||||
|
if re.match('^[0-9]{1,4}$', confid):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def set_return(result):
|
||||||
|
set_var(asterisk_agi, options.varname, result)
|
||||||
|
|
||||||
|
def clean_tmp():
|
||||||
|
if 'picotts' in globals():
|
||||||
|
global picotts
|
||||||
|
picotts.clean_tmp()
|
||||||
|
|
||||||
|
def play_msg(msg, read=False, max_digits=False):
|
||||||
|
global asterisk_agi, options
|
||||||
|
filepath = picotts.getAudioFile(msg)
|
||||||
|
|
||||||
|
if not max_digits:
|
||||||
|
max_digits = options.read_maxdigits
|
||||||
|
|
||||||
|
# Playback message
|
||||||
|
logging.debug('Play file %s' % filepath)
|
||||||
|
result = playback(asterisk_agi, filepath, simulate_play=options.simulate_play,
|
||||||
|
intkey=options.intkey, read=read,
|
||||||
|
read_timeout=options.read_timeout,
|
||||||
|
read_maxdigits=max_digits)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def play_msg_and_hangup(msg):
|
||||||
|
global asterisk_agi
|
||||||
|
play_msg(msg)
|
||||||
|
clean_tmp()
|
||||||
|
set_return(None)
|
||||||
|
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 call is answered
|
||||||
|
check_answered(asterisk_agi)
|
||||||
|
|
||||||
|
authorized = False
|
||||||
|
confid = None
|
||||||
|
start = True
|
||||||
|
while not authorized:
|
||||||
|
nb_confid_try =0
|
||||||
|
while not confid:
|
||||||
|
if nb_confid_try >= options.read_maxtry:
|
||||||
|
play_msg_and_hangup(u"Vous avez atteint le nombre maximum d'essai. Peut-être avez un problème avec votre clavier de téléphone ? Nous en sommes désolé. Au revoir.")
|
||||||
|
msg = u"Merci de saisir votre numéro de conférence en terminant par la touche dièse."
|
||||||
|
if start:
|
||||||
|
start = False
|
||||||
|
msg = u"Bonjour et bienvenue sur le service de conférence téléphonique. " + msg
|
||||||
|
confid = play_msg(msg, read=True)
|
||||||
|
|
||||||
|
if not check_confid(confid):
|
||||||
|
confid = None
|
||||||
|
play_msg(u"Ce numéro de conférence est invalide. Il doit comporté entre 1 et 4 chiffres.")
|
||||||
|
nb_confid_try += 1
|
||||||
|
logging.info('User choice conference %s' % confid)
|
||||||
|
|
||||||
|
# Check number of current calls in this conference
|
||||||
|
nb_calls = int(get_var(asterisk_agi, '${GROUP_COUNT(%s@conference)}' % confid))
|
||||||
|
logging.debug('Nb current calls in conference %s : %s' % (confid, nb_calls))
|
||||||
|
|
||||||
|
if nb_calls > 0:
|
||||||
|
logging.info('Conference %s already exist' % confid)
|
||||||
|
# Check PIN
|
||||||
|
pin = get_var(asterisk_agi, '${DB(conf/%s/pin)}' % confid)
|
||||||
|
logging.info('Current PIN of conference %s : "%s"' % (confid, pin))
|
||||||
|
if pin:
|
||||||
|
nb_pin_try = 0
|
||||||
|
while not authorized:
|
||||||
|
if nb_pin_try >= options.read_maxtry:
|
||||||
|
play_msg_and_hangup(u"Vous avez atteint le nombre maximum d'essai. Merci de vérifier le mot de passe d'accès à la conférence auprès de l'organisateur avant de rééssayer. Au revoir.")
|
||||||
|
|
||||||
|
if nb_pin_try == 0:
|
||||||
|
check_pin = play_msg(u"Cette conférence est protégé par un mot de passe. Merci de le saisir en terminant par la touche dièse.", read=True)
|
||||||
|
else:
|
||||||
|
check_pin = play_msg(u"Merci de saisir le mot de passe d'accès de la conférence en terminant par la touche dièse.", read=True)
|
||||||
|
|
||||||
|
if check_pin != pin:
|
||||||
|
play_msg(u"Le mot de passe saisi est invalide.")
|
||||||
|
nb_pin_try += 1
|
||||||
|
else:
|
||||||
|
authorized = True
|
||||||
|
else:
|
||||||
|
authorized = True
|
||||||
|
set_var(asterisk_agi, 'CONFBRIDGE(user,admin)', 'no')
|
||||||
|
set_var(asterisk_agi, 'CONF_CREATOR', 'no')
|
||||||
|
elif options.can_create:
|
||||||
|
logging.info('Conference %s does not exist.' % confid)
|
||||||
|
choice = play_msg(u"Cette conférence n'existe pas. Pour la créer, appuyer sur la touche 1, sinon, merci de patienter ou d'appuyer sur une autre touche pour saisir un autre numéro de conférence.", read=True, max_digits=1)
|
||||||
|
if choice != "1":
|
||||||
|
confid = None
|
||||||
|
continue
|
||||||
|
|
||||||
|
pin = None
|
||||||
|
nb_pin_try = 0
|
||||||
|
while not pin:
|
||||||
|
if nb_pin_try >= options.read_maxtry:
|
||||||
|
play_msg_and_hangup(u"Vous avez atteint le nombre maximum d'essai. Vous avez peut-être un problème avec le clavier de votre téléphone. Nous en sommes désolé. Au revoir")
|
||||||
|
|
||||||
|
if nb_pin_try == 0:
|
||||||
|
pin = play_msg(u"Si vous souhaitez protéger votre conférence par un mot de passe, merci de le saisir en terminant par la touche dièse. Sinon, merci de patienter ou d'appuyer sur le touche dièse.", read=True)
|
||||||
|
else:
|
||||||
|
pin = play_msg(u"Merci de saisir un nouveau mot de passe en terminant par la touche dièse. Si vous ne souhaitez finalement pas protéger votre conférence, merci de patienter ou d'appuyer sur la touche dièse.", read=True)
|
||||||
|
|
||||||
|
if pin:
|
||||||
|
verif_pin = play_msg(u"Merci de confirmer le mot de passe de votre conférence en terminant par la touche dièse.", read=True)
|
||||||
|
if verif_pin != pin:
|
||||||
|
play_msg(u"Les mots de passe saisies ne correspondent pas.")
|
||||||
|
nb_pin_try += 1
|
||||||
|
pin = None
|
||||||
|
continue
|
||||||
|
logging.info('Conference %s created with PIN "%s"' % (confid, pin))
|
||||||
|
set_var(asterisk_agi, 'DB(conf/%s/pin)' % confid, pin)
|
||||||
|
else:
|
||||||
|
logging.info('Conference %s created without PIN' % confid)
|
||||||
|
set_var(asterisk_agi, 'DB(conf/%s/pin)' % confid, "")
|
||||||
|
break
|
||||||
|
|
||||||
|
play_msg(u"Votre conférence a été créé. Vous pouvez désormais communiquer son numéro à vos invités, à savoir, le numéro %s." % confid)
|
||||||
|
if pin:
|
||||||
|
play_msg(u"N'oubliez pas de leur communiquer également le mot de passe d'accès.")
|
||||||
|
set_var(asterisk_agi, 'CONFBRIDGE(user,admin)', 'yes')
|
||||||
|
set_var(asterisk_agi, 'CONF_CREATOR', 'yes')
|
||||||
|
authorized = True
|
||||||
|
else:
|
||||||
|
logging.info('Conference %s does not exist and user can not create it.' % confid)
|
||||||
|
choice = play_msg(u"La conférence numéro %s n'existe pas ou n'a pas encore commencé. Si vous pensez avoir fait une erreur, appuyer sur la touche 1 pour saisir un autre numéro de conférence. Sinon, merci de raccrocher, de vérifier le numéro de votre conférence et de rééssayer ultèrieurement." % confid, read=True, max_digits=1)
|
||||||
|
if choice == "1":
|
||||||
|
confid = None
|
||||||
|
continue
|
||||||
|
play_msg_and_hangup(u"Au revoir.")
|
||||||
|
|
||||||
|
play_msg(u"Vous allez maintenant entrer en conférence. Vous pourrez accéder au menu en appuyant sur la touche étoile.")
|
||||||
|
|
||||||
|
set_var(asterisk_agi, "GROUP(conference)", confid)
|
||||||
|
set_return(confid)
|
||||||
|
clean_tmp()
|
||||||
|
sys.exit(0)
|
||||||
|
except agi.AGIAppError as e:
|
||||||
|
logging.info('An AGI error stop script : %s' % e)
|
||||||
|
clean_tmp()
|
||||||
|
sys.exit(0)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(traceback.format_exc())
|
||||||
|
set_return(None)
|
||||||
|
clean_tmp()
|
||||||
|
sys.exit(1)
|
25
extensions-conference.conf
Normal file
25
extensions-conference.conf
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
[conference]
|
||||||
|
exten => s,1,GotoIf($[${CONF_EXT_CALL} == "1"]?extcall)
|
||||||
|
exten => s,n,AGI(/home/bn8/dev/astpicotts/conference.py,--cache,-d,--can-create)
|
||||||
|
exten => s,n,Goto(enterconf)
|
||||||
|
|
||||||
|
exten => s,n(extcall),AGI(/home/bn8/dev/astpicotts/conference.py,--cache)
|
||||||
|
exten => s,n,Goto(enterconf)
|
||||||
|
|
||||||
|
exten => s,n(enterconf),Verbose(Conf ID : ${CONFID})
|
||||||
|
exten => s,n,ExecIf($[${CONFID} == "None"]?Hangup())
|
||||||
|
exten => s,n,Goto(${CONFID},1)
|
||||||
|
|
||||||
|
exten => _.,1,Verbose(User is creator : ${CONF_CREATOR})
|
||||||
|
exten => _.,n,Set(CONFBRIDGE(user,marked)=no)
|
||||||
|
exten => _.,n,Set(CONFBRIDGE(user,announce_join_leave)=yes)
|
||||||
|
exten => _.,n,Set(CONFBRIDGE(user,music_on_hold_when_empty)=yes)
|
||||||
|
exten => _.,n,Set(CONFBRIDGE(user,music_on_hold_class)=default)
|
||||||
|
exten => _.,n,Set(CONFBRIDGE(user,end_marked)=yes)
|
||||||
|
exten => _.,n,ConfBridge(${CONFID})
|
||||||
|
|
||||||
|
|
||||||
|
; Macro(ext2conf)
|
||||||
|
[macro-ext2conf]
|
||||||
|
exten => s,1,Set(CONF_EXT_CALL=1)
|
||||||
|
exten => s,n,Macro(conference)
|
113
helpers.py
Normal file
113
helpers.py
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
#!/usr/bin/env python2
|
||||||
|
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
def get_path(soft):
|
||||||
|
try:
|
||||||
|
path = subprocess.check_output(['which', soft]).strip()
|
||||||
|
return path
|
||||||
|
except subprocess.CalledProcessError, e:
|
||||||
|
logging.fatal('%s not found' % soft)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_sox_ver(sox_path=None):
|
||||||
|
global sox_ver
|
||||||
|
if 'sox_ver' not in globals() or not sox_ver:
|
||||||
|
if not sox_path:
|
||||||
|
sox_path = get_path('sox')
|
||||||
|
try:
|
||||||
|
result = subprocess.check_output([sox_path, '--version'])
|
||||||
|
logging.debug('Sox accept --version parameter : consider as version 14')
|
||||||
|
sox_ver = 14
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
logging.debug('Sox not accept --version parameter : consider as version 12')
|
||||||
|
sox_ver = 12
|
||||||
|
return sox_ver
|
||||||
|
|
||||||
|
def enable_simulate_mode():
|
||||||
|
global SIMULATE_MODE
|
||||||
|
SIMULATE_MODE = True
|
||||||
|
|
||||||
|
def check_simulate_mode():
|
||||||
|
return 'SIMULATE_MODE' in globals() and SIMULATE_MODE
|
||||||
|
|
||||||
|
def get_var(asterisk_agi, varname):
|
||||||
|
if check_simulate_mode():
|
||||||
|
result = raw_input('Simulate mode : please enter "%s" variable value =>> ' % varname)
|
||||||
|
else:
|
||||||
|
result = asterisk_agi.get_full_variable(varname)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def set_var(asterisk_agi, varname, value):
|
||||||
|
if check_simulate_mode():
|
||||||
|
print('Simulate mode : set variable %s to "%s"' % (varname, value))
|
||||||
|
else:
|
||||||
|
logging.info('Set variable %s to "%s"' % (varname, value))
|
||||||
|
asterisk_agi.set_variable(varname, value)
|
||||||
|
|
||||||
|
def detect_format(asterisk_agi):
|
||||||
|
if check_simulate_mode():
|
||||||
|
return ("sln", 8000)
|
||||||
|
nativeformat = asterisk_agi.get_full_variable('${CHANNEL(audionativeformat)}')
|
||||||
|
logging.debug('Native audio format : %s' % nativeformat)
|
||||||
|
if re.match('(silk|sln)12', nativeformat):
|
||||||
|
return ("sln12", 12000)
|
||||||
|
elif re.match('(speex|slin|silk)16|g722|siren7', nativeformat):
|
||||||
|
return ("sln16", 16000)
|
||||||
|
elif re.match('(speex|slin|celt)32|siren14', nativeformat):
|
||||||
|
return ("sln32", 32000)
|
||||||
|
elif re.match('(celt|slin)44', nativeformat):
|
||||||
|
return ("sln44", 44100)
|
||||||
|
elif re.match('(celt|slin)48', nativeformat):
|
||||||
|
return ("sln48", 48000)
|
||||||
|
else:
|
||||||
|
return ("sln", 8000)
|
||||||
|
|
||||||
|
def remove_ext(filepath):
|
||||||
|
return re.sub('\.[a-zA-Z0-9]+$','',filepath)
|
||||||
|
|
||||||
|
any_intkeys = "0123456789#*"
|
||||||
|
def playback(asterisk_agi, filepath, simulate_play=False, read=False, read_timeout=3000, read_maxdigits=20, intkey=None):
|
||||||
|
if check_simulate_mode():
|
||||||
|
logging.debug('Simulate mode : Play file %s' % filepath)
|
||||||
|
try:
|
||||||
|
mplayer_path = get_path('mplayer')
|
||||||
|
subprocess.check_output([mplayer_path, filepath])
|
||||||
|
except Exception, e:
|
||||||
|
logging.warning('Fail to play %s file : %s' % (filepath, e))
|
||||||
|
if read:
|
||||||
|
result = raw_input('=>> ')
|
||||||
|
else:
|
||||||
|
# Simulate empty result
|
||||||
|
result = ''
|
||||||
|
else:
|
||||||
|
play_filepath = remove_ext(filepath)
|
||||||
|
logging.debug('Asterisk play file path : %s' % play_filepath)
|
||||||
|
if read:
|
||||||
|
result = asterisk_agi.get_data(play_filepath, timeout=read_timeout, max_digits=read_maxdigits)
|
||||||
|
else:
|
||||||
|
if intkey == "any":
|
||||||
|
global any_intkeys
|
||||||
|
intkey = any_intkeys
|
||||||
|
result = asterisk_agi.stream_file(play_filepath, escape_digits=intkey)
|
||||||
|
logging.debug('User enter "%s"' % result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def check_answered(asterisk_agi):
|
||||||
|
if check_simulate_mode():
|
||||||
|
print('Simulate mode : Channel answered')
|
||||||
|
else:
|
||||||
|
status = asterisk_agi.channel_status()
|
||||||
|
if status == 4:
|
||||||
|
logging.debug('Call is riging. Answer it and wait one second.')
|
||||||
|
asterisk_agi.answer()
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
def hangup(asterisk_agi):
|
||||||
|
if check_simulate_mode():
|
||||||
|
print('Simulate mode : Hangup')
|
||||||
|
else:
|
||||||
|
asterisk_agi.hangup()
|
382
picotts.py
Executable file
382
picotts.py
Executable file
|
@ -0,0 +1,382 @@
|
||||||
|
#!/usr/bin/env python2
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import re
|
||||||
|
from asterisk import agi
|
||||||
|
import subprocess
|
||||||
|
import logging
|
||||||
|
import hashlib
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from helpers import get_path, get_sox_ver, detect_format, check_simulate_mode
|
||||||
|
|
||||||
|
class PicoTTS:
|
||||||
|
|
||||||
|
cache = False
|
||||||
|
cachedir = None
|
||||||
|
cache_prefix = 'picotts_'
|
||||||
|
|
||||||
|
tmp_filepaths = []
|
||||||
|
|
||||||
|
def __init__(self, lang=u'fr-FR', tmpdir='/tmp', pico2wave_path=None, sox_path=None, samplerate=None, speed=1, cachedir=None, asterisk_agi=None):
|
||||||
|
if not pico2wave_path:
|
||||||
|
pico2wave_path=get_path('pico2wave')
|
||||||
|
if not pico2wave_path:
|
||||||
|
raise Exception('pico2wave not found')
|
||||||
|
self.pico2wave_path = pico2wave_path
|
||||||
|
if not sox_path:
|
||||||
|
sox_path=get_path('sox')
|
||||||
|
if not sox_path:
|
||||||
|
raise Exception('sox not found')
|
||||||
|
self.sox_path = sox_path
|
||||||
|
|
||||||
|
self.asterisk_agi = asterisk_agi
|
||||||
|
if not samplerate:
|
||||||
|
if not asterisk_agi and not check_simulate_mode():
|
||||||
|
logging.warning('You must provide samplerate or asterisk_agi parameter to correctly handle sample rate.')
|
||||||
|
(self.samplerate, self.fext) = (8000, 'sln')
|
||||||
|
else:
|
||||||
|
(self.fext, self.samplerate) = detect_format(asterisk_agi)
|
||||||
|
else:
|
||||||
|
self.samplerate = samplerate
|
||||||
|
self.fext = self.samplerate2fext(samplerate)
|
||||||
|
|
||||||
|
self.speed = speed
|
||||||
|
|
||||||
|
if cachedir:
|
||||||
|
self.cachedir = cachedir
|
||||||
|
self.check_or_create_cachedir()
|
||||||
|
|
||||||
|
|
||||||
|
self.tmpdir = tmpdir
|
||||||
|
self.lang = lang
|
||||||
|
|
||||||
|
|
||||||
|
def samplerate2fext(self, samplerate):
|
||||||
|
# Check/detect sample-rate and final output format
|
||||||
|
if samplerate == 8000:
|
||||||
|
return "sln"
|
||||||
|
elif samplerate == 12000:
|
||||||
|
return "sln12"
|
||||||
|
elif samplerate == 16000:
|
||||||
|
return "sln16"
|
||||||
|
elif samplerate == 32000:
|
||||||
|
return "sln32"
|
||||||
|
elif samplerate == 44100:
|
||||||
|
return "sln44"
|
||||||
|
elif samplerate == 48000:
|
||||||
|
return "sln48"
|
||||||
|
else:
|
||||||
|
raise Exception('Invalid sample rate value')
|
||||||
|
|
||||||
|
|
||||||
|
def check_or_create_cachedir(self):
|
||||||
|
self.cache = True
|
||||||
|
if not os.path.exists(self.cachedir):
|
||||||
|
try:
|
||||||
|
logging.info('Create cache directory %s' % self.cachedir)
|
||||||
|
os.mkdir(self.cachedir)
|
||||||
|
except Exception, e:
|
||||||
|
logging.warning("Fail to create cache directory (%s) : %s" % (self.cachedir, e))
|
||||||
|
logging.info('Disable cache')
|
||||||
|
self.cache = False
|
||||||
|
else:
|
||||||
|
if not os.path.isdir(self.cachedir):
|
||||||
|
logging.warning("Cache directory %s is not a directory : disable cache" % self.cachedir)
|
||||||
|
self.cache = False
|
||||||
|
elif not os.access(self.cachedir, os.W_OK):
|
||||||
|
logging.warning("Cache directory %s is not writable : disable cache" % self.cachedir)
|
||||||
|
self.cache = False
|
||||||
|
else:
|
||||||
|
logging.debug("Cache directory %s already exists" % self.cachedir)
|
||||||
|
if self.cache:
|
||||||
|
logging.debug('Cache is enabled')
|
||||||
|
else:
|
||||||
|
logging.debug('Cache is disabled')
|
||||||
|
|
||||||
|
return self.cache
|
||||||
|
|
||||||
|
def _getCachePath(self, text, lang=None):
|
||||||
|
md5sum=hashlib.md5()
|
||||||
|
if isinstance(text, str):
|
||||||
|
text = text.decode('utf-8', 'ignore')
|
||||||
|
cache_key = u'%s--%s--%s' % (text, (lang or self.lang), self.speed)
|
||||||
|
logging.debug('Cache key : "%s"' % cache_key)
|
||||||
|
md5sum.update(cache_key.encode('utf-8'))
|
||||||
|
cache_md5key = md5sum.hexdigest()
|
||||||
|
logging.debug('Cache MD5 key : "%s"' % cache_md5key)
|
||||||
|
cache_filename=self.cache_prefix + cache_md5key
|
||||||
|
logging.debug('Cache filename : %s' % cache_filename)
|
||||||
|
cache_filepath = os.path.join(self.cachedir, cache_filename)
|
||||||
|
logging.debug('Cache filepath : %s' % cache_filepath)
|
||||||
|
return cache_filepath
|
||||||
|
|
||||||
|
def _getAudioFileFromCache(self, text, lang=None):
|
||||||
|
if not self.cache:
|
||||||
|
return False
|
||||||
|
cache_filepath = self._getCachePath(text, lang=lang) + "." + self.fext
|
||||||
|
if os.path.isfile(cache_filepath):
|
||||||
|
logging.debug('File already exists in cache. Use it')
|
||||||
|
return cache_filepath
|
||||||
|
logging.debug('File does not exists in cache.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def getAudioFile(self, text, lang=None):
|
||||||
|
if self.cache:
|
||||||
|
cache_filepath = self._getAudioFileFromCache(text, lang=lang)
|
||||||
|
if cache_filepath:
|
||||||
|
return cache_filepath
|
||||||
|
|
||||||
|
# Create temp files
|
||||||
|
logging.debug('Temporary directory : %s' % self.tmpdir)
|
||||||
|
tmpfile = os.path.join(self.tmpdir, 'picotts_%s' % str(uuid.uuid4()))
|
||||||
|
tmpwavfile = tmpfile + ".wav"
|
||||||
|
logging.debug('Temporary wav file : %s' % tmpwavfile)
|
||||||
|
tmpoutfile = tmpfile + "." + self.fext
|
||||||
|
logging.debug('Temporary out file : %s' % tmpoutfile)
|
||||||
|
|
||||||
|
# Convert text to autio wav file using pico2wave
|
||||||
|
cmd = [ self.pico2wave_path, '-l', (lang or self.lang), '-w', tmpwavfile, text ]
|
||||||
|
|
||||||
|
try:
|
||||||
|
logging.debug('Run command : %s' % cmd)
|
||||||
|
result = subprocess.check_output(cmd)
|
||||||
|
logging.debug('Command return : %s' % result)
|
||||||
|
except subprocess.CalledProcessError, e:
|
||||||
|
raise Exception('Fail to convert text to audio file using pico2wave : %s' % e)
|
||||||
|
|
||||||
|
# Convert wav file to final output format using sox
|
||||||
|
cmd = [ self.sox_path, tmpwavfile, "-q", "-r", str(self.samplerate), "-t", "raw", tmpoutfile ]
|
||||||
|
|
||||||
|
# Handle speed change
|
||||||
|
if self.speed != 1:
|
||||||
|
if get_sox_ver(sox_path=self.sox_path) >= 14:
|
||||||
|
cmd += ['tempo', '-s', str(self.speed)]
|
||||||
|
else:
|
||||||
|
cmd += ['stretch', str(1/self.speed), "80"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
logging.debug('Run command : %s' % cmd)
|
||||||
|
result = subprocess.check_output(cmd)
|
||||||
|
logging.debug('Command return : %s' % result)
|
||||||
|
except subprocess.CalledProcessError, e:
|
||||||
|
logging.fatal('Fail to convert text to audio file using pico2wave : %s' % e)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
logging.debug('Remove tmp wav file')
|
||||||
|
os.remove(tmpwavfile)
|
||||||
|
except Exception, e:
|
||||||
|
logging.warning('Fail to remove temporary WAV audio file')
|
||||||
|
|
||||||
|
# Move audio file in cache
|
||||||
|
if self.cache:
|
||||||
|
cache_filepath = self._getCachePath(text, lang=lang) + "." + self.fext
|
||||||
|
try:
|
||||||
|
logging.debug('Move audio file in cache directory')
|
||||||
|
shutil.move(tmpoutfile, cache_filepath)
|
||||||
|
except Exception,e:
|
||||||
|
logging.warning('Fail to move audio file in cache directory : %s' % e)
|
||||||
|
return cache_filepath
|
||||||
|
else:
|
||||||
|
self.tmp_filepaths.append(tmpoutfile)
|
||||||
|
logging.debug('Cache disabled. Directly play tmp file.')
|
||||||
|
return tmpoutfile
|
||||||
|
|
||||||
|
def clean_tmp(self):
|
||||||
|
try:
|
||||||
|
logging.debug('Clean temporaries files and directory')
|
||||||
|
for filepath in self.tmp_filepaths:
|
||||||
|
if os.path.exists(filepath):
|
||||||
|
os.remove(filepath)
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning('Fail to remove temporaries files and directory : %s' % e)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from optparse import OptionParser
|
||||||
|
from helpers import playback, enable_simulate_mode
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
default_logfile = '/var/log/asterisk/picotts.log'
|
||||||
|
default_lang = 'fr-FR'
|
||||||
|
default_intkey = ""
|
||||||
|
default_result_varname = "USER_INPUT"
|
||||||
|
default_speed = 1
|
||||||
|
default_cachedir = '/tmp/'
|
||||||
|
default_read_timeout = 3000
|
||||||
|
default_read_maxdigits = 20
|
||||||
|
|
||||||
|
any_intkeys = "0123456789#*"
|
||||||
|
|
||||||
|
#######
|
||||||
|
# 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('-r', '--read',
|
||||||
|
action="store_true",
|
||||||
|
dest="read",
|
||||||
|
help="Enable read mode")
|
||||||
|
|
||||||
|
parser.add_option('-t', '--read-timeout',
|
||||||
|
action="store", type="int",
|
||||||
|
dest="read_timeout", default=default_read_timeout,
|
||||||
|
help="Read timeout in ms (Default : %i)" % default_read_timeout)
|
||||||
|
|
||||||
|
parser.add_option('-m', '--read-max-digits',
|
||||||
|
action="store", type="int",
|
||||||
|
dest="read_maxdigits", default=default_read_maxdigits,
|
||||||
|
help="Read max digits (Default : %i)" % default_read_maxdigits)
|
||||||
|
|
||||||
|
parser.add_option('-n', '--name',
|
||||||
|
action="store", type="string",
|
||||||
|
dest="varname", default=default_result_varname,
|
||||||
|
help="User input result variable name (Default : %s)" % default_result_varname)
|
||||||
|
|
||||||
|
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 : No)")
|
||||||
|
|
||||||
|
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)")
|
||||||
|
|
||||||
|
(options, args) = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
text=" ".join(args).strip()
|
||||||
|
if len(text) == 0:
|
||||||
|
usage(msg="You provide text to say as first parameter.")
|
||||||
|
|
||||||
|
# Valid intkey parameter
|
||||||
|
if options.intkey == "any":
|
||||||
|
options.intkey = any_intkeys
|
||||||
|
elif not re.match('^[0-9#*]*$', options.intkey):
|
||||||
|
logging.warning('Invalid interrupt key(s) provided ("%s"), use any.' % options.intkey)
|
||||||
|
options.intkey = any_intkeys
|
||||||
|
|
||||||
|
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 text :\n{text}'.format(
|
||||||
|
lang=options.lang, intkey=options.intkey,
|
||||||
|
speed=options.speed, text=text)
|
||||||
|
)
|
||||||
|
|
||||||
|
picotts_args = {}
|
||||||
|
# Start Asterisk AGI client
|
||||||
|
if not options.simulate:
|
||||||
|
a = agi.AGI()
|
||||||
|
picotts_args['asterisk_agi'] = a
|
||||||
|
else:
|
||||||
|
a = None
|
||||||
|
|
||||||
|
if options.cache:
|
||||||
|
picotts_args['cachedir']=options.cachedir
|
||||||
|
|
||||||
|
picotts = PicoTTS(lang = options.lang, speed=options.speed, **picotts_args)
|
||||||
|
|
||||||
|
filepath = picotts.getAudioFile(text)
|
||||||
|
|
||||||
|
# Playback message
|
||||||
|
result = playback(a, filepath, simulate_play=options.simulate_play,
|
||||||
|
intkey=options.intkey, read=options.read,
|
||||||
|
read_timeout=options.read_timeout,
|
||||||
|
read_maxdigits=options.read_maxdigits)
|
||||||
|
|
||||||
|
logging.debug('User enter : "%s"' % result)
|
||||||
|
if options.simulate:
|
||||||
|
logging.debug('Simulate mode : set variable %s to "%s"' % (options.varname, result))
|
||||||
|
else:
|
||||||
|
logging.info('Set variable %s to "%s"' % (options.varname, result))
|
||||||
|
a.set_variable(options.varname, result)
|
||||||
|
|
||||||
|
picotts.clean_tmp()
|
||||||
|
except agi.AGIAppError as e:
|
||||||
|
logging.info('An AGI error stop script : %s' % e)
|
||||||
|
if 'picotts' in globals():
|
||||||
|
picotts.clean_tmp()
|
||||||
|
sys.exit(0)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(traceback.format_exc())
|
||||||
|
if 'picotts' in globals():
|
||||||
|
picotts.clean_tmp()
|
||||||
|
sys.exit(1)
|
||||||
|
sys.exit(0)
|
Loading…
Reference in a new issue