You need to enable JavaScript to run this app.
导航
实时字幕
最近更新时间:2024.09.29 17:18:05首次发布时间:2024.09.03 11:03:15

在进行对话式 AI 实时交互场景下,你可能需要实时字幕显示你与智能体的对话内容。在 StartVoiceChat接口中配置 SubtitleConfig结构体参数后,你可以开启房间内字幕回调功能。开启此功能后,你可在应用服务器和客户端接收字幕结果,以做后续操作。客户端通过 onRoomBinaryMessageReceived 接口实时返回字幕结果,你可以用来显示实时字幕。服务端通过设定的ServerMessageUrl按照每句话返回字幕结果,你可以用来存储分析对话数据。

服务端

字幕回调格式

返回的字幕回调格式如下:

参数名类型描述
messageStringBase 64 编码的二进制消息内容。格式参看二进制消息格式
signatureString鉴权签名。可与StartVoiceChat接口中传入的ServerMessageSignature字段值进行对比以进行鉴权验证。

二进制消息格式如下:
alt

参数名类型描述
magic numberbinary消息格式,固定为 subtitle
lengthbinary字幕消息长度,单位为 bytes。存放方式为大端序。
subtitle_messagebinary字幕消息详细信息。格式参看subtitle_message 格式

subtitle_message 格式:

参数名类型描述
typeString消息格式,固定为 subtitle
datadata字幕详细信息。

data

参数名类型描述
textString字幕文本。
languageString字幕语言。
userIdString说话人 ID。
sequenceInt字幕序号。
definiteBoolean字幕是否为完整的一句话。
  • true:是。
  • false:否。
paragraphBoolean字幕是否为一段完整的文本。
  • true:是。
  • false:否。

解析字幕信息内容

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

客户端

字幕回调格式

参数名类型描述
uidString消息发送者 ID。
messageStringBase 64 编码的二进制消息内容。与服务端返回二进制消息格式相同,详细参看二进制消息格式

解析字幕回调信息

你可以参考以下示例代码对回调信息中的message内容进行解析。

//定义结构体
struct SubtitleMsgData {
    bool definite;
    std::string language;
    int mode;
    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(subtitles);
    // 存储解析后的数据
    std::vector<SubtitleMsgData> subtitles;
    // 遍历 JSON 数据并填充结构体
    for (const auto& item : json_data["data"]) {
        SubtitleMsgData subData;
        subData.definite = item["definite"];
        subData.language = item["language"];
        subData.mode = item["mode"];
        subData.paragraph = item["paragraph"];
        subData.sequence = item["sequence"];
        subData.text = item["text"];
        subData.userId = item["userId"];
        subtitles.push_back(subData);
    }
}