三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

基于Whisper与Python构建本地化语音输入法:从原理到实践

基于Whisper与Python构建本地化语音输入法:从原理到实践

最近在折腾一些语音识别相关的项目时,发现很多现成的方案要么太重,要么太贵,要么就是隐私性不够好。于是萌生了自己动手,用一些轻量级工具和开源模型,搭建一个本地化、可定制的“废物”语音输入法的想法。这里的“废物”并非贬义,而是指它可能不够完美、功能简单,但足够轻便、可控,能满足特定场景下的基础需求。

本文就将围绕如何从零开始,一步步构建这样一个本地语音输入法原型展开。整个过程会涉及语音采集、模型选择与调用、文本处理以及简单的界面集成。适合对Python有一定基础,并且对语音技术、本地化AI应用感兴趣的开发者。通过本文,你将掌握如何利用开源工具链,在不依赖大型云服务的情况下,实现一个基本的语音转文字功能模块。

1. 背景与核心概念:为什么需要本地语音输入法?

在深入代码之前,我们有必要厘清几个核心概念和做这件事的动机。

语音识别(ASR, Automatic Speech Recognition)的核心任务是将人类语音中的词汇内容转换为计算机可读的文本。主流的实现方式分为云端和本地两种。

  • 云端ASR:如各大厂商提供的语音识别API。优势是识别率高、功能丰富(如方言、实时流式识别),但缺点也很明显:需要网络、存在延迟、有调用费用,并且语音数据需要上传到第三方服务器,涉及隐私和安全问题。
  • 本地ASR:在用户自己的设备上完成识别。优势是离线可用、零延迟、数据完全私有。传统的挑战在于模型精度和速度,但随着像Whisper这类优秀开源模型的出现,本地ASR的实用性大大增强。

我们所要构建的“废物语音输入法”,其核心就是一个本地ASR应用。它不追求媲美商业产品的识别率和丰富功能,而是聚焦于轻量、隐私、可定制和低成本。典型的应用场景包括:

  • 在断网环境下进行文字记录。
  • 处理敏感内容的语音转录,不希望数据出本地。
  • 作为学习项目,理解ASR的工作流程。
  • 为特定领域(如某个专业术语库)定制简单的识别能力。

2. 环境准备与版本说明

工欲善其事,必先利其器。我们先来搭建开发环境。本文示例主要使用Python,因为它有丰富的AI生态库。

操作系统:Windows 10/11, macOS 或 Linux (Ubuntu 20.04+) 均可。本文命令以Linux/macOS的bash和Windows的PowerShell为例。Python版本:推荐使用Python 3.8 到 3.10。部分深度学习库对新版本支持可能滞后。主要依赖库

  • PyAudio/sounddevice: 用于音频采集。
  • wave/soundfile: 用于音频文件读写。
  • openai-whisper: OpenAI开源的语音识别模型。
  • faster-whisper: 一个Whisper的优化实现,使用CTranslate2,速度更快,内存占用更少(推荐)。
  • PyQt5/Tkinter: 用于构建简单的图形界面(可选)。
  • pynput/pyautogui: 用于模拟键盘输入,将识别文本“输入”到其他应用(可选)。

版本说明:以下版本在撰写时经过测试,但深度学习领域更新较快,请根据实际情况调整。

# 创建虚拟环境(推荐) python -m venv asr_env source asr_env/bin/activate # Linux/macOS # asr_env\Scripts\activate # Windows # 安装核心依赖 pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu # 如果无GPU,安装CPU版本 # 或者根据你的CUDA版本安装GPU版本,例如 pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu118 pip install faster-whisper # 推荐使用这个,效率更高 # 或者安装原版 whisper: pip install openai-whisper pip install sounddevice numpy # 用于实时录音 pip install pynput # 用于模拟键盘输入

项目结构预览

local_asr_input_method/ ├── main.py # 主程序入口 ├── asr_core.py # 语音识别核心逻辑 ├── audio_utils.py # 音频录制与处理工具 ├── ui.py # 图形界面(可选) ├── config.yaml # 配置文件 ├── requirements.txt # 依赖列表 └── models/ # 存放下载的Whisper模型(可选)

3. 核心组件与原理拆解

