homes/common/.local/bin/spd-read

254 lines
6.9 KiB
Python
Executable file

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