nateshmbhat / pyttsx3

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

How to make Pyttsx3 stop talking / interrupt speech with new speech? #138

Open whogben opened 4 years ago

whogben commented 4 years ago

I am trying to interrupt pyttsx3 while it is talking to say something else.

I am using pyttsx3 with an external event loop, which works great - except for not being able to stop the speech and interrupt it. Here is some code demonstrating the issue:

import pyttsx3
import time

engine = pyttsx3.init()
engine.startLoop(False)

engine.say("knock knock, who's there")

start = time.time()

while time.time() - start < 1:
    engine.iterate()
    time.sleep(.01)

engine.stop()
engine.say('interrupting cow!')

while time.time() - start < 10:
    engine.iterate()
    time.sleep(.01)

engine.endLoop()

Expected behavior: "interrupting cow" is said partway through the "knock knock who's there" Actual behavior: "knock knock who's there" is said, and "interrupting cow" is never said

Does anyone have a workaround that would make it possible to interrupt pyttsx3 and have it speak something else? Am I misunderstanding the docs on this / is there a different approved way to interrupt it?

I've tried using endLoop and startLoop(false) again to create a new loop, but no luck there - that just causes "interrupting cow" to be said after the sentence, instead of interrupting it.

issue-label-bot[bot] commented 4 years ago

Issue-Label Bot is automatically applying the label bug to this issue, with a confidence of 0.52. Please mark this comment with :thumbsup: or :thumbsdown: to give our bot feedback!

Links: app homepage, dashboard and code for this bot.

whogben commented 4 years ago

not sure if this is a bug or there's a different approach

20Tauri commented 4 years ago

I not really familar with espeak but I don't understand how this could work. For example, with espeak implementation, the sentence is generated and played with aplay command. So, I don't see how the module can stop play the sound. Another problem, the stop() command call _espeak.IsPlaying() but as espeak no longer play sound due to the change the parameter AUDIO_OUTPUT_RETRIEVAL the function will always return False... and to stop() method do nothing.

All those changes seem to come with save_to_file()... Perhaps an old version work better...

TimCve commented 4 years ago

I worked out a method to stop the text to speech. Basically, if you press 'q' the test to speech stops. It uses multithreading so the code execution doesn't pause during the talking:

import multiprocessing
import pyttsx3
import keyboard

def sayFunc(phrase):
    engine = pyttsx3.init()
    engine.setProperty('rate', 160)
    engine.say(phrase)
    engine.runAndWait()

def say(phrase):
    if __name__ == "__main__":
        p = multiprocessing.Process(target=sayFunc, args=(phrase,))
        p.start()
        while p.is_alive():
            if keyboard.is_pressed('q'):
                p.terminate()
            else:
                continue
        p.join()

say("this process is running right now")
20Tauri commented 4 years ago

I have made the test. On Linux (eSpeak), the program returns but the whole text will be read.

And, if I add a started-word callback, the callback is called for each word before I heard the first word.

    engine.connect("started-word", on_word)

In fact, after a short evaluation, I no longer use this module.

It's sad because I like the libraries based approach of pyttsx module. Even if I chose to not use it, thank to all the maintainers, this library seem appreciated.

akashgupta77500 commented 3 years ago

Any answer regarding the given question. i am also facing the same problem.

janthmueller commented 2 years ago

That is my solution:

import multiprocessing
import pyttsx3
import time
from threading import Thread

def threaded(fn):
    def wrapper(*args, **kwargs):
        thread = Thread(target=fn, args=args, kwargs=kwargs)
        thread.start()
        return thread
    return wrapper

def speak(phrase):
    engine = pyttsx3.init()
    engine.say(phrase)
    engine.runAndWait()
    engine.stop()

def stop_speaker():
    global term
    term = True
    t.join()

@threaded
def manage_process(p):
    global term
    while p.is_alive():
        if term:
            p.terminate()
            term = False
        else:
            continue

def say(phrase):
    global t
    global term
    term = False
    p = multiprocessing.Process(target=speak, args=(phrase,))
    p.start()
    t = manage_process(p)

if __name__ == "__main__":
    say("this process is running right now")
    time.sleep(1)
    stop_speaker()
    say("this process is running right now")
    time.sleep(1.5)
    stop_speaker()

I got the decorator function from this https://stackoverflow.com/a/19846691 post. And the idea how to terminate the Process from @z3r0flag.

I had no problems so far with my early implementation within my home assistant. If someone comes up with a more efficient/structured solution i would be very happy if it is shared with me.

styles3544 commented 1 year ago

thanks a you guys for providing your solutions ^-^

lamdmorgan commented 3 months ago

`#Hi`` Guys, I added a change of voice to the above code and it works perfectly.

import multiprocessing
import pyttsx3
import time
from threading import Thread

def threaded(fn):
        def wrapper(*args, **kwargs):
             thread = Thread(target=fn, args=args, kwargs=kwargs)
             thread.start()
             return thread
        return wrapper

def speak(phrase,vc):
        engine = pyttsx3.init()
        voices = engine.getProperty("voices")
        engine.setProperty('voice', voices[vc].id)
        engine.say(phrase)
        engine.runAndWait()
        engine.stop()

def stop_speaker():
        global term
        term = True
        t.join()

@threaded
def manage_process(p):
                  global term
                  while p.is_alive():
                                    if term:
                                            p.terminate()
                                            term = False
                                    else:
                                            continue

def say(phrase,vc):
        global t
        global term
        term = False
        p = multiprocessing.Process(target=speak, args=(phrase,vc))
        p.start()
        t = manage_process(p)

if __name__ == "__main__":
        say("this process is running right now",1)#vc is voice ID
        time.sleep(4)
        stop_speaker()
        say("this process is running right now",0)
        time.sleep(4)
        stop_speaker()
flurixoww commented 3 months ago

That is my solution:

import multiprocessing
import pyttsx3
import time
from threading import Thread

def threaded(fn):
    def wrapper(*args, **kwargs):
        thread = Thread(target=fn, args=args, kwargs=kwargs)
        thread.start()
        return thread
    return wrapper

def speak(phrase):
    engine = pyttsx3.init()
    engine.say(phrase)
    engine.runAndWait()
    engine.stop()

def stop_speaker():
    global term
    term = True
    t.join()

@threaded
def manage_process(p):
  global term
  while p.is_alive():
      if term:
          p.terminate()
          term = False
      else:
          continue

def say(phrase):
  global t
  global term
  term = False
  p = multiprocessing.Process(target=speak, args=(phrase,))
  p.start()
  t = manage_process(p)

if __name__ == "__main__":
  say("this process is running right now")
  time.sleep(1)
  stop_speaker()
  say("this process is running right now")
  time.sleep(1.5)
  stop_speaker()

I got the decorator function from this https://stackoverflow.com/a/19846691 post. And the idea how to terminate the Process from @z3r0flag.

I had no problems so far with my early implementation within my home assistant. If someone comes up with a more efficient/structured solution i would be very happy if it is shared with me.

I can't understand why but when I'm calling these 2 functions in another Python file

def on_publicmsg(connection, event):
    """
    Called when a public message is received in the channel.
    Prints the message and triggers the dubbing function.
    """
    print(f"Message from {event.source.nick}: {event.arguments[0]}")
    chat_message = event.arguments[0]
    say(chat_message)
    time.sleep(1)
    stop_speaker()

It's just not working. It's opening the new tkinter window when the process is starting, it's even not starting the speech