pyttsx3 icon indicating copy to clipboard operation
pyttsx3 copied to clipboard

can't get pyttsx3 to stop talking mid utterance more than one time

Open Sleuth56 opened this issue 5 years ago • 15 comments

I can get it to stop the first time with this error AttributeError: 'Engine' object has no attribute '_pump' if I comment this out pyttsx3.driver.DriverProxy.setBusy(engine, busy=False) it works but then when I try to do it again it doesn't work I've looked through all the docs I can find but nothing fixed it any thoughts or suggestions would be appreciated. This is the code it's a smaller part of a bigger project so there's a lot of extra code.

#!/usr/bin/env python3

#put imports here
import pyttsx3
import _thread	

SpeekOnOff = True
engine = pyttsx3.init()

#use under scores '_' insted of spaces
def quorry():
  return ['stop', 'shutup', 'shut_up']


def f(text):
  if SpeekOnOff == True:
    rate = engine.getProperty('rate')
    engine.setProperty('rate', rate-50)
    engine.say(text)
    engine.runAndWait()
  else:
    print(text)

def Speek(text):
  _thread.start_new_thread( f, (text,) )


def stop():
  pyttsx3.engine.Engine.stop(engine)
  pyttsx3.driver.DriverProxy.setBusy(engine, busy=False)



#put the code specific to making your code work with Steve
#exp. removing all the text from the string that isn't needed
def Steve(quorry=''):
  stop()

if __name__ == '__main__':
  while True:
    Speek(input(': '))

Sleuth56 avatar Jan 01 '19 23:01 Sleuth56

This is something I would like to do as well, but I've not put significant effort into making it work. Near as I can tell, it's a feature that's not actually in pyttsx3. So, you would actually have to modify pyttsx3 to make it work. A possible workaround would be to kill the thread somehow and then create a new one every time you have it speak something. Though, I guess that's what you're trying to do.

josephalway avatar Feb 06 '19 22:02 josephalway

I think it starts it's own thread because my thread only set it up and runs it. I wonder if I kill my thread from the main one if it will take the pyttsx one with it?

Sleuth56 avatar Feb 06 '19 22:02 Sleuth56

Theoretically, it should. Though, when I was looking into killing threads, I kept seeing how it wasn't a good thing to do. So, I've not gotten back to it, yet.

josephalway avatar Feb 06 '19 22:02 josephalway

Looking at it more, there is a "connect" function, so perhaps this kind of functionality is implemented, but not with the basic say command.

josephalway avatar Feb 06 '19 22:02 josephalway

You can look at the engine.py file yourself for some detailed notes. https://github.com/nateshmbhat/pyttsx3/blob/master/pyttsx3/engine.py

josephalway avatar Feb 06 '19 22:02 josephalway

Wonder if endLoop would work

Sleuth56 avatar Feb 06 '19 22:02 Sleuth56

Have you looked at the examples here? https://pyttsx3.readthedocs.io/en/latest/engine.html#examples

josephalway avatar Feb 06 '19 23:02 josephalway

Thanks for the help, I think that has the answer I was looking for don't know why I didn't see that before I did read those docs.

Sleuth56 avatar Feb 06 '19 23:02 Sleuth56

Cool, hopefully you get it to do what you want. I would like to see what example you went with or how you implemented it, if you're willing to share.

josephalway avatar Feb 06 '19 23:02 josephalway

Of course when I have it done it will be on my github https://github.com/lerker100/Steve it might be a while until I have it done.

Sleuth56 avatar Feb 06 '19 23:02 Sleuth56

Looking at the code examples more, it should be a simple matter of using engine.connect and linking it to a function that stops the engine, if it meets a certain criteria.

Example: (This example triggers onWord, every word it speaks. So, you should be able to interrupt the speech at the end / beginning? of each word spoken.)

import pyttsx3


def onWord(name, location, length):
    print('word', name, location, length)
    if location > 10:
        engine.stop()


engine = pyttsx3.init()
rate = engine.getProperty('rate')
engine.setProperty('rate', rate - 160)
engine.connect('started-word', onWord)
engine.say('The quick brown fox jumped over the lazy dog.')
engine.runAndWait()

josephalway avatar Feb 07 '19 19:02 josephalway

I was having the same issues, trying to thread the engines loop has been tricky. It seems that if you try to use the engine.connect method and thread the engine, the engine can't find the callback function it needs.

Here is how I worked around it:

import pyttsx3
import logging
from time import sleep
from multiprocessing.dummy import Process as Thread
#from threading import Thread

logger = logging.getLogger(__name__)

class VoiceBox(object):
    def __init__(self):
        self.t = None
        self._running = False
        self.engine = None

    def _processSpeech(self, text):
        self.engine = pyttsx3.init()
        self.engine.say(str(text))
        self.engine.startLoop(False)
        while self._running:
            self.engine.iterate()
        logger.debug('Thread loop stopped')

    def say(self, text, noInter=2):
        # check if thread is running
        if self.t and self._running:
            logger.debug('Interupting...')
            # stop it if it is
            self.stop()
        # iterate speech in a thread
        logger.debug('Talking: %s', text)
        self.t = Thread(target=self._processSpeech, args=(text,))
        self._running = True
        self.t.daemon = True
        self.t.start()
        # give the thread some space
        # without this sleep and repeatitive calls to 'say'
        # the engine may not close properly and errors will start showing up
        sleep(noInter)

    def stop(self):
        self._running = False
        try:
            self.engine.endLoop()
            logger.debug('Voice loop stopped')
        except:
            pass
        try:
            self.t.join()
            logger.debug('Joined Voice thread')
        except Exception as e:
            logger.exception(e)



if __name__ == '__main__':
    logging.basicConfig()
    logger.setLevel(logging.DEBUG)
    text = '''
    Hobsbawm joined the Communist Party in 1936 and stayed in it for about fifty years. Not only did the cause to which he had devoted his life expire in infamy but the rubbish that it had promised to sweep from the stage-ethnic and national chauvinism-would, in time, make a new bid for legitimacy.
    '''
    text2 = '''
    pepe hands
    '''
    v = VoiceBox()
    v.say(text, 5)
    v.say(text2)
    # I would like to have v.say() know when it is finished talking
    # kill the voice engine and join the talking thread.
    #v.stop()

I put the engine's iterate loop in a thread using engine.startLoop(False) to start it and engine.endLoop() to stop it (works mid utterance). So now every time you call the VoiceBox.say() method it will stop an utterance if it is currently speaking and say the new utterance.

s4w3d0ff avatar May 14 '19 18:05 s4w3d0ff

The onWord() callback doesn't work, at least not in python 3.8.5. It doesn't trigger for every word, it triggers once when utterance is finished.

TimCve avatar Sep 02 '20 12:09 TimCve

I am having the same issue too. When I engine.stop() and try to call the function the second time, the text to speech doesn't seem to work.

import pyttsx3
engine = pyttsx3.init()

def texttospeech(text):
    engine.stop()

    engine.setProperty("rate", 200)  
    engine.setProperty('voice', engine.getProperty('voices')[1].id)
    engine.say(text)
    
    engine.runAndWait()

def texttospeech_stop(event):
    engine.stop()

thenithinbalaji avatar Feb 14 '22 04:02 thenithinbalaji

I think there is something funky with the way it sets itself busy here and checks whether it's busy. At the end of endLoop it sets itself as busy and I don't see it set itself as not busy anywhere.

Gallion avatar Jan 03 '23 15:01 Gallion