You need to enable JavaScript to run this app.
导航
实时字幕(对话记录)
最近更新时间:2025.12.04 11:13:23首次发布时间:2024.09.03 11:03:15
复制全文
我的收藏
有用
有用
无用
无用

在与智能体对话过程中,系统会自动生成用户和智能体的对话文本。你可以通过客户端或服务端实时获取该数据,用于实时展示字幕、存储对话记录用于后续分析,或根据字幕状态(如整句结束)触发新一轮对话。
Image

应用场景

场景

描述

实时显示字幕

将真人用户和智能体的语音实时转为文字,并在应用终端界面上展示,提升用户理解和参与度。

业务存储分析

将真人用户和智能体的对话文本内容记录并存储下来,用于后续的业务数据分析、服务质量监控或模型优化。

根据字幕返回时机手动触发新一轮对话

利用字幕返回的特定状态(如整句话结束)作为信号,手动触发下一轮对话开启,实现更灵活的交互逻辑。

客户端实现

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

步骤 1:接收字幕

  1. 调用 StartVoiceChat 在启动智能体时,在 Config 中添加 SubtitleConfig 配置:

    配置参数

    说明

    DisableRTSSubtitle

    设置为 false,开启房间内客户端字幕回调功能。

    SubtitleMode

    • 0 :对齐音频时间戳。该模式下,智能体输出字幕返回的是 TTS 模块转义后的字幕,与智能体朗读的时间戳一一对齐。但注意该字幕结果经过转写和处理,表情等符号可能无法显示。
    • 1 :不对齐音频时间戳。该模式下,智能体输出字幕返回的是 LLM 模块转义后的字幕,未经过 TTS 模块处理不带时间戳,但该模式下字幕生成速度更快。

    SubtitleConfig 配置示例

    // 仅展示字幕相关配置,其他 ASR/TTS/LLM 配置请参考对应文档
    "Config": {
        // ... 其他配置 ...
        "SubtitleConfig": {
            "DisableRTSSubtitle": false, // 开启客户端字幕回调
            "SubtitleMode": 0            // 0: 对齐音频时间戳
        }
    }
    
  2. 通过 ByteRTC SDK 的 onRoomBinaryMessageReceived(嵌入式硬件场景使用on_message_received)回调接收字幕结果。

步骤 2:解析字幕

收到的字幕结果为二进制,使用前需解析。字幕回调格式如下:

参数名

类型

描述

uid

String

消息发送者 ID。

message

String

字幕消息内容(二进制格式),详见下方的二进制消息格式说明。

二进制消息格式如下:
Image

参数名

类型

描述

magic number

binary

消息格式,固定为 subv

length

binary

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

subtitle_message

binary

字幕消息详细信息。格式参看subtitle_message 格式

subtitle_message

参数名

类型

描述

type

String

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

data

data

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

data

参数名

类型

描述

text

String

字幕文本。

language

String

字幕语言。

userId

String

字幕来源者 ID。若字幕来源为真人用户,则该值为真人用户 UserId。若来源为智能体,则该值为智能体 UserId。

sequence

Int

字幕序号。

definite

Boolean

字幕是否为完整的分句。

  • true:是。
  • false:否。

paragraph

Boolean

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

  • true:是。
  • false:否。

roundId

Int

对话轮次。

voiceprintName

String

声纹名称。

voiceprintId

String

声纹 ID。

subtitle_message 的示例如下:

{
"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" 
}]
}

你可以参考以下示例代码对回调信息中的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 在启动智能体时,在 Config 中添加 SubtitleConfig 配置:

配置参数

说明

ServerMessageUrl

业务服务端接收字幕结果的 URL 地址。你指定的 URL 地址将收到来自 RTC 服务器的 HTTP(S) POST 请求发送的字幕结果,格式为二进制,使用前需解析。

ServerMessageSignature

鉴权签名。你可传入该鉴权字段,在收到字幕结果后,与结果中的 signature 字段的值进行对比,用于鉴权,保证消息的可靠性与安全性。

SubtitleMode

  • 取值为 0 时,表示对齐音频时间戳。该模式下,返回的是 TTS 模块生成的智能体字幕,与智能体朗读的时间戳一一对齐。但注意该字幕结果经过转写和处理,表情等符号可能无法显示。
  • 取值为 1 时,表示不对齐音频时间戳。该模式下,返回的是 LLM 模块生成的智能体回复内容,未经过 TTS 模块处理不带时间戳,但该模式下字幕生成速度更快。

SubtitleConfig 示例

// 仅展示字幕相关配置,其他 ASR/TTS/LLM 配置请参考对应文档
"Config": {
    // ... 其他配置 ...
    "SubtitleConfig": {
        "ServerMessageUrl": "https://example-domain.com/vertc/subtitle",
        "ServerMessageSignature": "b46ab****ad6a", 
        "SubtitleMode": 0            // 0: 对齐音频时间戳
    }
}

步骤 2:解析字幕

