nateshmbhat / pyttsx3

Offline Text To Speech synthesis for python
Mozilla Public License 2.0
2.06k stars 326 forks source link

Library seems not to clean up properly #3

Open E14 opened 6 years ago

E14 commented 6 years ago

The very simple program below results in Windows killing the thread:

import pyttsx3
engine = pyttsx3.init("sapi5")
engine.say('This program will self-destruct in 10; 9; 8; 7.')  # ...
engine.runAndWait()

image

I debugged it, and it does not throw any errors whatsoever, only when I exit the debugger will Windows once again cry out that the program crashes:

PS> python.exe -m pdb .\TTS.py
> .\tts.py(1)<module>()
-> from pyttsx3 import init
(Pdb) continue
The program finished and will be restarted
> .\tts.py(1)<module>()
-> from pyttsx3 import init
(Pdb) continue
The program finished and will be restarted
> .\tts.py(1)<module>()
-> from pyttsx3 import init
(Pdb) continue
The program finished and will be restarted
> .\tts.py(1)<module>()
-> from pyttsx3 import init
(Pdb) exit ## Thread blocks and Windows will kill python thread here

Hence the suspicion that the cleanup does not work properly

josephalway commented 6 years ago

The code works fine for me in my Python Editor. Tested with PyCharm Community Edition, Python 3.6, and latest version of pyttsx3.

E14 commented 6 years ago

It applied to python 3.6.2 and pyttsx3-2.6 and the then-fully-patched version of Windows.

However, I can't reproduce the same behaviour now. Neither with pyttsx3-2.6, nor pyttsx3-2.7 . What still consistently happens is that the program will not end properly - if you put a simple debug output right after engine.runAndWait(), it will block for about 5 seconds after it has printed the debug message.

Or if you go the debugging route - the script will end, and when you exit the debugger, it will block for about 5 seconds before ending. This part is consistent with previous behaviour, where it would block for about 5 seconds before crashing.

Maybe a Windows update in the meantime has helped here to at least prevent the crashing...

josephalway commented 6 years ago

I used the exact script you posted and it still runs correctly for me. Current version of Python, Pyttsx3, and Windows 10 64-bit. I don't get a python has stopped working pop-up and it doesn't take "extra time" to close.

What is odd though, is that you're getting "from pyttsx3 import init" in your debugger. While I am getting "import pyttsx3" in my debugger.

I tried using "from pyttsx3 import init" instead of "import pyttsx3" as the import statement and it failed to run the script with more errors. So, not sure what you're doing that is making it not work.

Are you using plain Python? Anaconda? Or something else?

E14 commented 6 years ago

I'm using python 3.6.x installed on Windows directly from python.org binary distribution

I might have run a slightly different script in the debugger with from x import y style imports - not sure now, but I have tested the posted code many times (I had to properly time the countdown after all, for it to crash at the right time :P (timing fluctuated though...))

It's certainly weird. Do you know, is there some way to call the win32 "api close" procedures directly? I could not find any documentation on it and the try&fail path resulted only in fail...

josephalway commented 6 years ago

I am unable to reproduce the crash you saw and unable to detect a significant time block.

Assuming, there's an issue, it's probably something to do with whatever driver you're using.

So, possibly something to do with the sapi5 driver wrapper in the drivers/sapi5.py file.

I'm using threading with a tkinter gui to get pyttsx3 to work without blocking all interaction with the user interface. Though, I may be switching to using something like mulitprocessing, because I still have to wait for the utterance to finish, before starting another one. Due to there not being a way to terminate a thread manually. Whereas you can use the OS's process kill function with multiprocessing.

I do notice about a second delay between the end of the sound and when I can start another say. Though, I'm assuming this is partly due to the "clean up" of the thread. I check to see, if the thread .isAlive and only continue, if it's not "alive".

E14 commented 6 years ago

Ok... what other API are you using on Windows other than sapi5?

josephalway commented 6 years ago

sapi5 is the driver pyttsx3 uses on Windows. I'm guessing, it's something to do with the sapi5.py file, because that's what actually initiates the loop and tells the sapi5 driver what to do. Assuming, there's anything actually wrong with pyttsx3.

If there's anything wrong with sapi5 itself, that would be something Microsoft would have to fix.

You could do as I'm planning to do and implement multiprocessing in your code, so you can just kill the process, if it's still running.

josephalway commented 6 years ago

Ok, sorry, I was able to verify that when running the code from the Python interpreter the interpreter crashes when you exit.

The crash only occurs when you specify sapi5 in the init statement. This didn't crash my debugger in PyCharm.

Example That Doesn't Crash: Please note all I removed is "sapi5" from the pyttsx3.init() statement.

import pyttsx3
engine = pyttsx3.init()
engine.say('This program will self-destruct in 10; 9; 8; 7.')  # ...
engine.runAndWait()

