用 Rust + Tauri 做一个 Windows 光标跟随语音输入工具
EP005

用 Rust + Tauri 做一个 Windows 光标跟随语音输入工具

这次做的是一个 Windows 桌面语音输入工具:按住右 Alt 开始说话,松开停止,识别出的文字直接输入到当前光标所在位置。

它不是一个完整商业产品,更像是把输入法式语音转文字的核心链路先跑通:前端负责设置和状态展示,Rust 负责系统能力,火山 ASR 负责识别。

💡
一句话结果

项目已经完成 Tauri 桌面壳、本地配置、右 Alt 按住触发、麦克风采集、简易 VAD、火山 ASR WebSocket 客户端、系统文本输入和托盘驻留。

问题:语音输入最难的不是 UI

这个工具看起来只是一个小窗口,但真正麻烦的是底层链路:

  1. 要能采集麦克风 PCM 音频。
  2. 要能监听全局热键,并区分按下和松开。
  3. 要把语音识别结果输入到当前焦点位置。
  4. 要能驻留系统托盘,而不是关窗口就退出。
  5. 要把 API 密钥和统计数据放在本地,不接云端账号系统。
  6. 要接入火山 ASR 的 WebSocket 二进制协议。

如果把这些都塞到前端里,复杂度会很快失控。所以第一版就把职责切开:HTML/CSS/JavaScript 写 UI,Rust 写所有系统逻辑。

方案:Tauri 做壳,Rust 接系统能力

整体架构很简单:

HTML/CSS/JS UI
    ↓ Tauri commands/events
Rust session orchestrator

hotkey / audio / VAD / ASR / input / tray / config

前端只有三段布局:

  • 顶部显示今日字数、打字速度、累计字数。
  • 中部显示 API Key、Resource ID、快捷键、自启、自动发送。
  • 底部放一个余额充值占位,不做登录、充值、扣费。

Rust 后端按模块拆开:

asr.rs        火山 ASR WebSocket 协议
audio.rs      麦克风 PCM 采集
config.rs     本地 JSON 配置
hotkey.rs     Windows 右 Alt 状态检测
input.rs      系统文本输入
session.rs    按住说话会话编排
stats.rs      字数统计
tray.rs       系统托盘
vad.rs        简易静音过滤

这套拆分的好处是,每个模块都能单独理解。比如统计、配置、VAD、协议帧解析都可以写单元测试,不需要真的打开麦克风。

过程

1. 先把可运行骨架搭出来

项目从空目录开始,先搭 Tauri v2 + Vanilla 前端。UI 不做复杂交互,只保证能配置密钥、显示统计、承接后端事件。

这一步的目标不是漂亮,而是先让“前端设置 -> Rust 配置 -> 本地保存”这条线跑通。

2. 本地配置只存 JSON

配置文件放在 Tauri 的 app config 目录里:

%APPDATA%\com.codexpd.yuyin2\settings.json

里面保存:

{
  "app_id": "YOUR_APP_ID",
  "access_token": "YOUR_ACCESS_TOKEN",
  "api_key": "YOUR_API_KEY",
  "resource_id": "volc.bigasr.sauc.duration",
  "hotkey": "RightAlt",
  "autostart": false,
  "auto_send": true
}

真实密钥不进源码,也不写进文章。

3. 右 Alt 热键踩了一个坑

最开始用通用全局键盘监听库处理右 Alt,把它当成 AltGr。实际测试发现,在 Windows 上这个映射并不稳定,表现就是用户按右 Alt 没反应。

最后改成 Windows 原生方式:

GetAsyncKeyState(VK_RMENU)

用 10ms 轮询检测状态变化:

  • false -> true:开始收音
  • true -> false:停止收音

这比抽象热键库更直接,也更符合“按住说话”的产品语义。

4. 火山 ASR 不是普通 JSON WebSocket

火山流式 ASR 文档里最容易漏掉的一点是:它使用自定义二进制 WebSocket 协议。

客户端需要处理:

  • 请求头鉴权。
  • 初始化 JSON payload。
  • PCM 音频帧封包。
  • 最后一包负序列。
  • 服务端二进制响应解析。
  • enable_nonstream: true,让它支持实时增量和结束后的整句订正。

鉴权也分两种:

  • 旧版控制台:App ID + Access Token。
  • 新版控制台:API Key。

Secret Key 不用于这个 WebSocket ASR 接口。

5. 桌面应用一定要有本地日志

这个项目里最有用的调试文件是:

%APPDATA%\com.codexpd.yuyin2\yuyin2.log

日志记录这些关键节点:

  • 右 Alt 已按下。
  • 麦克风采集已启动。
  • 右 Alt 已松开。
  • ASR 连接失败原因。

桌面程序不像 Web 页面那样天然有浏览器控制台。没有日志时,用户说“没反应”,其实可能是热键没触发、麦克风没启动、ASR 鉴权失败、协议帧错误,或者输入模拟失败。日志能把问题切开。

结果

当前项目已经完成核心能力:

  • Tauri v2 桌面 UI。
  • 本地 JSON 配置。
  • 系统托盘驻留。
  • 右 Alt 按住说话。
  • 麦克风 PCM 采集。
  • 简易 VAD 静音过滤。
  • 火山 ASR 二进制 WebSocket 客户端。
  • 系统文本输入。
  • 字数统计。

测试侧也补了 13 个 Rust 单元测试,覆盖配置、统计、VAD、热键状态转换、ASR 协议帧和响应解析。

⚠️
发布注意

真实 App ID、Access Token、API Key 不应该提交到仓库,也不应该写进博客。文章里只保留配置结构和占位值。

总结

这个项目最大的收获不是 UI,而是把桌面语音输入拆成了几个稳定边界:

  • 热键检测要贴近平台,不要过度依赖抽象。
  • ASR 接入要严格按协议做,不要把二进制 WebSocket 当普通 JSON WebSocket。
  • 桌面工具要尽早加本地日志。
  • 第一版先跑通核心链路,登录、充值、扣费都可以后置。

接下来可以继续打磨悬浮窗、快捷键修改、最终文本订正替换,以及 macOS 和 Android 的平台适配。但第一步已经成立:光标在哪里,按住右 Alt 说话,文字就应该去哪里。