Issues 356 - Refactoring TTS

pull/420/head
Jonathan D'Orleans 2016-09-16 14:12:43 -04:00
parent 885fe0a1cf
commit e76c1b7d21
8 changed files with 114 additions and 132 deletions

View File

@ -16,18 +16,10 @@
# along with Mycroft Core. If not, see <http://www.gnu.org/licenses/>. # along with Mycroft Core. If not, see <http://www.gnu.org/licenses/>.
import logging from abc import ABCMeta, abstractmethod
import abc
from os.path import dirname, exists, isdir from os.path import dirname, exists, isdir
from mycroft.configuration import ConfigurationManager from mycroft.configuration import ConfigurationManager
from mycroft.tts import espeak_tts
from mycroft.tts import fa_tts
from mycroft.tts import google_tts
from mycroft.tts import mary_tts
from mycroft.tts import mimic_tts
from mycroft.tts import spdsay_tts
from mycroft.util.log import getLogger from mycroft.util.log import getLogger
__author__ = 'jdorleans' __author__ = 'jdorleans'
@ -42,15 +34,17 @@ class TTS(object):
It aggregates the minimum required parameters and exposes It aggregates the minimum required parameters and exposes
``execute(sentence)`` function. ``execute(sentence)`` function.
""" """
__metaclass__ = ABCMeta
def __init__(self, lang, voice, filename='/tmp/tts.wav'): def __init__(self, lang, voice, validator):
super(TTS, self).__init__() super(TTS, self).__init__()
self.lang = lang self.lang = lang
self.voice = voice self.voice = voice
self.filename = filename self.filename = '/tmp/tts.wav'
self.validator = validator
@abc.abstractmethod @abstractmethod
def execute(self, sentence, client): def execute(self, sentence):
pass pass
@ -61,48 +55,61 @@ class TTSValidator(object):
It exposes and implements ``validate(tts)`` function as a template to It exposes and implements ``validate(tts)`` function as a template to
validate the TTS engines. validate the TTS engines.
""" """
__metaclass__ = ABCMeta
def __init__(self): def __init__(self, tts):
pass self.tts = tts
def validate(self, tts): def validate(self):
self.__validate_instance(tts) self.validate_instance()
self.__validate_filename(tts.filename) self.validate_filename()
self.validate_lang(tts.lang) self.validate_lang()
self.validate_connection(tts) self.validate_connection()
def __validate_instance(self, tts): def validate_instance(self):
instance = self.get_instance() clazz = self.get_tts_class()
if not isinstance(tts, instance): if not isinstance(self.tts, clazz):
raise AttributeError( raise AttributeError('tts must be instance of ' + clazz.__name__)
'tts must be instance of ' + instance.__name__)
LOGGER.debug('TTS: ' + str(instance))
def __validate_filename(self, filename): def validate_filename(self):
filename = self.tts.filename
if not (filename and filename.endswith('.wav')): if not (filename and filename.endswith('.wav')):
raise AttributeError( raise AttributeError('file: %s must be in .wav format!' % filename)
'filename: ' + filename + ' must be a .wav file!')
dir_path = dirname(filename) dir_path = dirname(filename)
if not (exists(dir_path) and isdir(dir_path)): if not (exists(dir_path) and isdir(dir_path)):
raise AttributeError( raise AttributeError('filename: %s is not valid!' % filename)
'filename: ' + filename + ' is not a valid file path!')
LOGGER.debug('Filename: ' + filename)
@abc.abstractmethod @abstractmethod
def validate_lang(self, lang): def validate_lang(self):
pass pass
@abc.abstractmethod @abstractmethod
def validate_connection(self, tts): def validate_connection(self):
pass pass
@abc.abstractmethod @abstractmethod
def get_instance(self): def get_tts_class(self):
pass pass
class TTSFactory(object): class TTSFactory(object):
from mycroft.tts.espeak_tts import ESpeak
from mycroft.tts.fa_tts import FATTS
from mycroft.tts.google_tts import GoogleTTS
from mycroft.tts.mary_tts import MaryTTS
from mycroft.tts.mimic_tts import Mimic
from mycroft.tts.spdsay_tts import SpdSay
CLASSES = {
"mimic": Mimic,
"google": GoogleTTS,
"marytts": MaryTTS,
"fatts": FATTS,
"espeak": ESpeak,
"spdsay": SpdSay
}
@staticmethod @staticmethod
def create(): def create():
""" """
@ -116,28 +123,18 @@ class TTSFactory(object):
} }
""" """
logging.basicConfig() from mycroft.tts.remote_tts import RemoteTTS
config = ConfigurationManager.get().get('tts') config = ConfigurationManager.get().get('tts', {})
name = config.get('module') module = config.get('module', 'mimic')
lang = config.get(name).get('lang') lang = config.get(module).get('lang')
voice = config.get(name).get('voice') voice = config.get(module).get('voice')
clazz = TTSFactory.CLASSES.get(module)
if name == mimic_tts.NAME: if issubclass(clazz, RemoteTTS):
tts = mimic_tts.Mimic(lang, voice) url = config.get(module).get('url')
mimic_tts.MimicValidator().validate(tts) tts = clazz(lang, voice, url)
elif name == google_tts.NAME:
tts = google_tts.GoogleTTS(lang, voice)
google_tts.GoogleTTSValidator().validate(tts)
elif name == mary_tts.NAME:
tts = mary_tts.MaryTTS(lang, voice, config[name + '.url'])
mary_tts.MaryTTSValidator().validate(tts)
elif name == fa_tts.NAME:
tts = fa_tts.FATTS(lang, voice, config[name + '.url'])
fa_tts.FATTSValidator().validate(tts)
elif name == espeak_tts.NAME:
tts = espeak_tts.ESpeak(lang, voice)
espeak_tts.ESpeakValidator().validate(tts)
else: else:
tts = spdsay_tts.SpdSay(lang, voice) tts = clazz(lang, voice)
spdsay_tts.SpdSayValidator().validate(tts)
tts.validator.validate()
return tts return tts

