如何在 Tkinter 中实时显示 Vosk 语音识别结果?
数据小许
结论
不要在 Tkinter 主线程中运行持续读取音频的循环。题目中的代码先执行 while True,程序会一直停在循环里,无法到达 root.mainloop(),窗口事件也就无法正常处理。应将音频读取和 Vosk 识别放到工作线程,再用线程安全的 queue.Queue 将结果交给主线程,由主线程通过 root.after() 更新 Label。Tkinter 控件也应由创建它们的主线程操作。
下面的示例保留了 Model、KaldiRecognizer 和 PyAudio 的基本用法,并通过队列传递识别文本。
示例代码
import json
import queue
import threading
import tkinter as tk
from tkinter import ttk
import pyaudio
from vosk import KaldiRecognizer, Model
model = Model("vosk-model-small-cn-0.22")
rec = KaldiRecognizer(model, 16000)
p = pyaudio.PyAudio()
stream = p.open(
format=pyaudio.paInt16,
channels=1,
rate=16000,
input=True,
frames_per_buffer=8000,
)
result_queue = queue.Queue()
stop_event = threading.Event()
def recognize_audio():
"""在工作线程中读取音频并把识别文本放入队列。"""
try:
while not stop_event.is_set():
data = stream.read(4000, exception_on_overflow=False)
if rec.AcceptWaveform(data):
result = json.loads(rec.Result())
text = result.get("text", "")
else:
# PartialResult() 可用于显示尚未完成的识别内容。
result = json.loads(rec.PartialResult())
text = result.get("partial", "")
if text:
result_queue.put(text)
except OSError:
# 窗口关闭时音频流可能被关闭,此时结束工作线程。
pass
def update_label():
"""在 Tkinter 主线程中读取队列并更新控件。"""
try:
while True:
text = result_queue.get_nowait()
label.config(text=text)
except queue.Empty:
pass
if not stop_event.is_set():
root.after(50, update_label)
def close_window():
stop_event.set()
root.destroy()
root = tk.Tk()
root.title("Vosk 实时语音识别")
label = ttk.Label(root, text="正在等待语音…", wraplength=500)
label.pack(padx=16, pady=16)
root.protocol("WM_DELETE_WINDOW", close_window)
worker = threading.Thread(target=recognize_audio, daemon=True)
worker.start()
root.after(50, update_label)
root.mainloop()
stream.stop_stream()
stream.close()
p.terminate()
说明与注意事项
recognize_audio()在工作线程中执行stream.read()和 Vosk 识别,不会直接访问label或其他 Tkinter 控件。update_label()由root.after()安排在 Tkinter 主线程中运行,从队列取出文本并更新标签。不要在工作线程中调用label.config(),因为 Tkinter 通常要求在主线程中操作控件。- 识别器返回完整结果时,示例读取
Result();识别尚未完成时,则读取PartialResult(),因此标签也会显示临时识别文本。若只想显示完整句子,可以移除else分支。 stream.read()可能会等待音频数据返回,停止事件要等这次读取结束后才会被检查。示例使用较短的读取块,关闭窗口后线程通常会在下一次循环检查时退出。实际设备或参数不同,结束时间也可能不同。- 模型加载和音频流打开都发生在进入
mainloop()之前。如果初始化过程让窗口迟迟无法显示,可以将初始化移入后台线程,再通过队列把初始化完成或错误状态交回主线程。
备注:内容仅供参考。
