diff options
Diffstat (limited to 'client/speaker.py')
| -rw-r--r-- | client/speaker.py | 169 |
1 files changed, 153 insertions, 16 deletions
diff --git a/client/speaker.py b/client/speaker.py index c20c01b..a0fb693 100644 --- a/client/speaker.py +++ b/client/speaker.py @@ -8,45 +8,182 @@ Speaker methods: isAvailable - returns True if the platform supports this implementation """ import os +import re +import sys import json +import tempfile +import subprocess +from abc import ABCMeta, abstractmethod +import pyaudio +import wave +try: + import mad + import gtts +except ImportError: + pass -class eSpeakSpeaker: +class AbstractSpeaker(object): + """ + Generic parent class for all speakers + """ + + __metaclass__ = ABCMeta + + @classmethod + @abstractmethod + def isAvailable(cls): + pass + + @abstractmethod + def say(self, phrase, *args): + pass + + def play(self, filename, chunksize=1024): + f = wave.open(filename, 'rb') + p = pyaudio.PyAudio() + stream = p.open(format=p.get_format_from_width(f.getsampwidth()), + channels=f.getnchannels(), + rate=f.getframerate(), + output=True) + + data = f.readframes(chunksize) + while data: + stream.write(data) + data = f.readframes(chunksize) + stream.stop_stream() + stream.close() + p.terminate() + +class AbstractMp3Speaker(AbstractSpeaker): """ - Uses the eSpeak speech synthesizer included in the Jasper disk image + Generic class that implements the 'play' method for mp3 files """ @classmethod def isAvailable(cls): - return os.system("which espeak") == 0 + return True if 'mad' in sys.modules.keys() else False - def say(self, phrase, OPTIONS=" -vdefault+m3 -p 40 -s 160 --stdout > say.wav"): - os.system("espeak " + json.dumps(phrase, False, False) + OPTIONS) - self.play("say.wav") + def play_mp3(cls, filename): + f = mad.MadFile(filename) + p = pyaudio.PyAudio() + # open stream + stream = p.open(format=p.get_format_from_width(pyaudio.paInt32), + channels=2, + rate=f.samplerate(), + output=True) + + data = f.read() + while data: + stream.write(data) + data = f.read() - def play(self, filename): - os.system("aplay -D hw:1,0 " + filename) + stream.stop_stream() + stream.close() + p.terminate() +class eSpeakSpeaker(AbstractSpeaker): + """ + Uses the eSpeak speech synthesizer included in the Jasper disk image + Requires espeak to be available + """ + @classmethod + def isAvailable(cls): + return (super(cls, cls).isAvailable() and subprocess.call(['which','espeak']) == 0) -class saySpeaker: + def say(self, phrase, voice='default+m3', pitch_adjustment=40, words_per_minute=160): + with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as f: + fname = f.name + cmd = ['espeak', '-v', voice, + '-p', pitch_adjustment, + '-s', words_per_minute, + '-w', fname] + cmd = [str(x) for x in cmd] + subprocess.call(cmd) + self.play(fname) + os.remove(fname) +class saySpeaker(AbstractSpeaker): """ Uses the OS X built-in 'say' command """ @classmethod def isAvailable(cls): - return os.system("which say") == 0 - - def shellquote(self, s): - return "'" + s.replace("'", "'\\''") + "'" + return (subprocess.call(['which','say']) == 0) def say(self, phrase): - os.system("say " + self.shellquote(phrase)) + cmd = ['say', str(phrase)] + subprocess.call(cmd) def play(self, filename): - os.system("afplay " + filename) + cmd = ['afplay', str(filename)] + subprocess.call(cmd) + +class picoSpeaker(AbstractSpeaker): + """ + Uses the svox-pico-tts speech synthesizer + Requires pico2wave to be available + """ + @classmethod + def isAvailable(cls): + return (super(cls, cls).isAvailable() and subprocess.call(['which','pico2wave']) == 0) + + @property + def languages(self): + cmd = ['pico2wave', '-l', 'NULL', + '-w', '/dev/null', + 'NULL'] + with tempfile.SpooledTemporaryFile() as f: + 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)+)') + matchobj = pattern.match(output) + if not matchobj: + raise RuntimeError("pico2wave: valid languages not detected") + langs = matchobj.group(1).split() + return langs + + def say(self, phrase, language="en-US"): + with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as f: + fname = f.name + cmd = ['pico2wave', '--wave', fname] + if language: + if language not in self.languages: + raise ValueError("Language '%s' not supported by '%s'", language, cmd[0]) + cmd.extend(['-l',language]) + cmd.append(phrase) + + subprocess.call(cmd) + self.play(fname) + os.remove(fname) + +class googleSpeaker(AbstractMp3Speaker): + """ + Uses the Google TTS online translator + Requires pymad and gTTS to be available + """ + @classmethod + def isAvailable(cls): + return (super(cls, cls).isAvailable() 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'] + return langs + def say(self, phrase, language='en'): + if language not in self.languages: + raise ValueError("Language '%s' not supported by '%s'", language, cmd[0]) + tts = gtts.gTTS(text=phrase, lang=language) + with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as f: + tmpfile = f.name + tts.save(tmpfile) + self.play_mp3(tmpfile) + os.remove(tmpfile) def newSpeaker(): """ @@ -57,7 +194,7 @@ def newSpeaker(): ValueError if no speaker implementation is supported on this platform """ - for cls in [eSpeakSpeaker, saySpeaker]: + for cls in [googleSpeaker, picoSpeaker, eSpeakSpeaker, saySpeaker]: if cls.isAvailable(): return cls() raise ValueError("Platform is not supported") |
