diff options
Diffstat (limited to 'client/tts.py')
| -rw-r--r-- | client/tts.py | 114 |
1 files changed, 79 insertions, 35 deletions
diff --git a/client/tts.py b/client/tts.py index 8fef776..244c0f4 100644 --- a/client/tts.py +++ b/client/tts.py @@ -18,7 +18,6 @@ import logging from abc import ABCMeta, abstractmethod from distutils.spawn import find_executable -import yaml import argparse import wave @@ -28,12 +27,13 @@ try: except ImportError: pass + class AbstractTTSEngine(object): """ Generic parent class for all speakers """ __metaclass__ = ABCMeta - + @classmethod @abstractmethod def is_available(cls): @@ -50,7 +50,8 @@ class AbstractTTSEngine(object): # FIXME: Use platform-independent audio-output here # See issue jasperproject/jasper-client#188 cmd = ['aplay', '-D', 'hw:1,0', str(filename)] - self._logger.debug('Executing %s', ' '.join([pipes.quote(arg) for arg in cmd])) + self._logger.debug('Executing %s', ' '.join([pipes.quote(arg) + for arg in cmd])) with tempfile.TemporaryFile() as f: subprocess.call(cmd, stdout=f, stderr=f) f.seek(0) @@ -58,13 +59,15 @@ class AbstractTTSEngine(object): if output: self._logger.debug("Output was: '%s'", output) + class AbstractMp3TTSEngine(AbstractTTSEngine): """ Generic class that implements the 'play' method for mp3 files """ @classmethod def is_available(cls): - return (super(AbstractMp3TTSEngine, cls).is_available() and 'mad' in sys.modules.keys()) + return (super(AbstractMp3TTSEngine, cls).is_available() and + 'mad' in sys.modules.keys()) def play_mp3(self, filename): mf = mad.MadFile(filename) @@ -72,7 +75,8 @@ class AbstractMp3TTSEngine(AbstractTTSEngine): wav = wave.open(f, mode='wb') wav.setframerate(mf.samplerate()) wav.setnchannels(1 if mf.mode() == mad.MODE_SINGLE_CHANNEL else 2) - wav.setsampwidth(4L) # width of 32 bit audio + # 4L is the sample width of 32 bit audio + wav.setsampwidth(4L) frame = mf.read() while frame is not None: wav.writeframes(frame) @@ -80,6 +84,7 @@ class AbstractMp3TTSEngine(AbstractTTSEngine): wav.close() self.play(f.name) + class DummyTTS(AbstractTTSEngine): """ Dummy TTS engine that logs phrases with INFO level instead of synthesizing @@ -94,11 +99,12 @@ class DummyTTS(AbstractTTSEngine): def say(self, phrase): self._logger.info(phrase) - + def play(self, filename): self._logger.debug("Playback of file '%s' requested") pass + class EspeakTTS(AbstractTTSEngine): """ Uses the eSpeak speech synthesizer included in the Jasper disk image @@ -107,7 +113,8 @@ class EspeakTTS(AbstractTTSEngine): SLUG = "espeak-tts" - def __init__(self, voice='default+m3', pitch_adjustment=40, words_per_minute=160): + def __init__(self, voice='default+m3', pitch_adjustment=40, + words_per_minute=160): super(self.__class__, self).__init__() self.voice = voice self.pitch_adjustment = pitch_adjustment @@ -115,7 +122,8 @@ class EspeakTTS(AbstractTTSEngine): @classmethod def is_available(cls): - return (super(cls, cls).is_available() and find_executable('espeak') is not None) + return (super(cls, cls).is_available() and + find_executable('espeak') is not None) def say(self, phrase): self._logger.debug("Saying '%s' with '%s'", phrase, self.SLUG) @@ -127,7 +135,8 @@ class EspeakTTS(AbstractTTSEngine): '-w', fname, phrase] cmd = [str(x) for x in cmd] - self._logger.debug('Executing %s', ' '.join([pipes.quote(arg) for arg in cmd])) + self._logger.debug('Executing %s', ' '.join([pipes.quote(arg) + for arg in cmd])) with tempfile.TemporaryFile() as f: subprocess.call(cmd, stdout=f, stderr=f) f.seek(0) @@ -137,6 +146,7 @@ class EspeakTTS(AbstractTTSEngine): self.play(fname) os.remove(fname) + class FestivalTTS(AbstractTTSEngine): """ Uses the festival speech synthesizer @@ -147,13 +157,17 @@ class FestivalTTS(AbstractTTSEngine): @classmethod def is_available(cls): - if super(cls, cls).is_available() and find_executable('text2wave') is not None and find_executable('festival') is not None: + if (super(cls, cls).is_available() and + find_executable('text2wave') is not None and + find_executable('festival') is not None): logger = logging.getLogger(__name__) cmd = ['festival', '--pipe'] with tempfile.SpooledTemporaryFile() as out_f: with tempfile.SpooledTemporaryFile() as in_f: - logger.debug('Executing %s', ' '.join([pipes.quote(arg) for arg in cmd])) - subprocess.call(cmd, stdin=in_f, stdout=out_f, stderr=out_f) + logger.debug('Executing %s', ' '.join([pipes.quote(arg) + for arg in cmd])) + subprocess.call(cmd, stdin=in_f, stdout=out_f, + stderr=out_f) out_f.seek(0) output = out_f.read().strip() if output: @@ -169,14 +183,18 @@ class FestivalTTS(AbstractTTSEngine): in_f.write(phrase) in_f.seek(0) with tempfile.SpooledTemporaryFile() as err_f: - self._logger.debug('Executing %s', ' '.join([pipes.quote(arg) for arg in cmd])) - subprocess.call(cmd, stdin=in_f, stdout=out_f, stderr=err_f) + self._logger.debug('Executing %s', + ' '.join([pipes.quote(arg) + for arg in cmd])) + subprocess.call(cmd, stdin=in_f, stdout=out_f, + stderr=err_f) err_f.seek(0) output = err_f.read() if output: self._logger.debug("Output was: '%s'", output) self.play(out_f.name) + class MacOSXTTS(AbstractTTSEngine): """ Uses the OS X built-in 'say' command @@ -186,12 +204,15 @@ class MacOSXTTS(AbstractTTSEngine): @classmethod def is_available(cls): - return (platform.system() == 'darwin' and find_executable('say') is not None and find_executable('afplay') is not None) + return (platform.system() == 'darwin' and + find_executable('say') is not None and + find_executable('afplay') is not None) def say(self, phrase): self._logger.debug("Saying '%s' with '%s'", phrase, self.SLUG) cmd = ['say', str(phrase)] - self._logger.debug('Executing %s', ' '.join([pipes.quote(arg) for arg in cmd])) + self._logger.debug('Executing %s', ' '.join([pipes.quote(arg) + for arg in cmd])) with tempfile.TemporaryFile() as f: subprocess.call(cmd, stdout=f, stderr=f) f.seek(0) @@ -201,7 +222,8 @@ class MacOSXTTS(AbstractTTSEngine): def play(self, filename): cmd = ['afplay', str(filename)] - self._logger.debug('Executing %s', ' '.join([pipes.quote(arg) for arg in cmd])) + self._logger.debug('Executing %s', ' '.join([pipes.quote(arg) + for arg in cmd])) with tempfile.TemporaryFile() as f: subprocess.call(cmd, stdout=f, stderr=f) f.seek(0) @@ -209,6 +231,7 @@ class MacOSXTTS(AbstractTTSEngine): if output: self._logger.debug("Output was: '%s'", output) + class PicoTTS(AbstractTTSEngine): """ Uses the svox-pico-tts speech synthesizer @@ -223,7 +246,8 @@ class PicoTTS(AbstractTTSEngine): @classmethod def is_available(cls): - return (super(cls, cls).is_available() and find_executable('pico2wave') is not None) + return (super(cls, cls).is_available() and + find_executable('pico2wave') is not None) @property def languages(self): @@ -234,7 +258,8 @@ class PicoTTS(AbstractTTSEngine): subprocess.call(cmd, stderr=f) f.seek(0) output = f.read() - pattern = re.compile(r'Unknown language: NULL\nValid languages:\n((?:[a-z]{2}-[A-Z]{2}\n)+)') + pattern = re.compile(r'Unknown language: NULL\nValid languages:\n' + + r'((?:[a-z]{2}-[A-Z]{2}\n)+)') matchobj = pattern.match(output) if not matchobj: raise RuntimeError("pico2wave: valid languages not detected") @@ -247,10 +272,12 @@ class PicoTTS(AbstractTTSEngine): fname = f.name cmd = ['pico2wave', '--wave', fname] if self.language not in self.languages: - raise ValueError("Language '%s' not supported by '%s'", self.language, self.SLUG) + raise ValueError("Language '%s' not supported by '%s'", + self.language, self.SLUG) cmd.extend(['-l', self.language]) cmd.append(phrase) - self._logger.debug('Executing %s', ' '.join([pipes.quote(arg) for arg in cmd])) + self._logger.debug('Executing %s', ' '.join([pipes.quote(arg) + for arg in cmd])) with tempfile.TemporaryFile() as f: subprocess.call(cmd, stdout=f, stderr=f) f.seek(0) @@ -260,6 +287,7 @@ class PicoTTS(AbstractTTSEngine): self.play(fname) os.remove(fname) + class GoogleTTS(AbstractMp3TTSEngine): """ Uses the Google TTS online translator @@ -274,19 +302,23 @@ class GoogleTTS(AbstractMp3TTSEngine): @classmethod def is_available(cls): - return (super(cls, cls).is_available() and 'gtts' in sys.modules.keys()) + return (super(cls, cls).is_available() and + 'gtts' in sys.modules.keys()) @property def languages(self): - langs = ['af', 'sq', 'ar', 'hy', 'ca', 'zh-CN', 'zh-TW', 'hr', 'cs', 'da', 'nl', 'en', 'eo', 'fi', 'fr', 'de', - 'el', 'ht', 'hi', 'hu', 'is', 'id', 'it', 'ja', 'ko', 'la', 'lv', 'mk', 'no', 'pl', 'pt', 'ro', 'ru', - 'sr', 'sk', 'es', 'sw', 'sv', 'ta', 'th', 'tr', 'vi', 'cy'] + langs = ['af', 'sq', 'ar', 'hy', 'ca', 'zh-CN', 'zh-TW', 'hr', 'cs', + 'da', 'nl', 'en', 'eo', 'fi', 'fr', 'de', 'el', 'ht', 'hi', + 'hu', 'is', 'id', 'it', 'ja', 'ko', 'la', 'lv', 'mk', 'no', + 'pl', 'pt', 'ro', 'ru', 'sr', 'sk', 'es', 'sw', 'sv', 'ta', + 'th', 'tr', 'vi', 'cy'] return langs def say(self, phrase): self._logger.debug("Saying '%s' with '%s'", phrase, self.SLUG) if self.language not in self.languages: - raise ValueError("Language '%s' not supported by '%s'", self.language, self.SLUG) + raise ValueError("Language '%s' not supported by '%s'", + self.language, self.SLUG) tts = gtts.gTTS(text=phrase, lang=self.language) with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as f: tmpfile = f.name @@ -294,9 +326,11 @@ class GoogleTTS(AbstractMp3TTSEngine): self.play_mp3(tmpfile) os.remove(tmpfile) + def get_default_engine_slug(): return 'osx-tts' if platform.system() == 'darwin' else 'espeak-tts' + def get_engine_by_slug(slug=None): """ Returns: @@ -305,21 +339,26 @@ def get_engine_by_slug(slug=None): Raises: ValueError if no speaker implementation is supported on this platform """ - + if not slug or type(slug) is not str: raise TypeError("Invalid slug '%s'", slug) - selected_engines = filter(lambda engine: hasattr(engine, "SLUG") and engine.SLUG == slug, get_engines()) + selected_engines = filter(lambda engine: hasattr(engine, "SLUG") and + engine.SLUG == slug, get_engines()) if len(selected_engines) == 0: raise ValueError("No TTS engine found for slug '%s'" % slug) else: if len(selected_engines) > 1: - print("WARNING: Multiple TTS engines found for slug '%s'. This is most certainly a bug." % slug) + print("WARNING: Multiple TTS engines found for slug '%s'. " + + "This is most certainly a bug." % slug) engine = selected_engines[0] if not engine.is_available(): - raise ValueError("TTS engine '%s' is not available (due to missing dependencies, missing dependencies, etc.)" % slug) + raise ValueError("TTS engine '%s' is not available (due to " + + "missing dependencies, missing " + + "dependencies, etc.)" % slug) return engine + def get_engines(): def get_subclasses(cls): subclasses = set() @@ -327,30 +366,35 @@ def get_engines(): subclasses.add(subclass) subclasses.update(get_subclasses(subclass)) return subclasses - return [tts_engine for tts_engine in list(get_subclasses(AbstractTTSEngine)) if hasattr(tts_engine, 'SLUG') and tts_engine.SLUG] + return [tts_engine for tts_engine in + list(get_subclasses(AbstractTTSEngine)) + if hasattr(tts_engine, 'SLUG') and tts_engine.SLUG] if __name__ == '__main__': parser = argparse.ArgumentParser(description='Jasper TTS module') - parser.add_argument('--debug', action='store_true', help='Show debug messages') + parser.add_argument('--debug', action='store_true', + help='Show debug messages') args = parser.parse_args() logging.basicConfig() if args.debug: logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) - + engines = get_engines() available_engines = [] for engine in get_engines(): if engine.is_available(): available_engines.append(engine) + disabled_engines = list(set(engines).difference(set(available_engines))) print("Available TTS engines:") for i, engine in enumerate(available_engines, start=1): print("%d. %s" % (i, engine.SLUG)) - + print("") print("Disabled TTS engines:") - for i, engine in enumerate(list(set(engines).difference(set(available_engines))), start=1): + + for i, engine in enumerate(disabled_engines, start=1): print("%d. %s" % (i, engine.SLUG)) print("") |