View File

@ -20,14 +20,12 @@ import subprocess
from mycroft.tts import TTS, TTSValidator from mycroft.tts import TTS, TTSValidator
__author__ = 'seanfitz' __author__ = 'seanfitz', 'jdorleans'
NAME = 'espeak'
class ESpeak(TTS): class ESpeak(TTS):
def __init__(self, lang, voice): def __init__(self, lang, voice):
super(ESpeak, self).__init__(lang, voice) super(ESpeak, self).__init__(lang, voice, ESpeakValidator(self))
def execute(self, sentence, client): def execute(self, sentence, client):
subprocess.call( subprocess.call(
@ -35,20 +33,19 @@ class ESpeak(TTS):
class ESpeakValidator(TTSValidator): class ESpeakValidator(TTSValidator):
def __init__(self): def __init__(self, tts):
super(ESpeakValidator, self).__init__() super(ESpeakValidator, self).__init__(tts)
def validate_lang(self, lang): def validate_lang(self):
# TODO # TODO
pass pass
def validate_connection(self, tts): def validate_connection(self):
try: try:
subprocess.call(['espeak', '--version']) subprocess.call(['espeak', '--version'])
except: except:
raise Exception( raise Exception(
'ESpeak is not installed. Run on terminal: sudo apt-get ' 'ESpeak is not installed. Run: sudo apt-get install espeak')
'install espeak')
def get_instance(self): def get_tts_class(self):
return ESpeak return ESpeak

View File

@ -16,8 +16,6 @@
# along with Mycroft Core. If not, see <http://www.gnu.org/licenses/>. # along with Mycroft Core. If not, see <http://www.gnu.org/licenses/>.
import json
import requests import requests
from mycroft.tts import TTSValidator from mycroft.tts import TTSValidator
@ -25,8 +23,6 @@ from mycroft.tts.remote_tts import RemoteTTS
__author__ = 'jdorleans' __author__ = 'jdorleans'
NAME = 'fatts'
class FATTS(RemoteTTS): class FATTS(RemoteTTS):
PARAMS = { PARAMS = {
@ -39,7 +35,8 @@ class FATTS(RemoteTTS):
} }
def __init__(self, lang, voice, url): def __init__(self, lang, voice, url):
super(FATTS, self).__init__(lang, voice, url, '/say') super(FATTS, self).__init__(lang, voice, url, '/say',
FATTSValidator(self))
def build_request_params(self, sentence): def build_request_params(self, sentence):
params = self.PARAMS.copy() params = self.PARAMS.copy()
@ -50,23 +47,23 @@ class FATTS(RemoteTTS):
class FATTSValidator(TTSValidator): class FATTSValidator(TTSValidator):
def __init__(self): def __init__(self, tts):
super(FATTSValidator, self).__init__() super(FATTSValidator, self).__init__(tts)
def validate_lang(self, lang): def validate_lang(self):
# TODO # TODO
pass pass
def validate_connection(self, tts): def validate_connection(self):
try: try:
resp = requests.get(tts.url + "/info/version", verify=False) resp = requests.get(self.tts.url + "/info/version", verify=False)
content = json.loads(resp.content) content = resp.json()
if content['product'].find('FA-TTS') < 0: if content.get('product', '').find('FA-TTS') < 0:
raise Exception('Invalid FA-TTS server.') raise Exception('Invalid FA-TTS server.')
except: except:
raise Exception( raise Exception(
'FA-TTS server could not be verified. Check your connection ' 'FA-TTS server could not be verified. Check your connection '
'to the server: ' + tts.url) 'to the server: ' + self.tts.url)
def get_instance(self): def get_tts_class(self):
return FATTS return FATTS

View File

@ -23,12 +23,10 @@ from mycroft.util import play_wav
__author__ = 'jdorleans' __author__ = 'jdorleans'
NAME = 'gtts'
class GoogleTTS(TTS): class GoogleTTS(TTS):
def __init__(self, lang, voice): def __init__(self, lang, voice):
super(GoogleTTS, self).__init__(lang, voice) super(GoogleTTS, self).__init__(lang, voice, GoogleTTSValidator(self))
def execute(self, sentence, client): def execute(self, sentence, client):
tts = gTTS(text=sentence, lang=self.lang) tts = gTTS(text=sentence, lang=self.lang)
@ -37,20 +35,20 @@ class GoogleTTS(TTS):
class GoogleTTSValidator(TTSValidator): class GoogleTTSValidator(TTSValidator):
def __init__(self): def __init__(self, tts):
super(GoogleTTSValidator, self).__init__() super(GoogleTTSValidator, self).__init__(tts)
def validate_lang(self, lang): def validate_lang(self):
# TODO # TODO
pass pass
def validate_connection(self, tts): def validate_connection(self):
try: try:
gTTS(text='Hi').save(tts.filename) gTTS(text='Hi').save(self.tts.filename)
except: except:
raise Exception( raise Exception(
'GoogleTTS server could not be verified. Please check your ' 'GoogleTTS server could not be verified. Please check your '
'internet connection.') 'internet connection.')
def get_instance(self): def get_tts_class(self):
return GoogleTTS return GoogleTTS

View File

@ -23,8 +23,6 @@ from mycroft.tts.remote_tts import RemoteTTS
__author__ = 'jdorleans' __author__ = 'jdorleans'
NAME = 'marytts'
class MaryTTS(RemoteTTS): class MaryTTS(RemoteTTS):
PARAMS = { PARAMS = {
@ -37,7 +35,8 @@ class MaryTTS(RemoteTTS):
} }
def __init__(self, lang, voice, url): def __init__(self, lang, voice, url):
super(MaryTTS, self).__init__(lang, voice, url, '/process') super(MaryTTS, self).__init__(lang, voice, url, '/process',
MaryTTSValidator(self))
def build_request_params(self, sentence): def build_request_params(self, sentence):
params = self.PARAMS.copy() params = self.PARAMS.copy()
@ -48,22 +47,22 @@ class MaryTTS(RemoteTTS):
class MaryTTSValidator(TTSValidator): class MaryTTSValidator(TTSValidator):
def __init__(self): def __init__(self, tts):
super(MaryTTSValidator, self).__init__() super(MaryTTSValidator, self).__init__(tts)
def validate_lang(self, lang): def validate_lang(self):
# TODO # TODO
pass pass
def validate_connection(self, tts): def validate_connection(self):
try: try:
resp = requests.get(tts.url + "/version", verify=False) resp = requests.get(self.tts.url + "/version", verify=False)
if resp.content.find('Mary TTS server') < 0: if resp.content.find('Mary TTS server') < 0:
raise Exception('Invalid MaryTTS server.') raise Exception('Invalid MaryTTS server.')
except: except:
raise Exception( raise Exception(
'MaryTTS server could not be verified. Check your connection ' 'MaryTTS server could not be verified. Check your connection '
'to the server: ' + tts.url) 'to the server: ' + self.tts.url)
def get_instance(self): def get_tts_class(self):
return MaryTTS return MaryTTS

View File

@ -16,11 +16,10 @@
# along with Mycroft Core. If not, see <http://www.gnu.org/licenses/>. # along with Mycroft Core. If not, see <http://www.gnu.org/licenses/>.
import subprocess import subprocess
from os.path import join
import re
import random import random
import os import os
import time
from os.path import join
from mycroft import MYCROFT_ROOT_PATH from mycroft import MYCROFT_ROOT_PATH
from mycroft.tts import TTS, TTSValidator from mycroft.tts import TTS, TTSValidator
@ -31,9 +30,7 @@ __author__ = 'jdorleans'
config = ConfigurationManager.get().get("tts", {}) config = ConfigurationManager.get().get("tts", {})
NAME = 'mimic' BIN = config.get("path", join(MYCROFT_ROOT_PATH, 'mimic', 'bin', 'mimic'))
BIN = config.get(
"mimic.path", join(MYCROFT_ROOT_PATH, 'mimic', 'bin', 'mimic'))
# Mapping based on Jeffers phoneme to viseme map, seen in table 1 from: # Mapping based on Jeffers phoneme to viseme map, seen in table 1 from:
# http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.221.6377&rep=rep1&type=pdf # http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.221.6377&rep=rep1&type=pdf
@ -49,7 +46,7 @@ BIN = config.get(
class Mimic(TTS): class Mimic(TTS):
def __init__(self, lang, voice): def __init__(self, lang, voice):
super(Mimic, self).__init__(lang, voice) super(Mimic, self).__init__(lang, voice, MimicValidator(self))
self.args = ['-voice', self.voice] self.args = ['-voice', self.voice]
stretch = config.get('duration_stretch', None) stretch = config.get('duration_stretch', None)
if stretch: if stretch:
@ -151,19 +148,19 @@ class Mimic(TTS):
class MimicValidator(TTSValidator): class MimicValidator(TTSValidator):
def __init__(self): def __init__(self, tts):
super(MimicValidator, self).__init__() super(MimicValidator, self).__init__(tts)
def validate_lang(self, lang): def validate_lang(self):
# TODO
pass pass
def validate_connection(self, tts): def validate_connection(self):
try: try:
subprocess.call([BIN, '--version']) subprocess.call([BIN, '--version'])
except: except:
raise Exception( raise Exception(
'Mimic is not installed. Make sure install-mimic.sh ran ' 'Mimic is not installed. Run install-mimic.sh to install it.')
'properly.')
def get_instance(self): def get_tts_class(self):
return Mimic return Mimic

View File

@ -18,7 +18,6 @@
import abc import abc
import re import re
from requests_futures.sessions import FuturesSession from requests_futures.sessions import FuturesSession
from mycroft.tts import TTS from mycroft.tts import TTS
@ -38,8 +37,8 @@ class RemoteTTS(TTS):
whole sentence into small ones. whole sentence into small ones.
""" """
def __init__(self, lang, voice, url, api_path): def __init__(self, lang, voice, url, api_path, validator):
super(RemoteTTS, self).__init__(lang, voice) super(RemoteTTS, self).__init__(lang, voice, validator)
self.api_path = api_path self.api_path = api_path
self.url = remove_last_slash(url) self.url = remove_last_slash(url)
self.session = FuturesSession() self.session = FuturesSession()

View File

@ -22,12 +22,10 @@ from mycroft.tts import TTS, TTSValidator
__author__ = 'jdorleans' __author__ = 'jdorleans'
NAME = 'spdsay'
class SpdSay(TTS): class SpdSay(TTS):
def __init__(self, lang, voice): def __init__(self, lang, voice):
super(SpdSay, self).__init__(lang, voice) super(SpdSay, self).__init__(lang, voice, SpdSayValidator(self))
def execute(self, sentence, client): def execute(self, sentence, client):
subprocess.call( subprocess.call(
@ -35,20 +33,20 @@ class SpdSay(TTS):
class SpdSayValidator(TTSValidator): class SpdSayValidator(TTSValidator):
def __init__(self): def __init__(self, tts):
super(SpdSayValidator, self).__init__() super(SpdSayValidator, self).__init__(tts)
def validate_lang(self, lang): def validate_lang(self):
# TODO # TODO
pass pass
def validate_connection(self, tts): def validate_connection(self):
try: try:
subprocess.call(['spd-say', '--version']) subprocess.call(['spd-say', '--version'])
except: except:
raise Exception( raise Exception(
'SpdSay is not installed. Run on terminal: sudo apt-get' 'SpdSay is not installed. Run: sudo apt-get install '
'install speech-dispatcher') 'speech-dispatcher')
def get_instance(self): def get_tts_class(self):
return SpdSay return SpdSay