Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question on use of engine.iterate() #11

Closed
matatk opened this issue Apr 18, 2013 · 13 comments
Closed

Question on use of engine.iterate() #11

matatk opened this issue Apr 18, 2013 · 13 comments

Comments

@matatk
Copy link

matatk commented Apr 18, 2013

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.

class SpeakerManager(object):
    """Provide a context to for using Speaker in with statements."""
    def __enter__(self):    
        class Speaker(object):
            """Performs Text-To-Speech"""
            def __init__(self):
                self._engine = pyttsx.init()
                self._engine.startLoop(False)

            def say(self, message):
                sys.stdout.write("say: " + message)
                self._engine.say(message)
                self._engine.iterate()

            def cleanup(self):
                self._engine.endLoop()

        self.speaker = Speaker()
        return self.speaker

    def __exit__(self, type, value, traceback):
        self.speaker.cleanup()

This is how I am using the TTS, to speak everything that comes from the stdout of a process:

def launch(command_line):
    proc = subprocess.Popen(command_line, stdout=subprocess.PIPE)
    with SpeakerManager() as speaker:
        while True:
            retcode = proc.poll()
            line = proc.stdout.readline()
            speaker.say(line)
            if retcode is not None:
                break

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!

@parente
Copy link
Collaborator

parente commented Apr 22, 2013

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.

@matatk
Copy link
Author

matatk commented Apr 22, 2013

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 :-)).

@matatk
Copy link
Author

matatk commented Apr 24, 2013

I have tried a couple of different approaches, but each has presented further problems:

  • I tried using multiprocessing to spin off a separate process that contains the pyttsx loop and feed it things to say via a Queue. I got this error message in the terminal when calling 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().
  • I then tried the same approach but using threading instead and got this error in the terminal when calling 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.

@parente
Copy link
Collaborator

parente commented Apr 24, 2013

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

@matatk
Copy link
Author

matatk commented Apr 24, 2013

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.

@parente
Copy link
Collaborator

parente commented Apr 24, 2013

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.

@matatk
Copy link
Author

matatk commented Apr 25, 2013

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.

@parente
Copy link
Collaborator

parente commented Apr 26, 2013

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.

@matatk
Copy link
Author

matatk commented Apr 26, 2013

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:

  1. I can't put both the game process loop and pyttsx in a separate process or thread, because I get the OS X/pyttsx event loop errors encountered above.
  2. I can't switch to using Tkinter because on the Mac this is inaccessible to people using the VoiceOver screen reader to access the game.

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.

@parente
Copy link
Collaborator

parente commented Apr 26, 2013

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.

@matatk
Copy link
Author

matatk commented Apr 26, 2013

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 :-)).

@parente
Copy link
Collaborator

parente commented Apr 28, 2013

Glad it worked and good luck with your project.

@opalm
Copy link

opalm commented May 27, 2016

It seems a problem solved long time ago.

Recently I got the same problem on OS X, and I found there might a solution by modify the nsss.py below:

def iterate(self):
    while True:  # there should be a loop for the it
        self._proxy.setBusy(False)
        yield

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants