# LocalVoice **Repository Path**: calculuswjf/LocalVoice ## Basic Information - **Project Name**: LocalVoice - **Description**: 按住 CapsLock 说话,文字边说边出现在光标处。和常见的「说完再转写」不同,LocalVoice 是边说边出字:本地 FireRedVAD 只回答「一句话在哪里结束」,音频还在生长的时候就反复送云端识别,屏幕上先出现未定稿的文字、随后自动修正,VAD 切分那一刻即定稿。 除识别外全部本地:VAD 走 ONNX,约 16 倍实时,不需要 PyTorch、不需要显卡。 - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-19 - **Last Updated**: 2026-09-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # LocalVoice **按住 CapsLock 说话,文字边说边出现在光标处。** 不弹窗、不切窗口、不动输入法。 和常见的「说完再转写」不同,LocalVoice 是**边说边出字**:本地 FireRedVAD 只回答 「一句话在哪里结束」,音频还在生长的时候就反复送云端识别,屏幕上先出现**未定稿**的文字、 随后自动修正,VAD 切分那一刻即定稿。实测文字只落后声音约 **1 秒** (7.76 秒的录音,第 **1.59 秒**屏幕上就有字了),而不是落后「你这句话说了多久」。 除识别外全部本地:VAD 走 ONNX,约 16 倍实时,**不需要 PyTorch、不需要显卡**。 ``` ┌──────────────── 常驻后台(开机自启可选)────────────────┐ │ │ 按住 CapsLock ──► 麦克风 16kHz ──┬──► FireRedVAD:只决定「哪里是一句的结尾」 │ │ (说完就切一刀,切完就定稿) │ └──► 这一段音频还在长的时候, │ 就每 600ms 重新送一次 MiMo ASR │ │ │ ▼ │ 未定稿文字先打进输入框, │ 下一版只回退改动的尾巴 └────────────────── 切分那一刻的转写 = 该段定稿文字 ──────────┘ │ ▼ SendInput 打进前台输入框 ``` 关键点:**VAD 不参与「什么时候送 ASR」的决定,它只回答「哪里是一句的结尾」。** 这是它和「录完 → 切句 → 送识别」那种线性管线的根本区别,细节见「四、它是怎么实现的」。 --- ## 一、快速开始 **前置条件**:Windows 10/11、Python 3.10+、一个 **自己的 MiMo API Key** (本包**不包含** Key,需要去 https://platform.xiaomimimo.com 申请)。 ```powershell # 0. 解压后进入本 README 所在的目录(LocalVoice) cd <解压位置>\LocalVoice # 一键:检查 Python → 装依赖 → 生成配置 → 自检 powershell -ExecutionPolicy Bypass -File scripts\install.ps1 # 然后按提示编辑 config.toml,把 [mimo] 的 api_key 填上 # ── 或者手动四步 ────────────────────────────────── python -m pip install -r requirements.txt # 装依赖 python -m localvoice init --api-key sk-你的key # 生成配置并写入 Key python -m localvoice selftest # 自检:依赖/模型/麦克风/键盘钩子/MiMo 连通性 python -m localvoice run # 前台运行 ``` 看到 `LocalVoice 已就绪:按住 CapsLock 说话` 就可以用了:把光标点进任意输入框,**按住 CapsLock**(约 0.35 秒后会响一声提示音),说完松开,文字就会在**说话过程中**出现在光标处。 想后台常驻 + 开机自启: ```powershell python -m localvoice config set general.autostart true python -m localvoice start # 后台启动 python -m localvoice status # 看状态 python -m localvoice stop # 停止 ``` --- ## 二、配置文件 配置文件默认在 **`LocalVoice\config.toml`**(和项目同目录,方便找)。 完整带注释的模板见 `config.example.toml`。 三个你最可能改的开关: | 配置项 | 作用 | | --- | --- | | `general.autostart` | **是否开机自启**(写注册表 `HKCU\...\Run`) | | `vad.enabled` | **是否启用 VAD**。`true` = VAD 只负责切句,边说边出字;`false` = 跳过 VAD,原始音频按 `direct.chunk_s` 切片直送 ASR | | `stream.live` | **是否边说边出字**。`true` = 未定稿文字也打进输入框并随后自动修正;`false` = 老行为,说完才出字 | | `mimo.api_key` | **MiMo API Key** | 也可以用命令行改,不用手编 TOML: ```powershell python -m localvoice config set mimo.api_key sk-xxxx python -m localvoice config set vad.enabled false python -m localvoice config set stream.live false python -m localvoice config set general.autostart true python -m localvoice config set trigger.hold_ms 300 python -m localvoice config show python -m localvoice config edit # 用记事本打开 ``` 改完重启生效:`python -m localvoice restart`。 ### 常用调参 | 想干什么 | 怎么改 | | --- | --- | | 长按好难触发 | `trigger.hold_ms` 调小(如 `250`) | | 出字还不够快 | `stream.interval_ms` 调小(如 `300`,代价是请求更多) | | 觉得字在跳、在被改 | `stream.interval_ms` 调大,或 `stream.partial_min_interval_ms` 调大到 `150` | | 完全不想要未定稿文字 | `stream.live = false`(回到「说完才出字」) | | 说话被切得太碎 | `vad.min_silence_ms` 调大(如 `400`) | | 一句话太长被从中间切开 | `stream.force_cut_ms` 调大(如 `20000`) | | 安静时也在录 | `vad.speech_threshold` 调大(如 `0.55`) | | 麦克风选错了 | `audio.device` 填 `python -m localvoice devices` 里的序号 | | 某些输入框打不进字 | `output.inject_method = "paste"` | | 不想让麦克风常开 | `vad.always_listen = false` | | 不想听提示音 | `trigger.beep = false` | --- ## 三、命令行 ``` python -m localvoice <命令> init 创建配置文件(--api-key 可直接写入 Key) run 前台运行常驻服务 start 后台启动(无窗口) stop 停止后台服务(优雅退出,会释放麦克风和键盘钩子) restart 重启 status 查看运行状态(--json 输出原始 JSON) config 查看/修改配置:show | set | edit | path autostart 开机自启:enable | disable | status devices 列出麦克风设备 caps 交互式测试 CapsLock 长按检测(最实用的排错命令) vad 对 wav 文件单独跑一遍 FireRedVAD 切分 asr 把 wav 送去 MiMo 转写 selftest 启动前自检 beep-demo 试听各种提示音 ``` `run` 的选项:`--dry-run`(只打印识别结果,不真的输入文字)、`--no-beep`、`--no-vad`、`--config <路径>`。 --- ## 四、它是怎么实现的 ### CapsLock 长按(`localvoice/hotkey.py`) 用一个全局低级键盘钩子 `WH_KEYBOARD_LL` 拦截 CapsLock,按下和松开**都吞掉**,自己决定它的含义: | 情况 | 行为 | | --- | --- | | 按下瞬间 | 记下当前大小写锁定状态,起一个 `hold_ms` 定时器 | | 按住超过阈值 | 开始一次语音会话 | | 会话中松开 | 结束会话,并把 CapsLock 还原成**按下之前**的状态 | | 短按(没到阈值) | 补发一次合成的 CapsLock 切换 —— 所以「轻按切换大小写」照旧好使 | 用 `WH_KEYBOARD_LL` 而不是 `keyboard` 库,就是因为只有原始 API 能拿到钩子返回值(吞事件)和 `LLKHF_INJECTED` 标志(区分自己发的合成按键)。钩子回调里绝不做重活,更不能调 `SendInput`(会死锁),所以短按的补发被丢到独立线程处理。 ### 音频与 VAD(`localvoice/audio.py`、`localvoice/vad/`) - 麦克风以 16kHz 单声道常开(打不开就退到 WASAPI `auto_convert`,再不行才在 Python 里重采样)。 - FireRedVAD **只跑 ONNX Runtime,不需要 PyTorch**。上游 `import fireredvad` 会连带 import torch,且 `DetectModel` 还要求 `model.pth.tar`;而 ONNX 图里已经有网络本身,所以只把两个纯 Python 部件(CMVN 前端、流式后处理状态机)vendored 到 `localvoice/vad/firered_ref.py`。 - 实测 16 倍实时,单核占用约 2~3%。模型加载 ~20ms。 - 必须自己存音频:后处理报告起点时,真正的起音已经在 100~200ms 之前了,`AudioRing` 环形缓冲负责把它捞回来,否则每个字都会被吃掉字头。 - 除了「哪里结束」,VAD 还向外提供两样东西给实时管线用:**当前还没关闭的那段音频**(`open_pcm()`)和**每帧的语音概率**(`prob_trace()`)。前者让 ASR 能在切分之前就开始干活,后者让「不得不从中间切一刀」时能挑在概率最低的地方下刀。 ### 实时分区(`localvoice/service.py`) 这是延迟的关键,也是和「先录完 → 交给 VAD → 再交给 ASR」那种线性逻辑的区别: | 角色 | 决定什么 | **不**决定什么 | | --- | --- | --- | | VAD | 一句话在哪里开始、在哪里结束(= 切分点) | 什么时候调 ASR | | `stream.interval_ms` | 音频还在长的时候,多久重新送一次 ASR | 一句话在哪里结束 | 一个「窗口」(`Segment`)从 VAD 判定起音开始,到 VAD 判定这句话结束(或松手、或超过 `stream.force_cut_ms`)为止: 1. 窗口里每新增 `stream.interval_ms`(默认 600ms)音频,就把**整个窗口从头发一遍**给 MiMo (模型没有上下文记忆,只发增量会把词切坏),拿到的文字立刻打进输入框; 2. 下一版结果和屏幕上的文字做**最长公共前缀**比较,只有尾巴不一样才动它 —— 公共前缀留着, 其余回退(`Injector.revise`)后重打。这就是「未定稿文字」能被自动修正的原因; 3. VAD 一切分,这一段的文字就**定稿**,之后永不修改,同时新的窗口从切分点开始。 于是:出字延迟 ≈ 一次 ASR 往返(实测 TTFT 0.7~1.0s),而不是「你这句话的长短 + 一次 ASR」。 实测一段 7.76 秒的录音,**第 1.59 秒屏幕上就有字了**,全程刷新 9 次、只回退了 3 个字符。 同时只允许**一个** ASR 请求在途:下一个窗口的刷新要等上一个返回,所以刷新频率自然锁在 一次往返上下,不会堆积请求。切分发生时会把在途的实时请求 abort 掉,让定稿请求尽快拿到结果。 `vad.enabled = false` 时不跑 VAD,退化成按 `direct.chunk_s` 定时切片直送(同样有实时性, 只是切点不看语音内容)。 ### MiMo ASR(`localvoice/asr.py`) MiMo 没有 OpenAI 的 `/v1/audio/transcriptions`,语音识别挂在 **chat/completions** 上: ```http POST https://api.xiaomimimo.com/v1/chat/completions api-key: {"model":"mimo-v2.5-asr", "messages":[{"role":"user","content":[ {"type":"input_audio","input_audio":{"data":"data:audio/wav;base64,"}}]}], "asr_options":{"language":"auto"}, "stream":true} ``` 流式返回是 OpenAI 风格的 `chat.completion.chunk` SSE,文本在 `choices[0].delta.content`。 音频必须是 wav/mp3,base64 后不超过 10MB(16kHz 单声道约 4 分钟)。 实测延迟(本机,`mimo-v2.5-asr`):首字 0.65~1.0s,整句 0.9~1.3s,**和音频长短基本无关** (0.5s 和 10s 的音频都差不多)—— 这正是不停重发整个窗口可行的前提。 ### 文字注入(`localvoice/inject.py`) - `type`(默认):`SendInput` + `KEYEVENTF_UNICODE`,逐字输入,不碰剪贴板,中文/emoji 都能打,兼容性最好。 - `paste`:直接调 Win32 `SetClipboardData` 写 `CF_UNICODETEXT`,再发一次真正的 `Ctrl+V` 组合键(不是「Ctrl 松开再按 V」),最后把剪贴板还给用户。 注入前会先检查有没有卡住的 Ctrl/Alt/Shift/Win 并把目标窗口抢回前台,否则打出来会变成快捷键。 实时修正用的是 `Injector.revise(old, new)`:算最长公共前缀 → 用 Backspace 删掉不同的尾巴 → 重打新尾巴(删除按 UTF-16 码元计数,emoji 这种代理对要按 2 个算)。逐字输入和粘贴两种模式都支持。 ### 线程模型 | 线程 | 职责 | | --- | --- | | `localvoice-caps-hook` | 键盘钩子 + 消息循环,只做标记,不干活 | | `localvoice-caps-actions` | 串行执行手势回调,短按补发在这里做 | | PortAudio 回调 | 只把 10ms PCM 块塞进队列 | | `localvoice-dsp` | 队列唯一消费者,独占 VAD / 环形缓冲 / 切片器 / 当前窗口,**因此这些状态都不需要加锁**;顺便在每块音频到达时就决定「要不要再刷新一次」 | | `localvoice-asr` | 一次只跑一个 MiMo 请求并注入,保证文字顺序 | 队列顺序天然决定了会话边界:`session_start` 之前的音频被丢弃,`session_end` 之后的也一样。 --- ## 五、项目结构 ``` LocalVoice/ ├─ LICENSE ← Apache-2.0 正文 ├─ NOTICE ← 署名(本项目 + FireRedVAD) ├─ THIRD-PARTY-NOTICES.md ← 第三方组件清单 + 分发注意事项 ├─ config.toml ← 你的配置(含 API Key,不随包分发) ├─ config.example.toml ← 带完整注释的模板 ├─ localvoice/ │ ├─ __main__.py python -m localvoice 入口 │ ├─ cli.py 全部命令行命令 │ ├─ config.py TOML 读写、校验、默认值 │ ├─ service.py 总调度:会话状态机 + 线程编排 │ ├─ hotkey.py CapsLock 长按钩子 │ ├─ audio.py 麦克风采集与重采样 │ ├─ asr.py MiMo ASR 客户端 │ ├─ inject.py 文字注入(逐字 / 粘贴) │ ├─ notify.py 提示音 │ ├─ autostart.py 注册表开机自启 │ ├─ runtime.py PID / 状态 / 停止请求文件 │ ├─ winapi.py Win32 ctypes 绑定(不依赖 pywin32) │ └─ vad/ │ ├─ engine.py 流式 VAD 引擎(ONNX) │ ├─ ring.py 音频环形缓冲 │ └─ firered_ref.py 上游 FireRedVAD 的纯 Python 部件(Apache-2.0) ├─ bin/localvoice.pyw 开机自启用的无窗口启动器 ├─ scripts/latency_check.py 用现成音频量一遍「文字落后声音多久」 ├─ FireRedVAD/ 上游 FireRedVAD(含 ONNX 模型) ├─ tests/ │ ├─ test_offline.py 48 项:VAD 切分 / 实时分区 / 修正算法 / 配置 / 手势状态机 │ ├─ test_pipeline.py 7 项:真实 MiMo 端到端(含「录音没结束就该出字」) │ ├─ test_inject_live.py 4 项:真实注入到窗口(含实时修正要回退重打) │ └─ test_hook_live.py 3 项:真实键盘钩子链路 └─ logs/localvoice.log 运行日志(超过 2MB 自动轮转) ``` --- ## 六、排错 | 现象 | 原因 / 解法 | | --- | --- | | 按住 CapsLock 没反应 | 先跑 `python -m localvoice caps`。它会在 15 秒内打印每一次长按/轻按。完全没事件说明 CapsLock 被键盘驱动或改键软件抢走了 | | 轻按 CapsLock 不再切换大小写 | `trigger.passthrough_tap` 被设成了 `false` | | 大小写状态被弄乱 | `trigger.restore_caps_lock = true` 会在每次会话结束时还原 | | 提示 `未配置 MiMo API Key` | `python -m localvoice config set mimo.api_key sk-xxxx` | | 识别不出来 / 很慢 | 跑 `python -m localvoice selftest` 看麦克风电平,峰值 RMS 低于 3 基本就是麦克风静音或选错设备 | | 出字比过去还慢 | 看 `python -m localvoice status` 里的「实时」行:`刷新=0` 说明 `stream.live` 是关的;`最近首字延迟` 上千毫秒说明是网络/服务端慢,不是管线排队 | | 字在跳、在被改 | 正常现象(未定稿文字在修正)。嫌吵就把 `stream.interval_ms` 调大,或 `stream.partial_min_interval_ms` 调大 | | 打不进某些输入框 | `python -m localvoice config set output.inject_method paste` | | `start` 后没反应 | 看 `logs/localvoice.log` 和 `python -m localvoice status` | | 想改回默认配置 | `python -m localvoice init --force` | 日志:`LocalVoice\logs\localvoice.log`。每次会话结束都会打一行「本次会话:N 字 / N 段, 实时刷新 N 次(回退 N 处),首次出字 N ms」,可以直接看出实时性是否正常。 想看延迟的实际数字(不需要麦克风,用现成音频按真实时间喂进去): ```powershell python scripts/latency_check.py # 内置样例音频 ×3 python scripts/latency_check.py 你的录音.wav 2 # 自己的 16kHz 单声道 wav ``` 它会打印屏幕上文字逐次演变的时刻表,其中「首次出字时录到」远小于「录音总长」就说明 文字确实是在说话过程中出现的。 --- ## 七、已知限制 - 仅支持 **Windows**(键盘钩子、`SendInput`、注册表自启都是 Win32 API)。 - 需要 **能联网**:FireRedVAD 在本地跑,但识别必须调用 MiMo 云端 API。 - 麦克风常开(`vad.always_listen = true`,这是默认值,对应「开机后一直在跑」)。介意的话设成 `false`。 - 单段音频受 MiMo 10MB 上限约束,约 4 分钟;说超长句会被 `vad.max_speech_ms` 强制切开。 - 注入是模拟键盘,**极少数**游戏 / 全屏独占应用不接收。 - **实时模式(`stream.live = true`)下文字是「未定稿」的**:修正靠回退 + 重打,所以 说话期间**不要手动去改屏幕上那些字**,也不要把光标挪走 —— 程序只知道「我刚才打了 N 个字」, 不知道你有没有动过。要说长句又不想被打扰,就设 `stream.live = false`。 - `stream.interval_ms` 越小越实时,代价是同一个窗口会被重复识别(API 调用量上升)。 --- ## 八、许可与第三方组件 本项目以 **Apache License 2.0** 开源,正文见 `LICENSE`,署名见 `NOTICE`; 随包分发的第三方内容另有其许可,完整清单与分发注意事项见 **`THIRD-PARTY-NOTICES.md`**。 - **FireRedVAD**(**Apache-2.0**,Copyright 2026 Xiaohongshu / FireRedTeam)—— 许可正文在 `FireRedVAD/LICENSE`,**分发时请勿删除**。 包内含其完整源码树、预训练 ONNX 模型与示例音频。 `localvoice/vad/firered_ref.py` 是由上游两个模块合并改写的衍生文件, 已按 Apache-2.0 第 4(b) 条在文件头保留版权声明并列出改动清单。 - **MiMo API** —— https://platform.xiaomimimo.com 在线服务,不是被分发的软件;使用受其服务条款约束,Key 需自行申请(本包不含 Key)。 - 运行时依赖(numpy / onnxruntime / kaldi-native-fbank / kaldiio / sounddevice / scipy) 由使用者自行 `pip install`,不随本包分发。 - 想再分发给别人:直接用 `scripts/pack.ps1` 打包,它会排除 `config.toml`(含 Key) 与 `logs/`(含识别原文),并在成品里扫描密钥;保留 `LICENSE`、`NOTICE`、 `FireRedVAD/LICENSE` 即可。