dsh-plugin-mobile-gateway 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PROTOCOL.md +79 -2
- package/README.md +32 -2
- package/docs/voice-input-hold-thresholds.md +35 -0
- package/docs/voice-input-implementation.md +219 -0
- package/docs/voice-input-ipa-analysis.md +150 -0
- package/docs/voice-input-ipa-evidence/deepseek-asr-constructor.asm +338 -0
- package/docs/voice-input-ipa-evidence/deepseek-extraction.json +5154 -0
- package/docs/voice-input-ipa-evidence/deepseek-focused-context.txt +210 -0
- package/docs/voice-input-ipa-evidence/deepseek-localization.json +29 -0
- package/docs/voice-input-ipa-evidence/deepseek-voice-strings.txt +206 -0
- package/docs/voice-input-ipa-evidence/doubao/address-map.txt +11 -0
- package/docs/voice-input-ipa-evidence/doubao/code-string-refs.txt +22 -0
- package/docs/voice-input-ipa-evidence/doubao/end-longpress-disasm.txt +149 -0
- package/docs/voice-input-ipa-evidence/doubao/focused-context.txt +271 -0
- package/docs/voice-input-ipa-evidence/doubao/locate_string_refs.py +42 -0
- package/docs/voice-input-ipa-evidence/doubao/network-candidates.txt +39 -0
- package/docs/voice-input-ipa-evidence/doubao/objc-msgsend-stub.txt +1 -0
- package/docs/voice-input-ipa-evidence/doubao/ranges.txt +858 -0
- package/docs/voice-input-ipa-evidence/doubao/recognizer-recorder.txt +134 -0
- package/docs/voice-input-ipa-evidence/doubao/result-disasm.txt +189 -0
- package/docs/voice-input-ipa-evidence/doubao/settext-selector.txt +2 -0
- package/docs/voice-input-ipa-evidence/doubao/start-stop-disasm.txt +358 -0
- package/docs/voice-input-ipa-evidence/doubao/strings.txt +3536 -0
- package/docs/voice-input-ipa-evidence/doubao-extraction.json +7418 -0
- package/docs/voice-input-ipa-evidence/doubao-findings.md +52 -0
- package/docs/voice-input-ipa-evidence/hold-threshold-rea-evidence.json +1 -0
- package/docs/voice-input-ipa-evidence/hold-thresholds/deepseek-hold-callback-refs.txt +1 -0
- package/docs/voice-input-ipa-evidence/hold-thresholds/deepseek-input-area-metadata.txt +31 -0
- package/docs/voice-input-ipa-evidence/hold-thresholds/deepseek-input-gesture-init.asm +267 -0
- package/docs/voice-input-ipa-evidence/hold-thresholds/deepseek-static-summary.json +42 -0
- package/docs/voice-input-ipa-evidence/hold-thresholds/deepseek-voice-gesture-constructor.asm +371 -0
- package/docs/voice-input-ipa-evidence/hold-thresholds/doubao-findings.md +31 -0
- package/docs/voice-input-ipa-evidence/hold-thresholds/doubao-text-mask-objc-metadata.txt +119 -0
- package/docs/voice-input-ipa-evidence/hold-thresholds/doubao-text-mask-threshold-evidence.asm +156 -0
- package/docs/voice-input-ipa-evidence/hold-thresholds/doubao-text-mask-voice-view-getter.asm +14 -0
- package/docs/voice-input-ipa-evidence/rea-evidence.json +1 -0
- package/lib/client.js +412 -132
- package/lib/cloudflare-tunnel.mjs +29 -2
- package/lib/devices.js +23 -7
- package/lib/dsh-host-adapter.mjs +10 -0
- package/lib/gateway-probe.mjs +6 -0
- package/lib/index.mjs +32 -7
- package/lib/public-endpoint-check.mjs +61 -0
- package/lib/speech-input.mjs +192 -0
- package/package.json +3 -3
package/PROTOCOL.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
- **本机端点**:`ws://127.0.0.1:3080/ws/mobile`(与 dsh web GUI 同端口)
|
|
6
6
|
- **局域网端点**:`ws://<电脑的私有局域网 IP>:3081/ws/mobile`(插件独立监听,只提供经过鉴权的 WebSocket)
|
|
7
7
|
- **公网端点**:必须由 TLS 反向代理提供 `wss://<域名>/ws/mobile`
|
|
8
|
-
- **帧格式**:全部为 JSON 文本帧(UTF-8
|
|
8
|
+
- **帧格式**:全部为 JSON 文本帧(UTF-8);图片字节及语音 WAV 使用标准 Base64
|
|
9
9
|
- **连接即推送**:连上后服务端立刻发送一条 `hello`,之后 agent 输出以 `event` 帧实时推送
|
|
10
10
|
|
|
11
11
|
---
|
|
@@ -1130,7 +1130,9 @@ DSH Web 的“深度求索中,用时 22 秒 ···”是客户端文案,不
|
|
|
1130
1130
|
|
|
1131
1131
|
| 版本 | 新增 |
|
|
1132
1132
|
|---|---|
|
|
1133
|
-
| v0.
|
|
1133
|
+
| v0.10.1(当前源码) | 新增官方 speech Remote 语音输入桥接;模型准备与状态同步;移动端音频转文字与取消转写;连接关闭时清理语音任务 |
|
|
1134
|
+
| v0.10.0 | 响应式居中控制面板与主题同步;自定义下拉菜单;独立配对模态窗口与缩放动画;按运行实例关闭重启提醒;在线设备折叠列表与移动端设备名称上报;Tunnel 配置交互优化 |
|
|
1135
|
+
| v0.9.0 | DSH 0.2.0-rc.2 插件管理;PC 端 Cloudflare Quick / 命名 Tunnel 及独立重启;插件私有 `cloudflared` 缓存及校验;配对入口选择指引;网关重启与 DSH Web 停止工具 |
|
|
1134
1136
|
| v0.7.2 | 独立对话/控制连接;空 Session 创建;停止生成与稍后继续;排队消息同步及编辑/删除/Steer;App 归档/重命名 Session;WebUI 归档集合和名称变化实时同步到 App |
|
|
1135
1137
|
| v0.1.5 | workspace-create / directories / host |
|
|
1136
1138
|
| v0.1.6 | 修复消息分发器遗漏(host/directories/workspace-create 未路由) |
|
|
@@ -1234,3 +1236,78 @@ does not put file responses behind conversation frames. This isolates socket
|
|
|
1234
1236
|
queues; the underlying network link and Host resources remain shared. On connection
|
|
1235
1237
|
failure clients reconnect and authenticate both lanes, then resubscribe and catch up
|
|
1236
1238
|
history. Older gateways without the capability continue using one connection.
|
|
1239
|
+
|
|
1240
|
+
### 移动端设备名称
|
|
1241
|
+
|
|
1242
|
+
移动端在 WebSocket 握手中通过 `X-DSH-Device-Name` 上报 UTF-8 设备名称的标准 Base64 编码;配对和凭据重连时均发送。网关不使用控制面板指定的名称,未上报时使用“未命名设备”。可信设备接口保留原始 `name`,并提供 `displayName`;同名设备显示编号后缀,例如“我的手机 (1)”“我的手机 (2)”。
|
|
1243
|
+
|
|
1244
|
+
|
|
1245
|
+
## 官方语音输入(`speech-input-v1`)
|
|
1246
|
+
|
|
1247
|
+
网关代理 DSH 0.2.0-rc.2 的官方实验性 `speech` Remote。先在 DSH 插件管理中启用 `@deepseek-ai/dsh-experimental-voice-input-bundle`(“语音输入”),并在详情中下载、准备模型;也可由客户端通过下面的准备接口操作。识别模型运行在 DSH 主机上,不进入移动安装包。
|
|
1248
|
+
|
|
1249
|
+
所有语音请求必须来自已配对设备,并使用 **control 连接**(兼容 legacy 单连接)。即使关闭本机调试鉴权,未配对连接也不能调用语音接口。配对连接的 `hello.capabilities` 包含 `speech-input-v1`,表示网关协议支持;**是否已启用官方插件、模型是否就绪,需要查询目录**。
|
|
1250
|
+
|
|
1251
|
+
每个请求都必须提供非空 `requestId`(最多 128 字符),活动请求和订阅期间不要复用。转写结果不创建 Session、不启动 Agent、不写历史,也不自动发送;客户端根据请求 ID 确认录音仍属于当前草稿后插入文字。
|
|
1252
|
+
|
|
1253
|
+
| 请求 `type` | 字段(除 `requestId` 外) | 响应 `kind` |
|
|
1254
|
+
|---|---|---|
|
|
1255
|
+
| `speech-catalog` | 无 | `speech-catalog` |
|
|
1256
|
+
| `speech-subscribe` | 无 | `speech-subscribed`,随后推送 `speech-state` |
|
|
1257
|
+
| `speech-unsubscribe` | 无 | `speech-unsubscribed` |
|
|
1258
|
+
| `speech-configure` | `providerId?`, `language?`,至少一个 | `speech-configured` |
|
|
1259
|
+
| `speech-prepare` | `providerId`, `downloadSource?` | `speech-preparing` |
|
|
1260
|
+
| `speech-cancel-preparation` | `providerId` | `speech-preparation-cancelled` |
|
|
1261
|
+
| `speech-transcribe` | `audioBase64`, `providerId?`, `language?` | `speech-transcript` |
|
|
1262
|
+
| `speech-cancel` | `targetRequestId`,同连接的活动转写 ID | `speech-cancelled` |
|
|
1263
|
+
|
|
1264
|
+
### 目录、安装和进度
|
|
1265
|
+
|
|
1266
|
+
```json
|
|
1267
|
+
{ "type": "speech-catalog", "requestId": "speech-info-1" }
|
|
1268
|
+
```
|
|
1269
|
+
|
|
1270
|
+
启用后响应 `available:true`,并透传官方 `providers`, `selection`, `maxDurationSeconds`。`maxAudioBytes` 为官方限制和网关限制的较小者:网关最多接受 4 MiB 解码后 WAV,并按 `maxPayloadBytes` 为 JSON 信封预留空间。官方默认时长为 120 秒,实际值以目录为准。
|
|
1271
|
+
|
|
1272
|
+
`providers[]` 包含 `id`, `name`, `location`, `languages`, `preparation`,以及可选 `setupEstimate` 和 `downloadSources`。`preparation.phase` 为 `ready` 或 `standby` 时可转写;`unprepared` 需要准备。不要假设语言代码,使用所选 Provider 的 `languages`。`selection` 是主机保存的默认识别器和语言。
|
|
1273
|
+
|
|
1274
|
+
插件未启用/Remote 已撤销时,目录返回:
|
|
1275
|
+
|
|
1276
|
+
```json
|
|
1277
|
+
{ "kind": "speech-catalog", "requestId": "speech-info-1", "available": false,
|
|
1278
|
+
"providers": [], "code": "speech/unavailable",
|
|
1279
|
+
"message": "Enable the official voice input bundle in DSH plugin management" }
|
|
1280
|
+
```
|
|
1281
|
+
|
|
1282
|
+
```json
|
|
1283
|
+
{ "type": "speech-subscribe", "requestId": "speech-watch-1" }
|
|
1284
|
+
{ "type": "speech-prepare", "requestId": "speech-setup-1", "providerId": "sensevoice-local" }
|
|
1285
|
+
```
|
|
1286
|
+
|
|
1287
|
+
每条 `speech-state` 带订阅的 `requestId`,是完整目录快照。准备过程会提供阶段、步骤和真实下载字节。`speech-preparing` 只表示准备请求已提交,不能当作模型已就绪。`downloadSource` 必须来自目录公布的源,官方 Provider 会验证它。断开观察连接或取消订阅不取消 Host 拥有的准备任务;只有 `speech-cancel-preparation` 显式取消。订阅因服务禁用或正常结束而终止时会返回错误,重新启用后需要重新订阅。
|
|
1288
|
+
|
|
1289
|
+
### 松手后提交录音、回填文字
|
|
1290
|
+
|
|
1291
|
+
客户端长按时录音,松手停止并转换成 **16 kHz、单声道、PCM16 WAV**,将包含 WAV 头的完整文件编码为规范 Base64(不是 PCM 裸流、AAC/M4A 或 Opus)。
|
|
1292
|
+
|
|
1293
|
+
```json
|
|
1294
|
+
{ "type": "speech-transcribe", "requestId": "record-1",
|
|
1295
|
+
"providerId": "sensevoice-local", "language": "zh", "audioBase64": "<完整 WAV 的 Base64>" }
|
|
1296
|
+
```
|
|
1297
|
+
|
|
1298
|
+
```json
|
|
1299
|
+
{ "kind": "speech-transcript", "requestId": "record-1", "text": "帮我检查这段代码",
|
|
1300
|
+
"audioSeconds": 3.2, "inferenceSeconds": 0.4 }
|
|
1301
|
+
```
|
|
1302
|
+
|
|
1303
|
+
`text` 允许为空,表示未识别到讲话。录音开始时应从目录捕获识别器和语言并随请求发送,避免主机偏好在录音中改变。这里只支持官方插件的**完整录音转写**,没有实时增量文字、语音通话或 TTS。
|
|
1304
|
+
|
|
1305
|
+
每连接最多一个活动转写,网关总共最多四个,每连接最多八个待完成普通语音请求。转写包括目录查询在内有 180 秒超时;官方 Provider 还具有自己的准备/推理限制。
|
|
1306
|
+
|
|
1307
|
+
```json
|
|
1308
|
+
{ "type": "speech-cancel", "requestId": "cancel-1", "targetRequestId": "record-1" }
|
|
1309
|
+
```
|
|
1310
|
+
|
|
1311
|
+
取消回执只表示已发出取消信号;原转写随后返回 `speech/cancelled`,资源清理完成前仍占并发名额。断线、网关停止会中止该连接的活动转写和状态订阅。其他连接不能取消这个转写,即使属于同一设备。客户端上滑取消、切换页面或替换草稿后必须忽略旧请求的迟到结果。
|
|
1312
|
+
|
|
1313
|
+
错误统一为 `kind:error`,包含 `requestType`, `requestId`, `code`, `message`。网关错误包括 `authentication-required`, `wrong-channel`, `bad-request`, `speech/unavailable`, `speech/busy`, `speech/request-in-progress`, `speech/request-not-found`, `speech/invalid-audio`, `speech/cancelled`, `speech/timeout`, `speech/invalid-response`, `speech/stream-ended`;官方插件的 `speech/transcription-failed` 等错误代码保留。
|
package/README.md
CHANGED
|
@@ -10,7 +10,11 @@ DeepSeek Harness 的设备鉴权移动网关,支持会话与实时事件、排
|
|
|
10
10
|
>
|
|
11
11
|
> 实时流已改为独立 `assistant-stream` 帧,移动端需要按 [rc.2 接入说明](docs/dsh-rc2-mobile-integration.md) 更新订阅、缓存与分页处理。当前修改尚未发布。
|
|
12
12
|
>
|
|
13
|
-
> v0.
|
|
13
|
+
> v0.10.1(当前源码):新增官方语音输入接入,支持语音模型准备与状态同步、移动端音频转文字、取消转写及连接关闭后的任务清理。通过官方 speech Remote 处理识别,移动网关无需打包 ASR 模型。
|
|
14
|
+
>
|
|
15
|
+
> v0.10.0:移动设备面板改为响应式居中设置窗口,跟随主应用深浅色主题;统一自定义下拉菜单与连接状态高亮;配对二维码使用独立模态窗口并支持缩放进出动画;安装重启提醒可按网关运行实例关闭;可信设备默认仅显示在线设备,支持展开离线设备、移动端名称上报与同名编号;Tunnel 切换自动应用,命名配置编辑后保存。
|
|
16
|
+
>
|
|
17
|
+
> v0.9.0:新增 DSH 0.2.0-rc.2 插件管理能力;PC 端可在面板开启、重启 Cloudflare Quick / 命名 Tunnel,使用插件私有缓存中的 `cloudflared` 并校验文件;配对面板增加局域网与外网入口选择指引;工具菜单可重启网关或停止 DSH Web,重启时不再打开新标签页。
|
|
14
18
|
>
|
|
15
19
|
> v0.7.3:优化移动网关运行模式下拉框的箭头间距。
|
|
16
20
|
>
|
|
@@ -200,11 +204,19 @@ Quick Tunnel 无需 Cloudflare 账户、Token 或自己的公网域名,但地
|
|
|
200
204
|
### Cloudflare 命名 Tunnel(固定地址)
|
|
201
205
|
|
|
202
206
|
1. 在 [Cloudflare 控制台创建远程管理的 Tunnel](https://developers.cloudflare.com/tunnel/get-started/),添加一个公开域名(例如 `gateway.example.com`),将该路由的 **Service URL** 设为 `http://127.0.0.1:3082`。无需安装 Cloudflare 系统服务,也无需开放路由器入站端口。
|
|
203
|
-
2. 从 Tunnel 的「Add a replica」安装命令中复制 Token,在 DSH「移动设备 → Cloudflare Tunnel」选择「命名 Tunnel · 固定域名」,输入公开域名和 Token 后开启。插件会将网关设为常驻模式,启动本机专用入口和 `cloudflared
|
|
207
|
+
2. 从 Tunnel 的「Add a replica」安装命令中复制 Token,在 DSH「移动设备 → Cloudflare Tunnel」选择「命名 Tunnel · 固定域名」,输入公开域名和 Token 后开启。插件会将网关设为常驻模式,启动本机专用入口和 `cloudflared`。隧道传输连接后,面板会独立检测本机到公网入口的 DNS、TLS 和移动网关路由;检测通过后使用自动填入的 `wss://gateway.example.com/ws/mobile` 生成配对二维码。
|
|
204
208
|
3. 以后启动 `dsh web` 会自动恢复已开启的 Tunnel;在面板点击「关闭 Cloudflare Tunnel」会停止插件管理的 `cloudflared` 并关闭本机专用入口。若手动关闭网关,Tunnel 会暂停,重新开启网关后恢复。
|
|
205
209
|
|
|
206
210
|
两种 Cloudflare 入口都只接受 `/ws/mobile` WebSocket,其他 HTTP 路径(包括 DSH WebUI 和 `/mgw` 管理接口)返回 404;设备凭证始终必需,即使本机 Debug 鉴权被关闭也一样。命名 Tunnel 的 Token 保存在 `<deviceFile>.cloudflare.json`(默认 `~/.dsh/mobile-gateway-devices.json.cloudflare.json`),权限为 `0600`,不会返回给浏览器。默认使用 DSH 私有的 `cloudflared-bin` 缓存,不读取系统或其他 App 的 `cloudflared`;每次启动 Tunnel 都校验缓存文件,只有缓存缺失或损坏时才下载固定版本并核对 Cloudflare 官方发布资产的 SHA-256。仅在显式配置 `cloudflaredPath` 时使用外部程序。使用自定义 `cloudflarePort` 时,命名 Tunnel 路由的 Service URL 端口也要相应修改。
|
|
207
211
|
|
|
212
|
+
#### 公网入口检测与状态含义
|
|
213
|
+
|
|
214
|
+
`state: online` 只表示 cloudflared 已注册隧道连接,不保证公开域名、TLS 或源站路由可用。状态 API 另外提供 `endpointHealth`(状态、提示、检测时间及错误码)和 `publicReady`。面板在连接后自动检测一次,也可点击「检测公网 DNS / TLS / WebSocket」重试;该按钮调用受现有管理鉴权保护的 `POST /mgw/cloudflare/check`,不会重启隧道。
|
|
215
|
+
|
|
216
|
+
检测使用本机系统 DNS 和严格证书校验的 HTTPS WebSocket 请求,不携带配对码或设备 Token,不跟随重定向。只有 HTTP 401 且移动网关回显本次随机探测标记时,才确认受鉴权保护的网关路由可达;代理或 Cloudflare Access 的普通 401 不会被当作成功。该标记只在缺少有效设备凭证的拒绝响应中回显,不授予任何访问权限。结果是一次本机网络快照,不保证手机网络、设备鉴权或会话同步成功。
|
|
217
|
+
|
|
218
|
+
本机解析失败不代表公共 DNS 记录必然不存在:刚创建记录时可能仍有 NXDOMAIN 缓存,也可能受本机 DNS、VPN 或代理影响。核对公开主机名和已代理 CNAME,再稍后重试,不要关闭 TLS 校验。插件目前使用 Tunnel Token 运行远程管理的隧道,该 Token 不会自动创建域名路由或 DNS;具备额外账户授权的 cloudflared CLI / Cloudflare API 可以配置这些资源,但不属于本检测功能。
|
|
219
|
+
|
|
208
220
|
### Tailscale(推荐长期使用)
|
|
209
221
|
|
|
210
222
|
1. 在电脑和 iPhone 安装 [Tailscale](https://tailscale.com/download),并登录同一个 Tailnet。
|
|
@@ -289,3 +301,21 @@ npm test
|
|
|
289
301
|
```
|
|
290
302
|
|
|
291
303
|
面板右上角“工具”菜单提供“重启当前网关”和“停止 DSH Web”。重启只在通过 `dsh web` 启动时可用,会结束当前 Web 进程并沿用原启动参数重新运行;重启时自动加入 `--no-open`,不会再打开新的浏览器标签页,当前标签页会等待恢复并刷新。重启后的进程在后台运行,不再由原 Terminal 前台任务管理;需要关闭时可在同一菜单点击“停止 DSH Web”,或在 Terminal 找到监听 Web 端口的 DSH 进程并发送 `SIGTERM`。重启期间连接会短暂中断,Quick Tunnel 的外网地址可能随之改变。源码修改后需重新安装插件并重启 `dsh web`。
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
## 官方语音输入
|
|
307
|
+
|
|
308
|
+
网关已接入 DSH 0.2.0-rc.2 官方实验性语音输入插件。移动端可以查询识别器、订阅模型准备进度、准备模型、设置识别偏好,以及提交录音取得文字。取消和断线会中止转写,结果不会自动发送给 Agent。
|
|
309
|
+
|
|
310
|
+
1. 在 DSH 插件管理中启用“语音输入”(`@deepseek-ai/dsh-experimental-voice-input-bundle`),下载并准备模型。
|
|
311
|
+
2. 客户端通过已配对的 control WebSocket 查询 `speech-catalog`,确认插件可用及模型 `ready`/`standby`。
|
|
312
|
+
3. 长按录音、松手后,将完整音频转换为 16 kHz 单声道 PCM16 WAV,通过 `speech-transcribe` 上传 Base64。
|
|
313
|
+
4. 收到 `speech-transcript` 后将文字填入输入框,用户确认后使用现有 `message` 发送。
|
|
314
|
+
|
|
315
|
+
模型运行在 DSH 电脑上,手机无需携带识别权重。当前只提供完整录音转写;客户端的录音按钮、权限申请和音频转换仍须接入。接口、准备流程和错误处理见 [协议说明](PROTOCOL.md#官方语音输入speech-input-v1)。
|
|
316
|
+
|
|
317
|
+
可用已安装的官方插件核对真实 WAV 校验(识别 Provider 使用测试替身,不下载模型):
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
DSH_INSTALL_ROOT=/absolute/path/to/@deepseek-ai/dsh node test/speech-official.test.mjs
|
|
321
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# 按住说话的长按识别阈值
|
|
2
|
+
|
|
3
|
+
分析日期:2026-10-12。仅分析用户提供的豆包 12.2.1 和 DeepSeek 2.5.1 iOS IPA;未安装或运行第三方 App,也未读取服务端配置。阈值是手势判定时间,不是麦克风就绪或 ASR 开始的端到端延时。
|
|
4
|
+
|
|
5
|
+
## 豆包:输入框文本蒙层为 150ms
|
|
6
|
+
|
|
7
|
+
`FlowInputBizUI.MessagingInputView.textMaskLongPressGesture` 的初始化在 `0x1078f0964..0x1078f0974` 构造 double `0x3fc3333333333333`(0.15 秒),并传给 `setMinimumPressDuration:`。同一构造路径随后安装独立的 `handleTextMaskViewTapWithGesture:`,因此该数字属于输入框 tap/longpress 区分。
|
|
8
|
+
|
|
9
|
+
长按 action 为 `handleTextMaskViewLongPressWithGesture:`,回调链取得 `basicView.voiceInputView` 后转交当前手势。该关联排除了把文本选择或语音通话手势误当成语音输入。快捷输入按钮还有 100ms 路径,专用语音按钮还有 0ms 路径,它们不是该文本蒙层的阈值。
|
|
10
|
+
|
|
11
|
+
REA 安全提取 Evidence:`ev_adf3fc5bce309b0fcea619cdaf3d36b5766996f265fa8a7046639bf25a6319b8`。主程序 SHA-256:`8f81ead52957545efb22c2be3c03717d7fef4803b090d25a2daa0db613fbc47e`。本轮指令及调用链为本地 LLVM/结构解析补充证据,未虚构新的 REA ID。
|
|
12
|
+
|
|
13
|
+
证据:[详细调查](voice-input-ipa-evidence/hold-thresholds/doubao-findings.md)、[阈值与入口汇编](voice-input-ipa-evidence/hold-thresholds/doubao-text-mask-threshold-evidence.asm)、[语音视图 getter](voice-input-ipa-evidence/hold-thresholds/doubao-text-mask-voice-view-getter.asm)。完整后续 ASR 间接分发尚未全部恢复。
|
|
14
|
+
|
|
15
|
+
## DeepSeek:100/150ms 配置回退,另有 1ms 提前识别分支
|
|
16
|
+
|
|
17
|
+
解码的 `InputConfig` 字段包含 `voiceGestureDurationMs`、`voiceMaskAppearDurationMs`,编码键分别为 `voice_gesture_duration_ms`、`voice_mask_appear_duration_ms`。输入区配置回调 `0x1004c8b34` 无配置时在 `0x1004c8c5c..60` 装入 150 和 100,随后按 inputMode 的计算结果选择其中一项,转换成秒并设置长按识别器。该路径使用 `InputAreaView` 的 `_inputViewModel` 字段(实例 offset 40,offset 变量 `0x101349d48`)。
|
|
18
|
+
|
|
19
|
+
在 `0x1004c8d30..70`,选出的毫秒值除以 1000:其中一只手势根据布尔分支选择配置时长或 double 0.001 秒,另一只使用配置时长。该 literal 的链接地址 `0x101053c48` 已直接读取确认。说明早期手势识别和可见输入状态判定有不同路径,不能将 1ms、100ms 或 150ms 任意一个数字描述成所有模式的固定录音延时。
|
|
20
|
+
|
|
21
|
+
本轮没有执行 App、恢复远端配置或完整确定 inputMode/布尔分支的业务含义;因此结论限于包内回退值和静态设置路径,不声称实机当前一定采用这些值。
|
|
22
|
+
|
|
23
|
+
REA `inspect_macho` Evidence:`ev_c96cdf0dac4ab8f520e94b93dbee01a57ce337a76934895ca925e4a8cddc9f5c`。主程序 SHA-256:`027b12d8d3c3becb5c6e203b09d2d3387f57747f9f2a299ca90f194cab9e8c20`。已关闭本轮原生目标。
|
|
24
|
+
|
|
25
|
+
证据:[REA 完整记录](voice-input-ipa-evidence/hold-threshold-rea-evidence.json)、[结构与数值摘要](voice-input-ipa-evidence/hold-thresholds/deepseek-static-summary.json)、[配置回调汇编](voice-input-ipa-evidence/hold-thresholds/deepseek-input-gesture-init.asm)、[输入区字段](voice-input-ipa-evidence/hold-thresholds/deepseek-input-area-metadata.txt)。ADRP 候选扫描仅用于定位,结论使用对应指令核验,不把候选扫描视为完整调用图。
|
|
26
|
+
|
|
27
|
+
## 本客户端的调整与验证
|
|
28
|
+
|
|
29
|
+
旧 iOS 与 Android 长按门槛均为 350ms,蒙层淡入为 240ms。现在统一由 KMM `VoiceInputInteraction` 提供 100ms 长按门槛和 120ms 淡入时长;识别成功即发送公共层录音 intent,不再单独等待蒙层。淡出时长保留原值。
|
|
30
|
+
|
|
31
|
+
100ms 是本客户端的工程选择,参考上述静态路径并满足用户要求更快地区分 tap/longpress,不冒称两个 App 都使用 100ms。首次授权、音频会话和硬件启动仍由系统处理,不能将长按门槛称为真实录音就绪的总耗时。
|
|
32
|
+
|
|
33
|
+
Android 模拟器 7 项界面回归通过:真实 70ms 按下/松手正常进入编辑,160ms 按下/松手已进入语音长按的前置检查;还验证了草稿保留、回填后编辑、椭圆边缘和更快淡入。测试使用原始 down/up 事件,避免 Compose `longClick` 帮助方法强制至少 400ms 的限制。iOS 10 项语音/输入框回归通过;公共层 259 项测试通过,Kotlin 改动通过 ktlint 和 `git diff --check`。
|
|
34
|
+
|
|
35
|
+
当前用户本地 `DeepSeekHarnessMobile` scheme 覆盖了共享测试配置;本轮使用现有 `WorkspaceDrawerUITests` scheme,仅选择单元测试运行,未修改用户 scheme。验证日志:`/tmp/dsh-fast-voice-device.log`、`/tmp/dsh-fast-voice-ios.log`。测试不连接远端服务,不代表第三方 App 或远端真实 ASR 的运行观测。
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# KMM 语音输入实现与验证
|
|
2
|
+
|
|
3
|
+
## 当前交互
|
|
4
|
+
|
|
5
|
+
键盘收回时提示「描述你想要构建的内容或按住说话」。短按进入原生文字编辑,提示变为「描述你想要构建的内容」;长按录音,松手转写并回填草稿,上滑取消,不发送 Agent 消息。
|
|
6
|
+
|
|
7
|
+
长按出现录音蒙层、滑入取消状态、松手结束三个手势节点各有一次轻量触感,由 KMM 公共层统一触发,平台调用系统触感接口。
|
|
8
|
+
|
|
9
|
+
两端长按识别门槛为 100ms,蒙层淡入为 120ms,由公共层 `VoiceInputInteraction` 统一提供。静态逆向依据与 tap/longpress 验证见 [长按阈值调查](voice-input-hold-thresholds.md)。
|
|
10
|
+
|
|
11
|
+
录音遮罩覆盖底部统计条、输入框及工具栏,顶部以椭圆形透明度渐变过渡到会话内容,并整体淡入淡出。正常录音使用 Whaleness 深蓝色,上滑取消切换为浅红色。585 个粒子在静音时散乱分布,各自缓慢漂浮并改变大小、明暗,讲话时向中间聚拢,组成 45 列纯粒子波形。波形两侧粒子较小且暗,中间较大且亮,保留轻微扰动。高度由真实 RMS 音量历史驱动,不生成随机假音量。
|
|
12
|
+
|
|
13
|
+
录音遮罩不显示时间计数或「完成」按钮,松手即结束录音;只有转写等待阶段保留「取消」入口。
|
|
14
|
+
|
|
15
|
+
原生文本控件在录音和转写期间保持挂载。成功回填或确认插入后,公共层发出 `focusEditor` effect,两端恢复焦点与输入法;iOS 在下一轮主线程同步 UIKit 焦点,避免在 SwiftUI 布局过程中重入。原草稿在识别期间被编辑过时,先保留识别结果,由用户确认插入。
|
|
16
|
+
|
|
17
|
+
网关能力缺失、插件未启用或模型未准备好时,只呈现 Alert,不修改草稿、不在输入框中加入提示或按钮、不启动录音。提示用户在桌面端升级移动网关插件,并在「插件」模块启用官方语音输入插件、完成模型初始化。iOS 使用应用现有原生 Alert,Android 复用 `DshAlertDialog`。
|
|
18
|
+
|
|
19
|
+
录音过短或转写结果为空时,仅显示「未识别到人声」Toast,约两秒后自动消失,无需确认;草稿及编辑焦点不变。
|
|
20
|
+
|
|
21
|
+
## 分层
|
|
22
|
+
|
|
23
|
+
客户端仓库:`../dsh-mobile`。
|
|
24
|
+
|
|
25
|
+
- `shared/commonMain/.../facade/SharedVoiceInputStore.kt`:唯一业务状态源;能力与模型准备状态、录音 ID、取消、时长/数据限制、超时、草稿版本、跨会话隔离、音量归一化/平滑/历史、Alert 与焦点 effect。
|
|
26
|
+
- `shared/commonMain/.../facade/VoiceInputProtocol.kt`:官方语音目录解码、PCM 格式常量和 WAV 封装。
|
|
27
|
+
- `shared/commonMain/.../gateway/GatewayRuntime.kt`:控制通道请求 ID 关联、ASR 独立超时与官方错误透传。
|
|
28
|
+
- iOS `Core/VoiceInputAdapter.swift`:共享 Intent/effect 桥接;权限、AVAudioEngine/AVAudioConverter、平台时钟、网络和 UI 焦点回调。
|
|
29
|
+
- Android `platform/AndroidVoiceInputAdapter.kt`:共享 Intent/effect 桥接;权限、AudioRecord、平台时钟、网络和 UI 焦点回调。
|
|
30
|
+
- iOS `Components/VoiceRipplePanel.swift` 与 Android `ui/VoiceInputPanel.kt`:原生遮罩和粒子几何插值,不重复实现 ASR 判断。
|
|
31
|
+
|
|
32
|
+
两端采集 16 kHz 单声道 PCM16,公共层封装为 WAV Base64,网关调用 DSH 官方插件的 `speech` Remote API。客户端不内置 ASR 模型;模型运行在 DSH 主机。源码变更不代表已自动部署到运行中的远端服务。
|
|
33
|
+
|
|
34
|
+
## 验证(2026-10-12)
|
|
35
|
+
|
|
36
|
+
- 共享层:250 项通过,其中 20 项语音状态机测试及请求关联/超时回归。
|
|
37
|
+
- Android JVM 单元测试:116 项通过。
|
|
38
|
+
- Android APK、测试 APK 编译与 lint:通过;既有非阻塞 lint 警告保留。
|
|
39
|
+
- Android 17 模拟器:5 项真实 UI 测试通过,涵盖转写回填后的输入法编辑、关闭键盘后实际短按再编辑、实际长按显示统一 Alert、草稿保留、遮罩覆盖及蓝/浅红配色。
|
|
40
|
+
- iOS 模拟器:15 项语音、原输入控件和本地化相关测试通过,包括同一个 UITextView 保持挂载、回填后取得第一响应者、中文 marked text 可编辑,以及讲话/静音/取消三态原生截图。
|
|
41
|
+
- 最后渐变修正后,iOS 5 项语音测试和 Android 5 项 UI 测试再次通过。
|
|
42
|
+
- 新增语音 Kotlin 文件:ktlint 和 detekt 轻量静态分析通过。状态机允许前置拒绝条件,绘图固定几何数值有说明明确的局部豁免。
|
|
43
|
+
- 网关完整 `npm test` 与官方 DSH 0.2.0-rc.2 Speech Remote 契约测试在上一轮通过,网关协议本轮未改动;官方 Controller 校验真实 WAV,识别器使用 mock。
|
|
44
|
+
|
|
45
|
+
Android UI 测试原先间接使用 Espresso 3.5.0,在 Android 17 上反射已移除的 `InputManager.getInstance` 导致测试无法启动。增加仅用于 `androidTest` 的 Espresso 3.7.0 依赖后解决;该修复见 [官方发布记录](https://developer.android.com/jetpack/androidx/releases/test#espresso-3.7.0),不进入生产 APK。
|
|
46
|
+
|
|
47
|
+
iOS 上一轮全量 208 项中有 1 条既有权限选项断言失败:`GatewayProtocolTests.testKMPSessionControlAdapterOwnsStateAndEmitsRequestEffectOnce`,期望过滤 `future` 权限项,而现有实现保留该项,本语音任务未修改这项行为。
|
|
48
|
+
|
|
49
|
+
上述 UI 测试使用本地测试会话、共享状态机生成的录音状态和模拟转写,不需要远端鉴权,也不启用真实麦克风。截图是原生组件渲染结果,不代表已进入远端真实会话或完成真实 ASR 模型联调。
|
|
50
|
+
|
|
51
|
+
## 转写后焦点竞争修复(2026-10-12)
|
|
52
|
+
|
|
53
|
+
在完整 iOS `ConversationView` 中重现了转写后焦点被抢走的问题:晚到的 `keyboardDidHideNotification` 无条件清除编辑焦点;另一个竞争发生在原生文本框已聚焦、SwiftUI 尚未提交新的 Binding 值时,普通刷新把旧的 `false` 当作主动失焦请求。
|
|
54
|
+
|
|
55
|
+
焦点同步现在只执行发生变化的显式请求,原生编辑开始或结束时作废旧的排队请求。键盘隐藏通知不再清除输入框焦点,编辑状态由 `UITextViewDelegate` 回报。AppStore 支持注入语音 adapter,方便真实页面使用模拟转写验证,默认运行行为保持不变。
|
|
56
|
+
|
|
57
|
+
新增两项回归测试在修复前均失败;修复后 7 项语音测试与 2 项输入框展示测试全部通过。覆盖完整会话页面回填、迟到的键盘通知、原生聚焦与旧 Binding 竞争、中文组合输入、收起键盘后再聚焦,以及明确失焦请求仍正常生效。日志:`/tmp/dsh-voice-focus-fixed.log`。本轮只修改 iOS 焦点协调和测试依赖注入,不涉及 ASR 公共层或网关协议。
|
|
58
|
+
|
|
59
|
+
## 短音节触发完整波形(2026-10-12)
|
|
60
|
+
|
|
61
|
+
原实现将粒子聚拢比例直接映射到平滑音量:较轻的声音只能部分聚拢,短音节停声后动画目标回退。现在公共层独立维护 `waveformActive`:一次未平滑的归一化音量超过既有声音门限,即将聚拢目标设为完整波形,并在最后一次有效声音后保持 600ms。继续说话只延长保持窗口,柱高仍使用实际音量历史;持续静音后恢复自由粒子。两端传入平台单调时钟,公共层按时间管理保持窗口,不依赖音量回调次数。取消、松手和新录音会重置该状态。
|
|
62
|
+
|
|
63
|
+
iOS 与 Android 聚拢动画均为 160ms,散开为 320ms。触发使用现有 RMS 能量门限,不引入客户端 ASR 或人声分类模型。新增测试验证单次较轻声音随后立即静音仍保持完整聚拢目标、语句停顿后续说话延长窗口,以及静音/无效采样/旧录音回调和录音重置。
|
|
64
|
+
|
|
65
|
+
验证:共享层 253 项测试通过(其中语音状态机 23 项);Android 客户端与界面测试 APK 编译通过;iOS 9 项语音及输入框相关测试通过;改动 Kotlin 源码与共享测试通过 ktlint,`git diff --check` 通过。日志:`/tmp/dsh-waveform-gradle.log`、`/tmp/dsh-waveform-ios.log`。使用合成音量和模拟转写验证,未进行远端真实 ASR 联调。
|
|
66
|
+
|
|
67
|
+
## 纯粒子波形(2026-10-12)
|
|
68
|
+
|
|
69
|
+
删除 iOS 与 Android 波形中的实心圆角连接柱,波形完全由圆形粒子绘制。每列粒子由 7 个增加到 13 个,总数由 315 增至 585;聚拢后半径统一为 1.15pt/dp,保持清晰的点阵形态。静音排布调整为 39×15 的均匀分布,仍保留原有漂移和聚拢动画。公共层触发及 ASR 流程未修改。
|
|
70
|
+
|
|
71
|
+
Android Debug 构建、改动 Kotlin 文件 ktlint 和 `git diff --check` 通过;iOS 原生三态截图测试通过,并已检查讲话波形截图及更新 `voice-input-states.html` 所用三张原生预览图。日志:`/tmp/dsh-particle-density-android.log`、`/tmp/dsh-particle-density-ios.log`。
|
|
72
|
+
|
|
73
|
+
## 无语音轻提示(2026-10-12)
|
|
74
|
+
|
|
75
|
+
公共层将过短 PCM 与空白转写统一处理为无语音结果:停止录音及计时、回到 idle、清除错误与 Alert,发出一次 `showToast` effect,文案「未识别到人声」。不改草稿,不请求编辑焦点,不发送聊天消息。重复或旧的转写回调不会重复弹出。权限、网关初始化、无效音频及服务错误继续使用原 Alert 流程。
|
|
76
|
+
|
|
77
|
+
Android adapter 使用系统 `Toast.LENGTH_SHORT`;iOS adapter 只管理短暂展示及两秒自动隐藏,页面使用不拦截触摸的胶囊提示。离开页面会清除提示。新增中文与英文文案。
|
|
78
|
+
|
|
79
|
+
验证:公共层 255 项测试通过(语音状态机 25 项),涵盖过短录音、空白结果、重复回调、草稿保留与 Alert 分类;Android Debug 编译通过;iOS 语音、输入框和本地化回归通过,新增用例验证 Toast 自动隐藏且不发出草稿/焦点变更。Kotlin 修改通过 ktlint,`git diff --check` 通过。日志:`/tmp/dsh-no-speech-gradle.log`、`/tmp/dsh-no-speech-ios.log`。
|
|
80
|
+
|
|
81
|
+
## 粒子大小与明暗扰动(2026-10-12)
|
|
82
|
+
|
|
83
|
+
分散和波形状态始终使用同一批索引固定的 585 个粒子。删除聚拢时统一半径和大幅提高透明度的映射:每个粒子通过固定种子保留自己的基础半径(0.65~1.60pt/dp)和透明度(0.18~0.60),在两种状态间连续变化。加入异相、低频的半径扰动(±8%)和明暗变化,波形两端仅作轻微柔化。不在每帧重新抽取随机数,避免闪烁;音量历史和柱高不受视觉扰动影响。
|
|
84
|
+
|
|
85
|
+
两端仅修改绘制,公共层 ASR 与触发逻辑保持原行为。Android Debug 构建、Kotlin 文件 ktlint、`git diff --check` 通过;iOS 原生三态截图测试通过,已检查讲话截图中大小与明暗不规则的粒子波形,并更新三张预览图及 HTML 图片缓存版本。日志:`/tmp/dsh-particle-character-android.log`、`/tmp/dsh-particle-character-ios.log`。
|
|
86
|
+
|
|
87
|
+
## 减弱粒子差异(2026-10-12)
|
|
88
|
+
|
|
89
|
+
为避免波形呈现零散的缺点效果,两端基础半径收窄为 1.00~1.25pt/dp,基础透明度收窄为 0.44~0.56;大小动态扰动由 ±8% 降为 ±2.5%,明暗动态系数由 0.88±0.12 调整为 0.97±0.03,波形边缘柔化由 28% 减至 12%。保留各粒子的固定种子与连续低频变化,波形更均匀。
|
|
90
|
+
|
|
91
|
+
Android Debug 构建、Kotlin 文件 ktlint、`git diff --check` 及 iOS 原生三态截图测试通过;已检查讲话截图并更新三张预览图与 HTML 缓存版本。日志:`/tmp/dsh-particle-softness-android.log`、`/tmp/dsh-particle-softness-ios.log`。
|
|
92
|
+
|
|
93
|
+
## 自由漂浮与波形中心包络(2026-10-12)
|
|
94
|
+
|
|
95
|
+
分开计算自由状态与波形状态的大小、明暗,再按聚拢进度连续插值,避免把波形的规则化参数应用到自由粒子。自由状态恢复原始基础半径 0.7~1.5pt/dp 与透明度 0.12~0.38;按最新补充要求采用独立随机起点、不同速度与方向的连续漂移,叠加异相的大小呼吸(±18%)与缓慢明暗变化。Android 漂移采用整数频率,20 秒循环首尾连续;不逐帧重抽随机数。
|
|
96
|
+
|
|
97
|
+
波形使用平滑的中心包络:基础半径从两侧约 0.8 递增到中心约 1.3pt/dp,透明度从约 0.26 递增到约 0.66;仅叠加小范围的粒子差异、±2.5% 大小扰动与轻微明暗变化。音量、聚拢触发和 ASR 公共层未修改。
|
|
98
|
+
|
|
99
|
+
两端构建、Kotlin 文件 ktlint、`git diff --check` 及 iOS 三态截图测试通过,已分别检查自由漂浮与讲话截图并更新预览。日志:`/tmp/dsh-free-particle-drift-android.log`、`/tmp/dsh-free-particle-drift-ios.log`。
|
|
100
|
+
|
|
101
|
+
## 椭圆渐变与蒙层淡入淡出(2026-10-12)
|
|
102
|
+
|
|
103
|
+
删除顶部纵向线性透明度渐变,改用横向拉伸的径向透明度遮罩:中间向上扩散,两侧边缘沿椭圆弧渐隐。渐变限制在顶部 144pt/dp,下面完整不透明覆盖;蒙层高度改为至少 320pt/dp,并在输入区域高度上增加 144pt/dp,以确保弧形边缘不露出输入工具栏。粒子同时接受遮罩,避免透明区仍有明显粒子边界。
|
|
104
|
+
|
|
105
|
+
iOS 对独立蒙层容器应用 opacity transition,通常 240ms 淡入淡出,减少动态效果时 120ms。Android 使用 AnimatedVisibility,240ms 淡入、200ms 淡出。Android 淡出保留最后可见的 UI snapshot,避免浅红取消画面先跳回蓝色;这是渲染缓存,不判断 ASR 业务。退出后立即停止遮罩拦截触摸及按钮交互,输入控件保持挂载与可编辑。
|
|
106
|
+
|
|
107
|
+
验证:iOS 10 项语音与输入框回归通过,覆盖真实会话页回填焦点和三态截图;Android 17 模拟器 6 项界面测试通过,新增用例暂停动画时钟,检查淡入/淡出的中间透明帧、完成退出后移除、椭圆顶部两侧比中间更透明。Android 构建、改动 VoiceInputPanel.kt 的 ktlint 和 `git diff --check` 通过。已检查原生截图并更新三态预览。日志:`/tmp/dsh-elliptical-mask-ios.log`、`/tmp/dsh-elliptical-mask-device.log`。使用本地测试会话与模拟转写,不连接远端服务。
|
|
108
|
+
|
|
109
|
+
## 三节点触感(2026-10-12)
|
|
110
|
+
|
|
111
|
+
公共层在有效长按进入 authorizing、取消区边界由未进入变为进入、用户松手结束持有时,分别发出 `hapticStart`、`hapticCancel`、`hapticRelease`。停留在取消区或重复松手回调不重复触发;退出后再滑入是新的进入事件。提前松手也提供结束触感;自动录音上限、后台取消、断线和失败清理不伪造手势触感。网关不支持或模型未准备等拒绝路径不出现蒙层,也不触发这些事件。
|
|
112
|
+
|
|
113
|
+
iOS 使用 soft impact、selection feedback、light impact,移除原麦克风真正启动时的触感,避免迟到或重复。Android adapter 转发事件,UI 的 View 使用 LONG_PRESS、CLOCK_TICK、KEYBOARD_TAP,尊重系统触感设置且无需新增震动权限;离开页面释放回调。触发与去重判断只在 KMM 公共层,UI 不保存另一套手势业务状态。
|
|
114
|
+
|
|
115
|
+
验证:公共层 259 项测试通过(语音状态机 29 项,新增四项触感回归),Android 6 项界面回归及 iOS 10 项语音/输入框回归通过;两端编译、改动共享源码/测试及 Android adapter 的 ktlint、`git diff --check` 通过。模拟器验证事件和交互流程,不用于评价实体设备的马达手感。日志:`/tmp/dsh-voice-haptics-gradle.log`、`/tmp/dsh-voice-haptics-device.log`、`/tmp/dsh-voice-haptics-ios.log`。
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
## Android 持续长按与输入法冲突修复(2026-10-12)
|
|
119
|
+
|
|
120
|
+
语音在 100ms 后接管长按,但 BasicTextField 自身的原生长按选字手势此前已收到 down,继续持有时仍可能请求焦点与输入法。录音活动期间将文本框设为 disabled(保持挂载与草稿),同时清除焦点、隐藏键盘;语音手势在活动期间保持启用,避免迟到的 IME 状态取消持有手势。ASR 业务状态仍来自 KMM 公共层。
|
|
121
|
+
|
|
122
|
+
转写后的 focusEditor effect 先登记 UI 焦点请求,再于 Compose 提交恢复 enabled 后聚焦并显示键盘,避免文本框尚未恢复编辑时直接请求焦点。普通短按和语音结束后的文字修改继续走原生文本框。
|
|
123
|
+
|
|
124
|
+
验证:Android 17 Pixel_10 模拟器 8 项界面回归全部通过。新增用例先给真实文本框发送 down,再模拟录音硬件成功,持续持有 1200ms,验证文本框无焦点、IME 未出现、录音未被打断;随后发送真实 up,验证进入停止状态,再模拟转写回填并通过输入法编辑。其余用例覆盖 70ms 短按、160ms 长按前置检查、回填再编辑、发送收键盘及蒙层颜色/渐变。Debug 与 AndroidTest 编译、git diff --check 通过。测试仅使用本地会话、模拟采集及转写,不连接远端服务器或启用真实麦克风。日志:`/tmp/dsh-voice-gesture-fixed-final.log`。
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
## 触摸到蒙层的阶段耗时(2026-10-12)
|
|
128
|
+
|
|
129
|
+
没有把长按阈值继续降低来掩盖等待:公共层阈值仍为 100ms,淡入仍为 120ms。使用系统单调时钟及真实 MotionEvent 测量 Android 完整 ConversationScreen(14 条本地消息),保留原生编辑框、手势、触感和蒙层;替换权限回调,使其停留 authorizing,不打开麦克风或连接服务器。
|
|
130
|
+
|
|
131
|
+
最终一次 Android 17 模拟器测量:长按识别 107ms、发布 authorizing 108ms、权限检查 108ms、Canvas 蒙层首绘 170ms。独立页面 draw listener 记录 167ms,和实际 Canvas 首绘有约 3ms 的执行顺序差异。此前完整页面测量为 124ms 到权限回调、185ms 到 draw;这些是模拟器单次观测,不是用户真机的性能分位数,也不证明真机没有一秒延迟。
|
|
132
|
+
|
|
133
|
+
iOS 发现明确的主线程阻塞路径:VoiceInputAdapter.apply 在 MainActor 上同步调用 AVAudioSession.setActive、AVAudioEngine.prepare/start,蒙层状态虽已发布,绘制仍会等硬件调用返回。改为同一串行后台队列执行 start、stop 和 PCM Base64 编码;只有带录音 ID 的结果回到 MainActor,再由共享 Store 判断是否仍有效。启动期间取消会先结束 UI 状态,硬件启动完成后在队列中清理;迟到回调不能恢复已取消的录音。权限与采集器可在测试中替换,默认实现不变,ASR、超时、取消判定和草稿更新仍在 KMM 公共层。
|
|
134
|
+
|
|
135
|
+
iOS 新增回归模拟一秒同步硬件启动:gesture-dispatched uptime=274178.058896,overlay-mounted uptime=274178.062806,capture-start-begin uptime=274178.063235,capture-start-end uptime=274179.063994(1000.76ms)。蒙层提交发生在硬件启动之前;测试在约 108ms 检查主线程响应、starting 状态,并在硬件仍启动时取消。overlay-mounted 是 SwiftUI 挂载节点,不能冒称实际显示屏首帧;此测试从业务 press 调用开始,不含 UIKit 的实际触摸识别时间,也没有测量真机音频启动耗时。
|
|
136
|
+
|
|
137
|
+
Debug 阶段日志方便后续真机复现定位:Android Logcat tag 为 VoiceInputLatency,依次输出 touch-down、gesture-recognized、state-authorizing、permission-check、overlay-first-draw、capture-start-dispatched、state-recording;均为相对触摸的毫秒数。iOS 日志 subsystem 为 ai.dsh.mobile.ios,category 为 voice-input-latency,标记 VoiceInputLatency,输出触摸、识别、状态、蒙层挂载及 category/activate/prepare/start 的单调时钟和耗时。诊断不输出音频、转写或草稿;Release 不输出这些阶段日志。
|
|
138
|
+
|
|
139
|
+
验证:Android 9 项界面测试通过,iOS 11 项语音与输入框测试通过;包含持续长按不弹键盘、短按编辑、转写修改、蒙层及启动取消。Android 改动 adapter/panel/新增测试通过 ktlint,相关文件 git diff --check 通过;两端编译通过。日志:/tmp/dsh-voice-latency-android-final.log、/tmp/dsh-voice-latency-ios-traced.log。用户真机反馈的一秒延迟尚未通过设备阶段日志归因,不能直接宣称这就是其设备上的唯一原因。
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
## 浅深色模式的语音蒙层配色(2026-10-12)
|
|
143
|
+
|
|
144
|
+
两端根据当前有效主题选择相同配色。浅色录音保留品牌蓝(底色 #15335C、光晕 #2C5D9C),取消红色加深为玫瑰红(#DC8799 / #F0B1B8,前景 #642434)。深色录音使用低亮度蓝(#101F35 / #1B3B61,前景 #D4E1F1),取消使用暗玫瑰红(#4C2733 / #754050,前景 #F1D8DE),避免明亮粉红和纯白前景的强光感。Android 的 isSystemInDarkTheme 读取 DshTheme 提供的有效 LocalConfiguration,支持应用手动主题;iOS 读取 colorScheme。粒子大小、明暗扰动、收束、椭圆边缘和 ASR 公共逻辑保持原行为。
|
|
145
|
+
|
|
146
|
+
验证:两端编译通过,既有 Android 蒙层覆盖及取消配色回归通过;既有 iOS 截图用例扩展为浅/深色各三态,六张截图已导出并目视检查浅色取消、深色取消和深色录音,更新预览为可切换主题。Android 面板 ktlint、相关源码 git diff --check 通过。日志:/tmp/dsh-voice-theme-android.log、/tmp/dsh-voice-theme-ios.log。预览仍使用本地模拟音量,没有远端 ASR 联调。
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
## Android 系统导航区域白边(2026-10-12)
|
|
150
|
+
|
|
151
|
+
根因是 ConversationScreen 的 Scaffold 默认把系统导航栏底部 Insets 作为内容 padding,再由父 Column consume;蒙层位于该内容里的 HorizontalPager 中,不能绘制到被排除的导航区域。已有遮罩截图只截取自身 bounds,未包含系统导航区,因此没有发现白边。
|
|
152
|
+
|
|
153
|
+
将 Scaffold 的 contentWindowInsets 限制为横向 safeDrawing,TopAppBar 仍负责顶部安全区,页面背景及蒙层延伸到真实屏幕底边。Composer 原有 navigationBarsPadding/imePadding 继续保护可操作内容,并让测量高度包含其底部安全区;轨迹页单独保留 navigationBarsPadding,避免改变非语音内容的安全区。没有改动 KMM 语音业务、粒子或系统导航模式。
|
|
154
|
+
|
|
155
|
+
扩展既有 Android 蒙层测试,在和 MainActivity 一样的 edge-to-edge 窗口下,比较蒙层屏幕坐标和 UiAutomation 全屏截图高度,并验证最底部左右两侧实际像素是蓝色录音/红色取消背景。修复前该断言失败,修复后通过。手势导航下全部 9 项界面回归通过,三键导航下该配色/屏幕边界用例也通过;临时切换的模拟器系统导航模式已恢复。Debug/AndroidTest 编译、相关文件 git diff --check 通过。日志:/tmp/dsh-voice-navbar-before.log、/tmp/dsh-voice-navbar-after.log、/tmp/dsh-voice-navbar-threebutton.log。所有测试使用本地会话和模拟音量,不连接远端 ASR。
|
|
156
|
+
|
|
157
|
+
补充:已目视检查手势导航下的两张完整原生截图,蓝色/红色背景均覆盖导航手势条后方直至最底边;截图存入预览目录 voice-android-navigation-recording.png、voice-android-navigation-cancel.png,预览页面增加全屏截图链接。
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
## 扩展并平滑蒙层边缘(2026-10-12)
|
|
161
|
+
|
|
162
|
+
原渐变虽有 144pt/dp 高度,但完整不透明一直保持到归一化半径 0.68,实际变化集中于外侧约 46pt/dp,容易形成明显边界。现在两端渐变高度增至 224pt/dp,用 33 个采样点描述 smoothstep 透明度曲线;开始和结束的斜率为零,实际中心过渡宽度约 84pt/dp。椭圆横向半径保持视图宽度的 0.8,完全不透明半径设为 0.625,正好涵盖两侧与下方实色区的连接位置,避免在渐变底端产生横向接缝。
|
|
163
|
+
|
|
164
|
+
蒙层整体增高 80pt/dp(最小高度 320→400,输入区域上方额外高度 144→224),新增空间向上展开。文字和波形按原内容区域加 80pt/dp 偏移,保持距底部的位置;自由粒子的漂移和扰动参数沿用原值。输入区与系统导航区仍由下方实色部分完全覆盖;浅深色配色保持上一版。
|
|
165
|
+
|
|
166
|
+
验证:Android 8 项现有界面回归通过,包括椭圆边缘、淡入淡出、全屏导航区覆盖、长按和编辑;iOS 原生截图用例生成浅/深色六态,两端构建通过,Android 面板 ktlint 和相关文件 git diff --check 通过。已检查 iOS 讲话/深色取消及 Android 全屏录音截图,更新六张主题预览、两张 Android 全屏截图和图片缓存版本。日志:/tmp/dsh-voice-soft-edge-android.log、/tmp/dsh-voice-soft-edge-ios.log、/tmp/dsh-voice-soft-edge-screenshots.log。没有修改公共层 ASR 或触发阈值。
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
## 转写只回填,不主动聚焦(2026-10-12)
|
|
170
|
+
|
|
171
|
+
根据最新交互要求,ASR 转写结果填入草稿后保持未聚焦、键盘收起,只有用户点选输入框才进入编辑。KMM SharedVoiceInputStore 的 acceptTranscript 与 insertPending 均不再发出 focusEditor effect;删除 Android/iOS adapter 与页面里的对应回调,Android 保留录音期间的失焦/隐藏键盘保护,不在录音结束时安排聚焦。其他来源的用户主动编辑(例如队列草稿恢复)保留原行为。
|
|
172
|
+
|
|
173
|
+
更新既有回归:共享层验证自动回填及冲突后的手动插入都不请求焦点;Android 真实会话验证结果已写入、未聚焦、IME 未出现,随后真实触摸短按输入框才显示键盘并修改文字;持续长按松手后的回填同样验证。iOS 真实会话页与原生 UITextView 验证回填后不是 firstResponder,用户主动调用原生聚焦路径后能够编辑和处理 marked text,迟到的键盘隐藏通知也不能打断新编辑会话。
|
|
174
|
+
|
|
175
|
+
验证:KMM 259 项公共测试、Android 8 项会话界面回归、iOS 11 项语音与输入框回归全部通过;两端编译、修改共享源码/测试及 Android adapter 的 ktlint、相关文件 git diff --check 通过。日志:/tmp/dsh-voice-no-autofocus-gradle.log、/tmp/dsh-voice-no-autofocus-ios.log。测试使用本地会话及模拟转写,无远端 ASR 联调。
|
|
176
|
+
|
|
177
|
+
## 紧凑蒙层与短词录音完整性(2026-10-13)
|
|
178
|
+
|
|
179
|
+
Android 按屏幕高度、iOS 按所在 UIWindow 高度将语音蒙层限制为四分之一,包括底部系统导航区域。渐变占蒙层高度的 60%,文字和波形随之缩紧;底部普通输入区在录音期间仅隐藏绘制,保持挂载与手势链。移除 iOS 语音授权、启动、转写状态的 ProgressView;两端正常淡入为 320ms、淡出为 280ms,iOS 减弱动态效果设置下保持短过渡。椭圆渐变按设备像素对齐,iOS 实色区额外重叠一个像素,避免小尺寸下出现横向接缝。粒子的自由漂移与波形扰动沿用原参数。
|
|
180
|
+
|
|
181
|
+
短词问题的业务修正在 KMM 公共层:不再用 200ms 作为音频提交下限,非空且完整的 PCM16 交给官方识别;正常松手保留 200ms 采集尾段,取消仍立即停止并丢弃。用户在硬件 starting 阶段松手时进入 stopping,等待同一录音 ID 的启动回调,再停止并提交已有缓冲,不再直接 discard。未获授权就松手仍取消,不允许迟到权限开启录音。官方目录明确声明支持 auto 时本次请求采用自动语言,否则沿用原选择;不修改桌面插件的全局语言或 VAD 配置。两端适配器仅执行共享层提供的延迟停止效果。
|
|
182
|
+
|
|
183
|
+
用户报告 iOS 单说 Hello 返回“未识别到人声”,句中 Hello 会漏掉,但 Good morning 可识别。检查安装版本 0.2.0-rc.2 的官方 Web 录音路径发现,它等待 MediaRecorder 最后一块数据,不存在移动端此前的 200ms 提交门槛。官方 SenseVoice worker 使用 Silero VAD 分段,其默认 minSpeechSeconds 为 0.25 秒。
|
|
184
|
+
|
|
185
|
+
通过本机已下载模型临时启动官方 worker,以 macOS Samantha 合成音频进行离线检查:保留完整发音、去首尾低电平的 Hello 样本约 0.49 秒,auto/zh 均返回 Hello;截去开头 200ms 后两种语言均返回空结果,额外添加 200ms 静音不能恢复丢失的开头。其他样本存在 Yello、漏词等模型误差。这些结果说明短词对音频完整性敏感,但不能证明用户真机的唯一根因,也不能宣称真实 Hello 问题已完全解决。测试未使用用户录音,未改动运行中的官方服务、模型或配置;临时 worker 已退出。实际 iPhone 录音与远端转写效果仍待复测。
|
|
186
|
+
|
|
187
|
+
验证:KMM 261 项公共测试、Android 8 项会话回归、iOS 11 项语音/输入框回归通过。最终像素修正后,Android 全屏蒙层单测与 iOS 六态截图单测再次通过;六张原生预览已更新。网关官方 API 合同测试通过(真实官方验证器,识别器使用替身)。改动公共 Kotlin、Android 面板/适配器通过 ktlint,相关文件 git diff --check 通过。模拟器使用本地会话与模拟转写,不能代替带设备签名的真机远端联调。日志:/tmp/dsh-compact-voice-gradle.log、/tmp/dsh-compact-voice-ios.log、/tmp/dsh-compact-voice-final-android.log、/tmp/dsh-compact-voice-seam-ios.log。
|
|
188
|
+
|
|
189
|
+
## 缩短触摸到蒙层显现的等待(2026-10-13)
|
|
190
|
+
|
|
191
|
+
上一版 320ms 淡入采用慢起步的 easeInOut 曲线,初段透明度很低,放大了等待感。两端从 KMM 读取的长按判定由 100ms 缩短到 80ms,淡入由 320ms 缩短到 160ms;入场采用 cubic-bezier(0,0,0.2,1),前段迅速显现、后段平滑收尾。Android 使用 LinearOutSlowInEasing,iOS 页面与慢启动测试共用 voiceOverlayAnimation。淡出保持 280ms;iOS 减弱动态效果时使用 80ms 线性过渡。授权及录音启动前已发布蒙层状态,后台采集仍不阻塞 UI。
|
|
192
|
+
|
|
193
|
+
Android 原生界面回归增加淡入前 32ms 的不透明度断言(超过最终效果的 25%,同时低于 90%,兼顾即时反馈和动画),保留 70ms 短按、持续长按不弹键盘、回填后点击编辑等回归。Android 17 模拟器一次真实 MotionEvent 测量记录:权限回调约 73ms,页面 draw listener 约 83ms,实际蒙层 Canvas 首绘约 100ms。draw listener 不是蒙层首绘,Canvas 首绘也不是实体显示屏的呈现时刻;测试时权限保持等待、没有打开麦克风或连接远端,不代表真机性能分位数。
|
|
194
|
+
|
|
195
|
+
验证:公共层 261 项、Android 9 项、iOS 11 项测试全部通过。iOS 一秒硬件启动替身测试使用实际入场动画,验证蒙层挂载与主线程响应在硬件完成前发生,并保留取消/迟到回调保护;该用例从业务 press 开始,不含实际 UIKit 触摸识别耗时。共享参数及 Android 延迟测试 ktlint、git diff --check 通过,原生两端编译通过。预览文字参数已同步。日志:/tmp/dsh-voice-response-20261013-gradle.log、/tmp/dsh-voice-response-20261013-ios.log。
|
|
196
|
+
|
|
197
|
+
## 蒙层高度调整为五分之二(2026-10-13)
|
|
198
|
+
|
|
199
|
+
按最新要求,将高度比例从四分之一改为五分之二(40%),由公共层 VoiceInputInteraction.OVERLAY_HEIGHT_FRACTION 统一提供。Android 按屏幕高度、iOS 按 UIWindow 高度应用同一比例,文字、粒子与椭圆渐变随蒙层尺寸展开。长按 80ms、快速淡入 160ms、淡出 280ms 及 ASR 逻辑保持原行为。
|
|
200
|
+
|
|
201
|
+
验证:两端编译通过;Android 原生蒙层单测验证 40% 高度与底部系统导航区覆盖,iOS 原生截图单测验证新高度并生成浅深色六态,均通过。已更新六张 iOS 预览与两张 Android 全屏截图,目视确认布局和底部覆盖;共享参数 ktlint 与 git diff --check 通过。日志:/tmp/dsh-two-fifths-android.log、/tmp/dsh-two-fifths-ios.log。
|
|
202
|
+
|
|
203
|
+
## 首次有声后保持波形与 iOS 尾部采样收尾(2026-10-13)
|
|
204
|
+
|
|
205
|
+
原先公共层在最后一次有效音量后的 600ms 将 waveformActive 复位,表现为波形退回自由粒子;这条路径只控制视觉状态,并没有停止录音。根据最新要求,移除静音保持计时,以现有 waveformActive 锁定本次录音是否已经进入波形:首次有效音量前自由漂浮,首次有声后保持波形直到录音结束。level 继续接收真实音量并平滑降低振幅,tick 不再因静音解除波形。上滑进入红色取消区再滑回蓝色同样保留波形;取消、松手、下一次录音及迟到旧录音回调的隔离仍生效。公共层回归验证说话后长停顿保持 recording,期间没有 stop effect 或转写请求,继续说话后仅在松手时提交整段 PCM,首尾字节完整。
|
|
206
|
+
|
|
207
|
+
iOS 实时采样转换原先只在下一块数据暂未到达时返回 noDataNow,停止时直接取已写入的 PCM,没有指示转换器结束输入。[Apple 的 AVAudioConverter 结束输入接口](https://developer.apple.com/documentation/avfaudio/avaudioconverterinputstatus/endofstream) 与当前 SDK 头文件说明:部分输入可能缓存到更多输入到达或 endOfStream 时才输出。现在 Capture 保留同一转换器,音频 tap 停止后在同一采集队列与锁内发送 endOfStream,取完尾部 PCM 后再返回录音结果;取消仍直接丢弃,转换失败显式报告,不提交不完整结果。正常松手的 200ms 采集尾段保持原值。这修复了一条确实会遗漏尾部样本的客户端路径,但不证明它是用户末尾漏字的唯一原因,也不保证官方模型逐字识别正确。
|
|
208
|
+
|
|
209
|
+
验证:KMM 262 项、Android 8 项、iOS 12 项测试全部通过。新增 iOS 硬件转换单测使用 48kHz 浮点合成输入,最后 24 帧有声,验证 16kHz PCM16 转换结束时有声尾部缓存被输出、总采样数量正确、重复结束没有重复数据。两端编译、共享源码与测试 ktlint、git diff --check 通过。原生预览扩展为浅深色各四态,新增说话后停顿的低振幅波形,首次说话前仍保持散乱粒子;目视确认新状态。日志:/tmp/dsh-voice-latched-20261013-gradle.log、/tmp/dsh-voice-latched-20261013-ios.log。测试没有真实麦克风或远端 ASR 联调,真机末尾漏字仍需复测。
|
|
210
|
+
|
|
211
|
+
## 短句 ASR 输入上下文与弱音量处理(2026-10-13)
|
|
212
|
+
|
|
213
|
+
用户报告 Android 简短“你好”及连续“你好”无法识别。本地官方 worker 对完整合成录音复现:正常音量约 0.529 秒“你好”返回空,连续三个“你好”漏掉一个,句首“你好”也会丢失。只加尾部静音或只提高音量不能解决所有样本,200ms 首部静音为 VAD 提供开头上下文后正常音量恢复;较弱输入结合增益才完整。此处是离线证据支持的输入边界问题,不是确认用户真机的唯一原因。
|
|
214
|
+
|
|
215
|
+
新增 KMM VoiceAudioFormat.prepareRecognitionPcm,captureFinished 在封装 WAV 前统一执行:增加 200ms 首部与 500ms 尾部静音,对峰值在 128..8191 的 PCM16 且峰均比至少 3 的输入施加整段固定增益,目标峰值 16384,最多 8 倍。正常音量、近零输入和低峰均比持续噪声保持原值,官方 VAD 与 ASR 仍负责最终人声识别。补静音只使用官方字节/时长限制内的剩余空间,不为增加上下文截掉已录发音;正常松手 200ms 硬件尾段与蒙层时机保持原行为。padding 是内存中补零,不增加麦克风启动等待;识别音频有额外静音长度及字节量。
|
|
216
|
+
|
|
217
|
+
验证:公共层 268 项通过,六项新音频回归覆盖首尾静音、原样保留正常发音、固定增益、增益上限、近零/持续噪声保护以及官方限额。Android 8 项交互回归通过,最终共享代码 Android 编译通过;iOS 使用最终框架的 12 项回归通过,ktlint 和 git diff --check 通过。真实编译 KMM 产生的 WAV 经网关测试适配器送入本地官方原生识别器,五个短句的三档音量正样本全部完整,静音和持续噪声负样本均为空;检测到的候选增益误识别问题已通过峰均比条件修正。
|
|
218
|
+
|
|
219
|
+
详细证据与范围见 [短句离线证据](voice-input-short-utterance-evidence/README.md)。没有修改官方 VAD 阈值或运行插件配置;没有进行用户真机录音/设备签名远端联调,因此不能宣称所有短句问题完全消失。日志:/tmp/dsh-short-speech-20261013-final-gradle.log、/tmp/dsh-short-speech-20261013-gradle.log、/tmp/dsh-short-speech-20261013-final-ios.log。
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# 豆包与 DeepSeek 的按住说话输入分析
|
|
2
|
+
|
|
3
|
+
分析日期:2026-10-12。范围为用户提供的两份 iOS IPA,关注「长按输入区域录音、松手结束、识别文字进入输入框」,区分语音通话、语音朗读和语音直接发送。没有安装或执行 App,没有请求包内接口,没有抓取实际会话网络流量;不能据此描述 Android 实现或当前其他版本。
|
|
4
|
+
|
|
5
|
+
## 结论
|
|
6
|
+
|
|
7
|
+
1. 豆包 12.2.1 存在独立的 `VoiceToComplete` 输入组件。已经从引用 ASR 结果回调日志的函数追到实际 `setText:` 消息调用,支持「识别结果回填文本控件」。这不是仅从语音通话 SDK 的存在推断出来的。
|
|
8
|
+
2. 豆包的 FlowAudio 模块包含 `SAMIAssistantVoiceRecognizer`、录音数据流、ASR 增量结果和结束结果通道,以及发送音频日志。它具有网络识别实现;但本次没有恢复 VoiceToComplete 到具体识别器的完整动态派发关系,因此将「该输入功能主要走网络 ASR」列为有依据的推断,而非运行时验证事实。
|
|
9
|
+
3. DeepSeek 2.5.1 包含专用的 `DSWebsocketSpeechRecognizer`、`DSVoiceInputService` 和 `/api/v0/asr/ws`、`/api/v0/asr/fetch_cached` 接口。类字段、协议消息结构和错误分支共同支持网络 ASR 架构推断,不能仅从用户觉得识别快便断言使用端侧模型。
|
|
10
|
+
4. 两个 App 的录音交互不能简单等同。DeepSeek 简体中文资源写的是「松手发送,上滑取消」,存在语音提交和发送相关事件;本次未确认其所有模式都是「回填草稿后等待点击发送」。
|
|
11
|
+
5. 本次未找到这条输入链路使用完整端侧识别模型的充分证据。文件扩展名扫描未见显式 ASR 权重,不足以排除自定义容器、下载模型或离线备用方案。
|
|
12
|
+
|
|
13
|
+
## 目标身份与证据
|
|
14
|
+
|
|
15
|
+
| 目标 | Bundle ID / 包内版本 | IPA SHA-256 |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| 豆包 | `com.bot.doubao` / `12.2.1`(build `12020141`) | `365a15b5d4aa4357e3432659866ebee782834a56624ee3a77b70fb524106031e` |
|
|
18
|
+
| DeepSeek | `com.deepseek.chat` / `2.5.1`(build `1`) | `4750aaa95a5781ec880d0755992d69582f26767fdb1351f6a88ad20559e22778` |
|
|
19
|
+
|
|
20
|
+
上述身份来自用户提供的包内元数据;未验证它们与厂商官方发行包的字节一致性。
|
|
21
|
+
|
|
22
|
+
- 豆包安全提取 REA Evidence:`ev_adf3fc5bce309b0fcea619cdaf3d36b5766996f265fa8a7046639bf25a6319b8`。完整记录:[doubao-extraction.json](voice-input-ipa-evidence/doubao-extraction.json)。主程序 `Payload/Grace.app/Grace` SHA-256:`8f81ead52957545efb22c2be3c03717d7fef4803b090d25a2daa0db613fbc47e`。
|
|
23
|
+
- DeepSeek 安全提取 REA Evidence:`ev_fff6425722209231116bfe8c540c87864896f1fd66913a059095791692025baa`。完整记录:[deepseek-extraction.json](voice-input-ipa-evidence/deepseek-extraction.json)。主程序 `Payload/DeepSeek Chat.app/DeepSeek Chat` SHA-256:`027b12d8d3c3becb5c6e203b09d2d3387f57747f9f2a299ca90f194cab9e8c20`。
|
|
24
|
+
- DeepSeek Info.plist REA Evidence:`ev_2cc337d1e296db2b348f9210a1bad7cd270a9e5a97780ec15664953f6599a24b`。
|
|
25
|
+
- 豆包 Info.plist REA Evidence:`ev_7f5cbf558b531500adb06dd63fa361e993bc58c95283e8f445eebc12e839354d`。
|
|
26
|
+
- 两份 plist 记录导出:[rea-evidence.json](voice-input-ipa-evidence/rea-evidence.json)。
|
|
27
|
+
|
|
28
|
+
下面的字符串扫描和反汇编来自普通本地工具,是这些已提取字节的补充静态证据,没有伪造额外 REA Evidence ID。字符串偏移为对应主程序的文件偏移;反汇编地址为 Mach-O 链接地址,不是实际运行时地址。
|
|
29
|
+
|
|
30
|
+
## 豆包:独立的语音回填组件
|
|
31
|
+
|
|
32
|
+
### 输入入口与生命周期
|
|
33
|
+
|
|
34
|
+
`Grace` 中包含构建路径 `MessagingInputViewVoiceToCompleteComponent.swift`(文件偏移 `0xbb2a7f0`),以及以下日志/元数据:
|
|
35
|
+
|
|
36
|
+
| 文件偏移 | 内容 | 能支持的结论 |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `0xbb2aba0` | `startLongPressUseVoiceToComplete()` | 存在专门的长按语音补全入口 |
|
|
39
|
+
| `0xbb2acd0` | `voiceInputDidResult(id:config:asrResult:)` | 有识别结果回调 |
|
|
40
|
+
| `0xbb2b031` | `asrTextContext` | 存在识别文本上下文 |
|
|
41
|
+
| `0xbb2b220` | `asrStreamPostProcess` | 存在流式结果后处理成员 |
|
|
42
|
+
| `0xb35a410` | `touchDownToRecordingStart` | 追踪按下到开始录音 |
|
|
43
|
+
| `0xb35a430` | `touchUpToRecordingStop` | 追踪松手到停止录音 |
|
|
44
|
+
| `0xb35a450` | `touchUpToReceiveASRResult` | 追踪松手到收到识别结果 |
|
|
45
|
+
|
|
46
|
+
`VoiceInputSessionImpl` 还包含开始、停止、取消三种会话日志,以及「录音时间短于最小录音时间则取消 ASR」的分支日志。不能把这三种动作合并成同一状态。
|
|
47
|
+
|
|
48
|
+
### 文本回填:已确认具体指令
|
|
49
|
+
|
|
50
|
+
通过 ARM64 字符串引用和 `LC_FUNCTION_STARTS` 定位到引用结果回调日志的函数 `0x1090270bc`。在其静态代码路径中:
|
|
51
|
+
|
|
52
|
+
- `0x10902724c–0x109027250` 从 selector reference `0x10dbdd720` 读取 selector。
|
|
53
|
+
- 对应文件偏移 `0xdbdd720` 的指针为 `0x10c452da4`;指向的字符串为 **`setText:`**。
|
|
54
|
+
- `0x10902725c` 调用 `0x10a7996f8`,Mach-O indirect symbol 表将该 stub 标识为 **`_objc_msgSend`**。
|
|
55
|
+
- 此调用的 `x0` 为此前取得的对象,`x1` 为 `setText:`,`x2` 为此前处理产生的字符串。
|
|
56
|
+
|
|
57
|
+
因此,已经观察到「结果回调路径对对象设置文本」的真实静态调用。具体控件类型、选择区插入规则、已有草稿如何合并,以及最终是否自动发送,未完整恢复。
|
|
58
|
+
|
|
59
|
+
证据:[结果回调反汇编](voice-input-ipa-evidence/doubao/result-disasm.txt)、[selector 记录](voice-input-ipa-evidence/doubao/settext-selector.txt)、[导入符号](voice-input-ipa-evidence/doubao/objc-msgsend-stub.txt)。
|
|
60
|
+
|
|
61
|
+
### 录音与识别
|
|
62
|
+
|
|
63
|
+
FlowAudio 包含 AudioQueue、AudioUnit 和 AVAudioEngine 三种录音实现;包内同时存在不代表本次入口同时使用三者。具体选择由运行配置决定,尚未确认。
|
|
64
|
+
|
|
65
|
+
识别模块中的直接观察包括:
|
|
66
|
+
|
|
67
|
+
- `SAMIAssistantVoiceRecognizer`(`0xb35b990`)与 `VoiceRecognizer+SAMIAssistant.swift`。
|
|
68
|
+
- `recognize(dataStream:)`、`asrRun(dataStream:)`。
|
|
69
|
+
- `[ASR] begin sending audio`、`[ASR] finish sending audio`、`[ASR] wait stop done`。
|
|
70
|
+
- `asrIncrementResultSubject`、`asrFinishSubject`。
|
|
71
|
+
- `samiAssistantSession(session:didReceiveASREvent:)`。
|
|
72
|
+
|
|
73
|
+
这些证据支持「录音流交给 SAMI 识别模块,接收增量及结束结果」的模块设计推断。未恢复 VoiceToComplete 到该具体识别器的完整调用关系,不断言这是唯一识别后端。
|
|
74
|
+
|
|
75
|
+
包内存在 `wss://sami.bytedance.com/internal/api/v1/ws`,但相邻上下文归属 Annie/Hybrid 识别模块。**本次不能把该 URL 直接认定为长按输入实际使用的端点。** 同样,实时语音通话的 SAMI/RTC 组件没有被当作上述输入链路的证明。
|
|
76
|
+
|
|
77
|
+
### 停止动作的限制
|
|
78
|
+
|
|
79
|
+
引用 `voice to complete stop voice input` 的函数 `0x1090261b8` 使用 Swift witness table 间接调用;未恢复其目标,不能仅根据日志证明实际调用的是 `stop` 或 `cancel`。
|
|
80
|
+
|
|
81
|
+
引用 `endedLongPressUseVoiceToCompleteStatus()` 的候选函数 `0x10902a6c4` 还含有超时日志,可能属于长按结束后的状态清理/超时处理。本次不把它直接认定为原始松手事件处理器。
|
|
82
|
+
|
|
83
|
+
完整补充记录:[豆包发现记录](voice-input-ipa-evidence/doubao-findings.md)。
|
|
84
|
+
|
|
85
|
+
## DeepSeek:专用网络语音识别链路
|
|
86
|
+
|
|
87
|
+
### 长按输入与状态
|
|
88
|
+
|
|
89
|
+
在 `InputAreaView` 中发现 `VoiceGesture`、`VoiceRecord`、`VoiceInputButton`,包括 `onLongPress`、`onTouchesEndedAndFailed`、`recordListener`、`stopSessionTask` 和 `startTask`。
|
|
90
|
+
|
|
91
|
+
状态元数据包括 `starting`、`recording`、`idle`、`stopping`;界面还记录剩余录音时间和声谱。它是 App 自己的录音交互,不应把录音权限或声谱动画视为识别模型在手机上执行的证明。
|
|
92
|
+
|
|
93
|
+
### 采集、编码与识别边界
|
|
94
|
+
|
|
95
|
+
观察到 `AVAudioEngineSource`、`CaptureSessionAudioSource`、`AudioHub`、`AudioPipeline`、`AudioSpectrumProcessor`。`AudioPipeline` 包含 `opusFormat`、`converter`、`packetBuffer` 等字段,识别协议相关元数据同时包含 `pcm` 与 `opus`。本次没有确认具体会话选择何种编码或采样率。
|
|
96
|
+
|
|
97
|
+
专用语音识别类和字段如下:
|
|
98
|
+
|
|
99
|
+
| 文件偏移 | 观察 |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `0xfa82c0` | `DSVoiceInputService`,邻接字段有 `recognizer`、`audioHub`、`cancel` |
|
|
102
|
+
| `0xfa8430` | `DSWebsocketSpeechRecognizer` |
|
|
103
|
+
| `0xfa8370` | ASR 对象字段 `socket` |
|
|
104
|
+
| `0xfa8380` | `sendFinishToServer` |
|
|
105
|
+
| `0xfa83b4` | `audioID` |
|
|
106
|
+
| `0xfa8410` | `waitResultTimeoutSecond` |
|
|
107
|
+
| `0x110246b` / `0x1102472` | 识别结果的 `string` / `isFinal` |
|
|
108
|
+
|
|
109
|
+
专用 ASR 类型元数据还包含 `start`、`result`、`end`、`finish_reason`、`finish`,以及 `websocketError`、`disconnectedWithoutEnd`、`connectTimeout` 等错误状态。这支持「网络结果事件和结束标记分离」的设计推断,但未恢复完整报文 schema。
|
|
110
|
+
|
|
111
|
+
### 服务接口与异常处理
|
|
112
|
+
|
|
113
|
+
- 文件偏移 `0xf75473`:`/api/v0/asr/ws`。
|
|
114
|
+
- `0xf75490`:`/api/v0/asr/fetch_cached`。
|
|
115
|
+
- `0xf76f60`:`https://chat.deepseek.com/api/v0/asr/fetch_cached`。
|
|
116
|
+
- `0xf76010`:`asr_timeout_ms_before_fetch_cache`。
|
|
117
|
+
- 存在 `ASRFetchCache`、`ASRCacheContent`、`voice_fetch_cache`、`voice_fetch_cache_success`。
|
|
118
|
+
|
|
119
|
+
类字段、专用接口和缓存补取配置共同支持网络 ASR 与超时后补取结果的架构推断。缓存查询的精确触发条件、认证字段和真实网络时序未验证。
|
|
120
|
+
|
|
121
|
+
### 不能忽略的交互差异
|
|
122
|
+
|
|
123
|
+
该版本简体中文资源中的 `recordingLabel` 是 **「松手发送,上滑取消」**,`recordSwipedUpLabel` 是「松手取消」。还存在 `voice_input_commit`、`voice_input_send`、`voice_input_send_failure` 等事件。
|
|
124
|
+
|
|
125
|
+
这支持至少存在「松手触发提交/发送」的设计;不能将所有 DeepSeek 长按输入模式描述为「只填草稿」。资源文案不是实际执行观测,本次没有恢复它是否存在配置开关或另一种仅回填模式。
|
|
126
|
+
|
|
127
|
+
证据:[字符串及偏移](voice-input-ipa-evidence/deepseek-voice-strings.txt)、[中文资源](voice-input-ipa-evidence/deepseek-localization.json)、[涉及 preferred_language 的构造路径反汇编](voice-input-ipa-evidence/deepseek-asr-constructor.asm)。该构造范围只用于补充静态观察,不代表完整识别器调用图。
|
|
128
|
+
|
|
129
|
+
## 对我们的客户端意味着什么
|
|
130
|
+
|
|
131
|
+
长按录音与松手回填是客户端交互,既可以搭配端侧模型,也可以搭配服务端 ASR;交互本身不要求把大模型打进安装包。
|
|
132
|
+
|
|
133
|
+
若采用网络 ASR,推荐的独立设计为:按下后建立录音会话和识别任务;录音中发送音频并接收临时结果;松手停止采集、发送结束标记;最终结果返回后填入草稿,由用户确认发送。上滑取消、页面退出和断网需要独立取消/失败状态,迟到结果应按会话 ID 丢弃。
|
|
134
|
+
|
|
135
|
+
这是面向本项目的实现建议,不是声称两个 App 逐字采用该状态机。若使用自托管服务或 DSH 的识别插件,客户端只承担录音与协议处理,模型保留在服务器/运行 DSH 的电脑上。若坚持离线,则仍需端侧权重和识别运行库,可采用按需下载。
|
|
136
|
+
|
|
137
|
+
## 覆盖清单与未解决问题
|
|
138
|
+
|
|
139
|
+
| 问题 | 状态 | 依据或缺口 |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| 区分豆包语音输入与语音通话 | 已回答 | 专用 VoiceToComplete 模块及输入结果回调 |
|
|
142
|
+
| 是否存在按下/松手录音生命周期 | 部分回答 | 两包入口、状态、指标;完整事件分派未恢复 |
|
|
143
|
+
| 豆包是否将 ASR 文本写入控件 | 已回答(静态) | 结果回调内 `setText:` / `_objc_msgSend` |
|
|
144
|
+
| DeepSeek 是否总是只回填草稿 | 未确认 | 文案支持松手发送,缺少完整模式分支 |
|
|
145
|
+
| 是否包含网络语音识别实现 | 已回答(静态) | 专用识别器、接口、发送及结果模块 |
|
|
146
|
+
| 长按入口实际选择哪个识别器 | 部分回答 | 豆包 Swift witness 间接目标未恢复;DeepSeek 完整调用图未恢复 |
|
|
147
|
+
| 是否无需网络、使用端侧 ASR 模型 | 未确认 | 无充分端侧模型证据,不能排除下载/备用链路 |
|
|
148
|
+
| 精确编码、采样率、包间隔及服务端模型 | 未确认 | 需要更完整调用关系或运行时录音/网络观测 |
|
|
149
|
+
|
|
150
|
+
DeepSeek 的 REA/Ghidra 原生查询在默认自动分析阶段触发 MCP 300 秒超时,未取得完整反编译结果。本报告使用已取得的安全提取、plist、定向字符串和 LLVM 反汇编证据;没有把超时查询当成成功证据。已关闭 REA 原生目标。
|