在进行实时对话式 AI场景下,你可能需要实时字幕显示你与智能体的对话内容。在 StartVoiceChat
接口中配置 SubtitleConfig
结构体参数后,你可以开启房间内字幕回调功能。开启此功能后,你可在应用服务器和客户端接收字幕结果,以做后续操作。客户端通过 onRoomBinaryMessageReceived 接口实时返回字幕结果,你可以用来显示实时字幕。服务端通过设定的ServerMessageUrl
按照每句话返回字幕结果,你可以用来存储分析对话数据。
返回的字幕回调格式如下:
参数名 | 类型 | 描述 |
---|---|---|
message | String | Base 64 编码的二进制消息内容。格式参看二进制消息格式。 |
signature | String | 鉴权签名。可与StartVoiceChat 接口中传入的ServerMessageSignature 字段值进行对比以进行鉴权验证。 |
二进制消息格式如下:
参数名 | 类型 | 描述 |
---|---|---|
magic number | binary | 消息格式,固定为 subv 。 |
length | binary | 字幕消息长度,单位为 bytes。存放方式为大端序。 |
subtitle_message | binary | 字幕消息详细信息。格式参看subtitle_message 格式。 |
subtitle_message 格式:
参数名 | 类型 | 描述 |
---|---|---|
type | String | 消息格式,固定为 subtitle 。 |
data | data | 字幕详细信息。 |
data
参数名 | 类型 | 描述 |
---|---|---|
text | String | 字幕文本。 |
language | String | 字幕语言。 |
userId | String | 说话人 ID。 |
sequence | Int | 字幕序号。 |
definite | Boolean | 字幕是否为完整的一句话。
|
paragraph | Boolean | 字幕是否为一段完整的文本。
|
subtitle_message 的示例如下:
{
"type": "subtitle",
"data" :[{
"text": "武松打虎是《水浒传》中的故事,主要讲述梁山好汉武松回家"
"language": "zh"
"userId": "User1",
"sequence":1,
"definite":false,
"paragraph":false
}]
}
{
"type": "subtitle",
"data" :[{
"text": "武松打虎是《水浒传》中的故事,主要讲述梁山好汉武松回家探望兄长,途经景阳冈时,酒家规定三碗不过冈,武松沽饮十八碗,醉后欲行赶路。"
"language": "zh"
"userId": "Bot_Name",
"sequence":2,
"definite":true,
"paragraph":false,
"mode":0
}]
}
你可以参考以下示例代码对回调信息中的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()
}
参数名 | 类型 | 描述 |
---|---|---|
uid | String | 消息发送者 ID。 |
message | String | Base 64 编码的二进制消息内容。与服务端返回二进制消息格式相同,详细参看二进制消息格式。 |
你可以参考以下示例代码对回调信息中的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);
}
}