summaryrefslogtreecommitdiff
path: root/client/tts.py
diff options
context:
space:
mode:
Diffstat (limited to 'client/tts.py')
-rw-r--r--client/tts.py114
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("")