You need to enable JavaScript to run this app.
实时音视频

实时音视频

复制全文
进阶功能
实时字幕(对话记录)
复制全文
实时字幕(对话记录)

在与 AI 对话过程中,系统会自动生成用户和 AI 的对话文本。您可以通过客户端或服务端实时获取该数据,用于实时 UI 展示、存储或根据字幕状态触发新一轮对话等。
Image

应用场景

场景

描述

实时字幕展示

将真人用户和 AI 的语音实时转为文字,并在应用终端界面上展示。

业务存储分析

记录并存储对话文本,用于后续的业务数据分析、服务质量监控或模型优化。

交互逻辑控制

利用字幕的特定状态(如整句结束信号)作为触发器,手动开启新一轮对话或执行业务指令,实现灵活交互。

客户端实现

适用于需要在终端界面实时展示字幕的场景。数据通过客户端 SDK 回调获取。

步骤 1:开启配置

调用 StartVoiceChat 在启动 AI 时,添加 Config.SubtitleConfig 配置:

配置参数

说明

DisableRTSSubtitle

设置为 false,开启客户端字幕回调。

SubtitleMode

  • 0 :对齐音频时间戳。字幕来源于 TTS,与 AI 实际播报的音频精准对齐。但字幕生成稍慢,部分特殊符号可能被转义而无法显示。
  • 1 :不对齐音频时间戳。字幕来源于 LLM 原始回复。字幕生成速度较快,能保留所有原始文本(如表情符号),但与音频无时间关联。

注意

当使用以下模型或服务时,SubtitleMode 仅支持设置为 1(不对齐音频时间戳):语音合成大模型 2.0、声音复刻大模型 2.0、数字人服务、端到端实时语音大模型。若设置为 0,字幕将无法正常返回。

配置示例

// 仅展示字幕配置
"SubtitleConfig": {
  "DisableRTSSubtitle": false,
  "SubtitleMode": 0
}

步骤 2:接收并解析字幕

  1. 监听回调:通过 ByteRTC SDK 的 onRoomBinaryMessageReceived(嵌入式硬件场景使用on_message_received)接收字幕消息(二进制格式)。
  2. 解析数据:字幕以二进制消息格式 message(magic number 为 subv)回调给客户端,收到消息后需解析 message
  • message 格式:参见字幕数据格式

  • message 解析示例:

    //定义结构体
       struct SubtitleMsgData {
           bool definite;
           std::string language;
           bool paragraph;
           int sequence;
           std::string text;
           std::string userId;
       };
       
       //回调事件
       void onRoomBinaryMessageReceived(const char* uid, int size, const uint8_t* message) {
           std::string subtitles;
           bool ret = Unpack(message, size, subtitles);
           if(ret) {
               ParseData(subtitles);
           }
       }
       
       //拆包校验
       bool Unpack(const uint8_t *message, int size, std::string& subtitles) {
           int kSubtitleHeaderSize = 8;
           if(size < kSubtitleHeaderSize) { 
               return false;
           }
           // magic number "subv"
           if(static_cast<uint32_t>((static_cast<uint32_t>(message[0]) << 24) 
                  | (static_cast<uint32_t>(message[1]) << 16) 
                  | (static_cast<uint32_t>(message[2]) << 8) 
                  | static_cast<uint32_t>(message[3])) != 0x73756276U) {
               return false;
           }
           
           uint32_t length = static_cast<uint32_t>((static_cast<uint32_t>(message[4]) << 24) 
                  | (static_cast<uint32_t>(message[5]) << 16) 
                  | (static_cast<uint32_t>(message[6]) << 8) 
                  | static_cast<uint32_t>(message[7]));
                  
           if(size - kSubtitleHeaderSize != length) {
               return false;
           }
       
           if(length) {
               subtitles.assign((char*)message + kSubtitleHeaderSize, length);
           } else {
               subtitles = "";
           }
           return true;
       }
       
       //解析
       void ParseData(const std::string& msg) {
           // 解析 JSON 字符串
           nlohmann::json json_data = nlohmann::json::parse(msg);
           // 存储解析后的数据
           std::vector<SubtitleMsgData> subtitles;
           // 遍历 JSON 数据并填充结构体
           for (const auto& item : json_data["data"]) {
               SubtitleMsgData subData;
               subData.definite = item["definite"];
               subData.language = item["language"];
               subData.paragraph = item["paragraph"];
               subData.sequence = item["sequence"];
               subData.text = item["text"];
               subData.userId = item["userId"];
               subtitles.push_back(subData);
           }
       }
    

服务端实现

适用于需要在服务端进行对话记录、存储和分析的场景。数据通过 HTTP(S) POST 请求推送到业务服务器。

步骤 1:开启配置并接收字幕

调用 StartVoiceChat 在启动 AI 时,在 Config 中添加 SubtitleConfig 配置:

配置参数

说明

ServerMessageUrl

您的后端服务地址,用于接收字幕消息。必须是公网可访问的 URL,且能正确处理无 Content-Type 的 HTTP POST 请求。