我们的系统主要由三个部分组成:音频采集语音识别文本输出

3.1 音频采集:如何录制声音?

我们需要从麦克风捕获音频数据。sounddevice库提供了一个简洁的接口。

# audio_utils.py import sounddevice as sd import numpy as np import soundfile as sf import threading import queue import time class AudioRecorder: def __init__(self, samplerate=16000, channels=1, dtype='float32'): """ 初始化录音器。 :param samplerate: 采样率,Whisper模型通常使用16000 Hz。 :param channels: 声道数,1为单声道。 :param dtype: 音频数据类型。 """ self.samplerate = samplerate self.channels = channels self.dtype = dtype self.is_recording = False self.audio_queue = queue.Queue() self.stream = None def _audio_callback(self, indata, frames, time, status): """这是sounddevice录音流的回调函数,每次有音频数据块就会调用。""" if status: print(f"录音状态: {status}") # 将音频数据放入队列,供其他线程消费 self.audio_queue.put(indata.copy()) def start_recording(self): """开始录音,打开音频输入流。""" if self.is_recording: print("已经在录音中...") return self.is_recording = True self.audio_queue = queue.Queue() # 清空队列 # 打开输入流,指定回调函数 self.stream = sd.InputStream( samplerate=self.samplerate, channels=self.channels, dtype=self.dtype, callback=self._audio_callback ) self.stream.start() print("录音开始...") def stop_recording(self): """停止录音,关闭流,并返回所有录制的音频数据。""" if not self.is_recording: print("未在录音。") return None self.is_recording = False if self.stream: self.stream.stop() self.stream.close() self.stream = None print("录音停止。") # 从队列中取出所有音频数据块并拼接 audio_chunks = [] while not self.audio_queue.empty(): try: chunk = self.audio_queue.get_nowait() audio_chunks.append(chunk) except queue.Empty: break if audio_chunks: # 沿着时间轴(第0轴)拼接 audio_data = np.concatenate(audio_chunks, axis=0) return audio_data else: return None def record_for_duration(self, duration_seconds=5): """录制指定时长的音频(简化版,非流式)。""" print(f"开始录制 {duration_seconds} 秒...") audio_data = sd.rec( int(duration_seconds * self.samplerate), samplerate=self.samplerate, channels=self.channels, dtype=self.dtype ) sd.wait() # 等待录制完成 print("录制完成。") return audio_data

关键参数解释

  • samplerate=16000: Whisper模型训练时使用的采样率是16kHz,保持一致能获得最好效果,也减少计算量。
  • channels=1: 单声道足以满足语音识别需求,且数据量减半。
  • callback模式 vssd.rec模式:callback模式适合需要实时处理或长时间录制的场景(如“按住说话”)。sd.rec模式更简单,适合录制固定时长的片段。

3.2 语音识别:调用 Whisper 模型

我们将使用faster-whisper,因为它比原版whisper效率更高。它支持多种模型尺寸:tiny,base,small,medium,large-v2。模型越大越准,但也越慢。对于本地“废物”输入法,basesmall是平衡的选择。

# asr_core.py from faster_whisper import WhisperModel import numpy as np class ASREngine: def __init__(self, model_size="base", device="cpu", compute_type="int8"): """ 初始化ASR引擎。 :param model_size: 模型大小,可选 "tiny", "base", "small", "medium", "large-v2" :param device: 运行设备,"cpu" 或 "cuda" :param compute_type: 计算精度,可选 "int8", "float16", "float32"。int8量化可大幅减少内存和提升速度,精度略有损失。 """ print(f"正在加载 Whisper {model_size} 模型到 {device}...") # 首次运行会自动从Hugging Face Hub下载模型 self.model = WhisperModel(model_size, device=device, compute_type=compute_type) print("模型加载完毕。") def transcribe_audio(self, audio_numpy_array, samplerate=16000): """ 将numpy格式的音频数据转换为文字。 :param audio_numpy_array: 形状为 (samples,) 或 (samples, channels) 的numpy数组。 :param samplerate: 音频采样率。 :return: 识别出的文本字符串。 """ # 确保是单声道,并转换为float32 if audio_numpy_array.ndim > 1: audio_numpy_array = audio_numpy_array.mean(axis=1) # 多声道取平均 audio_numpy_array = audio_numpy_array.astype(np.float32) # faster-whisper 支持直接传入numpy数组和采样率 segments, info = self.model.transcribe( audio_numpy_array, beam_size=5, # 束搜索大小,影响准确性和速度 language="zh", # 指定语言为中文,可加快识别并提高准确性 vad_filter=True, # 启用语音活动检测(VAD)过滤,能有效去除静音段 initial_prompt="以下是普通话语音。" # 可选的初始提示,引导模型风格 ) # segments是一个生成器,包含带时间戳的片段。我们拼接所有文本。 full_text = "".join([segment.text for segment in segments]) return full_text.strip() def transcribe_file(self, audio_file_path): """直接转录音频文件。""" segments, info = self.model.transcribe( audio_file_path, language="zh", vad_filter=True ) full_text = "".join([segment.text for segment in segments]) return full_text.strip()

