在与智能体对话过程中,系统会自动生成用户和智能体的对话文本。你可以通过客户端或服务端实时获取该数据,用于实时展示字幕、存储对话记录用于后续分析,或根据字幕状态(如整句结束)触发新一轮对话。
场景 | 描述 |
|---|---|
实时显示字幕 | 将真人用户和智能体的语音实时转为文字,并在应用终端界面上展示,提升用户理解和参与度。 |
业务存储分析 | 将真人用户和智能体的对话文本内容记录并存储下来,用于后续的业务数据分析、服务质量监控或模型优化。 |
根据字幕返回时机手动触发新一轮对话 | 利用字幕返回的特定状态(如整句话结束)作为信号,手动触发下一轮对话开启,实现更灵活的交互逻辑。 |
适用于需要在终端界面实时展示字幕的场景。数据通过客户端 SDK 回调获取。
调用 StartVoiceChat 在启动智能体时,在 Config 中添加 SubtitleConfig 配置:
配置参数 | 说明 |
|---|---|
| 设置为 |
|
|
SubtitleConfig 配置示例
// 仅展示字幕相关配置,其他 ASR/TTS/LLM 配置请参考对应文档 "Config": { // ... 其他配置 ... "SubtitleConfig": { "DisableRTSSubtitle": false, // 开启客户端字幕回调 "SubtitleMode": 0 // 0: 对齐音频时间戳 } }
通过 ByteRTC SDK 的 onRoomBinaryMessageReceived(嵌入式硬件场景使用on_message_received)回调接收字幕结果。
收到的字幕结果为二进制,使用前需解析。字幕回调格式如下:
参数名 | 类型 | 描述 |
|---|---|---|
uid | String | 消息发送者 ID。 |
message | String | 字幕消息内容(二进制格式),详见下方的二进制消息格式说明。 |
二进制消息格式如下:
参数名 | 类型 | 描述 |
|---|---|---|
magic number | binary | 消息格式,固定为 |
length | binary | 字幕消息长度,单位为 bytes。存放方式为大端序。 |
subtitle_message | binary | 字幕消息详细信息。格式参看subtitle_message 格式。 |
subtitle_message
参数名 | 类型 | 描述 |
|---|---|---|
type | String | 消息格式,固定为 |
data | 字幕详细信息,包含字幕的文本、语言、说话人 ID 等具体信息。 |
data
参数名 | 类型 | 描述 |
|---|---|---|
text | String | 字幕文本。 |
language | String | 字幕语言。 |
userId | String | 字幕来源者 ID。若字幕来源为真人用户,则该值为真人用户 UserId。若来源为智能体,则该值为智能体 UserId。 |
sequence | Int | 字幕序号。 |
definite | Boolean | 字幕是否为完整的分句。
|
paragraph | Boolean | 字幕是否为完整的一句话。
|
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 请求推送到业务服务器。
调用 StartVoiceChat 在启动智能体时,在 Config 中添加 SubtitleConfig 配置:
配置参数 | 说明 |
|---|---|
| 业务服务端接收字幕结果的 URL 地址。你指定的 URL 地址将收到来自 RTC 服务器的 HTTP(S) POST 请求发送的字幕结果,格式为二进制,使用前需解析。 |
| 鉴权签名。你可传入该鉴权字段,在收到字幕结果后,与结果中的 |
|
|
SubtitleConfig 示例
// 仅展示字幕相关配置,其他 ASR/TTS/LLM 配置请参考对应文档 "Config": { // ... 其他配置 ... "SubtitleConfig": { "ServerMessageUrl": "https://example-domain.com/vertc/subtitle", "ServerMessageSignature": "b46ab****ad6a", "SubtitleMode": 0 // 0: 对齐音频时间戳 } }
你指定的 URL 地址将收到来自 RTC 服务器的 HTTP(S) POST 请求发送的字幕结果,格式为二进制。具体格式如下:
参数名 | 类型 | 描述 |
|---|---|---|
message | String | Base 64 编码的二进制消息内容。格式参看二进制消息格式。 |
signature | String | 鉴权签名。可与 |
二进制消息格式如下:
参数名 | 类型 | 描述 |
|---|---|---|
magic number | binary | 消息格式,固定为 |
length | binary | 字幕消息长度,单位为 bytes。存放方式为大端序。 |
subtitle_message | binary | 字幕消息详细信息。格式参看subtitle_message 格式。 |
subtitle_message 格式:
参数名 | 类型 | 描述 |
|---|---|---|
type | String | 消息格式,固定为 |
data | 字幕详细信息,包含字幕的文本、语言、说话人 ID 等具体信息。 |
data
参数名 | 类型 | 描述 |
|---|---|---|
text | String | 字幕文本。 |
language | String | 字幕语言。 |
userId | String | 字幕来源者 ID。若字幕来源为真人用户,则该值为真人用户 UserId。若来源为智能体,则该值为智能体 UserId。 |
sequence | Int | 字幕序号。 |
definite | Boolean | 字幕是否为完整的分句。
|
paragraph | Boolean | 字幕是否为完整的一句话。
|
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. 智能体)的不同而有差异。
真人用户 | 智能体 | |
|---|---|---|
客户端回调 |
|
|
服务端回调 |
|
|
你可以根据 definite、 paragraph 和 sequence 字段处理字幕。
paragraph=false,definite=false,用序号大的字幕覆盖序号小的。paragraph=false,definite=true,重新开始新的一句话,覆盖前一句话。paragraph=true,则说明一整句话结束。此时如果继续解析显示字幕,字幕会重复显示。definite=true 且 paragraph=true 的字幕,减少存储的数据量,并确保保存的字幕是完整的。