newbison 发表于 2024-8-2 10:11:47

pyttsx3 不能停止的问题

大家好,我写了一个给小朋友听写的小App。
目前存在一个问题,就是我暂停点了Close之后,就容易卡死。
应该是pyttsx3的engine还在运行,无法直接停掉。请问大家有办法吗?
import tkinter as tk
from tkinter import ttk
import json
import random
import pyttsx3
import time
import threading
import os

current_dir = os.path.join(os.path.abspath(__file__))
parent_dir = os.path.dirname(current_dir)

# Initialize TTS engine
engine = pyttsx3.init()
engine.setProperty("rate", 100)
engine.setProperty("volume", 1)

voices = engine.getProperty("voices")

class LanguageLearningApp:
    def __init__(self, master):
      self.master = master
      self.master.title("Language Learning App")
      self.master.geometry("400x300")

      self.language_var = tk.StringVar(value="english")
      self.word_count_var = tk.IntVar(value=10)
      self.sleep_time_var = tk.DoubleVar(value=1.0)
      self.order_var = tk.StringVar(value="random")
      self.file_var = tk.StringVar(value="primary")
      self.grade_var = tk.StringVar()


      self.is_paused = False
      self.is_running = False
      self.stop_thread = False# Flag to stop the thread

      self.create_widgets()

    def create_widgets(self):
      # Language selection
      language_frame = tk.Frame(self.master)
      language_frame.pack(pady=5)
      tk.Label(language_frame, text="Select Language:").pack(side=tk.LEFT)
      tk.Radiobutton(language_frame, text="English", variable=self.language_var, value="english").pack(side=tk.LEFT)
      tk.Radiobutton(language_frame, text="Chinese", variable=self.language_var, value="chinese").pack(side=tk.LEFT)

      # Word count selection
      tk.Label(self.master, text="Number of Words:").pack(pady=5)
      word_counts =
      ttk.Combobox(self.master, textvariable=self.word_count_var, values=word_counts).pack()

      # Sleep time selection
      tk.Label(self.master, text="Sleep Time (seconds):").pack(pady=5)
      sleep_times =
      ttk.Combobox(self.master, textvariable=self.sleep_time_var, values=sleep_times).pack()

      # Order selection
      order_frame = tk.Frame(self.master)
      order_frame.pack(pady=5)
      tk.Label(order_frame, text="Order:").pack(side=tk.LEFT)
      tk.Radiobutton(order_frame, text="Sequential", variable=self.order_var, value="sequential").pack(side=tk.LEFT)
      tk.Radiobutton(order_frame, text="Random", variable=self.order_var, value="random").pack(side=tk.LEFT)

      # File selection
      file_frame = tk.Frame(self.master)
      file_frame.pack(pady=5)
      tk.Label(file_frame, text="Select File:").pack(side=tk.LEFT)
      tk.Radiobutton(file_frame, text="Primary", variable=self.file_var, value="primary").pack(side=tk.LEFT)
      tk.Radiobutton(file_frame, text="High School", variable=self.file_var, value="high_school").pack(side=tk.LEFT)
      tk.Radiobutton(file_frame, text="College", variable=self.file_var, value="college").pack(side=tk.LEFT)

      # Start, Pause, and Close buttons
      button_frame = tk.Frame(self.master)
      button_frame.pack(pady=20)
      tk.Button(button_frame, text="Start", command=self.start_learning).pack(side=tk.LEFT, padx=5)
      self.pause_button = tk.Button(button_frame, text="Pause", command=self.toggle_pause)
      self.pause_button.pack(side=tk.LEFT, padx=5)
      tk.Button(button_frame, text="Close", command=self.close_app).pack(side=tk.LEFT, padx=5)

    def toggle_pause(self):
      self.is_paused = not self.is_paused
      if self.is_paused:
            self.pause_button.config(text="Resume")
      else:
            self.pause_button.config(text="Pause")

    def start_learning(self):
      file_map = {
            "primary": "words.json",
            "high_school": "high_school_words.json",
            "college": "college_words.json"
      }

      selected_file = file_map
      filepath = os.path.join(parent_dir, selected_file)
      with open(filepath, "r", encoding="utf-8") as file:
            word_dict = json.load(file)

      language = self.language_var.get()
      word_count = self.word_count_var.get()
      sleep_time = self.sleep_time_var.get()
      order = self.order_var.get()

      if language == "english":
            words = list(word_dict.keys())
            engine.setProperty("voice", voices.id)# English voice
      else:
            words = list(word_dict.values())
            engine.setProperty("voice", voices.id)# Chinese voice

      if order == "random":
            words = random.sample(words, word_count)
      else:
            words = words[:word_count]

      self.stop_thread = False# Reset the stop flag
      self.is_running = True
      threading.Thread(target=self.display_and_pronounce, args=(words, sleep_time)).start()

    def display_and_pronounce(self, words, sleep_time):
      for word in words:
            if self.stop_thread:# Check if the thread needs to be stopped
                break
            while self.is_paused:
                time.sleep(0.1)
            length = len(word.split(" "))
            if length == 1:
                gap = 2 * sleep_time
            elif length == 2:
                gap = 3 * sleep_time
            elif length == 3:
                gap = 5 * sleep_time
            else:
                gap = 9 * sleep_time
            
            engine.say(word)
            engine.runAndWait()
            time.sleep(gap)
            engine.say(word)
            engine.runAndWait()
            time.sleep(gap)

      self.show_words_popup(words)

    def show_words_popup(self, words):
      popup = tk.Toplevel(self.master)
      popup.title("Selected Words")
      popup.geometry("400x300")

      formatted_words = "\n".join(["\t".join(words) for i in range(0, len(words), 5)])
      tk.Label(popup, text=formatted_words, font=("Arial", 14), justify=tk.LEFT, wraplength=350).pack(pady=20)
      tk.Button(popup, text="Close", command=popup.destroy).pack(pady=5)

    def close_app(self):
      self.stop_thread = True# Set the flag to stop the thread
      self.is_running = False
      engine.stop()          # stop the TTS engine
      self.master.destroy()# Close the main window