为什么选择 faster-whisper?

  1. 效率:使用CTranslate2运行时,推理速度比原版快4倍,内存占用减半。
  2. VAD过滤:内置语音活动检测,能自动跳过静音部分,对于有停顿的长语音效果更好。
  3. 流式支持:虽然本例未展示,但它支持流式转录,为未来实现“实时字幕”功能留有余地。

3.3 文本输出:模拟键盘输入

识别出文本后,我们需要将它“输入”到任何光标所在的位置(如记事本、浏览器)。pynput库可以模拟键盘事件。

# text_output.py from pynput.keyboard import Controller, Key import time class TextInjector: def __init__(self): self.keyboard = Controller() def type_text(self, text): """ 模拟键盘输入文本。 注意:使用前请确保光标位于目标输入框。 """ if not text: return # 简单的逐字符输入 for char in text: self.keyboard.type(char) # 可以添加微小延迟以避免某些应用处理不过来,但通常不需要 # time.sleep(0.01) # 或者一次性输入(某些应用可能不支持) # self.keyboard.type(text) def type_text_with_shortcut(self, text, paste_shortcut=False): """ 高级功能:通过模拟快捷键(如Ctrl+V)输出文本。 这需要先将文本复制到剪贴板。 """ import pyperclip # 需要安装 pip install pyperclip if paste_shortcut: pyperclip.copy(text) # 模拟 Ctrl+V (Windows/Linux) 或 Cmd+V (macOS) # 这里以Windows为例 with self.keyboard.pressed(Key.ctrl): self.keyboard.press('v') self.keyboard.release('v') time.sleep(0.1) # 等待粘贴完成 else: self.type_text(text)

重要警告:模拟键盘输入是一个强大的功能,但也存在风险。务必在可控的环境下测试,避免在输入密码或进行关键操作时意外触发。最好为你的输入法设置一个明确的“激活”和“关闭”开关。

4. 完整实战案例:构建一个简单的语音输入法

现在,我们将上述组件组合起来,创建一个具有图形界面(使用Tkinter,因为它最轻量)的简易语音输入法。

4.1 创建项目结构与配置文件

首先,创建项目目录和文件。

mkdir local_asr_input_method cd local_asr_input_method touch main.py asr_core.py audio_utils.py text_output.py config.yaml

config.yaml内容:

# config.yaml asr: model_size: "base" # 模型大小: tiny, base, small, medium, large-v2 device: "cpu" # cpu 或 cuda compute_type: "int8" # int8, float16, float32 language: "zh" # 识别语言 audio: samplerate: 16000 channels: 1 dtype: float32 ui: hotkey_start: "<ctrl>+<alt>+v" # 开始录音的全局热键(需配合pynput) hotkey_stop: "<ctrl>+<alt>+b" # 停止录音的全局热键 auto_inject: true # 识别后是否自动输入文本

4.2 编写核心逻辑集成

main.py将作为我们程序的主入口,集成所有功能。

