【直播切片工作台】第24章:ASR 客户端:提交与轮询

第24章:ASR 客户端:提交与轮询

本章分析 internal/pkg/asr/client.go:豆包 BigModel 录音文件识别的封装------提交任务、状态码驱动的轮询循环、进度回调、格式探测。这是一个「外部异步 API 客户端」的完整范本:超时分层、幂等提交、进度估算,全部模式可平移到任何轮询型第三方服务。

流程

TranscribeWithProgress(ctx, audioURL, onProgress) 的完整流程:

  1. 前置校验 APIKey;DetectFormat(audioURL) 从 URL 后缀推断音频格式(asr/format.go);
  2. 生成本地 taskID(UUID,用作提交幂等键);
  3. submit:POST 提交载荷(user/audio/request 三段),请求参数硬编码最优组合:show_utterances=true(分句+词级时间戳)、enable_itn=true(数字规整,如「一百二」→「120」)、enable_punc=true(标点)、enable_speaker_info=true(说话人分离)------这四个开关正是后续切片精度的数据基础(第 26 章);
  4. poll:以 PollInterval(默认 10 秒)间隔查询结果,按响应头 X-Api-Status-Code 分流:20000000 成功返回完整 JSON;20000001/20000002 进行中,回调 onProgress(5~95) 后继续;其他码即失败带 message;
  5. 上限 MaxPolls=360(约 60 分钟)后超时失败。

输入是一个公网可访问的音频 URL(不是本地文件)------所以第 28 章的音频预处理器必须先把本地 MP3 上传对象存储。输出是豆包的完整原始 JSON(RawMessage),调用方再按需解析(时长、分句等,第 26 章)。

实现

实现上的分层细节:

  • 两级超时 :http.Client{Timeout: 30s} 管单次 HTTP 请求(提交、每次轮询都是短请求);总时长由 MaxPolls × PollInterval 控制(默认 3600 秒)。二者独立配置,互不耦合------「单请求超时」与「任务总超时」是两个概念,混用是常见错误。
  • 状态码走响应头 :豆包用 X-Api-Status-Code / X-Api-Message 头表达业务状态(常量 statusSuccess="20000000" 等),body 才是结果数据------客户端严格按此分流,不做 body 猜测。
  • 进度回调区间 5~95:刻意避开 0 与 100------进度 0 表示未开始、100 表示完成(由调用方在前后设置),轮询期间的估算只占中间区间,语义不越界。素材表的 asr_progress 因此单调且有明确语义。
  • 本地 taskID 而非服务端 ID:提交时自己生成 UUID 作为请求标识,豆包按此返回查询句柄------客户端对「身份」的控制权更大,重试场景可以复用同一 ID 排查。
  • 匿名结构体反序列化 :ParseDurationMs 等解析函数用内联 struct 只取需要的字段(audio_info.duration),不为完整响应建模巨型结构------按需解析降低耦合,豆包响应格式变化时影响面最小。
  • client_test.go 用 httptest 假服务器覆盖提交成功、轮询中、失败、超时分支------外部依赖的测试解法与 LLM/capcutmate 包一致。

📌 设计决策

  • 轮询而非 Webhook 回调:豆包该接口为轮询模型;服务端回调需要公网可达的回调端点与鉴权设计,对私有化部署不友好。
  • 同步方法(Transcribe 一次调用跑到完成)而非返回句柄由调用方轮询:调用方是 ASR Worker(第 29 章),它有自己的心跳机制(每轮回调刷新进度与 asr_updated_at),把轮询折叠进一次调用让 Worker 的循环最简单。
  • 10 秒间隔 × 360 次:豆包长音频任务典型耗时数分钟到数十分钟,10 秒粒度既不过压(单任务 360 次请求)又能让进度条平滑。

代码示例

提交载荷的最优参数组合:

go 复制代码
// internal/pkg/asr/client.go(节选)
type submitPayload struct {
    User struct {
        UID string `json:"uid"` // "live-mixer"
    } `json:"user"`
    Audio struct {
        Format string `json:"format"` // DetectFormat 从 URL 推断
        URL    string `json:"url"`
    } `json:"audio"`
    Request struct {
        ModelName         string `json:"model_name"` // "bigmodel"
        ShowUtterances    bool   `json:"show_utterances"`    // 分句+词级时间戳
        EnableITN         bool   `json:"enable_itn"`         // 数字规整
        EnablePunc        bool   `json:"enable_punc"`        // 标点
        EnableSpeakerInfo bool   `json:"enable_speaker_info"` // 说话人
    } `json:"request"`
}

进度回调与状态码分流:

go 复制代码
// ProgressCallback 轮询进度回调,progress 取值 5~95。
type ProgressCallback func(progress int)

// TranscribeWithProgress 同步转写并在轮询时回调估算进度。
func (c *Client) TranscribeWithProgress(ctx context.Context,
    audioURL string, onProgress ProgressCallback) (json.RawMessage, error) {
    if strings.TrimSpace(c.cfg.APIKey) == "" {
        return nil, fmt.Errorf("ASR API Key 未配置")
    }
    format, err := DetectFormat(audioURL)
    if err != nil {
        return nil, err
    }
    taskID := uuid.NewString()
    if err := c.submit(ctx, taskID, audioURL, format); err != nil {
        return nil, err
    }
    return c.poll(ctx, taskID, onProgress) // 内部按 X-Api-Status-Code 分流
}

小结

  • ASR 客户端 = 提交(四开关最优参数)+ 头部状态码驱动的轮询 + 5~95 进度回调。
  • 两级超时、按需解析、httptest 假服务器三件套值得平移到任何轮询型 API。
  • 输入是公网 URL------上游必须先完成「抽音频 + 上传」预处理。

思考题

  1. 轮询间隔若改为自适应(首次 5 秒,逐次退避到 30 秒),能省多少请求?进度条平滑性如何补偿?
  2. 提交成功但轮询进程崩溃,重新 Transcribe 会导致重复计费------如何利用本地 taskID 实现断点续查?

项目信息

相关推荐
Hommy882 天前
【直播切片工作台】第23章:媒体探测与转码:ffprobe 与 ffmpeg 封装
人工智能·ffmpeg·直播切片
Hommy8810 天前
【直播切片工作台】第16章:统一响应封装
人工智能·直播切片
Hommy882 个月前
【直播切片工作台】第6章:环境搭建与本地开发流程
直播切片
Hommy882 个月前
【直播切片工作台】第2章:技术选型与分层架构设计
直播切片