I am just starting to learn Python and in order to do so, I was working on implementing a simple chat-bot. That worked fine until I wanted to implement some text-to-speech functionality which speaks the lines while they are appearing on the screen. To achieve that, I had to dive into multi-threading and that's where I'm stuck:
import concurrent.futures
import pyttsx3
from time import sleep
import sys
# Settings
engine = pyttsx3.init()
voices = engine.getProperty('voices')
engine.setProperty('voice', voices[0].id)
typing_delay=0.035
def textToSpeech(text):
engine.say(text)
engine.runAndWait()
def typing(sentence):
for char in sentence:
sleep(typing_delay)
sys.stdout.write(char)
sys.stdout.flush()
# def parallel(string):
# tasks = [lambda: textToSpeech(string), lambda: typing("\n> "+string+"\n\n")]
# with ThreadPoolExecutor(max_workers=2) as executor:
# futures = [executor.submit(task) for task in tasks]
# for future in futures:
# try:
# future.result()
# except Exception as e:
# print(e)
def parallel(text):
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
future_tasks = {executor.submit(textToSpeech, text), executor.submit(typing, "\n> "+text+"\n\n")}
for future in concurrent.futures.as_completed(future_tasks):
try:
data = future.result()
except Exception as e:
print(e)
# Test Sentence
parallel("Greetings Professor Falken")
The two functions on top are supposed to run in parallel. I've tried two different implementations for my parallel() function (one is commented out), both of which yield the same result however. For the first line of text that the chat-bot puts out, I do actually get both text & speech, but then I get the error:
'NoneType' object has no attribute 'earlierDate_'
After that, I only get text, no more speech and the error: run loop already started
I assume that somewhere in concurrent.futures
is the attribute 'earlierDate_'
and that I'm not handling it correctly, so that the text-to-speech thread never stops. But I have no idea how to fix it.
I hope someone here has an idea that might help. I've cut down my code to something that is as small as possible but still can be run and tested.
Addendum: I had issues with importing pyttsx3
on Python 3.8, so I downgraded to Python 3.7 where it seems to work.
UPDATE: So it occurred to me, that while I was focusing on the multithreading, the issue might have been with my text-to-speech implementation all along.
The obvious bit was me initialising my speech engine globally. So I moved my settings into the textToSpeech function:
def textToSpeech(text):
engine = pyttsx3.init()
voices = engine.getProperty('voices')
engine.setProperty('voice', voices[0].id)
engine.say(text)
engine.runAndWait()
The run loop already started
Error now doesn't appear right away and I get text & speech throughout the first couple of chatbot interactions.
I still get the 'NoneType' object has no attribute 'earlierDate_'
error, now after every chat-bot output though and eventually the run loop already started
Error kicks in again and I lose the sound. Still, one step closer I guess.
UPDATE2:
After another day of digging, I think I'm another step closer. This seems to be a Mac-specific issue related to multi-threading. I've found multiple issues across different areas where people ran into this problem.
I've located the issue within PyObjCTools/AppHelper.py
There we have the following function:
def runConsoleEventLoop(
argv=None, installInterrupt=False, mode=NSDefaultRunLoopMode, maxTimeout=3.0
):
if argv is None:
argv = sys.argv
if installInterrupt:
installMachInterrupt()
runLoop = NSRunLoop.currentRunLoop()
stopper = PyObjCAppHelperRunLoopStopper.alloc().init()
PyObjCAppHelperRunLoopStopper.addRunLoopStopper_toRunLoop_(stopper, runLoop)
try:
while stopper.shouldRun():
nextfire = runLoop.limitDateForMode_(mode)
if not stopper.shouldRun():
break
soon = NSDate.dateWithTimeIntervalSinceNow_(maxTimeout)
nextfire = nextfire.earlierDate_(soon)
if not runLoop.runMode_beforeDate_(mode, nextfire):
stopper.stop()
finally:
PyObjCAppHelperRunLoopStopper.removeRunLoopStopperFromRunLoop_(runLoop)
This line close to the button is the culprit: nextfire = nextfire.earlierDate_(soon)
The object nextfire
seems to be a date. In Objective-C, NSDate objects do indeed have an earlierDate()
method, so it should work. But something's wrong with the initialization. When I print(nextfire)
, I get None
. No surprise then that a NoneType object doesn't have the attribute 'earlierDate_'.