# main.py import yaml import threading import time import sys import os from pathlib import Path # 导入我们写的模块 from audio_utils import AudioRecorder from asr_core import ASREngine from text_output import TextInjector class VoiceInputMethod: def __init__(self, config_path="config.yaml"): # 加载配置 with open(config_path, 'r', encoding='utf-8') as f: self.config = yaml.safe_load(f) # 初始化组件 self.recorder = AudioRecorder( samplerate=self.config['audio']['samplerate'], channels=self.config['audio']['channels'], dtype=self.config['audio']['dtype'] ) self.asr_engine = ASREngine( model_size=self.config['asr']['model_size'], device=self.config['asr']['device'], compute_type=self.config['asr']['compute_type'] ) self.injector = TextInjector() self.is_listening = False self.recording_thread = None def start_listening(self): """开始监听(录音并识别)""" if self.is_listening: print("已在监听中") return self.is_listening = True print("*** 开始监听,请说话... ***") self.recorder.start_recording() def stop_listening_and_process(self): """停止监听,处理录音并识别""" if not self.is_listening: print("未在监听") return self.is_listening = False print("*** 停止监听,处理中... ***") # 1. 停止录音并获取数据 audio_data = self.recorder.stop_recording() if audio_data is None or len(audio_data) < self.config['audio']['samplerate'] * 0.5: # 小于0.5秒视为无效 print("录音太短或无效,已忽略。") return # 2. 在后台线程中进行识别,避免阻塞UI def transcribe_task(): try: text = self.asr_engine.transcribe_audio( audio_data, samplerate=self.config['audio']['samplerate'] ) print(f"识别结果: {text}") # 3. 自动输入文本 if self.config['ui']['auto_inject'] and text: self.injector.type_text(text) print("文本已输入。") except Exception as e: print(f"识别过程中出现错误: {e}") process_thread = threading.Thread(target=transcribe_task) process_thread.start() def run_cli(self): """命令行交互模式""" print("=== 本地语音输入法 CLI 模式 ===") print("命令: 'start' 开始录音, 'stop' 停止并识别, 'exit' 退出") while True: cmd = input("> ").strip().lower() if cmd == 'start': self.start_listening() elif cmd == 'stop': self.stop_listening_and_process() elif cmd == 'exit': if self.is_listening: self.stop_listening_and_process() time.sleep(1) # 等待处理线程结束 print("再见!") sys.exit(0) else: print("未知命令。") if __name__ == "__main__": app = VoiceInputMethod() app.run_cli()

4.3 创建简易图形界面(Tkinter)

对于不习惯命令行的用户,一个简单的GUI很有必要。

