58 KiB
录音资料库、说话人分离与反馈素材编排开发计划
- 状态:待评审
- 编制日期:2026-07-23
- 适用仓库:
teaching-feedback-assistant - 目标部署:Ryzen 7 5700U、CPU FunASR、远程 PostgreSQL、Docker Compose
- 计划性质:产品、交互、数据、API、转录、迁移、测试和发布的统一实施基线
1. 执行摘要
本次改造不应继续把录音能力作为“反馈生成”页面中的一个折叠区域扩展。目标能力已经包含录音和文件上传、长期管理、音频播放、异步转录、说话人分离、角色映射、逐段选择、跨录音选择、AI 总结、总结预览和写入反馈输入框,实际边界是一个独立的“录音资料库”子系统。
最终产品职责划分如下:
现场录音 / 上传音频
|
v
录音资料库
|
v
转录 + 说话人分离
|
v
对话校对、角色映射、片段选择
/ \
v v
直接加入反馈输入框 多录音或片段交给 AI
|
v
总结预览
|
v
写入反馈输入框
本计划采用以下总体策略:
- 录音从
feedback_session的附属数据升级为用户拥有的独立资产。 - 反馈页面只负责编辑反馈、选择素材、预览总结和保存结果。
- 录音列表、录音详情和反馈素材选择使用独立页面,不以大弹窗承载主流程。
- 说话人分离只产生匿名说话人簇,教师、学生、家长等业务角色由用户确认。
- AI 总结使用选中对话的不可变快照,避免转录重跑或角色修改改变历史结果。
- 数据库按 expand、backfill、switch、contract 渐进迁移,旧链路在切换完成前始终可回退。
- FunASR 说话人能力先通过 5700U CPU 技术预研门槛,再决定 CAM++ 或 ERes2NetV2,不直接在生产中试错。
- 音频默认保留到用户主动删除;自动保留期限作为后续可配置能力,不在首版擅自清理历史录音。
2. 当前状态与约束
2.1 当前业务链路
当前链路为:
反馈草稿
-> 创建 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 当前结构性限制
lesson_sessions.feedback_session_id非空,录音不能脱离反馈草稿独立存在。- 录音查询、重试和删除接口只允许访问
active反馈批次。 - 保存反馈后批次变为
finalized,当前小程序没有独立入口重新管理其录音。 audio_segments.transcript只有纯文本,没有开始时间、结束时间、说话人或置信信息。- 当前总结来源表只关联整节
lesson_session,不支持逐段选择。 - Rust API 成功转录后保留原始音频,但没有认证播放接口和统一保留策略。
- 数据库级联删除和重复序号上传可能留下磁盘孤儿文件。
- 当前本地 ASR 响应只有
text、duration_ms和request_id。 - 腾讯备用实现明确关闭
speaker_diarization,不能承担当前目标。 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 首个完整版本目标
用户能够:
- 在录音资料库中查看现场录音和上传录音。
- 按学生、日期、来源和处理状态筛选录音。
- 查看录音时长、文件状态、转录状态、说话人数和失败原因。
- 播放本人录音,并从对话时间点跳转播放。
- 上传支持格式的音频文件并在后台完成转录。
- 查看按时间排序、按说话人分组标识的对话文本。
- 将匿名说话人映射为教师、学生、家长或自定义显示名。
- 选择整份录音、某位说话人的全部对话或若干对话片段。
- 将选择内容直接追加到反馈输入框,受 2000 字限制保护。
- 选择一份或多份录音的部分或全部对话生成 AI 总结。
- 预览总结,确认后替换反馈输入框。
- 重试失败转录、重新转录、修改录音标题和删除录音。
- 在退出页面、应用切后台或请求重试后恢复可恢复的任务状态。
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 项底部导航真机评审:
反馈生成 | 录音资料 | 学生档案 | 反馈记录 | 我的
通过条件:
- 320px 宽度真机上文字和图标不截断。
- 各入口点击区域满足现有小程序交互尺寸。
- 当前 tab 状态不会因从选择页返回而错乱。
若 5 项导航不通过,首版不强行增加 tab,改为在反馈页和“我的”页面提供录音资料入口;页面和后端边界保持不变,后续再调整导航。
4.3 录音资料列表页
页面结构:
标题与状态统计
筛选栏:学生 / 日期 / 来源 / 状态
录音列表
- 标题和日期
- 学生或未关联
- 现场录音或上传文件
- 时长、说话人数、字数
- 转录状态和失败操作
底部主操作:录音或上传
交互要求:
- 内容列表是主区域,工具栏保持紧凑,不使用大面积装饰卡片。
- 使用稳定高度的列表行,状态文字变化不能引起明显布局跳动。
- 支持下拉刷新和游标分页,不一次加载全部历史录音。
- 处理中项目按更新时间刷新;就绪项目不进入高频轮询。
- 批量模式下显示已选数量和一个明确的主操作。
- 失败项目提供“重试”和“删除”,不使用含糊的通用“处理”按钮。
- 删除入口必须显示是否会影响已生成反馈;历史总结只保留快照,不依赖原录音继续存在。
4.4 录音详情工作页
页面结构:
顶部导航:返回 / 标题 / 更多
紧凑播放器:播放、暂停、进度、当前时间、总时长
说话人映射栏:Speaker 0 -> 教师,Speaker 1 -> 学生
对话工具栏:全选、按说话人筛选、仅看已选
对话主区域:
[复选框] [00:12-00:18] [教师] 对话文本
[复选框] [00:19-00:27] [学生] 对话文本
固定底栏:已选 N 段 / 加入反馈或生成总结
交互要求:
- 对话内容占据主要视觉面积,播放器和筛选工具保持安静、紧凑。
- 点击时间范围后从对应位置播放。
- 播放进度变化只更新当前行状态,不重新渲染完整对话列表。
- 选择状态由
turn_id管理,不能依赖数组位置。 - 支持按说话人全选和取消,结果仍展开为具体
turn_id提交。 - 角色映射修改不改写历史总结快照。
- 转录失败时不渲染空工作台,直接显示失败原因、重试和删除。
- 说话人数量异常时允许保留“未知”角色,不强制映射为教师或学生。
4.5 反馈素材选择页
从反馈页打开,必须携带目标 feedback_session_id。该页复用录音列表和对话选择组件,但使用独立路由,以避免 tab 页面承担临时返回状态。
支持两种选择方式:
- 整份录音:服务端解析为该录音当前就绪转录版本的全部对话。
- 部分对话:提交明确的
turn_id集合。
返回反馈页前显示:
- 录音数量。
- 对话片段数量。
- 字符数估算。
- 选择范围中是否包含“未知说话人”。
选择草稿以 feedback_session_id 为键暂存在小程序本地存储,页面返回或小程序短时退出后可恢复;提交总结时服务端重新校验所有权、状态和转录版本。
4.6 反馈生成页调整
现有展开式“语音记录”区域改为紧凑的素材区:
开始录音。从录音资料选择。已选 2 个录音 / 18 段 / 1260 字。查看选择。生成总结。- 总结状态和预览。
现有按整节加入和总结能力在兼容期保留;新能力开关启用后,页面优先走录音资料选择链路。确认稳定后再删除旧前端入口,后端旧接口继续保留一个发布周期。
4.7 页面状态矩阵
| 状态 | 列表页 | 详情页 | 可用操作 |
|---|---|---|---|
| 上传中 | 进度或等待 | 不进入对话区 | 取消 |
| 排队中 | 排队状态 | 音频可播放 | 删除 |
| 转录中 | 处理中 | 音频可播放 | 删除 |
| 待确认 | 显示说话人数 | 对话可查看和映射 | 映射、选择、完成确认 |
| 已就绪 | 正常状态 | 全部能力 | 选择、总结、重转录、删除 |
| 失败 | 错误分类 | 错误详情 | 重试、删除 |
| 删除中 | 从正常列表隐藏 | 禁止进入 | 后台重试清理 |
5. 目标领域模型
5.1 设计原则
- 录音资产由用户拥有,反馈会话只引用素材,不拥有素材。
- 原始音频、转录运行、说话人和对话片段职责分离。
- 每次重新转录创建新版本,不原地覆盖已用于总结的版本。
- 总结来源在任务创建时展开并快照,worker 不读取会变化的最新文本。
- 用户删除录音后,历史总结仍可解释其来源数量和快照,但不能再播放音频。
- 所有查询都显式包含
owner_id边界,不依赖前端传入的用户标识。 - 删除必须同时覆盖数据库引用和物理文件,不再依赖数据库级联完成文件清理。
5.2 关系图
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)。创建总结任务时在同一事务中:
- 校验所有录音属于当前 owner。
- 校验所有 turn 属于对应录音的当前就绪 run。
- 将整份录音选择展开为 turn。
- 按录音时间和 turn 时间排序。
- 写入快照。
- 计算 source hash。
- 创建或复用幂等总结任务。
worker 只读取 text_snapshot,不得再次 join 最新 transcript_turns.text。
6. 录音和转录状态机
6.1 录音采集状态
uploading
| complete upload/finalize
v
complete
uploading --validation/storage error--> failed
failed --retry upload--> uploading
规则:
complete后 part 集合默认不可变;补传必须在 finalize 前完成。- finalize 是幂等操作,重复调用返回同一结果。
- live 录音停止后只有全部 part 成功上传才允许 finalize。
- 上传文件校验失败不得创建可见的半成品录音;失败记录只用于恢复时才保留。
6.2 转录运行状态
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 删除状态
删除采用“先隐藏、后物理清理”:
- DELETE 接口校验 owner 并写入
delete_requested_at。 - 列表和详情立即隐藏该录音。
- cleanup worker 删除所有物理文件。
- 删除 speakers、turns、runs、parts 和 recording 行。
- summary source 的快照保留,外键设空。
- 文件删除失败时保留待清理记录并指数退避重试。
不得先删数据库行再尝试删除文件,否则会再次制造不可追踪孤儿文件。
7. 音频存储与播放
7.1 存储键
新文件使用:
/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 预研实施步骤
- 建立不提交真实敏感音频的本地测试清单和标注格式。
- 在独立分支为
asr-service增加实验开关,不修改生产默认值。 - 先测试固定
funasr==1.3.14是否支持所需spk_model和输出字段。 - 若固定版本不支持,再验证最小兼容升级,并记录依赖 diff 和模型版本。
- 对相同音频分别运行无 speaker、CAM++、ERes2NetV2。
- 保存脱敏指标,不保存或提交真实转录正文。
- 在 5700U 单进程 CPU 环境测量 RTF、峰值内存、缓存大小和稳定性。
- 选择通过门槛的候选;均不通过时停止正式开发并重新评估方案。
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 直接视为同一人。
若长音频一次处理超出内存或模型限制,再实现:
- 每个 part 提取 speaker embedding。
- 在 recording 级对相邻 part 的 cluster 做相似度匹配。
- 设置信心阈值,低信心时生成新的匿名 speaker。
- 保存原始 provider label 和合并后的 recording speaker,不覆盖原始输出。
该备选方案必须单独评审,不能在首版临时拼接字符串完成。
8.7 ASR 服务响应契约
新响应建议:
{
"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
建议请求:
{
"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_readyrecording_upload_incompleterecording_audio_missingrecording_format_unsupportedrecording_too_largerecording_duration_exceededtranscription_unavailabletranscription_faileddiarization_failedspeaker_mapping_conflicttranscript_version_changedsummary_source_unavailableaudio_storage_delete_failed
HTTP 状态和用户提示在 API 文档中逐项映射,前端不能依赖错误字符串包含关系判断业务状态。
10. 后端模块拆分
避免继续扩大单个 recording_routes.rs,建议按职责拆分:
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 增加:
RecordingSummaryRecordingDetailRecordingSpeakerTranscriptTurnTranscriptionRunRecordingSelectionRecordingListPage
在 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 run;ready/failed 停止轮询。
- 列表空态、失败态和加载骨架使用稳定尺寸,避免布局跳动。
11.4 可访问性和视觉约束
- 对话内容是详情页主要区域,工具栏和状态栏保持紧凑。
- 一个区域内只保留一个蓝色主操作。
- 使用白、石墨和浅灰中性色,不新增装饰渐变和重阴影。
- 控件圆角遵循当前小程序设计,不用夸张胶囊按钮。
- 播放、删除、重试等按钮提供明确
aria-label。 - 说话人不能只靠颜色区分,同时显示角色文字。
- 长标题、长角色名和 2000 字边界在最小真机宽度验证。
12. 数据迁移与兼容发布
12.1 Expand
新增迁移建议从 0006_recording_library.sql 开始:
- 创建
recordings、recording_audio_parts、transcription_runs、recording_speakers、transcript_turns。 - 创建
feedback_summary_run_sources。 - 创建所有 CHECK、UNIQUE、FK 和必要索引。
recordings.current_transcription_run_id在 run 表创建后再添加 FK。- 不修改现有表的非空约束,不删除旧表或旧索引。
迁移必须在真实规模副本上测量锁时间。大表索引若不能在事务迁移中安全创建,应拆分为运维步骤并使用 PostgreSQL CREATE INDEX CONCURRENTLY。
12.2 Backfill
新增可重复运行的 migrate-recording-library 二进制或受控迁移任务:
- 每个
lesson_session创建一个source='legacy'recording。 legacy_lesson_id唯一保证重复执行不重复创建。- 每个
audio_segment创建一个 recording part;先将现有绝对audio_path规范化并验证位于AUDIO_STORAGE_DIR内,再转换为相对 storage key。目录外路径只报告,不自动导入。 - ready segment 的纯文本转为一个
unknownspeaker 下的 turn。 - turn 的时间范围按 segment 顺序和 duration 累计估算,并标记为 legacy 导入,不伪装成模型级精确时间。
- processing/failed segment 保留原状态映射,是否重转录由用户或后续任务决定。
- 每批固定数量提交,输出游标、成功数、跳过数和失败数。
- 回填不移动物理文件,避免大批量 I/O 和回滚困难。
一致性检查:
- legacy recording 数等于 lesson 数。
- part 数等于 audio segment 数。
- 每个 ready legacy segment 至少有一个非空 turn。
- duration 和 byte size 汇总无负值。
- 所有 storage key 可解析且文件存在,缺失文件单独报告。
12.3 双读和切换
- API 上线新表和新接口,功能开关默认关闭。
- 完成 backfill 并运行一致性报告。
- 录音资料列表先只对测试账号开放。
- 新现场录音写新表;兼容期如仍需旧页面显示,显式双写映射关系,不复制音频文件。
- summary worker 支持新 sources 和旧 lessons 两条读取路径。
- 小程序启用新页面和选择流程。
- 观察一个完整发布周期后,停止旧前端创建 lesson 链路。
- 旧 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. 功能开关和配置
建议增加:
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_createdrecording_part_uploadedrecording_completedrecording_upload_failedrecording_playback_failedtranscription_run_queuedtranscription_run_startedtranscription_run_finishedtranscription_run_failedspeaker_mapping_updatedrecording_delete_requestedrecording_cleanup_finishedrecording_cleanup_failedrecording_selection_appliedsummary_sources_snapshotted
14.2 允许记录的字段
recording_idtranscription_run_idfeedback_session_idsummary_run_idowner不记录真实 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. 安全与隐私
录音和对话属于敏感教学数据,必须满足:
- 每个录音、speaker、turn、播放和总结来源查询都通过 recording owner 反查授权。
- 不接受前端传入
owner_id作为授权依据。 - 上传文件执行大小、时长、格式和解码验证。
- ffmpeg 处理设置超时、资源上限和受控临时目录。
- 临时文件在成功、失败、超时和进程恢复路径都能清理。
- 播放接口不生成永久免认证 URL。
- 删除操作提供清晰确认并记录稳定审计事件,不记录正文。
- 数据库备份和音频卷备份使用相同的访问权限和恢复演练要求。
- 产品界面明确录音用途和删除后果,遵循微信麦克风权限说明。
- 不以说话人 embedding 建立跨录音身份库,除非后续完成单独隐私评审。
16. 分阶段实施与提交计划
每个阶段独立提交、可测试、可回滚。不得把数据库、ASR、全部页面和切换一次性合并为一个提交。
阶段 0:基线和技术预研
交付:
- 代表性音频测试清单和本地标注格式。
- 固定 FunASR 版本的 CAM++/ERes2NetV2 兼容性结论。
- 5700U CPU 质量、RTF、内存和缓存报告。
- 最终 speaker 模型和输出契约决定。
建议提交:
test(asr): add diarization evaluation fixturesdocs(asr): record speaker diarization benchmark
退出条件:第 8.5 节门槛通过,或形成明确否决结论。
阶段 1:数据和存储基础
交付:
- 新表、约束和索引。
- 流式 part 上传和 finalize。
- 新存储键、hash、时长和大小校验。
- 认证 Range 播放。
- cleanup worker 和 dry-run 孤儿报告。
建议提交:
feat(recordings): add recording library schemafeat(recordings): stream audio parts to storagefeat(recordings): add authenticated audio playbackfeat(recordings): add deferred audio cleanup
退出条件:上传、播放、删除、失败恢复和 owner 隔离集成测试通过。
阶段 2:结构化转录和说话人分离
交付:
- ASR 新响应契约。
- speaker 模型配置和健康信息。
- transcription run 版本、队列和原子写入 speakers/turns。
- 重试和重转录。
- 模型错误分类和指标。
建议提交:
feat(asr): return timestamped speaker turnsfeat(voice): persist versioned transcript turnsfix(voice): recover expired diarization jobs
退出条件:技术预研样本和端到端 DB 写入均通过。
阶段 3:录音资料页面
交付:
- 列表 API 和游标分页。
- 详情、turn 分页和 speaker 映射 API。
- 录音列表页、详情页和播放器。
- 处理状态、失败重试、删除和角色映射。
建议提交:
feat(recordings): add recording library APIsfeat(miniprogram): add recording library pagefeat(miniprogram): add transcript review workspace
退出条件:真机可浏览、播放、映射、重试和删除录音。
阶段 4:素材选择和总结
交付:
- 反馈素材选择页。
- 选择本地恢复。
- 直接加入反馈接口。
- summary source 快照。
- summary worker 新旧来源兼容。
- 反馈页精简和总结预览接入。
建议提交:
feat(feedback): select recording transcript turnsfeat(summary): snapshot selected transcript sourcesfeat(feedback): compose feedback from recording library
退出条件:未选中的 turn 不进入输入框或 AI 请求,历史总结不受重转录影响。
阶段 5:历史回填和灰度切换
交付:
- 可重复运行的 legacy backfill。
- 一致性报告。
- 测试账号功能开关。
- 生产灰度、监控和回滚演练。
- 小程序导航最终决策。
建议提交:
feat(recordings): backfill legacy voice lessonschore(recordings): enable recording library rolloutdocs(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 端到端验收场景
- 现场录制 20 分钟两人对话,跨越至少两个 part。
- 后台转录并保持同一说话人跨 part 一致。
- 用户映射教师和学生角色。
- 只选择学生的若干 turn 直接加入反馈。
- 再选择两份录音生成总结。
- 验证 AI 输入不包含未选 turn。
- 预览后替换输入框并保存反馈。
- 在录音库仍可查看录音;删除录音后历史反馈和总结仍存在。
- 重启 API 和 FunASR 后任务恢复,模型不重复下载,音频仍存在。
17.6 常规校验命令
开发提交前至少执行:
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 发布前
- 备份远程 PostgreSQL,并记录可恢复时间点。
- 备份或快照
teaching-feedback-audio卷。 - 记录当前镜像 tag、Git SHA、模型缓存大小和健康状态。
- 在生产规模副本验证迁移锁时间和 backfill 批次。
- 确认新 speaker 模型缓存所需磁盘和首次下载时间。
- 功能开关全部保持关闭。
18.2 发布顺序
- 拉取已经测试并推送的提交。
- 构建 API 和 FunASR 新镜像,不停止现有容器。
- 运行 expand migration。
- 启动新 API 和 FunASR,保持新功能关闭。
- 验证健康检查、旧录音链路和旧总结链路。
- 运行 legacy backfill dry-run,再执行正式 backfill。
- 对测试账号启用录音资料和 diarization。
- 完成真实录音、上传、播放、选择、总结和删除验收。
- 扩大灰度范围。
- 最后发布使用新入口的小程序版本。
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 低于 300ms;turn 分页 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
只有同时满足以下条件,本功能才算完成:
- 本文档中首版目标均有对应实现或明确批准的范围变更。
- 数据库迁移、backfill、API、worker、小程序和部署文档全部合并。
- 自动测试、代表性音频评测和真机端到端测试通过。
- owner 隔离、日志脱敏和删除恢复通过安全验收。
- 5700U CPU 上 diarization 达到质量和资源门槛。
- 生产完成灰度、监控、回滚和一次容器重启恢复验证。
- API 文档、调试指南、部署指南和观测 runbook 已同步更新。
- 所有提交使用仓库现有 Conventional Commit 风格并已推送。
- 服务器可通过
git pull --ff-only获取确定的 Git SHA 重新部署。 - 未完成项、残余风险和生产开关状态在发布报告中明确列出。
23. 参考资料
项目内依据:
README.mdDEBUG_GUIDE.mdDEPLOY_5700U.mdserver/API.mdserver/migrations/0002_voice_feedback_sessions.sqlserver/migrations/0003_async_transcription_jobs.sqlserver/migrations/0005_feedback_summary_runs.sqlserver/src/recording_routes.rsserver/src/transcription_worker.rsserver/src/summary_worker.rsserver/src/speech.rsasr-service/app.pypages/feedback/index.tspages/feedback/index.wxml
官方技术依据,均应在实际实现时锁定到使用版本对应的文档或提交:
- FunASR 官方仓库与 Paraformer speaker model 示例
- FunASR 官方教程中的 Speaker Verification / Diarization
- FunASR 官方 Paraformer 实现说明
官方 main 分支能力不等于本项目固定 funasr==1.3.14 已验证能力;阶段 0 的固定版本兼容测试是正式开发的阻断门槛。