From e76c1b7d21938bb0ddba7ede378e36f59dd7e80d Mon Sep 17 00:00:00 2001 From: Jonathan D'Orleans Date: Fri, 16 Sep 2016 14:12:43 -0400 Subject: [PATCH] Issues 356 - Refactoring TTS --- mycroft/tts/__init__.py | 119 +++++++++++++++++++------------------- mycroft/tts/espeak_tts.py | 19 +++--- mycroft/tts/fa_tts.py | 25 ++++---- mycroft/tts/google_tts.py | 16 +++-- mycroft/tts/mary_tts.py | 19 +++--- mycroft/tts/mimic_tts.py | 25 ++++---- mycroft/tts/remote_tts.py | 5 +- mycroft/tts/spdsay_tts.py | 18 +++--- 8 files changed, 114 insertions(+), 132 deletions(-) diff --git a/mycroft/tts/__init__.py b/mycroft/tts/__init__.py index a2724a39ce..e9dc158bf6 100644 --- a/mycroft/tts/__init__.py +++ b/mycroft/tts/__init__.py @@ -16,18 +16,10 @@ # along with Mycroft Core. If not, see . -import logging - -import abc +from abc import ABCMeta, abstractmethod from os.path import dirname, exists, isdir 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 __author__ = 'jdorleans' @@ -42,15 +34,17 @@ class TTS(object): It aggregates the minimum required parameters and exposes ``execute(sentence)`` function. """ + __metaclass__ = ABCMeta - def __init__(self, lang, voice, filename='/tmp/tts.wav'): + def __init__(self, lang, voice, validator): super(TTS, self).__init__() self.lang = lang self.voice = voice - self.filename = filename + self.filename = '/tmp/tts.wav' + self.validator = validator - @abc.abstractmethod - def execute(self, sentence, client): + @abstractmethod + def execute(self, sentence): pass @@ -61,48 +55,61 @@ class TTSValidator(object): It exposes and implements ``validate(tts)`` function as a template to validate the TTS engines. """ + __metaclass__ = ABCMeta - def __init__(self): - pass + def __init__(self, tts): + self.tts = tts - def validate(self, tts): - self.__validate_instance(tts) - self.__validate_filename(tts.filename) - self.validate_lang(tts.lang) - self.validate_connection(tts) + def validate(self): + self.validate_instance() + self.validate_filename() + self.validate_lang() + self.validate_connection() - def __validate_instance(self, tts): - instance = self.get_instance() - if not isinstance(tts, instance): - raise AttributeError( - 'tts must be instance of ' + instance.__name__) - LOGGER.debug('TTS: ' + str(instance)) + def validate_instance(self): + clazz = self.get_tts_class() + if not isinstance(self.tts, clazz): + raise AttributeError('tts must be instance of ' + clazz.__name__) - def __validate_filename(self, filename): + def validate_filename(self): + filename = self.tts.filename if not (filename and filename.endswith('.wav')): - raise AttributeError( - 'filename: ' + filename + ' must be a .wav file!') + raise AttributeError('file: %s must be in .wav format!' % filename) + dir_path = dirname(filename) - if not (exists(dir_path) and isdir(dir_path)): - raise AttributeError( - 'filename: ' + filename + ' is not a valid file path!') - LOGGER.debug('Filename: ' + filename) + raise AttributeError('filename: %s is not valid!' % filename) - @abc.abstractmethod - def validate_lang(self, lang): + @abstractmethod + def validate_lang(self): pass - @abc.abstractmethod - def validate_connection(self, tts): + @abstractmethod + def validate_connection(self): pass - @abc.abstractmethod - def get_instance(self): + @abstractmethod + def get_tts_class(self): pass 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 def create(): """ @@ -116,28 +123,18 @@ class TTSFactory(object): } """ - logging.basicConfig() - config = ConfigurationManager.get().get('tts') - name = config.get('module') - lang = config.get(name).get('lang') - voice = config.get(name).get('voice') + from mycroft.tts.remote_tts import RemoteTTS + config = ConfigurationManager.get().get('tts', {}) + module = config.get('module', 'mimic') + lang = config.get(module).get('lang') + voice = config.get(module).get('voice') + clazz = TTSFactory.CLASSES.get(module) - if name == mimic_tts.NAME: - tts = mimic_tts.Mimic(lang, voice) - mimic_tts.MimicValidator().validate(tts) - 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) + if issubclass(clazz, RemoteTTS): + url = config.get(module).get('url') + tts = clazz(lang, voice, url) else: - tts = spdsay_tts.SpdSay(lang, voice) - spdsay_tts.SpdSayValidator().validate(tts) + tts = clazz(lang, voice) + + tts.validator.validate() return tts diff --git a/mycroft/tts/espeak_tts.py b/mycroft/tts/espeak_tts.py index 848b74821d..ee16d1354f 100644 --- a/mycroft/tts/espeak_tts.py +++ b/mycroft/tts/espeak_tts.py @@ -20,14 +20,12 @@ import subprocess from mycroft.tts import TTS, TTSValidator -__author__ = 'seanfitz' - -NAME = 'espeak' +__author__ = 'seanfitz', 'jdorleans' class ESpeak(TTS): def __init__(self, lang, voice): - super(ESpeak, self).__init__(lang, voice) + super(ESpeak, self).__init__(lang, voice, ESpeakValidator(self)) def execute(self, sentence, client): subprocess.call( @@ -35,20 +33,19 @@ class ESpeak(TTS): class ESpeakValidator(TTSValidator): - def __init__(self): - super(ESpeakValidator, self).__init__() + def __init__(self, tts): + super(ESpeakValidator, self).__init__(tts) - def validate_lang(self, lang): + def validate_lang(self): # TODO pass - def validate_connection(self, tts): + def validate_connection(self): try: subprocess.call(['espeak', '--version']) except: raise Exception( - 'ESpeak is not installed. Run on terminal: sudo apt-get ' - 'install espeak') + 'ESpeak is not installed. Run: sudo apt-get install espeak') - def get_instance(self): + def get_tts_class(self): return ESpeak diff --git a/mycroft/tts/fa_tts.py b/mycroft/tts/fa_tts.py index 60ec5f6915..b2808ead7a 100644 --- a/mycroft/tts/fa_tts.py +++ b/mycroft/tts/fa_tts.py @@ -16,8 +16,6 @@ # along with Mycroft Core. If not, see . -import json - import requests from mycroft.tts import TTSValidator @@ -25,8 +23,6 @@ from mycroft.tts.remote_tts import RemoteTTS __author__ = 'jdorleans' -NAME = 'fatts' - class FATTS(RemoteTTS): PARAMS = { @@ -39,7 +35,8 @@ class FATTS(RemoteTTS): } 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): params = self.PARAMS.copy() @@ -50,23 +47,23 @@ class FATTS(RemoteTTS): class FATTSValidator(TTSValidator): - def __init__(self): - super(FATTSValidator, self).__init__() + def __init__(self, tts): + super(FATTSValidator, self).__init__(tts) - def validate_lang(self, lang): + def validate_lang(self): # TODO pass - def validate_connection(self, tts): + def validate_connection(self): try: - resp = requests.get(tts.url + "/info/version", verify=False) - content = json.loads(resp.content) - if content['product'].find('FA-TTS') < 0: + resp = requests.get(self.tts.url + "/info/version", verify=False) + content = resp.json() + if content.get('product', '').find('FA-TTS') < 0: raise Exception('Invalid FA-TTS server.') except: raise Exception( '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 diff --git a/mycroft/tts/google_tts.py b/mycroft/tts/google_tts.py index 24e6abc26e..b56804a008 100644 --- a/mycroft/tts/google_tts.py +++ b/mycroft/tts/google_tts.py @@ -23,12 +23,10 @@ from mycroft.util import play_wav __author__ = 'jdorleans' -NAME = 'gtts' - class GoogleTTS(TTS): def __init__(self, lang, voice): - super(GoogleTTS, self).__init__(lang, voice) + super(GoogleTTS, self).__init__(lang, voice, GoogleTTSValidator(self)) def execute(self, sentence, client): tts = gTTS(text=sentence, lang=self.lang) @@ -37,20 +35,20 @@ class GoogleTTS(TTS): class GoogleTTSValidator(TTSValidator): - def __init__(self): - super(GoogleTTSValidator, self).__init__() + def __init__(self, tts): + super(GoogleTTSValidator, self).__init__(tts) - def validate_lang(self, lang): + def validate_lang(self): # TODO pass - def validate_connection(self, tts): + def validate_connection(self): try: - gTTS(text='Hi').save(tts.filename) + gTTS(text='Hi').save(self.tts.filename) except: raise Exception( 'GoogleTTS server could not be verified. Please check your ' 'internet connection.') - def get_instance(self): + def get_tts_class(self): return GoogleTTS diff --git a/mycroft/tts/mary_tts.py b/mycroft/tts/mary_tts.py index 72f4a9cab1..bff2dbbb9b 100644 --- a/mycroft/tts/mary_tts.py +++ b/mycroft/tts/mary_tts.py @@ -23,8 +23,6 @@ from mycroft.tts.remote_tts import RemoteTTS __author__ = 'jdorleans' -NAME = 'marytts' - class MaryTTS(RemoteTTS): PARAMS = { @@ -37,7 +35,8 @@ class MaryTTS(RemoteTTS): } 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): params = self.PARAMS.copy() @@ -48,22 +47,22 @@ class MaryTTS(RemoteTTS): class MaryTTSValidator(TTSValidator): - def __init__(self): - super(MaryTTSValidator, self).__init__() + def __init__(self, tts): + super(MaryTTSValidator, self).__init__(tts) - def validate_lang(self, lang): + def validate_lang(self): # TODO pass - def validate_connection(self, tts): + def validate_connection(self): 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: raise Exception('Invalid MaryTTS server.') except: raise Exception( '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 diff --git a/mycroft/tts/mimic_tts.py b/mycroft/tts/mimic_tts.py index c965d15745..f9a3327409 100644 --- a/mycroft/tts/mimic_tts.py +++ b/mycroft/tts/mimic_tts.py @@ -16,11 +16,10 @@ # along with Mycroft Core. If not, see . import subprocess -from os.path import join -import re import random import os -import time + +from os.path import join from mycroft import MYCROFT_ROOT_PATH from mycroft.tts import TTS, TTSValidator @@ -31,9 +30,7 @@ __author__ = 'jdorleans' config = ConfigurationManager.get().get("tts", {}) -NAME = 'mimic' -BIN = config.get( - "mimic.path", join(MYCROFT_ROOT_PATH, 'mimic', 'bin', 'mimic')) +BIN = config.get("path", join(MYCROFT_ROOT_PATH, 'mimic', 'bin', 'mimic')) # 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 @@ -49,7 +46,7 @@ BIN = config.get( class Mimic(TTS): def __init__(self, lang, voice): - super(Mimic, self).__init__(lang, voice) + super(Mimic, self).__init__(lang, voice, MimicValidator(self)) self.args = ['-voice', self.voice] stretch = config.get('duration_stretch', None) if stretch: @@ -151,19 +148,19 @@ class Mimic(TTS): class MimicValidator(TTSValidator): - def __init__(self): - super(MimicValidator, self).__init__() + def __init__(self, tts): + super(MimicValidator, self).__init__(tts) - def validate_lang(self, lang): + def validate_lang(self): + # TODO pass - def validate_connection(self, tts): + def validate_connection(self): try: subprocess.call([BIN, '--version']) except: raise Exception( - 'Mimic is not installed. Make sure install-mimic.sh ran ' - 'properly.') + 'Mimic is not installed. Run install-mimic.sh to install it.') - def get_instance(self): + def get_tts_class(self): return Mimic diff --git a/mycroft/tts/remote_tts.py b/mycroft/tts/remote_tts.py index 74002b7761..192ecf3c8d 100644 --- a/mycroft/tts/remote_tts.py +++ b/mycroft/tts/remote_tts.py @@ -18,7 +18,6 @@ import abc import re - from requests_futures.sessions import FuturesSession from mycroft.tts import TTS @@ -38,8 +37,8 @@ class RemoteTTS(TTS): whole sentence into small ones. """ - def __init__(self, lang, voice, url, api_path): - super(RemoteTTS, self).__init__(lang, voice) + def __init__(self, lang, voice, url, api_path, validator): + super(RemoteTTS, self).__init__(lang, voice, validator) self.api_path = api_path self.url = remove_last_slash(url) self.session = FuturesSession() diff --git a/mycroft/tts/spdsay_tts.py b/mycroft/tts/spdsay_tts.py index 446d4abdf5..eaf1f3d50d 100644 --- a/mycroft/tts/spdsay_tts.py +++ b/mycroft/tts/spdsay_tts.py @@ -22,12 +22,10 @@ from mycroft.tts import TTS, TTSValidator __author__ = 'jdorleans' -NAME = 'spdsay' - class SpdSay(TTS): def __init__(self, lang, voice): - super(SpdSay, self).__init__(lang, voice) + super(SpdSay, self).__init__(lang, voice, SpdSayValidator(self)) def execute(self, sentence, client): subprocess.call( @@ -35,20 +33,20 @@ class SpdSay(TTS): class SpdSayValidator(TTSValidator): - def __init__(self): - super(SpdSayValidator, self).__init__() + def __init__(self, tts): + super(SpdSayValidator, self).__init__(tts) - def validate_lang(self, lang): + def validate_lang(self): # TODO pass - def validate_connection(self, tts): + def validate_connection(self): try: subprocess.call(['spd-say', '--version']) except: raise Exception( - 'SpdSay is not installed. Run on terminal: sudo apt-get' - 'install speech-dispatcher') + 'SpdSay is not installed. Run: sudo apt-get install ' + 'speech-dispatcher') - def get_instance(self): + def get_tts_class(self): return SpdSay