Files
teaching-feedback-assistant/docs/recording-library-diarization-development-plan.md

1461 lines
58 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 录音资料库、说话人分离与反馈素材编排开发计划
- 状态:待评审
- 编制日期2026-07-23
- 适用仓库:`teaching-feedback-assistant`
- 目标部署Ryzen 7 5700U、CPU FunASR、远程 PostgreSQL、Docker Compose
- 计划性质产品、交互、数据、API、转录、迁移、测试和发布的统一实施基线
## 1. 执行摘要
本次改造不应继续把录音能力作为“反馈生成”页面中的一个折叠区域扩展。目标能力已经包含录音和文件上传、长期管理、音频播放、异步转录、说话人分离、角色映射、逐段选择、跨录音选择、AI 总结、总结预览和写入反馈输入框,实际边界是一个独立的“录音资料库”子系统。
最终产品职责划分如下:
```text
现场录音 / 上传音频
|
v
录音资料库
|
v
转录 + 说话人分离
|
v
对话校对、角色映射、片段选择
/ \
v v
直接加入反馈输入框 多录音或片段交给 AI
|
v
总结预览
|
v
写入反馈输入框
```
本计划采用以下总体策略:
1. 录音从 `feedback_session` 的附属数据升级为用户拥有的独立资产。
2. 反馈页面只负责编辑反馈、选择素材、预览总结和保存结果。
3. 录音列表、录音详情和反馈素材选择使用独立页面,不以大弹窗承载主流程。
4. 说话人分离只产生匿名说话人簇,教师、学生、家长等业务角色由用户确认。
5. AI 总结使用选中对话的不可变快照,避免转录重跑或角色修改改变历史结果。
6. 数据库按 expand、backfill、switch、contract 渐进迁移,旧链路在切换完成前始终可回退。
7. FunASR 说话人能力先通过 5700U CPU 技术预研门槛,再决定 CAM++ 或 ERes2NetV2不直接在生产中试错。
8. 音频默认保留到用户主动删除;自动保留期限作为后续可配置能力,不在首版擅自清理历史录音。
## 2. 当前状态与约束
### 2.1 当前业务链路
当前链路为:
```text
反馈草稿
-> 创建 feedback_session
-> 创建 lesson_session
-> 小程序每 8 分钟录制一个音频片段
-> 上传 audio_segment
-> Rust worker 调用 FunASR
-> audio_segment 写入纯文本 transcript
-> 用户按 lesson_session 整节选择
-> summary worker 生成总结
-> 用户确认后替换反馈输入框
-> 保存反馈并将 feedback_session 标记 finalized
```
现有能力中可以复用的部分:
- 微信小程序录音权限、8 分钟自动切片和失败上传本地保留。
- Rust API 的用户认证、owner 隔离和请求 ID。
- 音频持久化目录和 Docker 命名卷。
- PostgreSQL 异步转录队列、租约、重试和失败状态。
- FunASR Paraformer、FSMN-VAD 和标点模型服务。
- 总结任务的幂等键、后台 worker、轮询、预览和应用流程。
- Grafana、Loki 和结构化日志规范。
### 2.2 当前结构性限制
1. `lesson_sessions.feedback_session_id` 非空,录音不能脱离反馈草稿独立存在。
2. 录音查询、重试和删除接口只允许访问 `active` 反馈批次。
3. 保存反馈后批次变为 `finalized`,当前小程序没有独立入口重新管理其录音。
4. `audio_segments.transcript` 只有纯文本,没有开始时间、结束时间、说话人或置信信息。
5. 当前总结来源表只关联整节 `lesson_session`,不支持逐段选择。
6. Rust API 成功转录后保留原始音频,但没有认证播放接口和统一保留策略。
7. 数据库级联删除和重复序号上传可能留下磁盘孤儿文件。
8. 当前本地 ASR 响应只有 `text``duration_ms``request_id`
9. 腾讯备用实现明确关闭 `speaker_diarization`,不能承担当前目标。
10. `recording_routes.rs` 已承载过多职责,继续叠加录音库接口会提高回归风险。
### 2.3 固定部署约束
- API 和 FunASR 使用 `compose.deploy.yml` 部署。
- FunASR 不暴露宿主机端口,只允许 API 通过 Compose 网络访问。
- PostgreSQL 使用现有远程数据库,不新增数据库容器。
- 首发继续使用 CPU 推理,不假设 CUDA、ROCm 或独立 GPU。
- 默认单 FunASR 进程和单转录 worker模型对象不在多线程中并发调用。
- 模型缓存和音频必须继续使用持久化卷。
- 不得把音频、模型、数据库凭据、微信密钥或 `.env` 提交到 Git。
- 日志不得包含转录正文、总结正文、学生姓名、原始文件名或真实磁盘路径。
### 2.4 已知但尚未获得的生产基线
编码前应从部署服务器补齐以下数据;缺少数据不阻止建表和接口开发,但阻止容量结论和自动清理上线:
- PostgreSQL 版本、现有表行数、数据库总大小和每日增长量。
- `teaching-feedback-audio` 卷当前文件数、容量和每日增长量。
- 录音平均时长、P95 时长、平均文件大小和最大文件大小。
- 单日录音数量、并发教师数量和转录队列 P95 等待时间。
- 5700U 上当前模型的 P50/P95 RTF、峰值内存和失败率。
- 用户期望的录音保留期限和可接受的删除恢复窗口。
## 3. 产品目标与非目标
### 3.1 首个完整版本目标
用户能够:
1. 在录音资料库中查看现场录音和上传录音。
2. 按学生、日期、来源和处理状态筛选录音。
3. 查看录音时长、文件状态、转录状态、说话人数和失败原因。
4. 播放本人录音,并从对话时间点跳转播放。
5. 上传支持格式的音频文件并在后台完成转录。
6. 查看按时间排序、按说话人分组标识的对话文本。
7. 将匿名说话人映射为教师、学生、家长或自定义显示名。
8. 选择整份录音、某位说话人的全部对话或若干对话片段。
9. 将选择内容直接追加到反馈输入框,受 2000 字限制保护。
10. 选择一份或多份录音的部分或全部对话生成 AI 总结。
11. 预览总结,确认后替换反馈输入框。
12. 重试失败转录、重新转录、修改录音标题和删除录音。
13. 在退出页面、应用切后台或请求重试后恢复可恢复的任务状态。
### 3.2 首版明确不做
- 不做实时字幕和实时说话人分离。
- 不自动断言“Speaker 0 就是教师”。
- 不做人脸、声纹实名识别或跨用户声纹库。
- 不在不同录音之间自动确认同一个自然人。
- 不做多人协作编辑、公开分享或外链下载。
- 不做音频裁剪、拼接、降噪参数编辑等专业音频编辑器。
- 不做波形级标注工作台;首版使用时间轴和播放进度定位。
- 不删除现有 `lesson_sessions``audio_segments` 或旧总结来源表。
- 不在没有代表性样本验证的情况下升级 FunASR 或切换识别模型。
- 不在首版启用自动到期删除,除非用户另行确认保留规则。
### 3.3 后续可选能力
- 用户手工修订转录文本并保留修订历史。
- 针对固定教师的声纹登记和跨录音角色建议。
- 自动提取课堂主题、作业、错误点和后续建议。
- 录音标签、收藏、批量归档和导出。
- 对象存储替代本地卷,以及分层存储和归档。
- Web 管理端或桌面端长文本校对工作台。
## 4. 信息架构与页面计划
### 4.1 页面边界
计划新增以下页面:
| 页面 | 路径建议 | 职责 |
|---|---|---|
| 录音资料列表 | `pages/recordings/index` | 管理、筛选、批量选择、录音和上传入口 |
| 录音详情 | `pages/recordings/detail` | 播放、说话人映射、对话浏览、选择、重试和删除 |
| 反馈素材选择 | `pages/recordings/select` | 从反馈页进入,选择录音或对话片段并返回反馈草稿 |
弹窗只用于:
- 删除录音确认。
- 重新转录确认。
- 总结覆盖输入框确认。
- 简短标题修改。
- 说话人显示名的单字段输入。
不得使用弹窗承载完整录音列表、长对话浏览或批量选择。
### 4.2 底部导航决策
产品目标上,“录音资料”是一级能力。实现前进行一次 5 项底部导航真机评审:
```text
反馈生成 | 录音资料 | 学生档案 | 反馈记录 | 我的
```
通过条件:
- 320px 宽度真机上文字和图标不截断。
- 各入口点击区域满足现有小程序交互尺寸。
- 当前 tab 状态不会因从选择页返回而错乱。
若 5 项导航不通过,首版不强行增加 tab改为在反馈页和“我的”页面提供录音资料入口页面和后端边界保持不变后续再调整导航。
### 4.3 录音资料列表页
页面结构:
```text
标题与状态统计
筛选栏:学生 / 日期 / 来源 / 状态
录音列表
- 标题和日期
- 学生或未关联
- 现场录音或上传文件
- 时长、说话人数、字数
- 转录状态和失败操作
底部主操作:录音或上传
```
交互要求:
- 内容列表是主区域,工具栏保持紧凑,不使用大面积装饰卡片。
- 使用稳定高度的列表行,状态文字变化不能引起明显布局跳动。
- 支持下拉刷新和游标分页,不一次加载全部历史录音。
- 处理中项目按更新时间刷新;就绪项目不进入高频轮询。
- 批量模式下显示已选数量和一个明确的主操作。
- 失败项目提供“重试”和“删除”,不使用含糊的通用“处理”按钮。
- 删除入口必须显示是否会影响已生成反馈;历史总结只保留快照,不依赖原录音继续存在。
### 4.4 录音详情工作页
页面结构:
```text
顶部导航:返回 / 标题 / 更多
紧凑播放器:播放、暂停、进度、当前时间、总时长
说话人映射栏Speaker 0 -> 教师Speaker 1 -> 学生
对话工具栏:全选、按说话人筛选、仅看已选
对话主区域:
[复选框] [00:12-00:18] [教师] 对话文本
[复选框] [00:19-00:27] [学生] 对话文本
固定底栏:已选 N 段 / 加入反馈或生成总结
```
交互要求:
- 对话内容占据主要视觉面积,播放器和筛选工具保持安静、紧凑。
- 点击时间范围后从对应位置播放。
- 播放进度变化只更新当前行状态,不重新渲染完整对话列表。
- 选择状态由 `turn_id` 管理,不能依赖数组位置。
- 支持按说话人全选和取消,结果仍展开为具体 `turn_id` 提交。
- 角色映射修改不改写历史总结快照。
- 转录失败时不渲染空工作台,直接显示失败原因、重试和删除。
- 说话人数量异常时允许保留“未知”角色,不强制映射为教师或学生。
### 4.5 反馈素材选择页
从反馈页打开,必须携带目标 `feedback_session_id`。该页复用录音列表和对话选择组件,但使用独立路由,以避免 tab 页面承担临时返回状态。
支持两种选择方式:
1. 整份录音:服务端解析为该录音当前就绪转录版本的全部对话。
2. 部分对话:提交明确的 `turn_id` 集合。
返回反馈页前显示:
- 录音数量。
- 对话片段数量。
- 字符数估算。
- 选择范围中是否包含“未知说话人”。
选择草稿以 `feedback_session_id` 为键暂存在小程序本地存储,页面返回或小程序短时退出后可恢复;提交总结时服务端重新校验所有权、状态和转录版本。
### 4.6 反馈生成页调整
现有展开式“语音记录”区域改为紧凑的素材区:
- `开始录音`
- `从录音资料选择`
- `已选 2 个录音 / 18 段 / 1260 字`
- `查看选择`
- `生成总结`
- 总结状态和预览。
现有按整节加入和总结能力在兼容期保留;新能力开关启用后,页面优先走录音资料选择链路。确认稳定后再删除旧前端入口,后端旧接口继续保留一个发布周期。
### 4.7 页面状态矩阵
| 状态 | 列表页 | 详情页 | 可用操作 |
|---|---|---|---|
| 上传中 | 进度或等待 | 不进入对话区 | 取消 |
| 排队中 | 排队状态 | 音频可播放 | 删除 |
| 转录中 | 处理中 | 音频可播放 | 删除 |
| 待确认 | 显示说话人数 | 对话可查看和映射 | 映射、选择、完成确认 |
| 已就绪 | 正常状态 | 全部能力 | 选择、总结、重转录、删除 |
| 失败 | 错误分类 | 错误详情 | 重试、删除 |
| 删除中 | 从正常列表隐藏 | 禁止进入 | 后台重试清理 |
## 5. 目标领域模型
### 5.1 设计原则
- 录音资产由用户拥有,反馈会话只引用素材,不拥有素材。
- 原始音频、转录运行、说话人和对话片段职责分离。
- 每次重新转录创建新版本,不原地覆盖已用于总结的版本。
- 总结来源在任务创建时展开并快照worker 不读取会变化的最新文本。
- 用户删除录音后,历史总结仍可解释其来源数量和快照,但不能再播放音频。
- 所有查询都显式包含 `owner_id` 边界,不依赖前端传入的用户标识。
- 删除必须同时覆盖数据库引用和物理文件,不再依赖数据库级联完成文件清理。
### 5.2 关系图
```text
users
|
+-- recordings
|
+-- recording_audio_parts
|
+-- transcription_runs
|
+-- recording_speakers
|
+-- transcript_turns
feedback_sessions
|
+-- feedback_summary_runs
|
+-- feedback_summary_run_sources
-> nullable transcript_turn reference
-> immutable text/speaker/time snapshot
```
### 5.3 `recordings`
职责:一份逻辑录音资产,不等同于某个 8 分钟物理片段。
建议字段:
| 字段 | 约束与用途 |
|---|---|
| `id UUID` | 主键 |
| `owner_id UUID` | 非空,引用 `users ON DELETE RESTRICT`;账号删除前必须完成受控录音清理 |
| `profile_id UUID` | 可空,引用学生档案;学生删除时建议 `SET NULL`,不连带删除录音 |
| `title VARCHAR(120)` | 非空,服务端生成默认标题,允许用户修改 |
| `source VARCHAR(16)` | `live``upload``legacy` |
| `recorded_at TIMESTAMPTZ` | 课堂或文件代表的录制时间 |
| `ingest_status VARCHAR(16)` | `uploading``complete``failed` |
| `duration_ms BIGINT` | 非负,所有 part 汇总 |
| `byte_size BIGINT` | 非负,所有 part 汇总 |
| `audio_format VARCHAR(8)` | 逻辑主格式,混合 part 时使用规范化输出格式 |
| `current_transcription_run_id UUID` | 可空,指向当前展示版本,扩展迁移后补 FK |
| `legacy_lesson_id UUID` | 可空且唯一,仅用于迁移追踪,后续可保留 |
| `delete_requested_at TIMESTAMPTZ` | 可空,存在时从正常列表隐藏 |
| `created_at/updated_at` | 非空 |
关键约束与索引:
- `CHECK (char_length(title) BETWEEN 1 AND 120)`
- `CHECK (source IN ('live', 'upload', 'legacy'))`
- `CHECK (ingest_status IN ('uploading', 'complete', 'failed'))`
- `CHECK (duration_ms >= 0 AND byte_size >= 0)`
- 列表索引:`(owner_id, created_at DESC, id DESC) WHERE delete_requested_at IS NULL`
- 学生筛选索引:`(owner_id, profile_id, created_at DESC, id DESC) WHERE delete_requested_at IS NULL`
- `legacy_lesson_id` 唯一索引仅覆盖非空值。
- `current_transcription_run_id` 不能只使用单列 FK应以
`(id, current_transcription_run_id)` 复合引用
`transcription_runs(recording_id, id)`,保证当前 run 一定属于当前 recording。
### 5.4 `recording_audio_parts`
职责:保存现场录音切片或上传文件经过规范化后的物理音频单元。
建议字段:
| 字段 | 约束与用途 |
|---|---|
| `id UUID` | 主键 |
| `recording_id UUID` | 非空,引用 `recordings ON DELETE RESTRICT` |
| `sequence INTEGER` | 从 1 开始,录音内唯一 |
| `duration_ms INTEGER` | 大于 0 |
| `byte_size BIGINT` | 大于 0 |
| `audio_format VARCHAR(8)` | `mp3``aac``wav``m4a``amr` |
| `storage_key TEXT` | 非空,只存内部相对键,不向客户端返回 |
| `sha256 CHAR(64)` | 非空,用于上传幂等和文件核验 |
| `created_at` | 非空 |
关键约束与索引:
- `UNIQUE (recording_id, sequence)`
- `UNIQUE (storage_key)`
- `CHECK (sequence > 0 AND duration_ms > 0 AND byte_size > 0)`
-`recording_id` 建普通索引,支持删除和顺序读取。
### 5.5 `transcription_runs`
职责:一次可审计、可重试、不可原地覆盖的转录版本。
建议字段:
| 字段 | 约束与用途 |
|---|---|
| `id UUID` | 主键 |
| `recording_id UUID` | 非空,引用 `recordings` |
| `revision INTEGER` | 从 1 开始,录音内唯一 |
| `status VARCHAR(16)` | `queued``processing``ready``failed``superseded` |
| `provider VARCHAR(32)` | 例如 `local-funasr` |
| `asr_model TEXT` | 实际模型标识 |
| `speaker_model TEXT` | 可空,实际说话人模型标识 |
| `config_version VARCHAR(32)` | 转录配置版本 |
| `source_hash CHAR(64)` | 输入 part 顺序、hash 和配置的摘要 |
| `attempts INTEGER` | 非负 |
| `processing_started_at` | 租约和恢复 |
| `completed_at` | 就绪或失败完成时间 |
| `error_class VARCHAR(64)` | 稳定错误分类 |
| `error_message TEXT` | 面向用户的脱敏错误信息 |
| `created_at/updated_at` | 非空 |
关键约束与索引:
- `UNIQUE (recording_id, revision)`
- `UNIQUE (recording_id, id)`,供 recording 当前 run 的复合 FK 使用。
- `UNIQUE (recording_id, source_hash)`,相同输入和配置重复请求返回既有 run。
- 队列部分索引:`(created_at, id) WHERE status IN ('queued', 'processing')`
- 状态结果约束保证 `ready``completed_at` 非空,`failed``error_class` 非空。
### 5.6 `recording_speakers`
职责:保存某次转录运行产生的匿名说话人簇及用户业务映射。
建议字段:
| 字段 | 约束与用途 |
|---|---|
| `id UUID` | 主键 |
| `transcription_run_id UUID` | 非空,引用 `transcription_runs` |
| `provider_label VARCHAR(32)` | 例如 `SPK0`run 内唯一 |
| `role VARCHAR(16)` | `teacher``student``guardian``unknown``other` |
| `display_name VARCHAR(60)` | 可空,例如“学生甲” |
| `created_at/updated_at` | 非空 |
约束:
- `UNIQUE (transcription_run_id, provider_label)`
- `UNIQUE (transcription_run_id, id)`,供 turn 的复合 FK 使用。
- 映射只改变业务显示,不修改模型原始 `provider_label`
- 首次生成时全部为 `unknown`,不得自动写入 `teacher`
### 5.7 `transcript_turns`
职责:一条连续说话人对话,是播放定位和选择的最小单位。
建议字段:
| 字段 | 约束与用途 |
|---|---|
| `id UUID` | 主键 |
| `transcription_run_id UUID` | 非空,引用 `transcription_runs` |
| `speaker_id UUID` | 非空,引用 `recording_speakers` |
| `sequence INTEGER` | 从 1 开始run 内唯一 |
| `start_ms BIGINT` | 非负 |
| `end_ms BIGINT` | 大于 `start_ms` |
| `text TEXT` | 非空且去除首尾空白后有内容 |
| `created_at` | 非空 |
约束与索引:
- `UNIQUE (transcription_run_id, sequence)`
- `(transcription_run_id, speaker_id)` 复合引用
`recording_speakers(transcription_run_id, id)`,禁止 turn 引用其他 run 的 speaker。
- `CHECK (start_ms >= 0 AND end_ms > start_ms)`
- `CHECK (char_length(btrim(text)) > 0)`
- `(transcription_run_id, start_ms, id)` 支持时间排序。
- `(speaker_id, start_ms, id)` 支持按说话人筛选。
首版不允许直接编辑 `text`。若后续增加校对,新增修订表或明确的 `edited_text` 和审计字段,不在本次建表中预留含糊字段。
### 5.8 `feedback_summary_run_sources`
职责:冻结 AI 实际接收的来源,保证重跑、删除和转录升级后仍可审计。
建议字段:
| 字段 | 约束与用途 |
|---|---|
| `summary_run_id UUID` | 引用 `feedback_summary_runs` |
| `source_order INTEGER` | 从 1 开始run 内唯一 |
| `recording_id UUID` | 可空,删除录音后 `SET NULL` |
| `transcript_turn_id UUID` | 可空,删除转录后 `SET NULL` |
| `recording_title_snapshot TEXT` | 非空 |
| `speaker_label_snapshot TEXT` | 非空 |
| `start_ms_snapshot BIGINT` | 非负 |
| `end_ms_snapshot BIGINT` | 大于开始时间 |
| `text_snapshot TEXT` | 非空,是 worker 唯一读取的正文 |
| `created_at` | 非空 |
主键建议为 `(summary_run_id, source_order)`。创建总结任务时在同一事务中:
1. 校验所有录音属于当前 owner。
2. 校验所有 turn 属于对应录音的当前就绪 run。
3. 将整份录音选择展开为 turn。
4. 按录音时间和 turn 时间排序。
5. 写入快照。
6. 计算 source hash。
7. 创建或复用幂等总结任务。
worker 只读取 `text_snapshot`,不得再次 join 最新 `transcript_turns.text`
## 6. 录音和转录状态机
### 6.1 录音采集状态
```text
uploading
| complete upload/finalize
v
complete
uploading --validation/storage error--> failed
failed --retry upload--> uploading
```
规则:
- `complete` 后 part 集合默认不可变;补传必须在 finalize 前完成。
- finalize 是幂等操作,重复调用返回同一结果。
- live 录音停止后只有全部 part 成功上传才允许 finalize。
- 上传文件校验失败不得创建可见的半成品录音;失败记录只用于恢复时才保留。
### 6.2 转录运行状态
```text
queued -> processing -> ready
| |
| +-> failed
+------------> failed
ready --create new revision--> new queued run
old ready --new run promoted--> superseded
```
规则:
- worker 使用 `FOR UPDATE SKIP LOCKED` 和租约领取任务。
- 进程异常后超过租约的 `processing` 可重新领取。
- 只有完整写入 speakers 和 turns 后才能把 run 标为 `ready` 并切换 `current_transcription_run_id`
- speakers、turns、run ready 和 current pointer 在一个数据库事务中提交。
- 新 run 失败时旧 ready run 仍保持当前展示版本。
- 重试同一个 failed run 可增加 attempts用户主动“重新转录”创建新 revision。
### 6.3 删除状态
删除采用“先隐藏、后物理清理”:
1. DELETE 接口校验 owner 并写入 `delete_requested_at`
2. 列表和详情立即隐藏该录音。
3. cleanup worker 删除所有物理文件。
4. 删除 speakers、turns、runs、parts 和 recording 行。
5. summary source 的快照保留,外键设空。
6. 文件删除失败时保留待清理记录并指数退避重试。
不得先删数据库行再尝试删除文件,否则会再次制造不可追踪孤儿文件。
## 7. 音频存储与播放
### 7.1 存储键
新文件使用:
```text
/data/audio/{owner_uuid}/{recording_uuid}/{part_uuid}.{format}
```
要求:
- 路径不包含学生姓名、上传原始文件名或业务标题。
- 数据库只保存相对 `storage_key`;运行时在受控根目录下解析。
- 写入临时文件后执行原子 rename数据库只引用完成文件。
- 校验解析后路径仍位于 `AUDIO_STORAGE_DIR`,防止目录穿越。
- 上传原始文件名只在确有 UI 需求时保存为脱敏元数据,默认不保存。
### 7.2 上传与格式规范化
现有 15 MB 内存读取方式不适合长文件上传。新接口必须流式写磁盘:
- 请求体按块写临时文件,不能一次读入 `Vec<u8>`
- 同步计算 SHA-256 和字节数。
- 使用文件头和解码结果验证格式,不能只信扩展名和 MIME。
- 通过 ffmpeg/ffprobe 获取真实时长、采样率和声道数。
- 超限立即停止写入并清理临时文件。
- 首版支持 `mp3``aac``wav``m4a``amr`,以实际 ffmpeg 镜像能力为准。
初始限制建议:
- 单文件最大 200 MiB。
- 单份录音最大 4 小时。
- 单用户同时上传 1 个文件。
- 限制值通过环境变量配置,并在 5700U 压测后确认。
如果微信小程序文件选择或上传通道的真机限制低于服务端限制,前端显示实际可选范围;不得用健康检查代替真机上传验证。
### 7.3 认证播放
新增 `GET /api/v1/recordings/{recording_id}/audio`
- 必须认证并校验 owner。
- 支持 HTTP Range、`206 Partial Content``Content-Range` 和正确 MIME。
- 不返回磁盘路径和永久公开 URL。
- 不在日志中记录 Range 内容、标题、文件名或路径。
- 对已请求删除或文件校验失败的录音返回稳定错误。
- 播放请求不触发转录或改变录音状态。
### 7.4 保留、容量和孤儿治理
首版默认保留到用户主动删除。上线前完成:
- 音频卷每日容量统计。
- 每用户录音数、总时长和总字节统计。
- 数据库 part 与磁盘文件双向巡检。
- 孤儿文件仅报告,不在首轮自动删除。
- 确认备份是否同时覆盖 PostgreSQL 和音频卷,以及二者恢复时间点如何对应。
自动保留策略后续通过配置和用户确认实现,不能仅靠 cron 删除文件。
## 8. FunASR 说话人分离技术计划
### 8.1 官方能力与本项目风险
FunASR 官方仓库当前说明 Paraformer 可组合 `spk_model`,模型列表包含 CAM++;官方教程还给出了 ERes2NetV2 说话人模型和带 speaker 的 ASR 示例。官方当前接口可返回句子级时间和说话人信息。
本项目不能直接据此上线,原因是:
- 仓库固定 `funasr==1.3.14`,官方 `main` 文档可能对应更高版本。
- 当前服务只读取结果中的 `text`,没有验证 `sentence_info` 契约。
- 当前 8 分钟片段分别转录,匿名 speaker 编号可能跨片段互换。
- 教室噪声、重叠说话、远场麦克风和多学生会显著影响 diarization。
- 增加 speaker 模型会改变 CPU、内存、模型缓存和推理耗时。
### 8.2 技术预研候选
候选 A当前 Paraformer + FSMN-VAD + CT-PUNC + CAM++。
- 优点官方快速示例直接支持CAM++ 参数量较小,适合作为 CPU 首选候选。
- 风险:短句、重叠语音和跨片段聚类质量必须实测。
候选 B当前 Paraformer + FSMN-VAD + CT-PUNC + ERes2NetV2。
- 优点:官方教程将其作为改进的短时说话人特征候选。
- 风险CPU 成本、FunASR 固定版本兼容性和模型缓存增加必须实测。
候选 C独立 diarization pipeline。
- 只有 A、B 未达到质量门槛时评估。
- 不在首轮同时引入第二套大型框架,避免镜像、许可和运维复杂度失控。
### 8.3 预研实施步骤
1. 建立不提交真实敏感音频的本地测试清单和标注格式。
2. 在独立分支为 `asr-service` 增加实验开关,不修改生产默认值。
3. 先测试固定 `funasr==1.3.14` 是否支持所需 `spk_model` 和输出字段。
4. 若固定版本不支持,再验证最小兼容升级,并记录依赖 diff 和模型版本。
5. 对相同音频分别运行无 speaker、CAM++、ERes2NetV2。
6. 保存脱敏指标,不保存或提交真实转录正文。
7. 在 5700U 单进程 CPU 环境测量 RTF、峰值内存、缓存大小和稳定性。
8. 选择通过门槛的候选;均不通过时停止正式开发并重新评估方案。
### 8.4 测试语料矩阵
至少包含经授权的以下场景:
| 场景 | 最低样本 |
|---|---:|
| 两人、安静、轮流说话 | 3 |
| 两人、真实教室噪声 | 3 |
| 三人及以上 | 2 |
| 明显重叠说话 | 2 |
| 跨 8 分钟切片同一说话人 | 2 |
| 上传的 mp3/m4a/wav | 每种至少 1 |
| 30 分钟以上长录音 | 2 |
需要人工标注说话人区间和参考文本,才能计算说话人错误率和识别退化。
### 8.5 预研通过门槛
以下门槛为首轮工程门槛,样本不足时不得宣称达标:
- 所有支持格式无崩溃、无内存持续增长、无空结构化结果。
- 两人安静样本的中位 DER 不高于 15%。
- 两人真实教室样本的中位 DER 不高于 25%。
- 跨 8 分钟边界的同人 cluster 一致率不低于 90%。
- 加入 speaker 模型后,中文转录错误率相对当前基线恶化不超过 5 个百分点。
- 5700U 上 P95 RTF 不高于 0.5,即 1 小时音频在 30 分钟内处理完成。
- 单 FunASR 进程峰值内存不超过 12 GiB且连续 20 次任务后不持续增长。
- 失败必须返回稳定错误分类,不能导致 API 或 worker 重启循环。
如质量达标但性能未达标,应保持异步处理并评估独立 speaker worker不得简单提高同一模型实例并发。
### 8.6 跨切片说话人一致性
首选方案是把一份逻辑录音的 part 按顺序规范化并作为一次完整 diarization 输入,使 speaker cluster 在录音级生成。不得把每个 8 分钟 part 的 `SPK0` 直接视为同一人。
若长音频一次处理超出内存或模型限制,再实现:
1. 每个 part 提取 speaker embedding。
2. 在 recording 级对相邻 part 的 cluster 做相似度匹配。
3. 设置信心阈值,低信心时生成新的匿名 speaker。
4. 保存原始 provider label 和合并后的 recording speaker不覆盖原始输出。
该备选方案必须单独评审,不能在首版临时拼接字符串完成。
### 8.7 ASR 服务响应契约
新响应建议:
```json
{
"text": "完整转录文本",
"duration_ms": 480000,
"request_id": "uuid",
"model": "paraformer-zh",
"speaker_model": "cam++",
"speakers": [
{ "key": "SPK0" },
{ "key": "SPK1" }
],
"turns": [
{
"sequence": 1,
"start_ms": 1200,
"end_ms": 6800,
"speaker_key": "SPK0",
"text": "今天先检查上节课的内容。"
}
]
}
```
契约要求:
- `turns` 按时间排序。
- 时间统一使用整数毫秒。
- `speaker_key` 必须能在 `speakers` 中找到。
- 相邻且同 speaker、间隔很短的句子可按明确规则合并但必须保持时间有序。
- 无法分离时返回一个 `SPK0`,而不是伪造多说话人。
- `text` 是 turns 文本的兼容聚合,不是新的独立事实来源。
- Rust 必须校验时间范围、顺序、空文本和 speaker 引用,再写数据库。
## 9. API 设计
### 9.1 录音列表
`GET /api/v1/recordings`
查询参数:
- `cursor`:基于 `(created_at, id)` 的不透明游标。
- `limit`:默认 20最大 100。
- `profile_id`:可选。
- `source``live``upload`
- `status`API 派生状态。
- `date_from/date_to`:可选。
响应只包含列表需要的字段,不返回 turns 和 storage key。
### 9.2 创建和上传
`POST /api/v1/recordings`
- 创建逻辑录音,参数包含 `source`、可选 `profile_id`、标题和 `recorded_at`
- 支持 `Idempotency-Key`,防止页面重试创建重复录音。
`POST /api/v1/recordings/{id}/parts`
- multipart 流式上传。
- 参数包含 `sequence``duration_ms``audio_format``audio`
- `(recording_id, sequence)` 唯一;相同 hash 重试返回原 part不同 hash 冲突返回 409。
`POST /api/v1/recordings/{id}/complete`
- 校验 part 连续、文件存在、时长和大小一致。
- 原子地把 ingest 标为 complete 并创建首个 transcription run。
- 重复调用幂等。
上传单文件在客户端表现为一个 recording服务端可按解码和 ASR 限制生成内部 part但不把内部切分暴露为多份用户录音。
### 9.3 详情和播放
- `GET /api/v1/recordings/{id}`:录音元数据、当前 run、speakers 和分页/分块 turns。
- `GET /api/v1/recordings/{id}/turns?cursor=&speaker_id=`:长对话分页。
- `GET /api/v1/recordings/{id}/audio`:认证 Range 播放。
- `PATCH /api/v1/recordings/{id}`:修改标题、学生关联和录制日期。
- `DELETE /api/v1/recordings/{id}`:进入删除队列。
### 9.4 说话人映射和重转录
- `PATCH /api/v1/recordings/{recording_id}/speakers/{speaker_id}`:更新 role 和 display name。
- `POST /api/v1/recordings/{id}/transcription-runs`:创建新 revision。
- `POST /api/v1/transcription-runs/{id}/retry`:重试 failed run。
角色更新使用乐观并发字段或 `updated_at` 前置条件,避免两个页面互相覆盖。
### 9.5 直接加入反馈
`POST /api/v1/feedback-sessions/{session_id}/recording-sources/apply`
建议请求:
```json
{
"mode": "append",
"selections": [
{ "recording_id": "uuid", "selection": "all" },
{
"recording_id": "uuid",
"selection": "turns",
"turn_ids": ["uuid", "uuid"]
}
]
}
```
服务端负责:
- 校验 session active、owner、run ready 和 turn 归属。
- 按时间顺序格式化带角色的文本。
- 检查合并后不超过 2000 字。
- 在一个事务中更新反馈草稿和记录来源应用状态。
- 返回更新后的 `FeedbackSession`
### 9.6 创建总结任务
扩展现有 summary run 请求,使用同一 `selections` 结构。服务端在创建 run 的事务中展开并写入 `feedback_summary_run_sources`
兼容策略:
- 新请求写 `feedback_summary_run_sources`
-`lesson_ids` 请求继续写 `feedback_summary_run_lessons`
- summary worker 优先读取新 sources没有新 sources 时走旧 lesson 查询。
- 新旧请求都保留现有幂等键和 2000 字结果约束。
新总结的来源和模型预算规则:
- 单次最多选择 20 份录音。
- 单次最多展开 2000 个 turn。
- source snapshot 总字符数初始上限为 200,000通过环境变量配置。
- 超限时返回明确的 `summary_source_too_large`,不能静默截断或丢弃尾部 turn。
- worker 按录音和时间顺序格式化来源,例如
`[00:12-00:18][教师] 对话文本`,匿名角色使用 `Speaker 0` 等稳定标签。
- 每份录音先按不拆分 turn 的原则组成有限字符块,再生成录音级摘要,最后结合
`existing_content` 生成不超过 2000 字的总反馈。
- `prompt_version` 升级为新的明确版本;旧 run 继续保留原版本。
- 转录文本按不可信输入处理,使用清晰分隔符包裹,并在系统提示中明确禁止执行转录内容中的指令。
- 记录 source turn 数、source 字符数、模型请求次数和输出字符数,但不记录正文。
### 9.7 错误语义
至少定义稳定错误分类:
- `recording_not_ready`
- `recording_upload_incomplete`
- `recording_audio_missing`
- `recording_format_unsupported`
- `recording_too_large`
- `recording_duration_exceeded`
- `transcription_unavailable`
- `transcription_failed`
- `diarization_failed`
- `speaker_mapping_conflict`
- `transcript_version_changed`
- `summary_source_unavailable`
- `audio_storage_delete_failed`
HTTP 状态和用户提示在 API 文档中逐项映射,前端不能依赖错误字符串包含关系判断业务状态。
## 10. 后端模块拆分
避免继续扩大单个 `recording_routes.rs`,建议按职责拆分:
```text
server/src/recordings/
mod.rs
routes.rs # 列表、详情、元数据、删除
upload.rs # 流式上传、格式校验、finalize
playback.rs # owner 校验和 Range 响应
selection.rs # 选择展开、格式化和快照
server/src/transcription_worker.rs
server/src/recording_cleanup_worker.rs
server/src/summary_worker.rs
server/src/speech.rs
```
拆分原则:
- 不为每条 SQL 创建无意义 repository trait。
- 跨路由复用的 owner 校验、选择展开和存储键解析提取为明确函数。
- 网络调用不放在数据库事务中。
- 上传文件写入和数据库提交之间保留可恢复状态。
- worker 状态转换使用条件 UPDATE防止重复 worker 覆盖完成结果。
### 10.1 预计文件变更矩阵
| 文件或目录 | 计划变更 |
|---|---|
| `server/migrations/0006_recording_library.sql` | 新录音、run、speaker、turn 和 summary source 表 |
| `server/src/recordings/` | 列表、上传、播放、选择和删除路由 |
| `server/src/routes.rs` | 合并新 router健康能力字段和 OpenAPI 注册 |
| `server/src/main.rs` | cleanup worker、配置和共享状态接线 |
| `server/src/config.rs` | 功能开关、上传限制、speaker 模型和 cleanup 配置 |
| `server/src/transcription_worker.rs` | 新 run 领取、结构化结果事务写入和版本切换 |
| `server/src/recording_cleanup_worker.rs` | 物理文件清理、重试和孤儿巡检 |
| `server/src/summary_worker.rs` | 新 source snapshot 读取和分层总结,保留旧来源兼容 |
| `server/src/speech.rs` | 解析 speakers/turns兼容纯文本响应 |
| `server/src/bin/migrate-recording-library.rs` | legacy backfill 和一致性报告 |
| `server/API.md``server/API_GUIDE.md` | 新接口、错误语义和示例 |
| `asr-service/app.py` | speaker 模型、结构化响应、长音频处理和校验 |
| `asr-service/requirements.txt` | 只在阶段 0 证明必要时锁定兼容升级 |
| `asr-service/README.md` | 模型、环境变量、响应和性能说明 |
| `asr-service/Dockerfile` | speaker 模型所需依赖和 ffmpeg 能力核验 |
| `pages/recordings/` | 列表、详情和选择页面 |
| `components/recording-*` | 经确认的列表行、播放器和 turn 复用组件 |
| `pages/feedback/index.*` | 精简语音区域并接入录音素材选择 |
| `utils/types.ts``utils/api.ts` | 新类型、API 和上传认证恢复 |
| `app.json``custom-tab-bar/` | 页面注册tab 仅在真机决策通过后修改 |
| `tests/` | 选择、状态和前端恢复测试 |
| `DEBUG_GUIDE.md``DEPLOY_5700U.md` | 调试、部署、模型缓存和验收更新 |
| `docs/observability-runbook.md` | 新事件、查询和故障定位 |
## 11. 小程序实现计划
### 11.1 类型和 API
`utils/types.ts` 增加:
- `RecordingSummary`
- `RecordingDetail`
- `RecordingSpeaker`
- `TranscriptTurn`
- `TranscriptionRun`
- `RecordingSelection`
- `RecordingListPage`
`utils/api.ts` 增加对应请求和 snake_case 映射。上传复用认证刷新逻辑,必须覆盖 `wx.uploadFile` token 失效重试和幂等键。
### 11.2 可复用组件
只为真实复用点增加组件:
- `components/recording-list-item`:列表页和选择页复用。
- `components/transcript-turn`:详情页和选择预览复用。
- `components/recording-player`:详情页复用,封装播放和时间跳转。
筛选栏和固定底栏先保留为页面局部结构,避免过早抽象。
### 11.3 性能要求
- turns 使用分页或分块加载,不一次 `setData` 数百条完整对象。
- 选择状态使用 ID 集合并只更新受影响行。
- 播放时间更新节流,不以 100ms 频率调用整页 `setData`
- 页面隐藏时停止播放进度更新和高频轮询。
- 只轮询 queued/processing runready/failed 停止轮询。
- 列表空态、失败态和加载骨架使用稳定尺寸,避免布局跳动。
### 11.4 可访问性和视觉约束
- 对话内容是详情页主要区域,工具栏和状态栏保持紧凑。
- 一个区域内只保留一个蓝色主操作。
- 使用白、石墨和浅灰中性色,不新增装饰渐变和重阴影。
- 控件圆角遵循当前小程序设计,不用夸张胶囊按钮。
- 播放、删除、重试等按钮提供明确 `aria-label`
- 说话人不能只靠颜色区分,同时显示角色文字。
- 长标题、长角色名和 2000 字边界在最小真机宽度验证。
## 12. 数据迁移与兼容发布
### 12.1 Expand
新增迁移建议从 `0006_recording_library.sql` 开始:
1. 创建 `recordings``recording_audio_parts``transcription_runs``recording_speakers``transcript_turns`
2. 创建 `feedback_summary_run_sources`
3. 创建所有 CHECK、UNIQUE、FK 和必要索引。
4. `recordings.current_transcription_run_id` 在 run 表创建后再添加 FK。
5. 不修改现有表的非空约束,不删除旧表或旧索引。
迁移必须在真实规模副本上测量锁时间。大表索引若不能在事务迁移中安全创建,应拆分为运维步骤并使用 PostgreSQL `CREATE INDEX CONCURRENTLY`
### 12.2 Backfill
新增可重复运行的 `migrate-recording-library` 二进制或受控迁移任务:
1. 每个 `lesson_session` 创建一个 `source='legacy'` recording。
2. `legacy_lesson_id` 唯一保证重复执行不重复创建。
3. 每个 `audio_segment` 创建一个 recording part先将现有绝对 `audio_path` 规范化并验证位于
`AUDIO_STORAGE_DIR` 内,再转换为相对 storage key。目录外路径只报告不自动导入。
4. ready segment 的纯文本转为一个 `unknown` speaker 下的 turn。
5. turn 的时间范围按 segment 顺序和 duration 累计估算,并标记为 legacy 导入,不伪装成模型级精确时间。
6. processing/failed segment 保留原状态映射,是否重转录由用户或后续任务决定。
7. 每批固定数量提交,输出游标、成功数、跳过数和失败数。
8. 回填不移动物理文件,避免大批量 I/O 和回滚困难。
一致性检查:
- legacy recording 数等于 lesson 数。
- part 数等于 audio segment 数。
- 每个 ready legacy segment 至少有一个非空 turn。
- duration 和 byte size 汇总无负值。
- 所有 storage key 可解析且文件存在,缺失文件单独报告。
### 12.3 双读和切换
1. API 上线新表和新接口,功能开关默认关闭。
2. 完成 backfill 并运行一致性报告。
3. 录音资料列表先只对测试账号开放。
4. 新现场录音写新表;兼容期如仍需旧页面显示,显式双写映射关系,不复制音频文件。
5. summary worker 支持新 sources 和旧 lessons 两条读取路径。
6. 小程序启用新页面和选择流程。
7. 观察一个完整发布周期后,停止旧前端创建 lesson 链路。
8. 旧 API 和表至少再保留一个发布周期。
### 12.4 Contract
只有满足以下条件才进入 contract
- 没有旧版本小程序仍在产生新 lesson 数据。
- 新录音、重试、删除、总结和回滚均通过生产验证。
- legacy 一致性报告无未解释差异。
- 已完成数据库和音频卷可恢复备份。
contract 阶段另建计划,不在本次直接安排删除 `lesson_sessions``audio_segments`
### 12.5 回滚
- 关闭 `RECORDING_LIBRARY_ENABLED`,反馈页恢复旧语音入口。
- 关闭 `ASR_DIARIZATION_ENABLED`,新转录回退纯文本模式。
- 关闭 `RECORDING_UPLOAD_ENABLED`,保留已上传数据但隐藏新上传入口。
- API 旧路由、旧 worker 查询和旧表保持可用。
- 新表保留,不在应用回滚时执行 down migration。
- 新录音数据不得自动反向写成不完整 lesson需要时提供显式兼容导出任务。
- 回滚过程中不删除任何音频文件。
## 13. 功能开关和配置
建议增加:
```env
RECORDING_LIBRARY_ENABLED=false
RECORDING_UPLOAD_ENABLED=false
ASR_DIARIZATION_ENABLED=false
ASR_SPEAKER_MODEL=cam++
MAX_RECORDING_UPLOAD_BYTES=209715200
MAX_RECORDING_DURATION_SECONDS=14400
RECORDING_CLEANUP_ENABLED=false
```
要求:
- 开关在健康接口中只返回启用状态,不返回敏感配置。
- 小程序根据 health capability 决定展示入口。
- speaker 模型变化必须更新 `config_version`,不能静默改变同一 run 的结果。
- cleanup 首次上线保持关闭,只运行 dry-run 巡检。
## 14. 可观测性计划
### 14.1 新事件
- `recording_created`
- `recording_part_uploaded`
- `recording_completed`
- `recording_upload_failed`
- `recording_playback_failed`
- `transcription_run_queued`
- `transcription_run_started`
- `transcription_run_finished`
- `transcription_run_failed`
- `speaker_mapping_updated`
- `recording_delete_requested`
- `recording_cleanup_finished`
- `recording_cleanup_failed`
- `recording_selection_applied`
- `summary_sources_snapshotted`
### 14.2 允许记录的字段
- `recording_id`
- `transcription_run_id`
- `feedback_session_id`
- `summary_run_id`
- `owner` 不记录真实 ID 之外的身份信息;需要统计时使用不可逆聚合方式。
- `status``error_class``attempt`
- `audio_size_bytes``audio_duration_ms``turn_count``speaker_count`
- `queue_wait_ms``inference_latency_ms``rtf``cleanup_latency_ms`
- `provider``asr_model``speaker_model``config_version`
禁止记录:
- 对话正文、总结正文和文件内容。
- 学生姓名、录音标题、说话人显示名。
- 原始文件名、绝对路径和 storage key。
- 上传请求体和第三方原始错误响应。
### 14.3 生产观察指标
- queued/processing run 数量和最老任务等待时间。
- 转录成功率、diarization 失败率和按 error_class 分布。
- P50/P95 RTF、内存峰值和音频时长分布。
- 每日上传字节、音频卷增长和剩余空间。
- cleanup 待处理数量、最老任务年龄和失败次数。
- 总结来源 turn 数、字符数和总结失败率。
- API 列表、详情、播放首字节和上传完成延迟。
## 15. 安全与隐私
录音和对话属于敏感教学数据,必须满足:
1. 每个录音、speaker、turn、播放和总结来源查询都通过 recording owner 反查授权。
2. 不接受前端传入 `owner_id` 作为授权依据。
3. 上传文件执行大小、时长、格式和解码验证。
4. ffmpeg 处理设置超时、资源上限和受控临时目录。
5. 临时文件在成功、失败、超时和进程恢复路径都能清理。
6. 播放接口不生成永久免认证 URL。
7. 删除操作提供清晰确认并记录稳定审计事件,不记录正文。
8. 数据库备份和音频卷备份使用相同的访问权限和恢复演练要求。
9. 产品界面明确录音用途和删除后果,遵循微信麦克风权限说明。
10. 不以说话人 embedding 建立跨录音身份库,除非后续完成单独隐私评审。
## 16. 分阶段实施与提交计划
每个阶段独立提交、可测试、可回滚。不得把数据库、ASR、全部页面和切换一次性合并为一个提交。
### 阶段 0基线和技术预研
交付:
- 代表性音频测试清单和本地标注格式。
- 固定 FunASR 版本的 CAM++/ERes2NetV2 兼容性结论。
- 5700U CPU 质量、RTF、内存和缓存报告。
- 最终 speaker 模型和输出契约决定。
建议提交:
- `test(asr): add diarization evaluation fixtures`
- `docs(asr): record speaker diarization benchmark`
退出条件:第 8.5 节门槛通过,或形成明确否决结论。
### 阶段 1数据和存储基础
交付:
- 新表、约束和索引。
- 流式 part 上传和 finalize。
- 新存储键、hash、时长和大小校验。
- 认证 Range 播放。
- cleanup worker 和 dry-run 孤儿报告。
建议提交:
- `feat(recordings): add recording library schema`
- `feat(recordings): stream audio parts to storage`
- `feat(recordings): add authenticated audio playback`
- `feat(recordings): add deferred audio cleanup`
退出条件:上传、播放、删除、失败恢复和 owner 隔离集成测试通过。
### 阶段 2结构化转录和说话人分离
交付:
- ASR 新响应契约。
- speaker 模型配置和健康信息。
- transcription run 版本、队列和原子写入 speakers/turns。
- 重试和重转录。
- 模型错误分类和指标。
建议提交:
- `feat(asr): return timestamped speaker turns`
- `feat(voice): persist versioned transcript turns`
- `fix(voice): recover expired diarization jobs`
退出条件:技术预研样本和端到端 DB 写入均通过。
### 阶段 3录音资料页面
交付:
- 列表 API 和游标分页。
- 详情、turn 分页和 speaker 映射 API。
- 录音列表页、详情页和播放器。
- 处理状态、失败重试、删除和角色映射。
建议提交:
- `feat(recordings): add recording library APIs`
- `feat(miniprogram): add recording library page`
- `feat(miniprogram): add transcript review workspace`
退出条件:真机可浏览、播放、映射、重试和删除录音。
### 阶段 4素材选择和总结
交付:
- 反馈素材选择页。
- 选择本地恢复。
- 直接加入反馈接口。
- summary source 快照。
- summary worker 新旧来源兼容。
- 反馈页精简和总结预览接入。
建议提交:
- `feat(feedback): select recording transcript turns`
- `feat(summary): snapshot selected transcript sources`
- `feat(feedback): compose feedback from recording library`
退出条件:未选中的 turn 不进入输入框或 AI 请求,历史总结不受重转录影响。
### 阶段 5历史回填和灰度切换
交付:
- 可重复运行的 legacy backfill。
- 一致性报告。
- 测试账号功能开关。
- 生产灰度、监控和回滚演练。
- 小程序导航最终决策。
建议提交:
- `feat(recordings): backfill legacy voice lessons`
- `chore(recordings): enable recording library rollout`
- `docs(recordings): add operations runbook`
退出条件:一个完整发布周期无高严重度数据或隐私问题。
## 17. 测试计划
### 17.1 Rust 单元测试
- 存储键只解析到允许目录。
- Range 请求边界、无效范围和完整下载。
- 选择展开排序、去重和所有权校验。
- 整份录音选择正确展开全部当前 turns。
- 2000 字限制和角色格式化。
- source hash 对顺序、文本和版本变化敏感。
- 状态机拒绝非法转换。
- 同一幂等键返回同一录音或总结任务。
### 17.2 PostgreSQL 集成测试
- 所有 CHECK、UNIQUE 和 FK 生效。
- 不同 owner 无法查询、播放、修改、选择或删除彼此录音。
- worker 并发领取不重复处理。
- ready run 原子切换 current pointer。
- 新 run 失败不覆盖旧 ready run。
- summary source 在录音删除后保留快照。
- cleanup 失败不会丢失待处理记录。
- backfill 重跑不产生重复 recording、part 或 turn。
- 游标分页无重复和遗漏。
### 17.3 Python/ASR 测试
- 无 speaker 模式保持当前响应兼容。
- speaker 模式返回合法 speakers 和 turns。
- 空语音、单人、多人、重叠语音和损坏音频。
- 所有支持格式和长音频。
- 临时文件在每条失败路径清理。
- 连续 20 次推理无内存持续增长。
- request ID 从 API 传播到 ASR 日志。
### 17.4 小程序测试
- 列表筛选、分页、刷新和空态。
- 上传成功、网络中断、认证刷新和重试。
- 播放、暂停、拖动、跳到 turn 时间。
- speaker 映射和按 speaker 全选。
- 跨页面返回后选择恢复。
- 后台转录完成后的状态刷新。
- 失败重试和删除确认。
- 直接加入超过 2000 字时不修改草稿。
- 总结 ready 后预览和替换输入框。
- 320px 宽度、常见安卓机和 iPhone 真机布局。
### 17.5 端到端验收场景
1. 现场录制 20 分钟两人对话,跨越至少两个 part。
2. 后台转录并保持同一说话人跨 part 一致。
3. 用户映射教师和学生角色。
4. 只选择学生的若干 turn 直接加入反馈。
5. 再选择两份录音生成总结。
6. 验证 AI 输入不包含未选 turn。
7. 预览后替换输入框并保存反馈。
8. 在录音库仍可查看录音;删除录音后历史反馈和总结仍存在。
9. 重启 API 和 FunASR 后任务恢复,模型不重复下载,音频仍存在。
### 17.6 常规校验命令
开发提交前至少执行:
```bash
npm test
cd server && cargo fmt --check
cd server && cargo test
cd server && cargo clippy --all-targets --all-features -- -D warnings
python3 -m py_compile asr-service/app.py asr-service/logging_config.py
docker compose -f compose.deploy.yml config
```
涉及 FunASR 依赖变更时还要在镜像内执行 `pip check` 和真实音频推理,不能只做 Python 语法检查。
## 18. 部署与发布计划
### 18.1 发布前
1. 备份远程 PostgreSQL并记录可恢复时间点。
2. 备份或快照 `teaching-feedback-audio` 卷。
3. 记录当前镜像 tag、Git SHA、模型缓存大小和健康状态。
4. 在生产规模副本验证迁移锁时间和 backfill 批次。
5. 确认新 speaker 模型缓存所需磁盘和首次下载时间。
6. 功能开关全部保持关闭。
### 18.2 发布顺序
1. 拉取已经测试并推送的提交。
2. 构建 API 和 FunASR 新镜像,不停止现有容器。
3. 运行 expand migration。
4. 启动新 API 和 FunASR保持新功能关闭。
5. 验证健康检查、旧录音链路和旧总结链路。
6. 运行 legacy backfill dry-run再执行正式 backfill。
7. 对测试账号启用录音资料和 diarization。
8. 完成真实录音、上传、播放、选择、总结和删除验收。
9. 扩大灰度范围。
10. 最后发布使用新入口的小程序版本。
### 18.3 回滚触发条件
出现以下任一情况立即关闭对应功能:
- 数据跨 owner 暴露或未授权播放。
- 新转录覆盖或丢失既有 ready 转录。
- 删除流程造成不可恢复的非目标音频删除。
- worker 重复处理导致队列持续增长。
- FunASR 容器内存持续增长或反复重启。
- speaker 输出结构错误导致批量任务失败。
- 新总结包含未选中的对话。
- 数据库迁移或查询导致生产明显锁等待和超时。
## 19. 验收标准
### 19.1 产品验收
- 用户可以独立进入录音资料,不依赖未保存反馈草稿。
- 现场录音和上传文件出现在统一列表。
- 用户可以播放、筛选、重试、重转录和删除本人录音。
- 详情页按时间和匿名说话人展示对话。
- 用户可以映射角色并按 turn 选择。
- 用户可以选择整份录音或多份录音生成总结。
- 总结可预览,确认后才写入反馈输入框。
- 录音保存、反馈保存和录音删除互不错误级联。
### 19.2 数据完整性验收
- recording、part、run、speaker、turn 数量关系满足约束。
- 所有 ready recording 都有 current ready run 和至少一个非空 turn。
- 所有 summary run 的新来源均有完整快照。
- legacy backfill 可重复执行且结果一致。
- 删除后无数据库悬挂 part失败删除仍有可重试记录。
- 孤儿巡检结果为零或每一项都有已记录处置。
### 19.3 安全验收
- 两个测试用户之间录音列表、详情、播放和选择完全隔离。
- 伪造 recording、speaker、turn ID 均返回 404 或等价资源不可见响应。
- 日志检索不到标题、学生姓名、转录文本、原始文件名和路径。
- Range 播放必须携带有效认证。
- 上传恶意扩展名、损坏文件和超限文件被拒绝且临时文件清理。
### 19.4 性能验收
- 录音列表 20 条 P95 API 延迟目标低于 300ms不含公网网络时间。
- 详情元数据 P95 低于 300msturn 分页 P95 低于 500ms。
- 音频 Range 首字节在局域网 P95 低于 500ms。
- 数据库查询计划使用 owner + 时间游标索引,无全量 offset 扫描。
- diarization 达到第 8.5 节质量和资源门槛。
- summary worker 在 100 份录音或大量 turns 输入时受明确字符/token 预算限制。
### 19.5 运维验收
- API、FunASR、migration 和 cleanup 状态可从结构化日志定位。
- 容器重启后 queued/processing 任务可恢复。
- 模型缓存和音频卷重启后保持。
- PostgreSQL 与音频卷完成一次关联恢复演练。
- 回滚开关和旧链路在生产实际演练成功。
## 20. 风险清单
| 风险 | 严重度 | 应对 |
|---|---|---|
| 说话人分离质量不足 | 高 | 技术预研门槛、匿名标签、用户映射、允许 unknown |
| 跨 8 分钟 speaker 编号互换 | 高 | 录音级 diarization禁止直接拼接 part 标签 |
| 数据库记录与文件不一致 | 高 | 受控删除队列、hash、巡检和恢复状态 |
| 跨用户音频泄露 | 阻断 | 每条接口 owner 反查、Range 集成测试 |
| 总结包含未选内容 | 高 | 事务内展开 turn 并快照worker 只读快照 |
| 大文件占满内存或磁盘 | 高 | 流式上传、限额、临时目录和磁盘告警 |
| FunASR 升级引入回归 | 高 | 固定版本优先、对照基线、独立镜像 tag 和回滚 |
| 新旧表迁移丢失历史 | 高 | expand/backfill/一致性报告,不提前 contract |
| 长对话导致小程序卡顿 | 中 | turn 分页、局部 setData、节流播放进度 |
| 五项 tab 拥挤 | 中 | 真机决策门槛,不通过则普通页面入口 |
| 自动角色识别误导用户 | 中 | 不自动断言角色,必须用户确认 |
| 存储长期增长 | 中 | 容量监控、配额、后续保留策略 |
## 21. 开工前需要确认的产品决定
以下决定有推荐默认值,不应在实现中隐式猜测:
| 决定 | 推荐默认值 |
|---|---|
| 录音保留期限 | 保留到用户主动删除 |
| 学生档案删除时录音处理 | 录音保留,`profile_id` 置空 |
| 说话人角色 | 教师、学生、家长、其他、未知,允许自定义显示名 |
| 是否自动认定教师 | 否 |
| 首版是否允许修改转录文本 | 否,只允许角色映射 |
| 上传限制 | 200 MiB、4 小时,完成真机和服务端验证后确认 |
| 录音删除后历史总结 | 保留来源快照,不再提供播放 |
| 底部导航 | 真机通过后增加“录音资料”,否则保留普通页面入口 |
| diarization 模型 | 预研后在 CAM++ 和 ERes2NetV2 中选择 |
## 22. Definition of Done
只有同时满足以下条件,本功能才算完成:
1. 本文档中首版目标均有对应实现或明确批准的范围变更。
2. 数据库迁移、backfill、API、worker、小程序和部署文档全部合并。
3. 自动测试、代表性音频评测和真机端到端测试通过。
4. owner 隔离、日志脱敏和删除恢复通过安全验收。
5. 5700U CPU 上 diarization 达到质量和资源门槛。
6. 生产完成灰度、监控、回滚和一次容器重启恢复验证。
7. API 文档、调试指南、部署指南和观测 runbook 已同步更新。
8. 所有提交使用仓库现有 Conventional Commit 风格并已推送。
9. 服务器可通过 `git pull --ff-only` 获取确定的 Git SHA 重新部署。
10. 未完成项、残余风险和生产开关状态在发布报告中明确列出。
## 23. 参考资料
项目内依据:
- `README.md`
- `DEBUG_GUIDE.md`
- `DEPLOY_5700U.md`
- `server/API.md`
- `server/migrations/0002_voice_feedback_sessions.sql`
- `server/migrations/0003_async_transcription_jobs.sql`
- `server/migrations/0005_feedback_summary_runs.sql`
- `server/src/recording_routes.rs`
- `server/src/transcription_worker.rs`
- `server/src/summary_worker.rs`
- `server/src/speech.rs`
- `asr-service/app.py`
- `pages/feedback/index.ts`
- `pages/feedback/index.wxml`
官方技术依据,均应在实际实现时锁定到使用版本对应的文档或提交:
- [FunASR 官方仓库与 Paraformer speaker model 示例](https://github.com/modelscope/FunASR)
- [FunASR 官方教程中的 Speaker Verification / Diarization](https://github.com/modelscope/FunASR/blob/main/docs/tutorial/README.md)
- [FunASR 官方 Paraformer 实现说明](https://github.com/modelscope/FunASR/blob/main/funasr/models/paraformer/model.py)
官方 `main` 分支能力不等于本项目固定 `funasr==1.3.14` 已验证能力;阶段 0 的固定版本兼容测试是正式开发的阻断门槛。