Share

外观
风格

把逗比输入法的语音引擎拆了

2026年4月4日 · 专栏

逗比输入法内置了一套基于 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-kdevice_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