# ui.py (可选,但推荐) import tkinter as tk from tkinter import ttk, scrolledtext import threading from main import VoiceInputMethod # 导入我们刚才写的核心类 class VoiceInputApp: def __init__(self, root): self.root = root self.root.title("废物语音输入法 v0.1") self.root.geometry("500x400") self.app_core = VoiceInputMethod() self.setup_ui() self.update_status("就绪") def setup_ui(self): # 状态标签 self.status_label = ttk.Label(self.root, text="状态: ", font=('Arial', 12)) self.status_label.pack(pady=10) # 控制按钮框架 btn_frame = ttk.Frame(self.root) btn_frame.pack(pady=10) self.start_btn = ttk.Button(btn_frame, text="🎤 开始录音", command=self.start_recording, width=15) self.start_btn.pack(side=tk.LEFT, padx=5) self.stop_btn = ttk.Button(btn_frame, text="⏹️ 停止并识别", command=self.stop_recording, state=tk.DISABLED, width=15) self.stop_btn.pack(side=tk.LEFT, padx=5) # 识别结果显示区域 ttk.Label(self.root, text="识别结果:").pack(pady=(20,5)) self.result_text = scrolledtext.ScrolledText(self.root, height=8, width=60, font=('Consolas', 10)) self.result_text.pack(padx=10, pady=5) # 操作按钮框架 op_frame = ttk.Frame(self.root) op_frame.pack(pady=10) self.copy_btn = ttk.Button(op_frame, text="📋 复制到剪贴板", command=self.copy_to_clipboard, state=tk.DISABLED) self.copy_btn.pack(side=tk.LEFT, padx=5) self.type_btn = ttk.Button(op_frame, text="⌨️ 输入文本", command=self.type_text, state=tk.DISABLED) self.type_btn.pack(side=tk.LEFT, padx=5) self.clear_btn = ttk.Button(op_frame, text="🗑️ 清空", command=self.clear_text) self.clear_btn.pack(side=tk.LEFT, padx=5) # 日志区域 ttk.Label(self.root, text="日志:").pack(pady=(20,5)) self.log_text = scrolledtext.ScrolledText(self.root, height=6, width=60, font=('Consolas', 9), state=tk.DISABLED) self.log_text.pack(padx=10, pady=5) def update_status(self, message): self.status_label.config(text=f"状态: {message}") self.log(message) def log(self, message): self.log_text.config(state=tk.NORMAL) self.log_text.insert(tk.END, f"{message}\n") self.log_text.see(tk.END) self.log_text.config(state=tk.DISABLED) def start_recording(self): self.start_btn.config(state=tk.DISABLED) self.stop_btn.config(state=tk.NORMAL) self.update_status("录音中...") # 在新线程中开始录音,避免阻塞UI threading.Thread(target=self.app_core.start_listening, daemon=True).start() def stop_recording(self): self.stop_btn.config(state=tk.DISABLED) self.update_status("处理中...") # 停止录音并处理 def process(): self.app_core.stop_listening_and_process() # 这里需要一个机制来获取识别结果,我们可以修改核心类来回调 # 为了简化,我们假设核心类处理完后会将结果存入一个变量,这里用延时模拟 self.root.after(2000, self.on_transcription_done) # 2秒后模拟完成 threading.Thread(target=process, daemon=True).start() def on_transcription_done(self): # 模拟获取识别结果 simulated_result = "这是模拟的识别结果。实际运行时会替换为真实文本。" self.result_text.delete(1.0, tk.END) self.result_text.insert(tk.END, simulated_result) self.copy_btn.config(state=tk.NORMAL) self.type_btn.config(state=tk.NORMAL) self.start_btn.config(state=tk.NORMAL) self.update_status(f"识别完成: {simulated_result[:20]}...") def copy_to_clipboard(self): text = self.result_text.get(1.0, tk.END).strip() if text: self.root.clipboard_clear() self.root.clipboard_append(text) self.update_status("已复制到剪贴板") def type_text(self): text = self.result_text.get(1.0, tk.END).strip() if text: self.app_core.injector.type_text(text) self.update_status("文本已输入") def clear_text(self): self.result_text.delete(1.0, tk.END) self.copy_btn.config(state=tk.DISABLED) self.type_btn.config(state=tk.DISABLED) if __name__ == "__main__": root = tk.Tk() app = VoiceInputApp(root) root.mainloop()

4.4 运行与验证

  1. 安装依赖:在项目根目录创建requirements.txt并安装。

    # requirements.txt PyYAML>=6.0 sounddevice>=0.4.6 numpy>=1.24.0 faster-whisper>=0.9.0 torch>=2.0.0 pynput>=1.7.6
    pip install -r requirements.txt
  2. 运行CLI版本

    python main.py

    在命令行输入start开始录音,说话,然后输入stop停止并查看识别结果。确保你的麦克风正常工作。

  3. 运行GUI版本

    python ui.py

    点击“开始录音”按钮,说话,点击“停止并识别”。识别结果会显示在文本框中,可以复制或直接输入到其他应用。

4.5 结果说明

运行成功后,你将看到一个简单的窗口。对着麦克风说话(例如:“今天天气真好”),点击停止后,几秒钟内(取决于你的CPU和模型大小),识别出的文字会出现在结果框。点击“输入文本”,这些文字就会被“敲”到当前光标所在的位置(比如一个打开的记事本)。

你完成了一个具备完整流程(录音->识别->输出)的本地语音输入法原型。它完全离线运行,所有数据都在本地处理。

5. 常见问题与排查思路

在搭建和运行过程中,你可能会遇到以下问题:

