astpicotts/picotts.py
2018-01-22 01:39:24 +01:00

382 lines
11 KiB
Python
Executable file

#!/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)