你指定的 URL 地址将收到来自 RTC 服务器的 HTTP(S) POST 请求发送的字幕结果,格式为二进制。具体格式如下:

参数名

类型

描述

message

String

Base 64 编码的二进制消息内容。格式参看二进制消息格式

signature

String

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


二进制消息格式如下:
Image

参数名

类型

描述

magic number

binary

消息格式,固定为 subv

length

binary

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

subtitle_message

binary

字幕消息详细信息。格式参看subtitle_message 格式


subtitle_message 格式:

参数名

类型

描述

type

String

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

data

data

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

data

参数名

类型

描述

text

String

字幕文本。

language

String

字幕语言。

userId

String

字幕来源者 ID。若字幕来源为真人用户,则该值为真人用户 UserId。若来源为智能体,则该值为智能体 UserId。

sequence

Int

字幕序号。

definite

Boolean

字幕是否为完整的分句。

  • true:是。
  • false:否。

paragraph

Boolean

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

  • true:是。
  • false:否。

roundId

Int

对话轮次。

voiceprintName

String

声纹名称。

voiceprintId

String

声纹 ID。

subtitle_message 的示例如下:

{
"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" 
}]
}

你可以参考以下示例代码对回调信息中的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()
}

字幕说明

字幕特性对比

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

真人用户

智能体

客户端回调

  • 来源:ASR 模块语音识别内容
  • 字幕返回方式:流式返回说话内容。例如:用户说话内容为:”你好。查询一下上海的天气。“返回效果如下:“你好。”“你好,查询”“你好。查询一下上海的天气”
  • 说话过程中 :
    • definite:始终为 false,表示一整句还未结束。
    • paragraph:始终为 false,表示一整句还未结束。
  • 说话结束时 :
    • definite :为 true ,表示一整句已结束。
    • paragraph :为 true ,表示一整句已结束。
  • 来源:根据 SubtitleMode 模式的不同,来源为 LLM 模块生成内容或 TTS 模块朗读内容。
  • 字幕返回方式 :
    • 流式返回说话内容。存在分句和整句的区别。例如:分句1:上海天气炎热。分句2:气温为 30 摄氏度。整句:上海天气炎热。气温为 30 摄氏度。
    • 当新的分句开始时,字幕不会显示上一个已结束的分句。
  • 说话过程中:
    • definite :可能为 false (表示当前分句尚未结束)或 true (表示当前分句已结束,但一整句话还未结束)。
    • paragraph:始终为 false。表示一整句还未结束。
  • 说话结束时 :
    • definite :为 true ,表示分句已结束。
    • paragraph :为 true ,表示一整句已结束。

服务端回调

  • 来源:ASR 模块语音识别内容。
  • 字幕返回方式 :识别到一个完整分句后返回。且当新的分句开始时,字幕不会显示上一个已结束的分句。例如:用户说话内容为:”你好。查询一下上海的天气。“返回效果如下:“你好。”“查询一下上海的天气”
  • 说话过程中 :
    • definite:始终为 true,表示分句已结束。
    • paragraph:始终为 false,表示一整句还未结束。
  • 说话结束时 :
    • definite :为 true ,表示分句已结束。
    • paragraph :为 true ,表示一整句已结束。
  • 来源:根据 SubtitleMode 模式的不同,来源为 LLM 模块生成内容或 TTS 模块朗读内容。
  • 字幕返回方式 :识别到一个完整分句后返回。且当新的分句开始时,字幕不会显示上一个已结束的分句。例如:用户说话内容为:”上海天气炎热。气温为 30 摄氏度。“返回效果如下:“上海天气炎热。”“气温为 30 摄氏度。”
  • 说话过程中 :
    • definite:始终为 true,表示分句已结束。
    • paragraph:始终为 false,表示一整句还未结束。
  • 说话结束时 :
    • definite :为 true ,表示分句已结束。
    • paragraph :为 true ,表示一整句已结束

字幕处理实践

你可以根据 definiteparagraphsequence 字段处理字幕。

  • 场景一:实时显示字幕
    • 如果 paragraph=falsedefinite=false,用序号大的字幕覆盖序号小的。
    • 如果 paragraph=falsedefinite=true,重新开始新的一句话,覆盖前一句话。
    • 如果 paragraph=true,则说明一整句话结束。此时如果继续解析显示字幕,字幕会重复显示。
  • 存储字幕
    如果仅为了存储字幕,可只保存 definite=trueparagraph=true 的字幕,减少存储的数据量,并确保保存的字幕是完整的。

FAQ

  • Q1:是否支持字幕在用户和智能体说话之前就返回给业务端?
    不支持。
  • Q2:字幕是否支持接收图片?
    不支持。
  • Q3:微信小程序端是否支持字幕功能?
    支持。
  • Q4:收到的字幕与实际内容相比有很多错误同音字。
    不同模型对于同音字的识别结果不同。你可以通过提升语音识别的准确性,来提升字幕准确度。具体操作,请参见提升语音识别准确性
  • Q5:字幕是否支持返回超链接内容?
    不支持。