diff --git a/common/.local/bin/spd-read b/common/.local/bin/spd-read new file mode 100755 index 0000000..007f18b --- /dev/null +++ b/common/.local/bin/spd-read @@ -0,0 +1,254 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Read from a file or from stdin. + +File is under EUPL1.2 +Author: kujiu +""" + +import argparse + +import os.path +import secrets +import speechd +import chardet +import queue +import threading +import sys + + +PUNCTUATION = { + 'none': speechd.PunctuationMode.NONE, + 'some': speechd.PunctuationMode.SOME, + 'all': speechd.PunctuationMode.ALL, +} + + +class SpdRead: + def __init__(self, module, voice, lang, rate, punctuation, spelling): + + """ + Spd read software + """ + self.module = module or 'default' + self.rate = rate + self.lang = lang + self.voice = voice + self.spelling = spelling + self.punctuation = punctuation + self._queue = queue.Queue() + self._semaphore = threading.Semaphore(1) + self._state = 'init' + self.spdc = speechd.client.Client() + + def __del__(self): + if getattr(self, 'spdc', None): + self.spdc.close() + + def run(self, source): + """ + Run application, blocking method + """ + self._state = "running" + try: + self.set_speechd_parameters() + if source: + self.queue_file(source) + else: + self.queue_stdin() + + self.start_worker() + self.join() + except KeyboardInterrupt: + self.stop() + + def stop(self): + self._state = "stopping" + self._semaphore.release() + self.spdc.close() + + def start_worker(self): + """ + Start play thread. + """ + threading.Thread(target=self.say_loop, daemon=True).start() + + def join(self): + """ + Join thread + """ + return self._queue.join() + + def set_speechd_parameters(self): + """ + Configure speechd + """ + self.spdc.set_priority('important') + if self.module != 'default' and self.module: + self.spdc.set_output_module(self.module) + + voices = [ + voice[0] + for voice + in self.spdc.list_synthesis_voices() + if self._voice_match(voice)] + self.spdc.set_language(self.lang) + if len(voices) == 1: + self.spdc.set_synthesis_voice(voices[0]) + elif len(voices) >= 1: + self.spdc.set_synthesis_voice(secrets.choice(voices)) + else: + print("No voices installed for lang %s." % self.lang) + self.stop() + + self.spdc.set_punctuation(PUNCTUATION[self.punctuation]) + self.spdc.set_rate(self.rate) + self.spdc.set_spelling(self.spelling) + + def queue_file(self, source): + """ + Queue speech from a file + """ + with open(source, "rb") as fin: + buffer = [] + for line in fin.readlines(): + encoding = chardet.detect(line)["encoding"] + line = line[:-1].strip() + line = line.decode(encoding, "replace").strip() + if line: + buffer.append(line) + elif len(buffer): + to_say = '\n'.join(buffer) + buffer = [] + self.queue_speech(to_say) + + if self._state != 'running': + break + + if len(buffer) and self._state == 'running': + self.queue_speech('\n'.join(buffer)) + + def queue_stdin(self): + """ + Queue speech from stdin + """ + try: + buffer = [] + for line in sys.stdin.buffer.readlines(): + if not len(line): + break + + encoding = chardet.detect(line)["encoding"] + line = line[:-1].strip() + line = line.decode(encoding, "replace").strip() + if line: + buffer.append(line) + elif len(buffer): + to_say = '\n'.join(buffer) + buffer = [] + self.queue_speech(to_say) + + if self._state != 'running': + break + + if len(buffer) and self._state == 'running': + self.queue_speech('\n'.join(buffer)) + + except KeyboardInterrupt: + self.stop() + + def next(self, *args, **kwargs): + """ + Wait for speechd + """ + pass + + def _voice_match(self, voice): + """ + Check if a voice is in correct language + """ + if self.voice: + return voice[0].lower() == self.voice.lower() + if voice[1].lower() != self.lang.lower()[0:2]: + return False + if len(self.lang) > 4 and voice[2].lower() != self.lang[3:5]: + return False + return True + + def queue_speech(self, text): + """ + Queue text to speech + """ + self._queue.put_nowait(text) + + def spd_callback(self, callback_type): + """ + Process and of speechd + """ + if callback_type == speechd.CallbackType.CANCEL: + print("Cancelled speaking") + self.stop() + self._semaphore.release() + + def say_loop(self): + """ + Say blocks + """ + while self._semaphore.acquire() and self._state == 'running': + try: + text = self._queue.get_nowait() + self.spdc.speak( + text, self.spd_callback, + event_types=( + speechd.CallbackType.CANCEL, + speechd.CallbackType.END)) + self._queue.task_done() + except queue.Empty: + print("End of speech queue") + self.stop() + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description="Read with TTS.") + parser.add_argument( + '-l', '--lang', default='en', + help='Language') + parser.add_argument( + '-V', '--voice', default='', + help='Voice') + parser.add_argument( + '-m', '--module', default='default', + help='Speech Dispatcher output module') + parser.add_argument( + '-r', '--rate', default=15, + help='Speech Dispatcher rate', type=int) + parser.add_argument( + '-p', '--punctuation', default='none', + help='Punctuation mode (none, all or some)') + parser.add_argument( + '-S', '--spelling', default=False, action='store_true', + help='Spelling mode.') + parser.add_argument( + 'source', default='', nargs='?', + help='Source file (if not stdin)') + + options = parser.parse_args() + + source = os.path.expanduser( + os.path.expandvars(options.source) + ) + + module = getattr(options, 'module', None) + rate = getattr(options, 'rate', 15) + if options.punctuation not in PUNCTUATION.keys(): + print('Punctuation must be one of all, some or none') + exit(2) + + reader = SpdRead( + module, options.voice, + options.lang, rate, + options.punctuation, options.spelling) + + reader.run(options.source)