Closed matatk closed 11 years ago
The iterate() code is largely untested and I think you've hit a limitation of that approach. On Mac, the callbacks never fire indicating the utterance is complete because there's no way to pump a true Mac event loop without starting such a loop. I need to update the documentation about this limitation.
For your particular case, though, there is a reasonable workaround. Assuming your subprocess isn't generating so much on stdout that the TTS engine can't keep up, the following will work:
def launch(command_line):
proc = subprocess.Popen(command_line, stdout=subprocess.PIPE)
speaker = pyttsx.init()
while True:
retcode = proc.poll()
line = proc.stdout.readline()
speaker.say(line)
speaker.runAndWait()
if retcode is not None:
break
The runAndWait() runs a real OSX event loop and queues a stopEvent() command to kill it after all queued say() calls.
Can you give this a try? It worked for me with a little bash script spitting out the date/time every 5 seconds endlessly.
Thanks for this. The result was a bit of a surprise, but first I think I should very briefly explain what my code is doing: the program I'm running is a mod for Quake that makes it accessible to vision-impaired and blind gamers. We're trying to dust off the project and make it more user-friendly on modern platforms. I have a GUI (written in Python, with PyGUI and pyttsx) that provides a simple UI to starting the game and handling TTS for the output from the game's console.
With your suggested code above, when runAndWait()
finishes, my Python GUI/TTS script is terminated and thus the GUI too just disappears. It seems to terminate the whole thread on which the event loop was running.
I was wondering, instead, about setting up a class to manage pyttsx as a subprocess, thus keeping pyttsx running and in its own event loop all the time. Then using a queue to send the text to that subprocess for pyttsx to speak -- perhaps that is a cleaner approach (as it wouldn't involve constantly terminating and re-starting pyttsx's event loop). I will give that a go and report back when I have been able to try it out.
Meanwhile, if you want to see the effects of the above runAndWait()
code, you can download a build of AudioQuake that uses it from our "experimental" folder on Dropbox. There are two versions: one with the code above and the other that only uses pyttsx to say "Welcome to Quake" at the start, as a test (for the rest of its TTS, it calls the "say" command, which results in everything being said at once :-)).
I have tried a couple of different approaches, but each has presented further problems:
startLoop
: Break on __THE_PROCESS_HAS_FORKED_AND_YOU_CANNOT_USE_THIS_COREFOUNDATION_FUNCTIONALITY___YOU_MUST_EXEC__() to debug. The process has forked and you cannot use this CoreFoundation functionality safely. You MUST exec().
startLoop
: objc[7037]: Object 0x7f8dfd9bb710 of class __NSDate autoreleased with no pool in place - just leaking - break on objc_autoreleaseNoPool() to debug objc[7037]: Object 0x7f8dfda2caa0 of class __NSCFTimer autoreleased with no pool in place - just leaking - break on objc_autoreleaseNoPool() to debug
These both seem like errors in code beyond my reach, whether pyttsx or in the system.
For now I have reverted to spinning off a thread and just calling the "say" command from it. This is somewhat hacky and far from ideal -- I would really like to use pyttsx, because I want to port to Windows -- but it does mean that things are working for now.
Please let me know if there is any way I can help you track down whether these are bugs in pyttsx and, if so, how I might be able to help you fix them.
With your suggested code above, when runAndWait() finishes, my Python GUI/TTS script is terminated and thus the GUI too just disappears. It seems to terminate the whole thread on which the event loop was running.
If the process in which you are trying to use pyttsx is already running an OSX event loop, you shouldn't need to worry about runAndWait(), iterate(), threads, or subprocesses. Simply call say() and let the event loop callbacks to the driver advance from one utterance to the next.
def launch(command_line):
proc = subprocess.Popen(command_line, stdout=subprocess.PIPE)
speaker = pyttsx.init()
while True:
retcode = proc.poll()
line = proc.stdout.readline()
speaker.say(line)
# no need for speaker.runAndWait() if you're already in the standard OS event loop
if retcode is not None:
break
Thanks for this further info. I have tried what you suggest and, unfortunately, the whole program just hangs if I don't call runAndWait()
(but if I do, the whole thing, GUI and all, disappears). I think the problem may be that this is all running within a PyGUI event loop.
PyGUI's event loop seems to, on the Mac, boil down to calling the run() method of NSApplication from AppKit (though there are a few layers to the code, it seems to come down to this). Unfortunately the source doesn't seem to be viewable online but it can be obtained from the PyGUI site. I'm out of my depth, but given what you've said, I would've expected that to constitute a normal OS X event loop, so I'm not sure why I'm seeing this behaviour.
In a couple of days I may be able to write an example that does not use PyGUI, to see if the problem is the interaction between it and pyttsx.
Try this slight modification where speaker.startLoop(False) is invoked right after the init:
def launch(command_line):
proc = subprocess.Popen(command_line, stdout=subprocess.PIPE)
speaker = pyttsx.init()
speaker.startLoop(False)
while True:
retcode = proc.poll()
line = proc.stdout.readline()
speaker.say(line)
# no need for speaker.runAndWait() if you're already in the standard OS event loop
if retcode is not None:
break
speaker.endLoop()
If this still fails, it'd be useful to know if it's something in PyGUI getting in the way in particular or something specific about your use of it. If you write a simple Hello World that initializes a pyttsx engine, tells the registers a timed callback using PyGUI.Task, and invokes engine.say() on that callback, does that work?
I can code that up myself to reproduce the problem, but it might be a few days before I can find the time.
Thank you for your patience and help. Unfortunately I have tried the latest code you suggested and still get a hang as if there is no event loop running. If I call iterate()
in the loop then I still only get the first utterance.
My usage of your suggested code can be viewed on GitHub (also a build of our app with your suggested code is in our experimental releases folder on Dropbox).
I will see if I can recreate the same problem outside of PyGUI and let you know the outcome.
OK. Now that I see the full context of your code, I believe I've spotted the problem. Your readline() call on the subprocess is in the same thread as the PyGUI event loop. readline() is a blocking call and it's in an infinite loop.
You have a couple options. You can move the subprocess reading to a separate thread and send it back to the main thread for output by pyttsx in the context of the PyGUI event loop. Alternatively, you can use the functions in the Python select module to do non-blocking polls of proc.stdout to determine if there's data to be read. If there is you can read what's there, say() it with pyttsx, and immediately return control to the PyGUI event loop.
First of all: apologies for making such a "d'oh!" error! Also thanks again for your patience and help. However I do have a question about your latest suggestion...
I can't use select()
on Windows (it would only work on sockets), so I thought about your suggestion of spinning off the launching and polling of the game process into a separate thread. That thread would then, via a Queue, send the output from the game to the main PyGUI/pyttsx thread for utterance, as it is generated. However, this seems to present the same problem as before: whilst the game is running, I need to loop (until the game process ends) on the Queue to wait for input that comes through. This would also surely block the PyGUI/pyttsx thread.
The constraints at the moment seem to be:
If I am wrong on the first one above, please let me know, as I would really like to be able to use pyttsx. Currently it looks, reluctantly, like I may be unable to due to constraint 2. Once again, thank you very much for your time and help on this.
Queue.get_nowait() is a non-blocking call. In the main thread running the PyGUI loop, you'll want to register a timed callback (a Task in PyGUI it looks like) and in that callback check if the queue has utterances to say. If it does, invoke say(utterance). If not, return immediately so that the main loop can continue.
In the second thread, stay in your block readline loop. When readline returns a value, shove it onto the queue for the main thread to pickup on its next Task callback.
Once again, many thanks for your time and help. Here is the actual finished and working code! :-)
Update (2017): that link no longer works as the file moved and has changed a lot since. I was asked for a working version of the link, so thought I'd put it here too (in case it's of any use to anyone in future). Here's that file at the time it was fixed (that's a helpful lesson: permalinks are good :-)).
Glad it worked and good luck with your project.
It seems a problem solved long time ago.
def iterate(self):
while True: # there should be a loop for the it
self._proxy.setBusy(False)
yield
I am trying to use pyttsx to speak everything that comes out of the stdout of a process (currently on Mac, but on Windows too in future). Because I have to loop over the stdout of the process, I am trying to use pyttsx with my own event loop. I believe I have followed the example given in the docs, but the speech output stalls after the first message. I have added some print statements to the pyttsx code and found that the
engine.iterate()
call does not appear to reach the driver I am using (Mac).I have put the TTS controller in a class and wrapped it so that the class can be used from a with statement, in order to ensure cleanup occurs.
This is how I am using the TTS, to speak everything that comes from the stdout of a process:
I only get the first message spoken, but I get all of them printed out, prefixed with "say: " as per the code above.
I noted from the documentation something about the event loop needing manual management, but it seems that
engine.iterate()
is the cross-platform way to do this. Wondering what I'm doing wrong :-).Thanks for writing this library; simple cross-platform TTS is just what I am looking for!