问题现象可能原因解决思路
导入sounddevice报错缺少系统级音频后端库(如PortAudio)。Linux:sudo apt-get install portaudio19-dev python3-pyaudio
macOS:brew install portaudio
Windows: 通常pip install sounddevice自带,若失败可尝试安装PyAudio(pip install PyAudio)。
加载 Whisper 模型失败或极慢网络问题导致无法从Hugging Face下载模型。1. 检查网络连接。
2. 手动下载模型:从 Hugging Face 下载对应模型文件(如base模型),放到~/.cache/huggingface/hub/目录下。
3. 使用国内镜像源(如果可用)。
识别结果全是英文或乱码未指定识别语言或模型未正确加载中文能力。ASREngine.transcribe方法中明确指定language="zh"。确保下载的是多语言模型(tiny,base,small,medium,large-v2都是多语言的)。
录音没有声音或音量极小1. 麦克风被其他程序占用或未启用。
2. 系统录音音量设置过低。
3.sounddevice选择了错误的输入设备。
1. 关闭可能占用麦克风的程序(如微信、会议软件)。
2. 检查系统声音设置的输入音量。
3. 在代码中查询并指定正确的设备ID:print(sd.query_devices()),然后在AudioRecorder初始化时传入device=你的麦克风ID
模拟键盘输入无效1. 权限问题(尤其是macOS/Linux)。
2. 光标不在可输入区域。
3. 目标应用(如某些游戏、安全软件)拦截了模拟输入。
1.macOS: 需在系统设置->隐私与安全性->辅助功能中授予终端或IDE权限。
2.Linux: 可能需要相应权限。
3. 确保先点击目标输入框再操作。
4. 尝试使用“复制到剪贴板”功能手动粘贴。
程序运行卡顿或无响应1. GUI在主线程进行大量计算(如语音识别)。
2. 模型太大(如medium,large),硬件跟不上。
1.务必将耗时的识别操作放在子线程中,如示例所示。
2. 换用更小的模型(tiny,base)。
3. 如果使用GPU,确保CUDA和PyTorch的GPU版本正确安装。
识别准确率不高1. 环境噪音大。
2. 说话距离麦克风太远或口音较重。
3. 模型太小。
1. 在安静环境下使用,靠近麦克风清晰发音。
2. 尝试使用更大的模型(small,medium),但会牺牲速度。
3. 在transcribe方法中调整beam_size(如增加到10),但会增加计算时间。
4. 使用initial_prompt参数提供一些上下文提示。

6. 最佳实践与工程建议

将这个原型改造成一个更健壮、可用的工具,还需要考虑以下几点:

  1. 热键激活:实现全局热键(如Ctrl+Alt+V)来触发录音,这样在任何应用中都能快速使用。可以使用pynputkeyboard库监听全局热键。
  2. 流式识别与实时反馈:目前的方案是“录音-停止-识别”,体验不连贯。可以改为“边录边识”,实现实时字幕效果。faster-whisper支持流式转录,需要更复杂的音频流处理。
  3. 音频预处理:增加噪音抑制、自动增益控制、静音检测(VAD)来提升录音质量。webrtcvad库是一个很好的VAD选择。
  4. 配置化管理:将模型路径、热键、录音参数等全部放入配置文件(如YAML),方便用户自定义。
  5. 错误处理与日志:增加更完善的异常捕获和日志记录,方便排查问题。例如,网络超时、模型加载失败、音频设备异常等。
  6. 性能优化
    • 模型量化:使用int8量化(faster-whisper已支持)能大幅减少内存占用并提升速度,对精度影响很小。
    • GPU加速:如果有NVIDIA GPU,务必使用device="cuda",速度能有数量级提升。
    • 缓存模型:避免每次启动都重新加载模型。
  7. 隐私与安全:这是本地输入法的最大优势。在代码中明确声明数据不离线,并避免任何网络请求(除非用户主动启用云备份等可选功能)。
  8. 打包与分发:使用PyInstallercx_Freeze将项目打包成可执行文件(.exe,.app, 二进制文件),方便非技术用户使用。
  9. 领域自适应(进阶):如果你主要用它识别某个专业领域的词汇(如医学、法律),可以尝试使用该领域的文本数据对Whisper模型进行微调(Fine-tuning),或构建一个后处理纠错词表。

从“废物”原型到一个真正顺手的工具,中间还有很长的工程化道路要走。但最重要的是,你已经掌握了核心的技术链条,并且拥有了一个完全受自己控制、隐私无忧的起点。接下来,你可以根据自己的需求,为它添加功能,比如支持多种语言切换、识别结果编辑、自定义命令词等等。

← 返回列表