#!/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)