【直播切片工作台】第27章:ASR 音频预处理

0 阅读4分钟

第27章:ASR 音频预处理

本章分析 internal/service/live_material_asr_audio.go:在调用远程 ASR 之前,把「任意格式的直播素材 URL」变成「公网可访问的标准 MP3」的五步预处理——下载、探测时间轴与分辨率、对齐转码、上传对象存储、清理临时文件。它是 ASR Worker 的第一协作方,也是「接口小抽象」模式的教科书案例。

流程

Prepare(ctx, materialID, sourceURL, onProgress) 的流水线:

  1. 前置校验:对象存储未配置直接报错(没有上传通道就没有公网 URL,远程 ASR 无从谈起);
  2. 准备临时目录与会话 ID:temp/ 下按 newASRSessionID() 生成唯一文件名(源文件 source 与目标 mp3),Cleanup 闭包负责删两个本地文件——调用方 defer Cleanup() 即不留垃圾;
  3. 下载:FileDownloader(含 URL 重写,第 22 章)拉源媒体到本地;
  4. 探测:ffprobe 探出时长、分辨率、音轨时间轴信息(MediaTimelineProber 接口)——分辨率结果随后回写素材(ASR 完成时 UpdateASRCompleted 一并落库,第 15 章);
  5. 对齐转码:按探测结果构造 ASRAlignOptions 调 ConvertToASRMP3Aligned(第 23 章)——音轨与视频时间轴对齐,转出 16kHz 单声道 MP3;
  6. 上传与校验: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/ 前缀,公网可达性与清理策略都由存储层统一解决。

思考题

  1. 若对象存储也内网化(ASR 服务无法访问公网 URL),整个预处理还需要存在吗?链路应如何重构?
  2. 下载与转码都支持 ctx 取消,上传为什么也必须支持?优雅退出时一个正在上传的 ASR 任务会发生什么?

项目信息

  • GitHub仓库:Chyona/live-mixer