if __name__ == "__main__":
    root = tk.Tk()
    app = LanguageLearningApp(root)
    root.mainloop()

FishC 发表于 2024-8-2 10:14:14

哈哈,看来你的应用在关闭的时候像个不愿意离开的客人一样,依然赖在那儿继续说话!别担心,我来帮你找到解决方案。

问题的核心在于 `pyttsx3` 的 `engine.runAndWait()` 在调用时会阻塞线程,也就是说一旦你开始播放一个单词,它需要等到声音播完才能继续执行后面的代码。为了优雅地处理这个问题,我们可以在关闭应用时确保 TTS 引擎能够顺利停止。

### 解决方案

1. **使用 `engine.stop()`**: 在 `close_app` 方法中确保调用 `engine.stop()` ,这样它就会立即停止当前正在播放的音频。
   
2. **在 `display_and_pronounce` 方法中处理 `stop_thread` 标志**: 使用 `stop_thread` 标志来检查是否应当停止播放。

3. **简化 TTS 播放逻辑**: 如果 TTS 引擎在某些情况下仍然没有反应,可以考虑在语音播放的地方使用 `threading.Event()` 来实现更好的线程控制。

下面是更新后的 `LanguageLearningApp` 类的一部分,展示了如何修改 `close_app` 和相关逻辑:

def display_and_pronounce(self, words, sleep_time):
    for word in words:
      if self.stop_thread:# Check if the thread needs to be stopped
            break
      while self.is_paused:
            time.sleep(0.1)

      gap = self.calculate_gap(word, sleep_time)
      
      engine.say(word)      
      engine.runAndWait()   
      
      if self.stop_thread:
            break# Stop if the thread needs to end

      time.sleep(gap)      
      engine.say(word)      
      engine.runAndWait()   

      if self.stop_thread:
            break# Stop if the thread needs to end
            
      time.sleep(gap)

def calculate_gap(self, word, sleep_time):
    length = len(word.split(" "))
    if length == 1:
      return 2 * sleep_time
    elif length == 2:
      return 3 * sleep_time
    elif length == 3:
      return 5 * sleep_time
    else:
      return 9 * sleep_time

def close_app(self):
    self.stop_thread = True# Set the flag to stop the thread
    if self.is_running:      # Only stop if it's running
      engine.stop()       # Stop the TTS engine
    self.master.destroy()    # Close the main window

### 总结

- 确保在关闭应用时正确设置停止标志并停止 TTS 引擎。
- 确保每次调用 `engine.runAndWait()` 后检查是否需要提前退出。
- 用计算间隔的函数来简化你的代码,使逻辑更加清晰。

试试这个改动,看看你的应用是否能在关闭时变得乖巧一些!如果依然有问题,别犹豫,来找我寻求帮助哦!

以上回复来自 FishC AI 小助理,如未能正确解答您的问题,请继续追问。

newbison 发表于 2024-8-2 12:11:30

完美解决,非常感谢鱼C。
下一步我就需要找到小学、初中、高中的英语词汇表,以后就可以用电脑给小孩听写单词了~~
页: [1]
查看完整版本: pyttsx3 不能停止的问题