注意
本文档不适用于实时对话式 AI 场景,该场景是通过接口 StartVoiceChat
实现字幕生成和接收,具体请参见实时对话式 AI 字幕。
通过实时字幕功能,可以将用户的语音实时转换为文字,并支持将文字翻译成其他语言,可用于直播字幕、会议记录生成、跨语言沟通实时翻译等场景。
支持通过客户端或服务端实现该功能。
根据使用场景的不同,可生成以下 2 种类型的字幕:
特性 | 按源语言生成字幕(无需翻译) | 生成指定语言的字幕(将语音转为文字后并翻译) |
---|---|---|
作用 | 识别用户语音,直接按照源语言转为文字,适用于实时字幕显示场景。 | 识别用户语音,先按照源语言转为文字,再被翻译为指定目标语言,适用于跨语言实时翻译场景。 |
所用服务 | 火山的流式语音识别 | 火山的机器翻译 |
可识别的语种 | 不同场景,支持的语言不同,具体请参见语种支持 | 仅支持 zh、ja、en |
翻译字幕 | 不支持 | 支持。支持翻译为的目标语言,请参见语言支持 |
返回的字幕数据 | 仅返回字幕原文 | 同时返回字幕原文和字幕译文 |
费用 | 音频订阅费、流式语音识别费 | 音频订阅费、实时语音翻译费 |
使用实时字幕功能除基础音视频通话费以外,还会产生以下费用:
费用 | 说明 |
---|---|
启动实时字幕任务后,系统会自动分配一个有机器人加入房间,订阅需要生成字幕的音频流,产生音频订阅费。 | |
若使用了流式语音识别服务(即识别模式),会产生流式语音识别费。 | |
若是使用了机器翻译(即翻译模式),会产生实时语音翻译费。 |
以下分别介绍识别模式和翻译模式下的字幕生成方法。
开通服务。
流式语音识别
,并记录 APP ID,Access Token,和语言的 Cluster ID,以备使用。注意
生成字幕并接收字幕。
实现步骤:
相关接口说明,请参见相关API。
joinRoom
接口进房时,设置源语言。extraInfo
参数传入 "source_language": "语种代号"
。如未指定源语言,SDK 会将系统语种设定为源语言。如果你的系统语种不是中文、英文和日文,此时 SDK 会自动将中文设为源语言。startSubtitle
启动字幕服务,模式(mode
)设置为识别模式,无需设置目标翻译语言。onSubtitleStateChanged
回调获取字幕内容,包括用户 ID、字幕文本、语言、是否为完整句子等。stopSubtitle
关闭字幕。开通服务。
生成字幕并接收字幕结果。
实现步骤:
相关接口说明,请参见相关API。
joinRoom
接口进房时,设置源语言。startSubtitle
启动字幕服务。模式(mode
)选择翻译模式,并指定字幕要翻译的语言。onSubtitleStateChanged
回调接收字幕生成结果。stopSubtitle
关闭字幕。stopSubtitle
接口停止。字幕回调格式
返回的字幕回调格式如下:
参数名 | 类型 | 描述 |
---|---|---|
message | String | 生成的字幕数据,格式为Base 64 编码的二进制,具体说明参看二进制消息格式。 |
signature | String | 鉴权签名。可与 startSubtitle 接口中传入的 signature 字段值进行对比,以进行鉴权验证。 |
二进制消息格式:
参数名 | 类型 | 描述 |
---|---|---|
magic number | binary | 消息格式标识,固定为 subc 。 |
length | binary | 字幕消息(subtitle_message)的长度,单位为 bytes,存放方式为大端序(Big-Endian)。 |
subtitle_message | binary | 字幕详细信息,格式参看 subtitle_message。 |
subtitle_message:
参数名 | 类型 | 描述 |
---|---|---|
type | String | 消息类型,固定为 subtitle(表示消息类型为字幕)。 |
data | data | 字幕详细信息,包含字幕的文本、语言、说话人 ID 等具体信息。 |
data:
参数名 | 类型 | 描述 |
---|---|---|
text | String | 字幕文本。 |
language | String | 字幕语言。 |
userId | String | 说话人 ID。 |
sequence | Int | 字幕序号。 |
definite | Boolean | 字幕是否为完整的一句话:
|
paragraph | Boolean | 字幕是否为一段完整的文本:
|
字幕解析示例
你可以参考以下示例代码对回调信息中的message
内容进行解析。
const ( subtitleHeader = "subc" exampleSignature = "example_signature" ) type RtsMessage struct { Message string `json:"message"` Signature string `json:"signature"` } type subc 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 } subc, err := Unpack(msg.Message) if err != nil { fmt.Printf("Unpack failed,err:%v\n", err) return } fmt.Println(subc) //业务逻辑 c.String(200, "ok") } func Unpack(msg string) (*subc, 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:] subc := &subc{} err = json.Unmarshal(subData, subc) if err != nil { return nil, fmt.Errorf("Unmarshal failed,err:%v\n", err) } return subc, 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 "subc" 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.paragraph = item["paragraph"]; subData.sequence = item["sequence"]; subData.text = item["text"]; subData.userId = item["userId"]; subtitles.push_back(subData); } }
渲染字幕
在前端显示字幕时,可以根据 definite
和 sequence
字段来决定如何更新字幕。
definite=false
,用序号大的字幕覆盖序号小的。definite=true
,重新开始新的一句话,覆盖前一句话。存储字幕
如果仅为了存储字幕,可只保存 definite=true
的字幕,减少存储的数据量,并确保保存的字幕是完整的。