docs(ios): plan PiP voice session lifecycle

Define a phased, privacy-first path for keeping keyboard dictation responsive while releasing the microphone between utterances.
This commit is contained in:
Rocky
2026-07-26 12:34:43 +08:00
parent 937ce33f05
commit 9d8914fbf3
+385
View File
@@ -0,0 +1,385 @@
# iOS PiP 语音会话保活规划
> **文档状态**:产品与架构规划(待验证,未进入实现)
> **适用范围**iOS 主 App + 键盘扩展 + Live Activity
> **目标版本**:待产品验证后确定
> **创建日期**2026-07-26
---
## 1. Executive Summary
### 1.1 目标
在不要求 OSGKeyboard 长时间占用麦克风的前提下,尽量保持主 App 可响应键盘扩展的听写指令,降低用户在宿主 App 与 OSGKeyboard 之间反复切换的频率。
核心方向是将两个当前耦合的能力拆开:
1. **会话可用性**:主 App 仍可接收键盘命令。
2. **麦克风采集**:仅在用户明确开始听写时启用,完成后立即释放。
PiP(画中画)只承担系统可见的多任务会话载体,不绕过麦克风授权,也不应使用静音音频循环伪造后台活动。
### 1.2 核心结论
| 决策 | 规划选择 |
|------|----------|
| 产品定位 | 将 PiP 作为可选的「免切换模式」,不替代普通 Flow |
| 麦克风策略 | PiP 空闲时关闭;键盘点按听写后按需激活 |
| 默认策略 | 保留当前隐私友好的 5 分钟 Flow;PiP 由用户主动开启 |
| 降级路径 | PiP 不可用或失效时回落到现有 `startflow` 冷启动流程 |
| 状态展示 | PiP 显示有意义的语音会话状态;Live Activity 继续负责锁屏与灵动岛 |
| 禁止方案 | 不播放静音文件保活,不使用定位或 VoIP 等无关后台模式 |
| 上线方式 | 先做真机技术验证和 TestFlight 审核验证,再决定正式产品化 |
### 1.3 非目标
- 不让键盘扩展直接访问麦克风;这是 iOS 平台限制。
- 不承诺 App 被用户强制退出后仍可免切换听写。
- 不承诺电话、Siri、相机或其他录音 App 抢占音频设备时继续录音。
- 不用 PiP 绕过麦克风权限、隐私提示或系统音频策略。
- 第一阶段不重写 ASR、润色、App Group 或 Darwin 通知管线。
---
## 2. 问题定义
### 2.1 平台约束
iOS 键盘扩展无法直接申请或使用麦克风。系统级语音键盘因此必须采用:
```text
键盘扩展
→ 发送开始/停止命令
→ 主 App 采集并转写
→ App Group 返回结果
→ 键盘插入文本
```
当主 App 被系统挂起或终止时,键盘无法即时启动录音,只能打开主 App 重新建立会话。当前 Flow 通过持续运行 `AVAudioEngine` 输入链路换取后台可用性,但会带来麦克风长期占用、橙色隐私指示、电量消耗和音频冲突。
### 2.2 用户问题
| 用户感知 | 当前根因 | 目标变化 |
|----------|----------|----------|
| 频繁跳转主 App | 后台主进程不可响应 | PiP 有效时直接响应键盘命令 |
| 麦克风指示长时间亮起 | Flow 会话级连续采集 | 空闲时释放麦克风 |
| 耗电或发热 | 音频引擎持续采样和处理 | 仅听写期间采样 |
| 其他 App 无法使用麦克风 | OSGKeyboard 持有输入设备 | 听写结束后主动释放 |
| 不知道会话是否可用 | Flow、麦克风和进程状态混为一体 | 分开展示「免切换已就绪」和「正在录音」 |
### 2.3 成功定义
PiP 模式下,用户应能:
1. 在 OSGKeyboard 主 App 中主动开启免切换模式。
2. 将 PiP 小窗收纳到屏幕边缘。
3. 回到微信、邮件等宿主 App。
4. 点击键盘麦克风后直接开始听写。
5. 停止听写后收到文本,同时麦克风在短时间内释放。
6. PiP 失效时收到明确提示,并能通过现有冷启动路径恢复。
---
## 3. 竞品与行业模式
### 3.1 Typeless
Typeless iOS 1.9.0 将该能力命名为 Picture in picture / Skip app switching
- 用户先在主 App 中主动开启。
- PiP 可拖到屏幕边缘收纳。
- 用户在其他 App 的 Typeless 键盘中开始说话。
- 官方产品说明强调麦克风空闲时关闭,以降低电量消耗。
其公开资料无法证明具体内部实现,因此本规划只借鉴产品模型,不假定其私有代码结构。
### 3.2 Wispr Flow、TypeWhisper 与同类开源项目
常见架构是主 App 持有 `AVAudioEngine`,键盘通过 App Group 与 Darwin 通知控制句子开始和停止。优点是首字延迟低,缺点是会话期间通常持续占用音频输入。
OSGKeyboard 当前 Flow 已属于此模式,并已具备:
- 主 App 会话所有权;
- 键盘与主 App IPC
- 连续采集与 utterance gate
- App Group 结果回传;
- Live Activity
- 冷启动与恢复流程。
因此 PiP 应作为会话生命周期的新载体,而不是重建整条语音管线。
### 3.3 SuperWhisper / App Intents 路线
更保守的方案是不做长期后台保活,使用 App Intents、Action Button、快捷指令或显式 App 切换启动录音。该方案最符合系统预期,但无法完全满足键盘内即时听写。
OSGKeyboard 应保留这类入口作为稳定降级,而不是依赖 PiP 达到 100% 可用。
### 3.4 合规边界
以下方式不应采用:
- 循环播放静音音频以防止挂起;
- 声明与产品无关的定位、VoIP 后台能力;
- 使用不可见或无实际产品意义的伪视频,仅为延长进程生命;
- 在用户未明确开启会话时自动恢复麦克风。
PiP 内容需要能被解释为真实的语音会话控制面,例如展示:
- 「免切换已就绪」;
- 「正在聆听」及音量反馈;
- 「正在转写」;
- 暂停、结束或返回 App 操作。
---
## 4. 目标产品模型
### 4.1 三层可用性
```text
层级 0:冷启动
主 App 不可用
→ 键盘打开 startflow
→ 主 App 建立语音会话
层级 1:短时 Flow
AVAudioEngine 会话保持
→ 最低首字延迟
→ 默认 5 分钟无活动后结束
层级 2PiP 免切换模式
PiP 保持用户可见的多任务会话
→ 空闲时麦克风关闭
→ 键盘命令触发按需开麦
```
三个层级必须共用同一份 `FlowSessionBridge` 状态合约,键盘不应根据实现细节分别写三套逻辑。
### 4.2 用户入口
建议在首页提供独立状态卡,而不是继续扩张设置开关:
- 未开启:`开启免切换模式`
- 启动中:`正在准备画中画`
- 已就绪:`免切换已就绪 · 麦克风未使用`
- 录音中:`正在聆听`
- 失效:`会话已断开,点击恢复`
首次开启时应明确说明:
1. 屏幕上会出现可收纳的 PiP 小窗。
2. 空闲时不会使用麦克风。
3. 用户关闭 PiP、强制退出 App 或系统回收进程后,需要重新开启。
### 4.3 键盘状态
键盘麦克风状态应从「主 App 是否活着」升级为明确能力状态:
| 状态 | 表现 | 点击结果 |
|------|------|----------|
| 不可用 | 灰色 | 引导权限或 Full Access |
| 需恢复 | 橙色 | 打开主 App 恢复会话 |
| PiP 就绪、麦克风关闭 | 绿色 | 请求主 App 按需开麦 |
| 正在激活麦克风 | 绿色加载态 | 等待真实音频 proof |
| 正在录音 | 红色/波形 | 发送停止命令 |
| 正在转写 | 处理中 | 等待结果 |
---
## 5. 目标架构
### 5.1 组件边界
```text
Keyboard Extension
└─ FlowSessionBridge / Darwin command
Host App
├─ VoiceSessionCoordinator
│ ├─ FlowSessionManager
│ ├─ PiPVoiceSessionController
│ └─ AudioCaptureLifecycle
├─ FlowContinuousCapture
├─ ASR + Polish pipeline
└─ Live Activity
```
规划职责:
- `PiPVoiceSessionController`:只管理 PiP 生命周期和展示状态。
- `AudioCaptureLifecycle`:管理按需激活、音频 proof、停止及释放。
- `FlowSessionManager`:继续负责命令、ASR、润色和结果回传。
- `FlowSessionBridge`:发布跨进程能力快照,不让键盘猜测主 App 状态。
### 5.2 状态机
```text
inactive
→ preparingPiP
→ pipReadyMicOff
→ activatingMic
→ recording
→ processing
→ releasingMic
→ pipReadyMicOff
任意状态
→ interrupted
→ recovering 或 inactive
```
重要不变量:
1. `pipReadyMicOff` 必须确认音频输入已停止并释放。
2. 键盘只有在收到 `recording` 和真实 audio proof 后才显示正在录音。
3. PiP 存活不能等价于麦克风可用。
4. 电话/Siri 中断后不得静默恢复录音。
5. 任何超时都要回收麦克风并写入明确错误。
### 5.3 PiP 内容方案
技术验证阶段应比较两类 Apple 官方能力:
1. 基于 `AVPlayerLayer` 的媒体 PiP
2. 基于 `AVSampleBufferDisplayLayer` / 视频通话内容源的实时 PiP。
选择标准不是「哪种最容易保活」,而是:
- 是否符合 OSGKeyboard 的真实产品用途;
- 能否展示动态语音会话状态;
- 麦克风激活/释放是否稳定;
- 收纳、锁屏、音频中断行为是否可预测;
- App Review 是否能清楚理解其用途。
在完成真机和审核验证前,不冻结具体 AVKit 实现。
---
## 6. 实施阶段
### Phase 0:技术与审核可行性验证
目标:证明「PiP 存活 + 闲时关麦 + 键盘触发按需开麦」在目标 iOS 版本可行。
验证项:
- PiP 启动、收纳、恢复与关闭;
- 空闲 30 分钟后主 App 是否仍能响应;
- 空闲期间系统麦克风指示是否消失;
- 键盘命令到首个有效音频帧的延迟;
- 连续 20 次开始/停止是否稳定;
- 电话、Siri、蓝牙切换、锁屏、低电量模式;
- 用户关闭 PiP 后的降级行为;
- TestFlight / App Review 说明是否被接受。
退出标准:
- 空闲时没有麦克风占用;
- P95 命令到有效音频帧小于 1 秒;
- 20 次连续听写无僵尸录音或失联状态;
- 失败后都能回到冷启动路径;
- 没有使用静音循环或无关后台能力。
### Phase 1:内部可用版本
- 新增 PiP 会话控制器;
- 将持续采集改造成可重复激活/释放;
- 扩展跨进程状态快照;
- 键盘增加激活中、PiP 就绪和失效状态;
- 复用现有 ASR、润色、结果回传和 Live Activity
- 添加状态机与 IPC 单元测试。
### Phase 2:产品化
- 首页免切换状态卡;
- 首次开启说明与 PiP 收纳引导;
- 中英文文案与隐私说明;
- 诊断页增加 PiP、音频会话和最近中断原因;
- 增加遥测指标,但不采集音频内容。
### Phase 3:灰度与决策
- TestFlight 小流量开启;
- 比较 PiP 与普通 Flow 的成功率、首字延迟和耗电;
- 根据审核反馈决定默认入口和长期支持范围;
- 若 PiP 不稳定或审核风险不可接受,保留为实验功能或停止上线。
---
## 7. 测试矩阵
### 7.1 功能场景
| 场景 | 预期 |
|------|------|
| PiP 空闲 | 主 App 可响应,麦克风未占用 |
| 键盘开始听写 | 按需激活并获得真实音频帧 |
| 停止听写 | 完成转写并及时释放麦克风 |
| 连续多句 | 每句均重新激活成功,无第二句无音频 |
| PiP 被关闭 | 键盘切为需恢复,不显示假就绪 |
| App 被强退 | 清除旧 generation 和僵尸状态 |
| 电话/Siri 中断 | 当前句失败并提示,不自动偷录 |
| 蓝牙设备变化 | 音频格式重建,不崩溃 |
| 网络失败 | 本地 ASR 保留;润色按现有策略降级 |
### 7.2 设备与系统
- 最低支持 iOS 版本、当前稳定版和最新 beta;
- 刘海机、灵动岛机型、iPad
- AirPods、普通蓝牙耳机、车载音频、有线设备;
- 微信、信息、邮件、Slack、Notes 及自定义文本输入控件;
- 锁屏、横竖屏、多窗口、低电量和后台刷新关闭状态。
---
## 8. 指标与验收
### 8.1 核心指标
| 指标 | 定义 | 目标 |
|------|------|------|
| 免切换成功率 | PiP 就绪时无需打开主 App完成听写 | ≥ 98% |
| 麦克风空闲占用 | 非录音期间仍占麦的时长比例 | 接近 0 |
| 首帧延迟 P95 | 键盘点击到真实音频 proof | < 1 秒 |
| 结果回传成功率 | 停止后键盘收到最终结果 | ≥ 99% |
| 僵尸状态率 | 键盘显示可用但主 App无法响应 | < 0.5% |
| 恢复成功率 | 失效后通过冷启动恢复 | ≥ 99% |
### 8.2 观察指标
- 每日 PiP 开启人数与启用留存;
- PiP 被用户主动关闭的比例;
- 每小时耗电和温升相对普通 Flow 的变化;
- 音频中断类型分布;
- 用户因橙色麦克风指示或隐私产生的反馈;
- App Review 反馈和政策变化。
---
## 9. 风险与应对
| 风险 | 等级 | 应对 |
|------|------|------|
| PiP 被认定与媒体用途不匹配 | 高 | 提供真实会话 UI、明确审核说明;先 TestFlight/审核验证 |
| 系统版本改变 PiP 行为 | 高 | 保持冷启动降级;按系统版本做兼容验证 |
| 按需开麦首字丢失 | 中 | 激活态 + audio proof + 预录缓冲,不提前向键盘宣告录音 |
| 频繁激活导致音频路由异常 | 中 | 串行状态机、格式重建、媒体服务重置恢复 |
| PiP 与 Live Activity 状态冲突 | 中 | 单一 coordinator 发布状态,两个 UI 只消费 |
| 用户误解 PiP 仍在监听 | 中 | 空闲状态明确写「麦克风未使用」,隐私说明可验证 |
| 其他 App 抢占麦克风 | 中 | 显式中断提示,不承诺并发录音 |
---
## 10. 产品决策门
进入代码实现前,需要确认:
1. 是否接受 PiP 作为用户主动开启、系统可见且可收纳的产品形态;
2. 是否优先「空闲关麦」而接受约数百毫秒的重新激活延迟;
3. PiP 中展示哪些真实功能,确保它不是纯保活黑窗;
4. 是否将现有「跳过 App 切换」重命名并拆为普通 Flow / PiP 两种模式;
5. 最低支持系统和目标测试设备;
6. 技术验证失败或审核风险过高时,是否接受回退到短 Flow + App Intents。
在上述决策和 Phase 0 证据完成前,不建议直接进入正式实现。