122 lines
6.0 KiB
Markdown
122 lines
6.0 KiB
Markdown
# 语音健康日记(语音转文字 + AI 整理)设计
|
|
|
|
> 2026-06-10 · 在「健康记录」(`DiaryQuickSheet`)里加语音输入:iOS 端侧语音识别实时转写,停止后由本地 LLM 整理成健康日记草稿,可编辑后保存。
|
|
|
|
## 背景
|
|
|
|
「健康记录」目前只能手打文字(`DiaryQuickSheet` → `DiaryEntry`),已有「AI 医生角度多轮追问」辅助。口述比打字门槛低得多,尤其适合身体不适时记录。
|
|
|
|
现有两个本地模型(Qwen3.5-2B 文本、Qwen3-VL 视觉)都没有音频编码器,无法做 ASR;引入 Whisper 类模型要 +0.5~1.5GB 体积和一条新推理链路,不可接受。`SFSpeechRecognizer` 支持强制端侧识别(`requiresOnDeviceRecognition = true`),中文质量够用、零体积,与「100% 本地」卖点完全一致。
|
|
|
|
## 决策(已与用户确认)
|
|
|
|
| 维度 | 决定 |
|
|
|---|---|
|
|
| 交互形态 | 说完 → 自动调 LLM 整理成日记草稿(非纯听写) |
|
|
| 整理样式 | 自适应:口述短 → 一段通顺的话;口述长且多方面 → 自动分点 |
|
|
| 入口 | `DiaryQuickSheet` 输入框旁麦克风按钮(不动 RecordSheet 骨架) |
|
|
| 转写链路 | 流式实时转写(AVAudioEngine buffer → 实时字幕),不落盘音频 |
|
|
| ASR 引擎 | `SFSpeechRecognizer` 端侧;不引入 Whisper;不做云端回退 |
|
|
|
|
## 架构
|
|
|
|
```
|
|
DiaryQuickSheet(mic 按钮 + 录音面板)
|
|
├─► SpeechDictationService(新)── AVAudioEngine + SFSpeechRecognizer(端侧)
|
|
└─► DiaryAssistService.organize(transcript:)(新方法)──► AIRuntime ──► MNN/MLX
|
|
```
|
|
|
|
符合模块边界:UI 不直接碰 AIRuntime;语音采集是系统能力,封装成独立 Service。
|
|
|
|
## 组件
|
|
|
|
### 1. `SpeechDictationService`(新,`Services/`,`@MainActor`)
|
|
|
|
封装 AVAudioEngine 麦克风采集 + `SFSpeechAudioBufferRecognitionRequest` 流式识别。
|
|
|
|
接口:
|
|
|
|
- `static var isAvailable: Bool` — 本机是否支持**端侧**中文识别(`supportsOnDeviceRecognition` + locale 检查;模拟器/老机型为 false)
|
|
- `func requestAuthorization() async -> Bool` — 麦克风 + 语音识别两个权限一起申请
|
|
- `func start(onPartial: @escaping (String) -> Void) throws` — 开始录音,partial 结果实时回调(录音面板字幕)
|
|
- `func stop() async -> String` — 停止并返回最终转写稿
|
|
|
|
实现要点:
|
|
|
|
- `requiresOnDeviceRecognition = true`(硬性,识别内容不出设备)
|
|
- `addsPunctuation = true`(自动标点)
|
|
- locale 跟随系统,不支持端侧时 `isAvailable = false`
|
|
- **不写任何音频文件**,buffer 即用即弃
|
|
- 录音上限 3 分钟,到点自动 stop
|
|
|
|
### 2. `DiaryAssistService.organize(transcript:)`(新方法)
|
|
|
|
```swift
|
|
func organize(transcript: String) async throws -> (text: String, decodeRate: Double)
|
|
```
|
|
|
|
- prompt 加在 `AI/Prompts/DiaryAssistPrompts.swift`:`organizePrompt(transcript:)`
|
|
- few-shot 两例:短口述 → 一段第一人称通顺文本;长口述(症状/用药/饮食多方面)→ 分点
|
|
- **硬性约束写进 prompt:只重组语言,不得增删改任何数值、单位、药名、时间**(健康数据,2B 模型改数即事故)
|
|
- 转写稿超长先截断(保护 context),非流式,await 完整结果
|
|
- 走 AIRuntime actor 队列,与「多轮追问」「拍照识别」自然串行
|
|
|
|
### 3. `DiaryQuickSheet` UI 改动
|
|
|
|
- 内容输入框 trailing 加 mic 按钮(`isAvailable == false` 时整个隐藏)
|
|
- 录音态:输入框下方展开录音面板 —— 实时字幕区 + 脉冲动画(sparkles/waveform `symbolEffect`)+「停止」按钮
|
|
- 整理态:面板转「AI 整理中」(复用 `AIFlowBar` + tok/s),可取消
|
|
- 完成:整理稿**追加**进输入框(沿用 `appendToContent`,不覆盖已写内容);面板收起
|
|
- 完成后显示一次性「改用原话」pill:点击把刚追加的整理稿替换为原始转写稿(原始稿在本次 sheet 生命周期内持有;再次录音或手动编辑该段后 pill 消失)
|
|
- 整理稿入框后,既有「AI 多轮追问」功能照常可用,无需特殊处理
|
|
|
|
## 状态机
|
|
|
|
```
|
|
idle ──(点 mic,权限 OK)──► recording ──(停止/3min 到点)──► organizing ──► done(回 idle)
|
|
```
|
|
|
|
- 实时字幕只显示在录音面板,**停止前不进输入框**
|
|
- `organizing` 期间 mic 按钮与「AI 追问」按钮禁用(AIRuntime 串行,避免排队困惑)
|
|
|
|
## 错误处理(红线 #5:全部有回退,不卡死)
|
|
|
|
| 故障 | 行为 |
|
|
|---|---|
|
|
| 权限被拒 | 弹说明 alert + 「前往设置」跳系统设置 |
|
|
| 本机不支持端侧识别(含模拟器) | mic 按钮隐藏,静默降级为纯手打 |
|
|
| 识别中途出错 | 已拿到的 partial 文本照常进 organizing |
|
|
| 转写结果为空 | 提示「没听清,再试一次」,回 idle |
|
|
| LLM 未就绪 / 整理失败 | **原始转写稿直接追加进输入框** + 提示「AI 整理失败,已填入原话」 |
|
|
|
|
不做云端识别回退(红线 #1:不引入云服务)。
|
|
|
|
## 权限(project.pbxproj 新增两条 INFOPLIST_KEY)
|
|
|
|
- `NSMicrophoneUsageDescription`:康康需要使用麦克风进行语音记录,识别全程在本机完成,声音不会上传。
|
|
- `NSSpeechRecognitionUsageDescription`:语音转文字使用 iOS 端侧识别,内容不会发送给 Apple 或任何服务器。
|
|
|
|
## 测试
|
|
|
|
- `organize` prompt:`DebugAIRunner` 加自检入口(短/长两条样例口述,肉眼验自适应样式 + 数值不被改动)
|
|
- 录音链路:真机手测清单(权限首次申请、录音字幕、3 分钟自动停、整理失败回退、「改用原话」)
|
|
- 模拟器:验证 `isAvailable == false` 时 mic 按钮隐藏
|
|
|
|
## 范围边界(不做)
|
|
|
|
- 症状 / AI 问答的语音入口
|
|
- 音频文件保存或回放
|
|
- Whisper / 任何新模型
|
|
- Live Activity 集成(前台短流程,无必要)
|
|
- 多语言听写优化(locale 跟系统,不支持即降级)
|
|
|
|
## 卖点映射(§12)
|
|
|
|
1. 降低记录门槛 → 卖点 1(影像档案之外的日常记录闭环)
|
|
2. 「系统端侧 ASR + 本地 LLM 整理」全链路不出设备 → 卖点 2(100% 本地)
|
|
3. 日记语料变多 → 卖点 3(本地 RAG 长期记忆)
|
|
|
|
## 排期
|
|
|
|
清单外新功能(红线 #6),本设计即立项讨论结论。工作量约 1~1.5 天,独立小分支插队,不挤占 C1/VL 主线。
|