ServerMessageSignature

自定义认证密钥。此字符串会在回调请求中原样返回,用于校验请求合法性。

SubtitleMode

  • 0 :对齐音频时间戳。字幕来源于 TTS,与 AI 实际播报的音频精准对齐。但字幕生成稍慢,部分特殊符号可能被转义而无法显示。
  • 1 :不对齐音频时间戳。字幕来源于 LLM 原始回复。字幕生成速度较快,能保留所有原始文本(如表情符号),但与音频无时间关联。

注意

当使用以下模型或服务时,SubtitleMode 仅支持设置为 1(不对齐音频时间戳):语音合成大模型 2.0、声音复刻大模型 2.0、数字人服务、端到端实时语音大模型。若设置为 0,字幕将无法正常返回。

配置示例

// 仅展示字幕配置
"SubtitleConfig": {
  "ServerMessageUrl": "https://example-domain.com/vertc/subtitle",
  "ServerMessageSignature": "b46ab****ad6a",
  "SubtitleMode": 0 // 0: 对齐音频时间戳
}

步骤 2:解析字幕

字幕消息会通过 HTTP(S) POST 请求发送给您指定的 URL 地址。

  • 回调内容:

    参数名

    类型

    描述

    message

    String

    字幕数据(Base 64 编码的二进制消息),格式详见字幕数据格式

    signature

    String

    鉴权签名。可与StartVoiceChat接口中传入的ServerMessageSignature字段值进行对比以进行鉴权验证。

  • message 解析示例:

    const (
        subtitleHeader   = "subv"
        exampleSignature = "example_signature"
    )
    
    type RtsMessage struct {
        Message   string `json:"message"`
        Signature string `json:"signature"`
    }
    
    type Subv struct {
        Type string `json:"type"`
        Data []Data `json:"data"`
    }
    
    type Data struct {
        Definite  bool   `json:"definite"`
        Paragraph bool   `json:"paragraph"`
        Language  string `json:"language"`
        Sequence  int    `json:"sequence"`
        Text      string `json:"text"`
        UserID    string `json:"userId"`
    }
    
    func HandleSubtitle(c *gin.Context) {
        msg := &RtsMessage{}
        if err := c.BindJSON(&msg); err != nil {
           fmt.Printf("BindJson failed,err:%v\n", err)
           return
        }
        if msg.Signature != exampleSignature {
           fmt.Printf("Signature not match\n")
           return
        }
    
        subv, err := Unpack(msg.Message)
        if err != nil {
           fmt.Printf("Unpack failed,err:%v\n", err)
           return
        }
    
        fmt.Println(subv)
    
        //业务逻辑
    
        c.String(200, "ok")
    }
    
    func Unpack(msg string) (*Subv, error) {
        data, err := base64.StdEncoding.DecodeString(msg)
        if err != nil {
           return nil, fmt.Errorf("DecodeString failed,err:%v", err)
        }
        if len(data) < 8 {
           return nil, fmt.Errorf("Data invalid")
        }
        dataHeader := string(data[:4])
        if dataHeader != subtitleHeader {
           return nil, fmt.Errorf("Header not match")
        }
        dataSize := binary.BigEndian.Uint32(data[4:8])
        if dataSize+8 != uint32(len(data)) {
           return nil, fmt.Errorf("Size not match")
        }
    
        subData := data[8:]
        subv := &Subv{}
        err = json.Unmarshal(subData, subv)
        if err != nil {
           return nil, fmt.Errorf("Unmarshal failed,err:%v\n", err)
        }
        return subv, nil
    }
    
    func main() {
    
        r := gin.Default()
        r.POST("/example_domain/vertc/subtitle", HandleSubtitle)
        r.Run()
    }
    

字幕数据格式

Image

参数名

类型

描述

magic number

binary

消息标识。固定为 subv,表示字幕数据。

length

binary

字幕消息长度,单位为 bytes,大端序存储。

subtitle_message

binary

字幕内容的 JSON 字符串,UTF-8 编码。格式参见 subtitle_message

subtitle_message

参数名

类型

描述

type

String

消息类型,固定为 subtitle,表示消息为字幕。

data

data

字幕详细信息,包含字幕的文本、语言、说话人 ID 等具体信息。

data

参数名

类型

描述

text

String

字幕文本内容。

language

String

字幕语言。

userId

String

字幕来源者 ID。若字幕来源为真人用户,则该值为真人用户 UserId。若来源为 AI ,则该值为 AI 的 UserId。

sequence

Int

字幕序号,在单轮对话中递增。

definite

Boolean

字幕是否为完整的分句。

  • true:是。
  • false:否。

paragraph

Boolean

字幕是否为完整的一句话。

  • true:是。表示说话者此轮完整的发言结束。
  • false:否。

roundId

Int

对话轮次。

voiceprintName

String

(若启用)声纹名称。

voiceprintId

String

(若启用)声纹 ID。

subtitle_message 的示例

以下展示的是两次连续收到的字幕消息内容,用于说明 sequencedefinitetext 字段的变化。

