第27章:ASR 音频预处理
本章分析 internal/service/live_material_asr_audio.go:在调用远程 ASR 之前,把「任意格式的直播素材 URL」变成「公网可访问的标准 MP3」的五步预处理——下载、探测时间轴与分辨率、对齐转码、上传对象存储、清理临时文件。它是 ASR Worker 的第一协作方,也是「接口小抽象」模式的教科书案例。
流程
Prepare(ctx, materialID, sourceURL, onProgress) 的流水线:
- 前置校验:对象存储未配置直接报错(没有上传通道就没有公网 URL,远程 ASR 无从谈起);
- 准备临时目录与会话 ID:
temp/下按newASRSessionID()生成唯一文件名(源文件 source 与目标 mp3),Cleanup闭包负责删两个本地文件——调用方defer Cleanup()即不留垃圾; - 下载:FileDownloader(含 URL 重写,第 22 章)拉源媒体到本地;
- 探测:ffprobe 探出时长、分辨率、音轨时间轴信息(
MediaTimelineProber接口)——分辨率结果随后回写素材(ASR 完成时 UpdateASRCompleted 一并落库,第 15 章); - 对齐转码:按探测结果构造
ASRAlignOptions调ConvertToASRMP3Aligned(第 23 章)——音轨与视频时间轴对齐,转出 16kHz 单声道 MP3; - 上传与校验:MP3 上传到对象存储
temp/前缀下(storage.SubDirTemp),返回公网 URL;若对齐后时长与目标差超过 0.15 秒(asrDurationMismatchWarnSec)打 Warn——音频时间轴与视频漂移会直接影响后续切片精度,阈值监控在这里。
输出 ASRAudioPrepareResult{AudioURL, Width, Height, Cleanup}:AudioURL 喂给 ASR 客户端(第 24 章),Width/Height 存素材,Cleanup 收尾。onProgress 贯穿全程向素材表刷进度。
实现
本文件是全仓库「小接口反转」密度最高的地方——三个依赖全部是本地接口而非具体类型:
type FileDownloader interface{ Download(url, dest string) (string, error) }
type AudioConverter interface{ ConvertToASRMP3Aligned(...) error }
type ObjectUploader interface{ UploadFile(ctx, localPath, objectKey string) (string, error) }
好处立竿见影:live_material_asr_audio_test.go 注入内存假实现,无需 ffmpeg、无需网络即可测完「下载失败、转码失败、时长漂移告警、清理调用」等分支。同时 utilsFileDownloader 适配器展示如何把包级函数(utils.DownloadFile)包装成接口实现——函数到接口的桥接就三行。
其他细节:
- 构造函数的 nil 兜底:downloader/converter/prober 任一为 nil 都换默认实现(PATH 查找 ffmpeg/ffprobe),组合根传
""即可,测试传 mock——一个构造函数服务两类调用者。 - objectKeyPrefix = storage.SubDirTemp:ASR 音频统一放对象存储
temp/子目录,与其他业务对象隔离,也方便按前缀做过期清理。 - Cleanup 显式返回而非内部 defer:转码与上传是长操作,中间失败时也要能清理;把清理时机交给调用方(成功、失败、超时三种结束路径统一
defer)是正确的资源管理姿势。 - 0.15 秒漂移阈值:常量注释写明用途。词级时间戳精度约 ±50ms,0.15s 已是三倍余量,超了说明对齐滤镜没生效或源异常。
📌 设计决策
- 预处理独立成 Preparer 而非塞进 ASR Worker:下载/转码/上传是三种可独立失败的外部交互,独立组件让 Worker 的 Process 主流程保持「取素材 → Prepare → Transcribe → 后处理 → 落库」的线性叙事。
- 临时文件放进程工作目录 temp/ 而非系统 /tmp:容器内路径可控(Dockerfile WORKDIR),与 webroot 暂存清理(第 46 章)的目录体系保持一致。
- 音频走对象存储而非直传 ASR:豆包该接口只收 URL;副作用是转写一次就留一份临时 MP3,靠 temp 前缀清理策略回收。
代码示例
五步流水线的入口与资源管理:
// internal/service/live_material_asr_audio.go(节选)
func (p *liveMaterialASRAudioPreparer) Prepare(
ctx context.Context, materialID uint, sourceURL string,
onProgress func(progress int16),
) (ASRAudioPrepareResult, error) {
if p.uploader == nil {
return ASRAudioPrepareResult{},
fmt.Errorf("对象存储未配置,无法上传 ASR 音频")
}
tempDir, err := p.ensureTempDir()
if err != nil {
return ASRAudioPrepareResult{}, err
}
sessionID := newASRSessionID()
sourcePath := buildASRSourceLocalPath(tempDir, sessionID)
mp3Path := buildASRLocalPath(tempDir, sessionID)
cleanup := func() { // 调用方 defer Cleanup()
_ = os.Remove(sourcePath)
_ = os.Remove(mp3Path)
}
// 下载 → ffprobe 探测 → 对齐转码 → 上传(后续章节化的私有方法)
// 时长漂移超过 0.15s 打 Warn
}
小接口 + 函数适配器的桥接:
// utilsFileDownloader 使用 internal/pkg/utils 的默认下载实现(无日志)。
type utilsFileDownloader struct{}
func (utilsFileDownloader) Download(url, dest string) (string, error) {
return utils.DownloadFile(url, dest)
}
小结
- 预处理五步:校验 → 临时文件 → 下载 → 探测+对齐转码 → 上传(0.15s 漂移监控)。
- 三个小接口 + nil 兜底构造 + Cleanup 闭包,是可测长流水线的标准形态。
- ASR 音频走对象存储 temp/ 前缀,公网可达性与清理策略都由存储层统一解决。
思考题
- 若对象存储也内网化(ASR 服务无法访问公网 URL),整个预处理还需要存在吗?链路应如何重构?
- 下载与转码都支持 ctx 取消,上传为什么也必须支持?优雅退出时一个正在上传的 ASR 任务会发生什么?
项目信息
- GitHub仓库:Chyona/live-mixer