把逗比输入法的语音引擎拆了
逗比输入法内置了一套基于 Seed-ASR 的流式语音识别。很能打。问题是焊死在输入法里了。
我把它拆出来了。拆到自己的 app 里。逗比可以不开。
$ ./run_hj_dictation.sh
[ws] frontier-audio-ime-ws.doubi.com connected
[asr] partial: "你好"
[asr] partial: "你好,世界"
[asr] partial: "你好,世界测试123。"
[asr] final: "你好世界,测试123。"
这是逗比输入法已经退出之后跑的。
拿到了什么
鉴权材料:
appKey = OrnqKvSSrs- token 来自
SAMITokenManager活跃单例,JWT,约 24h 有效 - LLDB attach 一次读出来,不需要伪造
WebSocket 握手:
wss://frontier-audio-ime-ws.doubi.com/ocean/api/v1/ws
?aid=685343&app_name=oime_macos&app_version=0.5
proto-version: v2
x-tt-e-k: <device_id>+W
x-tt-e-b: 1
x-tt-e-k 是 device_id 拼 +W。来源:OnServiceReady 回调的 encrypt.id。
protobuf 消息,四条:
StartTask token + appKey + "ASR" + session_id
StartSession token + appKey + "ASR" + payload_json + session_id
TaskRequest "ASR" + timestamp_ms + opus_audio + session_id
FinishSession appKey + "ASR" + session_id
手搓 encoder,没用 protoc。Python 80 行,Rust 120 行。
音频格式:Opus CBR 16kHz mono,20ms chunk 上行。
怎么拆的
先试了蛮力。枚举 url × header × transportType 的 72 种组合直调 SAMI 底层 API。全部 100002 SAMI_NOT_SUPPORT。换高层 SAMIHandle.process 又全是 100017。
72 组全挂说明方向错了。底层 API 需要的 hidden context,比接口签名暴露的多得多。
于是换路线:不调它的 API,抓它真正发出去的东西。
LLDB attach 到活跃的 SAMITokenManager 单例拿 token。这一步之前被 Frida 和 heap walk 折腾了很久——Frida 直接被 detach,heap.py 的表达式调用稳定 EXC_BAD_ACCESS。最后发现单例 getter 一把就读出来了。
然后是抓 wire truth。这里有个坑:hook 会杀死目标。
做了完整的 bisect:
attach 本身 → 安全
ObjC hook → 安全
native Interceptor → 有 UI,无转写,卡死
├─ backtrace → 安全
├─ 数字读取 → 安全
├─ libcxx string → 安全
├─ readMaybeCString → 有 UI,没波纹,没转写
└─ readStdStringLike → 同上
在 BiStream 配置指针的热路径上做一次轻量字符串解引用,就够让实时转写管线静默降级。不 crash。不报错。就是没结果了。
知道了哪类 hook 有毒之后,改成最小扰动采样:只挂 dispatch 和 ws binary slot,拿到第一个 StartSession 就撤。
最终在 ws binary 面上对齐了 StartTask / StartSession / FinishSession / session_id。同轮同字节。
拿到协议形状之后就是工程活了。写 standalone WebSocket client,验证端到端转写。然后包成 demo server,再用 Rust 写原生 macOS app。
最后做了 one-shot bootstrap:短暂唤醒逗比,抓 token,写 env,关掉逗比。之后自己的 app 直连 frontier,不需要逗比活着。
$ ./bootstrap_doubao_token_once.sh
[*] SAMITokenManager.currentToken → eyJhbG...
[*] bootstrap.env written
$ pkill DoubaoIme
$ python3 scripts/standalone/asr_ws_client.py --audio test.wav
[ws] connected
[asr] partial: "现在是"
[asr] partial: "现在是独立Demo"
[asr] final: "现在是独立demo验证。"
逗比已经不在了。转写正常。
还差什么
token 24 小时过期。刷新需要走 fetchTokenUrl,那个请求带着逗比自己的设备指纹和签名。这部分没剥离。
所以现状是:每 24 小时得借逗比进程 2 秒钟。
完全脱离逗比安装,还做不到。
数字
- 开始到首次独立转写:5 天
- 实验轮次:100+
- 协议消息:4 条
- bootstrap 耗时:< 3 秒
- 说话到出字:< 300ms
2026-04-04