// 第一次收到的消息 (分句未结束)
{
  "type": "subtitle",
  "data": [{
    "text": "上海天气炎热。气温为",
    "language": "zh",
    "userId": "bot1",
    "sequence": 1,
    "definite": false,
    "paragraph": false,
    "roundId": 1,
    "voiceprintName": "xx",
    "voiceprintId": "uuid"
  }]
} 
// 第二次收到的消息 (分句结束)
{
  "type": "subtitle",
  "data": [{
    "text": "上海天气炎热。气温为 30 摄氏度。",
    "language": "zh",
    "userId": "bot1",
    "sequence": 2,
    "definite": true,
    "paragraph": false,
    "roundId": 1,
    "voiceprintName": "xx",
    "voiceprintId": "uuid"
  }]
}

字幕特性

字幕的来源和返回方式,会因其接收方式(客户端回调 vs. 服务端回调)和归属方(真人用户 vs. AI )的不同而有差异。

接收方式

用户字幕

AI 字幕

客户端接收

字幕来源:ASR 模块语音识别内容
返回方式:流式返回,新字幕是之前内容的累加。

示例:用户说 您好,查询一下上海天气。,您会依次收到 您好, 您好,查询 您好,查询一下上海天气

字幕状态

  • 说话过程中
    • definite:始终为 false
    • paragraph:始终为 false
  • 说话结束时
    • definite:始终为 true
    • paragraph:始终为 true

字幕来源:LLM 文本或 TTS 朗读内容(SubtitleMode决定)
返回方式:流式返回。区分分句和整句,新字幕会覆盖旧的。

示例: AI 说 天气炎热。气温为 30 摄氏度。,您会先收到分句:天气炎热。天气炎热。气温为 30 摄氏度。

字幕状态

  • 说话过程中
    • definite:可能为 false(分句未结束)或 true(分句已结束,但整句话未完)。
    • paragraph:始终为 false,表示整句话未结束。
  • 说话结束时
    • definite:为 true,表示分句结束。
    • paragraph:为 true,表示整句话结束。

服务端接收

字幕来源:ASR 模块语音识别内容
返回方式:仅在识别到完整分句后才返回。每个分句都作为一条独立的消息发送,新的分句不包含上一个已结束的分句。

示例:用户说 您好。查询一下上海天气。,您将依次收到字幕:您好。查询一下上海天气

字幕状态

  • 说话过程中
    • definite:始终为 true
    • paragraph:始终为 false
  • 说话结束时
    • definite :为 true
    • paragraph :为 true

字幕来源:LLM 文本或 TTS 朗读内容(SubtitleMode决定)
返回方式:仅在识别到完整分句后才返回。每个分句都作为一条独立的消息发送,新的分句不包含上一个已结束的分句。

示例: AI 说 天气炎热。气温为 30 摄氏度。,您会依次收到分句:天气炎热。气温为 30 摄氏度。

字幕状态

  • 说话过程中
    • definite:始终为 true,表示分句已结束。
    • paragraph:始终为 false,表示一整句还未结束。
  • 说话结束时
    • definite :为 true ,表示分句已结束。
    • paragraph :为 true ,表示一整句已结束。

字幕处理实践

实时 UI 展示

字段组合状态

UI 处理

paragraph: falsedefinite: false

用序号大的字幕覆盖序号小的。

paragraph: falsedefinite: true

重新开始新的一句话,覆盖前一句话。

paragraph: true

此时如果继续解析显示字幕,字幕会重复显示。

存储字幕

仅在解析到 paragraph: truedefinite: true 时存储,减少存储的数据量,并确保保存的字幕是完整的。

说明

将字幕存储至火山 Viking 记忆库时,需将解析到的 text 按照 userId 映射为 userassistant 角色,再通过 添加会话-AddSession 接口写入 Viking 记忆库。

触发新一轮对话

利用字幕状态作为业务逻辑的触发器。例如,在需要用户完整说完一句话后再响应的场景:

  1. 通过服务端回调监控用户的字幕。
  2. 当监测到来自用户的消息满足 paragraph: true 时,判定用户已结束发言。
  3. 调用业务逻辑(如向 LLM 发起请求、执行特定指令等)。

处理多轮对话字幕

使用 roundId 字段对字幕进行分组,在复杂的对话流中,准确区分和关联每一轮对话的字幕。

FAQ

  • Q1:是否支持在用户和 AI 说话之前就将字幕返回给业务端?
    不支持。
  • Q2:字幕是否支持接收图片?
    不支持。
  • Q3:微信小程序端是否支持字幕功能?
    支持。
  • Q4:收到的字幕与实际内容相比有很多错误同音字。
    不同模型对于同音字的识别结果不同。您可以通过提升语音识别的准确性,来提升字幕准确度。具体操作,请参见提升语音识别准确性
  • Q5:字幕是否支持返回超链接内容?
    不支持。
最近更新时间:2026.01.09 15:58:13
这个页面对您有帮助吗?
有用
有用
无用
无用