Not exactly sure what's going on there, but something isn't handled right when you call it like that. Offending Code: engine = pyttsx3.init("sapi5")

The Fix: (I guess, the workaround.) engine = pyttsx3.init()

josephalway commented 6 years ago

Weird thing: engine = pyttsx3.init("sapi5", debug=False) and engine = pyttsx3.init("sapi5", debug=True)

Also, fix the issue.

josephalway commented 6 years ago

Not 100% sure what debug does, because setting it to True didn't give me any output as far as I know.

E14 commented 6 years ago

From reading the source and testing, I don't think adding or not adding "sapi5" has any meaning in the current version. Neither causes Windows to stop the process anymore. The "block time" at the end is however very randomly adding about 5 seconds -- or not.

debug is immediately set to False when omitted, then mostly passed down the chain and, when set to True, is supposed to print stack traces of exceptions if they are encountered. However... As far as I could see this argument is not used at all in the sapi5 driver, so has also no effect at all.

Two test files:

# TTS.py
import pyttsx3
engine = pyttsx3.init("sapi5")
engine.say('1')  # ...
engine.runAndWait()
print("End.")
# TTS-nosapi.py
import pyttsx3
engine = pyttsx3.init()
engine.say('1')  # ...
engine.runAndWait()
print("End.")

Commands (PS):

for ($i=0; $i -lt 100; $i++) {Measure-Command -Verbose { python .\TTS.py } |
select TotalSeconds}
for ($i=0; $i -lt 100; $i++) {Measure-Command -Verbose { python .\TTS-nosapi.py } |
select TotalSeconds}

I increased the count to 100 after initial testing, as the results vary significantly on low counts. Results attached... if you're interested.

If I'd to guess, I would say the problem lies here: https://github.com/nateshmbhat/pyttsx3/blob/cdeff8a5d9171245e4b2b48dc6065b407db328f1/pyttsx3/drivers/sapi5.py#L43-L44 Just setting an internal value is not likely to make Windows unregister the event, and Windows probably wants to hold on to the process until it gets to clean up its event queues - which could be every 5 seconds or so...

josephalway commented 6 years ago

I tested your example and I was getting varying times from 2.0 seconds and 2.1 seconds, but it would occasionally crash.

The destroy function doesn't initiate the end. It's a confusing name. There's also the stop and endLoop functions that are part of the engine stopping.

I did some searching and found that someone else working on SAPI5 with python had fixed a "lag" at the end of a speak. From what I could tell they used ISpAudio and SetState to tell the SAPI5 driver to end the audio stream.

Update: The following suggestion was incorrect and bad form. Something like it might be useful, but my code was all kinds of wrong. I added a line above line 57 in the sapi5.py file: self._tts.ISpAudio.SetState(1,0)

Could you see, if that solves your problem?

Adding that line, I no longer get the occasional crash (it was kind of regular, like once every 10 commands or so) and the time for each command seems to be more consistent.

E14 commented 6 years ago

I tested your example and I was getting varying times from 2.0 seconds and 2.1 seconds, but it would occasionally crash.

It is really weird, that your system behaves so differently. I'm starting to think that we're seeing two unrelated problems...

The destroy function doesn't initiate the end.

I did not go by name: DriverProxy defines a __del__ method, that calls driver.destroy: https://github.com/nateshmbhat/pyttsx3/blob/cdeff8a5d9171245e4b2b48dc6065b407db328f1/pyttsx3/driver.py#L86-L90

And I could find no other cleanup methods defined.

Could you see, if that solves your problem?

As far as I can tell, the stop method is normally not called, and is meant to finish talking early (otherwise it will return instantly). Also looping happens only during runAndWait(), which means my issue is outside of looping.

Nevertheless, I have tried and the results are the same. I also tried adding the line to destroy(), but got similar results to before. Actually, I don't think that field (ISpAudio) even exists in (my) sapi5 driver, though it only sometimes throws errors (...)

But wait, there is more!

While writing this, I did spot a close method in the event handle that is returned from withEvents (self._advise), so I changed the destroy method of sapi5.py to:

    def destroy(self):
        self._tts.EventInterests = 0
        self._advise.close()

This change achieved consistent 1.4-ish seconds execution time and still no crashes. Incidentally this aligns fully with my original assumption, which makes me want to like it (*cough ...)

Anyway, I still think this should be taken with a pinch of salt, all of this is very voodoo. Even though it does make sense, the basically non-existent documentation on most of the win32com stuff makes me itch.

Finally... I think I need to point out that I don't think my problem is a bug. The crashing was bad, but a random 5 seconds delay after the utterance is just weird and doesn't bother me. At this point I'm just curious of the weird behaviour ;)