按住键盘上的语音键说话,松手文字直接进当前光标。 macOS 常驻(贴底边的悬浮条 + 菜单栏),不碰剪贴板。
按住语音键 ──► 悬浮条弹开:波形 + 「听写中」,下面一行实时预览
松手 ──► 「上屏中」──► 文字打进前台应用的光标处 ──► 回到「空闲」
目前在 Ulanzi AU05(VibeKey) 上跑通全链路;自研语音键盘到手后只换设备层(src/voxkey/device/),
上面的软件部分不用动。硬件与固件在另一个仓库:c3ng-dev/OneSay。
- 不碰剪贴板:优先 AX 直写焦点控件(写完回读校验),失败就合成 Unicode 按键逐字打进光标; 两条都不成就明说「未上屏」,绝不降级到剪贴板(剪贴板方案会覆盖用户内容、也和输入法纠缠)。
- 不和输入法冲突:合成按键走的是 Unicode 载荷,中文/emoji/中英混排都过,拼音输入法激活时也不干扰。
- 触发走设备自己的 HID 报文:不需要全局热键、不依赖厂商 Studio 软件;代价是读设备要考虑独占(见下)。
- 长语音不吃亏:模型上下文装不下长句就按 22 秒切段,但录音期间就把录满的段解掉, 松手只剩下最后一段要解(实测 44 秒音频:松手后解码从 3176ms 降到 218ms)。
- 悬浮条动效:固定尺寸透明窗口 + Core Animation 弹簧动画 + 裁剪层(照开源刘海应用 boring.notch /
DynamicNotchKit / NotchDrop 的做法),动画在系统渲染服务里跑,Python 主线程不参与动画帧。
为什么这么做、踩过哪些坑,都写在
src/voxkey/pill.py的模块注释里。
# 推荐 uv(系统自带的 python 是 3.9,跑不了本项目)
uv venv --python 3.12 .venv
uv pip install --python .venv/bin/python -e '.[tools]'
# 或者你有 python3.12 的话:
python3.12 -m venv .venv && .venv/bin/pip install -e '.[tools]'别让 venv 的基础解释器指向仓库外:uv 默认把 Python 装在 ~/.local/share/uv/python,
仓库搬走/那个目录被清掉,.venv/bin/python 就成了死链接、整个环境废掉(真踩过:
旧环境的基础解释器在另一个目录里,那个目录一没,程序连重启都起不来)。
把基础解释器也放进仓库,就再也不会有这个问题:
export UV_PYTHON_INSTALL_DIR="$PWD/.uv-python" UV_CACHE_DIR="$PWD/.uv-cache" # 两个目录都已 gitignore本机没装 python3.12(系统只有 3.9),现有的 .venv 是用 uv 建的——uv 建的 venv 不带 pip,
要补装包先 .venv/bin/python -m ensurepip。新机器上装个 uv 直接 uv venv --python 3.12 .venv 更省事。
模型不进仓库(每个 1~3GB)。打包后的软件会自己在首次启动时下载(约 800MB,带 sha256 校验,
进度显示在悬浮条上),下到 VoxKey.app 旁边的 models/ 里——便携布局,卸载就是删文件夹,
不在 ~/Library 留东西(见 src/voxkey/paths.py 里那段说明:模型没法塞进 .app 内部,
包体是签名封死的)。从源码跑的时候手动放到仓库根的 models/(已被 gitignore),
或用环境变量 VOXKEY_MODELS_DIR 指到别处。常驻软件只认 funasr-nano-int8:
curl -L -o /tmp/funasr.tar.bz2 \
https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-funasr-nano-int8-2025-12-30.tar.bz2
tar -xjf /tmp/funasr.tar.bz2 -C models/ # 解出来约 972MB,tokenizer(Qwen3-0.6B)在里面tools/verify/run_verify.py 做模型质量对比还需要 sense-voice / zipformer / paraformer 那几个,
名字见 src/voxkey/models.py 的 REGISTRY;软件本体用不到。
PYTHONPATH=src .venv/bin/python -m voxkey.app # 常驻(默认模型 funasr-nano-int8)
PYTHONPATH=src .venv/bin/python -m voxkey.app --no-device # 不读按键,用菜单手动开始/结束权限(系统设置 → 隐私与安全性):
| 权限 | 用途 | 没有会怎样 |
|---|---|---|
| 辅助功能 | 合成按键上屏、AX 直写、读前台窗口与焦点控件 | 写不进输入框(会明说「未上屏」) |
| 输入监控 | 读设备(AU05)自己的 HID 按键报文 | 按语音键没反应 |
| 麦克风 | 采设备音频 | 录不到声音 |
开发期直接跑脚本时,权限记在启动它的终端头上;打包成 .app 之后才记在自己头上。
src/voxkey/
app.py 常驻主程序:托盘菜单、状态机(按住/松手/取消)、按键转发、注入调度
pill.py 悬浮状态条:固定窗口 + CALayer 弹簧 + 裁剪层,两行布局(状态行 / 内容行)
inject.py 上屏层:AX 直写 → 合成 Unicode 按键,回读校验,失败明说
audio.py 采集层:找设备、100ms 收块、伪流式预览、录音期间定稿满段
transcribe.py 推理层:recognizer 串行化 + 长音频分段/尾窗切分(模型上下文上限 512 token ≈ 28s)
models.py 模型注册表与加载(默认取仓库根 models/,可用 VOXKEY_MODELS_DIR 覆盖)
device/ 设备层:HID 报文读取(keyreader)、厂商通道协议(protocol)、设备对象(device)、监视脚本(monitor)
tools/
ptt_demo.py 命令行 demo:不装托盘也能跑通「按住说话 → 剪贴板上屏」(要装 .[tools]),验证硬件链路用
device_probe.py 设备探针:读按键、改键位映射、诊断(改完要拔插接收器才生效)
verify/ 模型质量对比:候选模型 × 语料,输出 CER / 首字延迟 / RTF
设备协议(AU05 的 HID 集合、厂商通道帧格式与 TEA、命令表、键位表、待机行为、以及实测踩过的坑)
整理成了 docs/au05-protocol.html,浏览器直接打开看。
任务唯一事实源是本仓库的 Issues(状态用 label:b:open / b:doing / b:done / b:closed)。这些条目是从硬件仓库 c3ng-dev/OneSay 迁过来的,编号有变化:
| OneSay | VoxKey | OneSay | VoxKey |
|---|---|---|---|
| #2 | #1 | #25 | #10 |
| #5 | #2 | #32 | #11 |
| #6 | #3 | #33 | #12 |
| #13 | #4 | #34 | #13 |
| #14 | #5 | #35 | #14 |
| #15 | #6 | #36 | #15 |
| #20 | #7 | #39 | #16 |
| #21 | #8 | #40 | #17 |
| #22 | #9 |
每条正文顶部标了来源;正文里出现的 #N 是 OneSay 的编号,不是本仓库的。硬件、固件、分离麦、
远程控制、定价等条目仍留在 OneSay。
改完代码先跑冒烟(不连设备、不需要权限,几秒钟):
PYTHONPATH=src .venv/bin/python tools/smoke.py # 全部
PYTHONPATH=src .venv/bin/python tools/smoke.py --no-model # 没下模型时覆盖:包导入、悬浮条四态几何断言(文字/波形都在裁剪层内且居中,是数值不是肉眼)、模型加载、
四个命令行入口的 --help。
packaging/build_macos.sh # 出 packaging/dist/VoxKey/ 和 VoxKey-macos-arm64.zip
packaging/build_macos.sh --sign # 顺带签名(要 VOXKEY_SIGN_IDENTITY)
packaging/build_macos.sh --sign --notarize # 再公证 + 装订(要 VOXKEY_NOTARY_PROFILE)产物是便携文件夹 packaging/dist/VoxKey/(里面 VoxKey.app + 读我.txt),分发的压缩包
就是把它整个打进去。用户那边模型和日志会落在 .app 旁边,卸载 = 删文件夹。
构建环境是单独的 .venv-build,不碰开发用的 .venv。图标由 packaging/make_icon.py
从设计稿 packaging/app-icon.svg 生成(应用图标出 icns、菜单栏出模板 png),icns 不进仓库。
发版就是打个 tag,GitHub Actions 会构建、建 Release、传上 VoxKey-macos-arm64.zip:
git tag v0.1.0 && git push origin v0.1.0资产文件名不能带版本号。 静态页(site/)用的是 GitHub 的固定跳转
releases/latest/download/VoxKey-macos-arm64.zip,名字一变链接就废——这条由冒烟的
「发版链路名字一致」用例钉着。
签名要 Apple Developer 账号($99/年)。没签名也能发,只是用户第一次打开得手动放行一次;
CI 里配好 MACOS_CERTIFICATE / APPLE_ID 等 secret 就会自动签名 + 公证 + 装订。
- 我们的进程一打开设备的键盘集合,macOS 就收不到该设备的按键报文(谁先打开谁独占)。所以设备上
的「确认 = 回车、取消 = 退格」由主程序转发(
app.py的ROUTE_KEYS)——实测按一次只触发一次,不重复。 - 远程桌面 / 虚拟机(RDP、VNC、Parallels、VMware)按 scancode 转发、丢掉 Unicode 载荷,会打出一串
a;软件层无解。 - 密码框、
sudo期间系统开了 Secure Input,注入会被吞,此时直接拒绝并提示。 - 只出了 macOS(Apple 芯片)的包。Windows / Linux 要另写 UI(托盘 + 悬浮条)和注入层 (Windows 用 SendInput;Linux 的 Wayland 没有通用注入协议,是已知的硬骨头)。
- 签名和公证还没实跑过(等 Apple Developer 账号)。脚本按官方文档写好放在
packaging/里了。 - 开机自启还没做。