离线中文语音输入小工具 —— 按住 CapsLock,说话,松开即输入。
一个用 C# / .NET 8 / Avalonia 重写的 HaujetZhao/CapsWriter-Offline。
- 单进程 + 单托盘。 告别原项目"客户端 + 服务端 + 两个黑色终端窗口"的部署方式 —— 一个 Avalonia 应用安静地待在系统托盘。
- CapsLock 一键说话。 按住录音,松开自动识别并把结果打到当前光标位置。
- 短按保留原始大小写切换语义。 不足 0.3 秒的短按仍会切换 CapsLock 状态(VoxPen 会自动补发一次按键)。
- 100% 兼容
hot-rule.txt。 正则 / 字面量替换、\1..\n反向引用,行为与原 Python 项目一致 —— 老配置直接迁移即可。 - 音素 RAG 热词。
hot.txt经拼音音素索引做模糊纠正,能修掉常见的同音字识别错误。 - 离线批量转录。 CLI 一条命令把
.mp3 / .m4a / .wav / .flac / .mp4 / .opus / …转成.txt / .srt / .json / .merge.txt。 - 可选的 Markdown 日记。 每句识别都会连同 WAV 一起归档到
recordings/YYYY/MM/DD.md,Typora 里一个正则替换就能变成<audio controls>内联播放器。 - 热重载。
config.json/hot-rule.txt/hot.txt修改后 3 秒内自动生效,无需重启。 - 单文件 exe。
dotnet publish产出一个自包含的~100 MB可执行文件,用户只需自备模型目录。
当前状态: Windows 10/11 x64 · v2 P7(P1–P7 完成)。macOS / Linux 的抽象层已在
VoxPen.Core内就位,具体实现是下一阶段。
flowchart LR
HK["全局快捷键<br/>CapsLock / 鼠标侧键 / F13..F16"]
AU["PortAudio<br/>16 kHz 单声道"]
ASR["Paraformer<br/>sherpa-onnx"]
PP["hot-rule + hot.txt RAG"]
OUT["SendInput 到当前前台窗口"]
HK --> AU --> ASR --> PP --> OUT
| 阶段 | 内容 | 状态 |
|---|---|---|
| P1 | 骨架 + Core 抽象接口 | ✅ |
| P2 | PortAudio + Paraformer 端到端(RTF ≈ 0.09) | ✅ |
| P3 | SharpHook CapsLock + SendInput 上屏 |
✅ |
| P4 | Avalonia 托盘 UI + 状态 / 历史 / 日志面板 | ✅ |
| P5 | hot-rule.txt + 末尾标点 + JSON 热重载 + 录音归档(后处理 10/10 通过) |
✅ |
| P6 | dotnet publish 单文件(~53 MB) |
✅ |
| P7 | 文件批量转录 · 音素 RAG · 鼠标侧键 · Toast · Markdown 日记 · xUnit(122 tests) | ✅ |
| P7 收尾 | HotRule 复制修复 · hot-rule.txt 随 publish · verify-p7 冒烟 |
✅ |
下一阶段(v2 规划): LLM 润色 / 角色系统 · UDP 广播 · Fun-ASR / Qwen3-ASR 引擎 · macOS / Linux 平台实现 · 繁体中文 & Chinese ITN。
-
下载最新 Release —— 从 Releases 页拿到
VoxPen-win-x64.zip并解压到任意目录(例如D:\Apps\VoxPen\)。 -
下载 Paraformer 模型(约 229 MB)。推荐:原项目预打包的 HaujetZhao/CapsWriter-Offline releases · models 里的
Paraformer.zip(含国内网盘镜像,SHA-256a12a3f97...)。上游 k2-fsa/sherpa-onnx 的sherpa-onnx-paraformer-zh-2023-09-14也可用。 -
(可选)下载标点模型(约 278 MB)。Paraformer 本身不输出标点 —— 从 k2-fsa/sherpa-onnx-releases 拿
sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12.tar.bz2(FunASR CT-Transformer 的 ONNX 版)。不放也能跑,但识别结果就没有标点,只有hot-rule.txt里的"逗号/句号/回车"这类语音关键字仍会生效。 -
摊平模型目录放到 exe 同级
models/下。注意:拷进去的是解压后目录里面的内容,而不是那个目录本身:D:\Apps\VoxPen\ ├─ VoxPen.App.exe ├─ config.json ← 首次启动自动生成 ├─ hot-rule.txt ← publish 时自动附带;可自由编辑 └─ models\ ├─ paraformer\ │ ├─ model.onnx (或 model.int8.onnx) │ ├─ tokens.txt │ └─ … (am.mvn / config.yaml / …) └─ Punct-CT-Transformer\ └─ sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12\ ├─ model.onnx └─ … (tokens.json / etc.) -
双击
VoxPen.App.exe。 任务栏右下角出现一个蓝底 "C" 托盘图标;首次模型加载约 2–3 秒。 -
鼠标点到任意可输入位置,按住 CapsLock 说话,松开自动上屏。
| 手势 | 行为 |
|---|---|
| 长按 CapsLock ≥ 0.3 秒 | 录音,松开识别并上屏 |
| 短按 CapsLock < 0.3 秒 | 跳过录音;自动补发一次 CapsLock,保留系统切换语义 |
| 长按期间 | 系统 CapsLock 切换被抑制(可在 config.json 里关闭) |
- 显示主窗口 —— 状态灯 / 识别历史 / 实时日志
- 暂停 / 继续监听
- 打开配置文件夹
- 退出
- 识别历史 —— 最近 50 条识别结果,一键复制最新一条
- 设置 —— 编辑模型目录、查看模型有效性、通过下拉框选择并保存快捷键(模型目录与快捷键改动需重启)
- 日志 —— 模型加载、hot-rule 重载、识别错误等事件实时滚动
关闭按钮不会退出应用,而是最小化到托盘。真正退出请用托盘菜单或主窗口底部的"退出"按钮。
首次启动时在 exe 同目录自动生成。字段名沿用原 Python 项目;VoxPen 只是新增了 transcribe / hotword / notification / shortcut.keys / audio.diaryEnabled —— 老配置无需改动,缺字段自动填默认值。
{
"shortcut": {
"key": "caps_lock",
"keys": ["caps_lock", "x2"],
"suppress": true,
"shortPressThresholdSeconds": 0.3
},
"audio": {
"inputDevice": null,
"saveRecording": true,
"audioNameLength": 20,
"diaryEnabled": true
},
"asr": { "engine": "paraformer", "modelDir": "models/paraformer", "numThreads": 2, "provider": "cpu" },
"punctuation": {
"modelDir": "models/Punct-CT-Transformer/sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12",
"numThreads": 2,
"provider": "cpu"
},
"output": { "mode": "Type", "restoreClipboard": true, "pasteApps": ["WeiXin.exe", "Telegram.exe"] },
"postprocess": {
"enableHotRule": true,
"hotRulePath": "hot-rule.txt",
"trashPunctuation": ",。,.",
"trashPuncThreshold": 8,
"trashPuncApps": ["WeiXin.exe"]
},
"transcribe": {
"segDurationSeconds": 60,
"segOverlapSeconds": 4,
"saveSrt": true,
"saveTxt": true,
"saveJson": true,
"saveMerge": false
},
"hotword": {
"enablePhonemeRag": true,
"hotwordPath": "hot.txt",
"matchThreshold": 0.85,
"similarThreshold": 0.6
},
"notification": {
"enabled": true,
"showOnRecordingStart": false,
"showOnError": true
},
"logLevel": "Information"
}热重载: config.json / hot-rule.txt / hot.txt 修改后 3 秒自动生效。快捷键、ASR 模型路径、标点模型路径、日记根目录的改动仍需重启。
shortcut.keys 支持的键名: 常用键盘键(字母、数字、修饰键、功能键、导航键和标点键)以及 x1、x2、mouse_left、mouse_right、mouse_middle。数组表示一个组合,必须同时按住其中全部键才会触发。设置页可直接录制组合;单独字母键(如 a)会被拒绝,但 left_ctrl + a 可以使用。
原项目文件可直接拿来用,无需转换:
毫安时 = mAh
赫兹 = Hz
(艾特)\s*(\w+)\s*(点)\s*(\w+) = @\2.\4
欧拉玛 = Ollama
#起始为注释- 左侧当正则;正则编译失败则视为字面量
- 右侧支持
\1..\n反向引用与\s(空格)
config.audio.saveRecording = true 时,每次识别都会同时写 WAV + 侧车 txt:
recordings/2026/07/assets/20260709-223245_你好世界.wav
_你好世界.txt
不需要时改成 false 即可。
把一段 / 多段音频转成 .txt / .srt / .json / .merge.txt 四件套。默认 60 秒切片 + 4 秒重叠;SegmentMerger 用 SequenceMatcher 在 token 级去重拼接,SubtitleAligner 输出标准 SRT 时间戳。
dotnet run --project src/VoxPen.Cli -- transcribe path/to/audio.mp3 another.wav
# 可选参数:
# --seg-duration 60 --seg-overlap 4
# --no-srt --no-json --no-txt --merge
# --model <dir>支持的输入:.wav / .mp3 / .m4a / .aac / .wma / .flac / .mp4 / .ogg / .opus(走 Windows Media Foundation 解码 + 重采样到 16 kHz mono)。
100% 兼容原项目的 hot.txt 格式:一行一个热词,支持 | 分隔别名(第一项为目标)、~~~ 后紧跟黑名单窗口词。用 ToolGood.Words.Pinyin 抽取声 / 韵 / 调音素序列,锚点扫描 + DP 距离找候选,右到左 splice 替换。
撒贝宁
北大青鸟|beidaqingniao|BDQN
GitHub|吉他不
先|xiān|xian ~~~ 首先 优先 领先
默认匹配阈值 0.85(低于则不替换);相似阈值 0.6 用于 UI 提示。可在 config.json 里调,也可直接改 hot.txt —— 3 秒热重载。
shortcut.keys 支持任意组合,例如 ["left_ctrl", "left_shift", "a"] 表示三键必须同时按住才会开始录音。["caps_lock", "x2"] 则要求 CapsLock 与鼠标 X2(前进键)同时按住。设置页点击“录制快捷键”后按下组合并全部松开即可录制;为避免正常输入误触,单独字母键不允许保存。
识别完成不再弹 Toast(噪音太大);出错时可通过 notification.showOnError 弹错误 Toast。首次触发会自动创建 AUMID 和开始菜单快捷方式(Windows 10 1903+);旧系统静默降级。
audio.diaryEnabled = true 时,每条识别追加写入 recordings/YYYY/MM/DD.md:
[12:34:56](assets/20260709-123456_你好世界.wav) 你好世界
[12:35:10](assets/20260709-123510_下一句.wav) 下一句首次创建文件时会自动写入 header —— 附一段 Typora 里能一键把音频链接换成 <audio controls> 控件的正则替换 Tip。
dotnet test tests/VoxPen.Core.Tests/覆盖 SequenceMatcher / SmartSplit / SegmentMerger / SubtitleAligner / SrtWriter / TranscriptJsonWriter / PhonemeExtractor / FastRag / PhonemeCorrector / HotwordFile / HotRuleReplacer / TrashPuncCleaner / DiaryWriter / AudioSegmenter / FileTranscriber,共 122 test cases 全绿。
前置:.NET 8 SDK。
dotnet build src/VoxPen.App# 自动生成开发版本号,便于本地预览发布包
pwsh -File scripts/package.ps1
# 指定正式或预发布版本号
pwsh -File scripts/package.ps1 -Version 0.1.0-rc.1脚本会生成两种 zip 及各自的 .sha256,并把未压缩的发布内容放在 staging/。未指定版本时会自动使用类似 0.1.0-dev.20260724153000 的开发版本号;GitHub Release 使用同一脚本打包。
VoxPen-<version>-win-x64.zip—— 推荐给所有用户。自包含 .NET 8,解压即可运行。VoxPen-<version>-win-x64-requires-dotnet-8-runtime.zip—— 更小,但需要先安装 .NET 8 x64 Runtime。未安装时,启动提示会提供下载链接。
压缩包内的 VoxPen.App.exe(P7 起约 100 MB,P6 时约 53 MB —— 增量来自 NAudio + Toolkit.Uwp.Notifications + ToolGood.Words.Pinyin 等 P7 依赖)自包含 .NET 运行时 + sherpa-onnx / PortAudio / SharpHook / MediaFoundation 全部原生依赖。
publish 目录会自动带上 hot-rule.txt;config.json 首次运行在 exe 同目录自动生成默认值。用户只需要自己放 models/paraformer/。
# 后处理端到端(HotRule + TrashPunc)
dotnet run --project src/VoxPen.Cli -- test-postprocess
# 标点模型冒烟(会加载 CT-Transformer,需要 models/Punct-CT-Transformer/... 就位)
dotnet run --project src/VoxPen.Cli -- test-punc "你好世界这是一段没有标点的文本"
# 音素 RAG 冒烟(内置样例,不加载模型)
dotnet run --project src/VoxPen.Cli -- test-hotword
# Markdown 日记冒烟(写到临时目录)
dotnet run --project src/VoxPen.Cli -- test-diary
# 段合并冒烟(模拟 3 段重叠文本)
dotnet run --project src/VoxPen.Cli -- test-merger
# 从 WAV 直接识别
dotnet run --project src/VoxPen.Cli -- --file models/paraformer/example/asr_example.wav
# 批量转录
dotnet run --project src/VoxPen.Cli -- transcribe path/to/audio.mp3
# 无 UI 常驻模式(真实 CapsLock 监听)
dotnet run --project src/VoxPen.Cli -- runVoxPen/
├─ src/
│ ├─ VoxPen.Core/ 抽象接口 · Pipeline 状态机 · 后处理
│ │ 配置 · 归档 · 转录 · 日记 · 音素 RAG
│ ├─ VoxPen.Platform.Windows/ SharpHook · PortAudio · sherpa-onnx · SendInput
│ │ MediaFoundation · UWP Toast
│ ├─ VoxPen.App/ Avalonia UI · Tray · AppHost(组合根)
│ └─ VoxPen.Cli/ 无 UI 冒烟测试 + 批量转录
├─ tests/VoxPen.Core.Tests/ xUnit 测试套件(122 tests)
├─ models/paraformer/ (用户自备,已 gitignore)
├─ models/Punct-CT-Transformer/ (可选标点模型,已 gitignore)
├─ hot-rule.txt 可选 · 与原项目 100% 兼容(正则替换)
├─ hot.txt 可选 · 音素 RAG 热词
├─ config.json 首次启动自动生成
└─ recordings/ 录音归档 + Markdown 日记(可关)
└─ 2026/07/
├─ 09.md
└─ assets/
Core 抽象接口(为 macOS / Linux 增量实现预留):
IGlobalHotkey · IAudioCapture · ITextOutput · IAsrEngine · IForegroundApp · IAudioDecoder · INotificationService
- 麦克风。 首次启动会由 Windows 弹权限请求。
- SmartScreen。 Release 里的 exe 未做代码签名,首次运行会弹"Windows 已保护你的电脑"。点 更多信息 → 仍要运行 即可,之后不再提示。
- 向管理员窗口输入。 目标窗口以管理员启动时,VoxPen 自身也需以管理员启动才能 SendInput。
- 杀毒软件误报。 单文件 exe 打包了大量本地 DLL,可能被误报,请加入白名单。
PowerShell Remove-Item -Recurse 处理 junction 时会跟穿,把目标目录(真的 models)里的内容删光;-Exclude 只保护 junction 本身,不阻止跟穿。要清理带 junction 的目录,先用 cmd /c rd <junction> 拆链,再操作父目录;或者直接 Copy-Item -Recurse 拷贝模型(~250 MB,ReFS/SSD 上瞬间完成),从根本避免这个陷阱。
hot-rule.txt通过VoxPen.App.csproj的<None CopyToOutputDirectory>随build/publish自动带到输出目录。config.json由AppHost.LoadOrCreateConfig首次启动写默认(含 P7 的transcribe / hotword / notification三节)。hot.txt是用户按需自备,不会自动生成。- 缺
models/paraformer/时启动会 fail-fast 报Model directory not found,并在日志里给出明确路径 —— 不要绕过它。 - 缺标点模型(
punctuation.modelDir指向的目录里没有model.onnx)时会静默降级到NullPunctuator:识别结果不加标点,仅在日志里给一条提示,App 本身仍能启动。
CAPSWRITER_LOG_FILE=<path>—— 把 App 的 Emit 日志实时写到文件。CAPSWRITER_AUTO_EXIT_SECS=<n>——n秒后自动 shutdown(冒烟专用)。
组合使用可在 CI / 脚本里做启动冒烟验证,示例见 publish/verify-p7/。
- HaujetZhao/CapsWriter-Offline —— 原项目 / 交互设计 / 配置约定。
- k2-fsa/sherpa-onnx —— ASR 引擎 + PortAudioSharp2。
- TolikPylypchuk/SharpHook —— 跨平台全局键盘 hook。
- AvaloniaUI/Avalonia —— 跨平台 XAML UI。
- ToolGood.Words.Pinyin —— 拼音 / 音素抽取。
MIT,跟随原项目。