Created
December 13, 2025 12:55
-
-
Save am009/b85cd06576f896080fe53b28673503b5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| # | |
| # Copyright (c) 2025 Xiaomi Corporation | |
| """ | |
| This file demonstrates how to use sherpa-onnx Python APIs | |
| with VAD and non-streaming SenseVoice for real-time speech recognition | |
| from a microphone. | |
| Requirements: | |
| wget https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-int8-2025-09-09.tar.bz2 | |
| wget https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/silero_vad.onnx | |
| Usage: | |
| ./python-api-examples/simulate-streaming-sense-voice-microphone.py \ | |
| --silero-vad-model=./silero_vad.onnx \ | |
| --sense-voice=./sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/model.onnx \ | |
| --tokens=./sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/tokens.txt | |
| """ | |
| import argparse | |
| import queue | |
| import sys | |
| import threading | |
| import time | |
| from pathlib import Path | |
| import numpy as np | |
| import os | |
| import tkinter as tk | |
| script_path = os.path.realpath(__file__) | |
| script_dir = os.path.dirname(script_path) | |
| try: | |
| import sounddevice as sd | |
| import sherpa_onnx | |
| except ImportError: | |
| print("正在安装需要的python包:\n", file=sys.stderr) | |
| print("尝试执行: pip install sounddevice sherpa_onnx==1.12.19\n", file=sys.stderr) | |
| ret = os.system(f"{sys.executable} -m pip install sounddevice sherpa_onnx==1.12.19") | |
| if ret == 0: | |
| import sounddevice as sd | |
| import sherpa_onnx | |
| else: | |
| print("安装python包失败!!", file=sys.stderr) | |
| sys.exit(-1) | |
| vad_model_url = 'https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/silero_vad.onnx' | |
| vad_model_path = os.path.join(script_dir, "silero_vad.onnx") | |
| onnx_model_url = 'https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-int8-2025-09-09.tar.bz2' | |
| onnx_model_path = os.path.join(script_dir, "sherpa-onnx-sense-voice-zh-en-ja-ko-yue-int8-2025-09-09", "model.int8.onnx") | |
| tokens_txt_path = os.path.join(script_dir, "sherpa-onnx-sense-voice-zh-en-ja-ko-yue-int8-2025-09-09", "tokens.txt") | |
| killed = False | |
| recording_thread = None | |
| sample_rate = 16000 # Please don't change it | |
| # buffer saves audio samples to be played | |
| samples_queue = queue.Queue() | |
| def get_args(): | |
| parser = argparse.ArgumentParser( | |
| formatter_class=argparse.ArgumentDefaultsHelpFormatter | |
| ) | |
| parser.add_argument( | |
| "--device", | |
| type=int, | |
| default=-1, | |
| help="输入设备的编号", | |
| ) | |
| parser.add_argument( | |
| "--silero-vad-model", | |
| type=str, | |
| help="silero_vad.onnx 文件路径", | |
| default=vad_model_path | |
| ) | |
| parser.add_argument( | |
| "--tokens", | |
| type=str, | |
| help="模型的tokens.txt文件路径", | |
| default=tokens_txt_path | |
| ) | |
| parser.add_argument( | |
| "--sense-voice", | |
| default=onnx_model_path, | |
| type=str, | |
| help="SenseVoice模型的model.onnx文件", | |
| ) | |
| parser.add_argument( | |
| "--num-threads", | |
| type=int, | |
| default=2, | |
| help="用于推理的线程数,默认为2", | |
| ) | |
| parser.add_argument( | |
| "--hr-lexicon", | |
| type=str, | |
| default="", | |
| help="If not empty, it is the lexicon.txt for homophone replacer", | |
| ) | |
| parser.add_argument( | |
| "--hr-rule-fsts", | |
| type=str, | |
| default="", | |
| help="If not empty, it is the replace.fst for homophone replacer", | |
| ) | |
| return parser.parse_args() | |
| def assert_file_exists(filename: str): | |
| assert Path(filename).is_file(), ( | |
| f"{filename} does not exist!\n" | |
| "Please refer to " | |
| "https://k2-fsa.github.io/sherpa/onnx/pretrained_models/index.html to download it" | |
| ) | |
| def try_download_model(vad_model_path, onnx_model_path): | |
| if not os.path.exists(vad_model_path): | |
| print(f"请下载文件到:{vad_model_path}", file=sys.stderr) | |
| print(f"下载链接:{vad_model_url}", file=sys.stderr) | |
| if not os.path.exists(onnx_model_path): | |
| print(f"请下载并解压文件夹到:{onnx_model_path}", file=sys.stderr) | |
| print(f"下载链接:{onnx_model_url}", file=sys.stderr) | |
| def create_recognizer(args): | |
| assert_file_exists(args.sense_voice) | |
| recognizer = sherpa_onnx.OfflineRecognizer.from_sense_voice( | |
| model=args.sense_voice, | |
| tokens=args.tokens, | |
| num_threads=args.num_threads, | |
| use_itn=False, | |
| debug=False, | |
| hr_rule_fsts=args.hr_rule_fsts, | |
| hr_lexicon=args.hr_lexicon, | |
| ) | |
| return recognizer | |
| def start_recording(): | |
| # You can use any value you like for samples_per_read | |
| samples_per_read = int(0.1 * sample_rate) # 0.1 second = 100 ms | |
| with sd.InputStream(channels=1, dtype="float32", samplerate=sample_rate) as s: | |
| while not killed: | |
| samples, _ = s.read(samples_per_read) # a blocking read | |
| samples = samples.reshape(-1) | |
| samples = np.copy(samples) | |
| samples_queue.put(samples) | |
| class MyPrinter: | |
| def __init__(self): | |
| self.prev_result = "" | |
| def do_print(self, result): | |
| if result and self.prev_result != result: | |
| self.prev_result = result | |
| print(result, end='\n', flush=True) | |
| def on_endpoint(self): | |
| print("\n", end="", flush=True) | |
| def select_input_device(devices): | |
| """弹出tkinter窗口让用户选择输入设备""" | |
| # 过滤有输入通道的设备 | |
| input_devices = [(i, d) for i, d in enumerate(devices) if d['max_input_channels'] > 0 and d['hostapi'] == 0] | |
| if not input_devices: | |
| return None | |
| selected_device = [None] | |
| def on_ok(): | |
| selected_device[0] = device_var.get() | |
| root.destroy() | |
| root = tk.Tk() | |
| root.title("选择输入设备") | |
| device_var = tk.IntVar(value=input_devices[0][0]) | |
| for idx, device in input_devices: | |
| rb = tk.Radiobutton( | |
| root, | |
| text=f"{idx}: {device['name']}", | |
| variable=device_var, | |
| value=idx | |
| ) | |
| rb.pack(anchor='w', padx=20, pady=5) | |
| tk.Button(root, text="确定", command=on_ok).pack(pady=10) | |
| root.mainloop() | |
| return selected_device[0] | |
| def main(): | |
| devices = sd.query_devices() | |
| if len(devices) == 0: | |
| print("没有任何输入设备!", file=sys.stderr) | |
| sys.exit(0) | |
| args = get_args() | |
| print(devices, file=sys.stderr) | |
| # 如果命令行没有指定设备,弹出选择框 | |
| if args.device < 0: | |
| selected = select_input_device(devices) | |
| if selected is not None: | |
| sd.default.device[0] = selected | |
| else: | |
| sd.default.device[0] = args.device | |
| # 如果你想要选择其他的输入设备,请解除下面这行的注释,并将 xxx 改为设备的序号 | |
| # sd.default.device[0] = xxx | |
| # 注意要选设备结尾的in大于零的,比如 (2 in, 0 out) 表示两声道输入,没有输出声道,说明是录音设备。 | |
| # 如果想要识别系统声音,尝试启用“立体声混音”,并设置识别设备为它。 | |
| default_input_device_idx = sd.default.device[0] | |
| print(f'使用输入设备: {devices[default_input_device_idx]["name"]}', file=sys.stderr) | |
| try_download_model(args.silero_vad_model, args.silero_vad_model) | |
| assert_file_exists(args.tokens) | |
| assert_file_exists(args.silero_vad_model) | |
| assert args.num_threads > 0, args.num_threads | |
| print("正在启动识别器,请稍后", file=sys.stderr) | |
| recognizer = create_recognizer(args) | |
| config = sherpa_onnx.VadModelConfig() | |
| config.silero_vad.model = args.silero_vad_model | |
| config.silero_vad.threshold = 0.5 | |
| config.silero_vad.min_silence_duration = 0.1 # seconds | |
| config.silero_vad.min_speech_duration = 0.25 # seconds | |
| # If the current segment is larger than this value, then it increases | |
| # the threshold to 0.9 internally. After detecting this segment, | |
| # it resets the threshold to its original value. | |
| config.silero_vad.max_speech_duration = 8 # seconds | |
| config.sample_rate = sample_rate | |
| window_size = config.silero_vad.window_size | |
| vad = sherpa_onnx.VoiceActivityDetector(config, buffer_size_in_seconds=100) | |
| print("识别已启动,请说话", file=sys.stderr) | |
| buffer = [] | |
| global recording_thread | |
| recording_thread = threading.Thread(target=start_recording) | |
| recording_thread.start() | |
| # display = sherpa_onnx.Display() | |
| printer = MyPrinter() | |
| started = False | |
| started_time = None | |
| offset = 0 | |
| while not killed: | |
| samples = samples_queue.get() # a blocking read | |
| buffer = np.concatenate([buffer, samples]) | |
| while offset + window_size < len(buffer): | |
| vad.accept_waveform(buffer[offset : offset + window_size]) | |
| if not started and vad.is_speech_detected(): | |
| started = True | |
| started_time = time.time() | |
| offset += window_size | |
| if not started: | |
| if len(buffer) > 10 * window_size: | |
| offset -= len(buffer) - 10 * window_size | |
| buffer = buffer[-10 * window_size :] | |
| if started and time.time() - started_time > 0.2: | |
| stream = recognizer.create_stream() | |
| stream.accept_waveform(sample_rate, buffer) | |
| recognizer.decode_stream(stream) | |
| text = stream.result.text.strip() | |
| if text: | |
| printer.do_print(text) | |
| # display.update_text(text) | |
| # display.display() | |
| started_time = time.time() | |
| while not vad.empty(): | |
| # In general, this while loop is executed only once | |
| stream = recognizer.create_stream() | |
| stream.accept_waveform(sample_rate, vad.front.samples) | |
| vad.pop() | |
| recognizer.decode_stream(stream) | |
| text = stream.result.text.strip() | |
| # display.update_text(text) | |
| printer.do_print(text) | |
| buffer = [] | |
| offset = 0 | |
| started = False | |
| started_time = None | |
| # display.finalize_current_sentence() | |
| # display.display() | |
| printer.on_endpoint() | |
| if __name__ == "__main__": | |
| try: | |
| main() | |
| except KeyboardInterrupt: | |
| killed = True | |
| if recording_thread: | |
| recording_thread.join() | |
| print("\n检测到 Ctrl + C. 正在退出", file=sys.stderr) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment