从 Protobuf 中提取文本数据
Protobuf(全称 Protocol Buffers)是由 Google 开发的一种高效的、跨语言的序列化数据格式,支持多种编程语言,包括 C++、Java、Python、Go 等,可在不同系统间进行数据交换。具有高效、灵活和易于使用的特点。
声网语音转文字使用 Protobuf 并通过一个 SttMessage.proto 文件定义语音转文字的消息格式,然后将转写后的文本数据序列化为高效的传输格式(例如二进制或 json),并通过数据流进行传输。本文介绍接收端如何在收到数据流后,通过 Protobuf Compiler(protoc)生成目标语言的代码并对收到的数据进行反序列化。你可以从反序列化后的数据结构中提取具体的文本字段。
前提条件
请按照以下要求准备开发环境:
-
可以访问互联网的计算机。如果你的网络环境部署了防火墙,参考应对防火墙限制以正常使用声网服务。
-
已安装 Protobuf Compiler,详见 Protobuf Compiler Installation。
注意由于 Protobuf 的格式在不同版本会有差异,声网建议生成代码和客户端反序列化使用的 Protobuf SDK 版本保持一致。
-
一个有效的声网账号以及声网项目。请确保你的项目已开通语音转文字功能,并参考开通服务从声网控制台获得以下信息:
- App ID:声网随机生成的字符串,用于识别你的项目。
- 临时 Token:Token 也称为动态密钥,在客户端加入频道时对用户鉴权。临时 Token 的有效期为 24 小时。
-
作为主播加入频道发流。你可以参考实现纯语音互动来在频道中发流。
实现流程
本节介绍接收端如何使用 protoc 编译生成不同语言的示例代码来反序列化接收到的数据。
使用 protoc 生成源代码
参考下列步骤来编写一个脚本调用 protoc 编译器以生成不同语言的代码。
创建脚本并生成源码
你可以根据自己实际的业务需求,选择生成不同语言的代码。下文提供生成 Java、Objective-C、C#、JavaScript 语言的代码脚本。
- Java
- Objective-C
- C#
- JavaScript
创建一个 Shell 脚本,将其命名为 generate_code.sh,然后在其中添加如下代码:
#!/bin/sh
# 指定 protoc 编译器的路径,示例代码中使用的 Protobuf 版本为 21.12,你可以根据你的实际需求替换
PROTOC_PATH=./protoc-21.12-osx-aarch_64/bin/protoc
# 指定 .proto 文件的路径,文件中数据结构的详细描述见参考信息
PROTO_FILE=./SttMessage.proto
# 指定输出目录
JAVA_OUT_DIR=$(pwd)/code/java
# 创建输出目录(如果不存在)
mkdir -p $JAVA_OUT_DIR
# 生成 Java 代码
$PROTOC_PATH --java_out=$JAVA_OUT_DIR $PROTO_FILE
# 生成代码完成后输出提示信息
echo "Generate code finished."
如果你需要生成 Objective-C 代码,请确保在生成代码前已经安装 Protobuf 的相关依赖。你可以参考下列步骤来安装依赖,如果已有依赖,可跳过此步骤。
-
打开你的项目的
Podfile文件,并在其中添加如下代码:Ruby# 21.12 表示 Protobuf 版本,你可以根据实际需求选择合适的版本
pod "Protobuf", "3.21.12" -
在 Terminal 中进入到包含
Podfile文件的目录下,运行pod install命令,CocoaPods 会下载、安装指定的依赖库版本。
成功安装后,项目文件夹下会生成一个后缀为 .xcworkspace 的文件,通过 Xcode 打开该文件进行后续操作。
创建一个 Shell 脚本,将其命名为 generate_code.sh,然后在其中添加如下代码:
#!/bin/sh
# 指定 protoc 编译器的路径,示例代码中使用的 Protobuf 版本为 21.12,你可以根据你的实际需求替换
PROTOC_PATH=./protoc-21.12-osx-aarch_64/bin/protoc
# 指定 .proto 文件的路径,文件中数据结构的详细描述见参考信息
PROTO_FILE=./SttMessage.proto
# 指定输出目录
OBJC_OUT_DIR=$(pwd)/code/objective-c
# 创建输出目录(如果不存在)
mkdir -p $OBJC_OUT_DIR
# 生成 Objective-C 代码
$PROTOC_PATH --objc_out=$OBJC_OUT_DIR $PROTO_FILE
# 生成代码完成后输出提示信息
echo "Generate code finished."
创建一个 Shell 脚本,将其命名为 generate_code.sh,然后在其中添加如下代码:
#!/bin/sh
# 指定 protoc 编译器的路径,示例代码中使用的 Protobuf 版本为 21.12,你可以根据你的实际需求替换
PROTOC_PATH=./protoc-21.12-osx-aarch_64/bin/protoc
# 指定 .proto 文件的路径,文件中数据结构的详细描述见参考信息
PROTO_FILE=./SttMessage.proto
# 指定输出目录
CSHARP_OUT_DIR=$(pwd)/code/csharp
# 创建输出目录(如果不存在)
mkdir -p $CSHARP_OUT_DIR
# 生成 Objective-C 代码
$PROTOC_PATH --csharp_out=$CSHARP_OUT_DIR $PROTO_FILE
# 生成代码完成后输出提示信息
echo "Generate code finished."
如果你需要生成 JavaScript 代码,请确保在生成代码前已经安装 Protobuf 的相关依赖。你可以参考下列步骤来安装依赖,如果已有依赖,可跳过此步骤。
-
打开你的项目根目录,编辑
package.json文件,添加以下依赖项:JSON{
"dependencies": {
...
"protobufjs": "^7.2.5"
},
"devDependencies": {
...
"pbjs": "^0.0.14",
"protobufjs-cli": "^1.1.2"
}
}你可以根据实际需求指定 Protobuf 库和 protobufjs 命令行工具的版本。
-
在终端运行以下命令以安装依赖:
Shellnpm install
创建一个 Shell 脚本,将其命名为 generate_code.sh,然后在其中添加如下代码:
# 将 protobufjs-cli 的可执行文件路径添加到 PATH 环境变量
# 需要将 {absolute path of protobufjs-cli in your node_modules}/bin 替换为 protobufjs-cli 在 node_modules 中的绝对路径
export "PATH=$PATH:{absolute path of protobufjs-cli in you node_modules}/bin"
# 生成 javascript 的示例代码
pbjs -t json-module -w es6 ./SttMessage.proto > ./SttMessage_es6.js
echo "JavaScript code generation finished."
运行脚本
在终端中运行以下命令来运行脚本:
# 将脚本设置为可执行文件
chmod +x generate_code.sh
# 运行脚本
./generate_code.sh
反序列化数据
客户端接收到数据流时,SDK 会触发接收到数据流消息的回调。本节介绍如何对接收到的数据进行反序列化,将其转换回数据结构或对象。以下为不同语言的示例代码:
- Java
- C#
- JavaScript
- Objective-C
- Swift
// 加入频道,添加回调事件
rtcManager.joinChannel(roomName, localUid, agora_token, roleType.equals(ROLE_TYPE_BROADCAST), new RtcManager.OnChannelListener() {
...
// 接收到数据流消息的回调
@Override
public void onStreamMessage(int uid, int streamId, byte[] data) {
// 检查远端用户 ID 是否为指定的推流机器人 ID,如果是则解码数据流为文本对象
if (String.valueOf(uid).equalsIgnoreCase(RTC_UID_STT_STREAM)) {
AgoraSpeech2TextProtobuffer.Text text = STTManager.getInstance().parseTextByte(roomName, data);
// 将解析后的文本对象转换为 JSON 格式并打印日志
LogUtil.d(originLogName, mGson.toJson(text));
}
}
...
});
public AgoraSpeech2TextProtobuffer.Text parseTextByte(String channel, byte[] data) {
// 声明一个 AgoraSpeech2TextProtobuffer.Text 类型的变量,用于存储反序列化后的对象
AgoraSpeech2TextProtobuffer.Text textStream;
try {
// 将字节数组 data 反序列化为 AgoraSpeech2TextProtobuffer.Text 对象
textStream = AgoraSpeech2TextProtobuffer.Text.parseFrom(data);
} catch (Exception ex) {
notifyErrorHandler(new ErrorInfo("parseTextByte", "-1", "parseTextByte parseFrom error >> " + ex.toString()));
return null;
}
...
}
private void InitRtcEngine()
{ // 创建一个 RTC 引擎实例
RtcEngine = Agora.Rtc.RtcEngine.CreateAgoraRtcEngine();
// 创建一个事件处理类的实例
AgoraEventHandler handler = new AgoraEventHandler(this);
// 创建 RtcEngineContext 对象,并设置频道场景为直播
RtcEngineContext context = new RtcEngineContext(_appID, 0,
CHANNEL_PROFILE_TYPE.CHANNEL_PROFILE_LIVE_BROADCASTING,
AUDIO_SCENARIO_TYPE.AUDIO_SCENARIO_DEFAULT);
// 初始化引擎
RtcEngine.Initialize(context);
// 添加回调事件
RtcEngine.InitEventHandler(handler);
}
// 定义一个类用于处理 RTC 相关回调,继承自 IRtcEngineEventHandler
internal class AgoraEventHandler: IRtcEngineEventHandler
{ // 接收到数据流消息的回调
public override void OnStreamMessage(RtcConnection connection, uint remoteUid, int streamId, byte[] data, uint length, ulong sentTs)
{
// Debug.Log(String.Format("remoteUid: {0}", remoteUid));
// 如果远端用户 ID 等于指定的推流机器人 ID
if (remoteUid == {pusher bot uid}) {
// 解析 Protobuf 数据
AgoraSTTSample.Protobuf.Text t = ProtobufUtility.ParseProtobufData(data);
...
}
}
}
import AgoraRTC from "agora-rtc-sdk-ng"
import protoRoot from "@/protobuf/SttMessage_es6.js"
// 创建 RTC 客户端实例
this.rtc.client = AgoraRTC.createClient({ mode: "live", codec: "vp8", role: this.role })
// 监听数据流事件并绑定事件处理函数
this.rtc.client.on("stream-message", this.onStreamMessage.bind(this))
// 接收到数据流消息的回调
function onStreamMessage(uid, stream) {
// 检查远端用户 ID 是否为指定的推流机器人 ID,如果不是则直接返回,不进行后续处理
if (uid != {pusher bot uid}) {
return
}
// 使用 Protobuf 解码收到的数据流
let textstream = protoRoot.Agora.SpeechToText.lookup("Text").decode(data)
...
}
// Temp.h
#import "AgoraRtcKit/AgoraRtcKit.h"
#import "./Protobuff/SttMessage.pbobjc.h"
NS_ASSUME_NONNULL_BEGIN
@interface Temp : NSObject<AgoraRtcEngineDelegate>
@end
NS_ASSUME_NONNULL_END
// Temp.m
@implementation Temp
// 接收到数据流消息的回调
- (void)rtcEngine:(AgoraRtcEngineKit *)engine receiveStreamMessageFromUid:(NSUInteger)uid streamId:(NSInteger)streamId data:(NSData *)data {
// 检查远端用户 ID 是否为指定的推流机器人 ID,如果不是则直接返回,不进行后续处理
if (uid != pusherUid) {
return;
}
NSError* error;
// 解码收到的数据流
SttText* st = [SttText parseFromData: data error: &error];
...
}
@end
// 接收到数据流消息的回调
func rtcEngine(_ engine: AgoraRtcEngineKit, receiveStreamMessageFromUid uid: UInt, streamId: Int, data: Data) {
// 检查远端用户 ID 是否为指定的推流机器人 ID,如果不是则直接返回,不进行后续处理
guard uid == {puher bot uid} else {
return
}
// 解码收到的数据流
let text = try? SttText.parse(from: data)
...
}
参考信息
本节提供使用 Protobuf 反序列化数据的其他相关信息。
示例项目
声网提供了开源的语音转文字示例项目供你参考,你可以前往下载或查看其中的源代码。
SttMessage.proto 文件说明
声网提供的 SttMessage.proto 文件中定义了转写后的文本数据结构,各字段说明详见如下:
syntax = "proto3";
package Agora.SpeechToText;
option objc_class_prefix = "Stt";
option csharp_namespace = "AgoraSTTSample.Protobuf";
option java_package = "io.agora.rtc.speech2text";
option java_outer_classname = "AgoraSpeech2TextProtobuffer";
message Text {
reserved 1 to 3, 5, 7 to 9, 11, 17;
int64 uid = 4;
int64 time = 6;
repeated Word words = 10;
int32 duration_ms = 12;
string data_type = 13;
repeated Translation trans = 14;
string culture = 15;
int64 text_ts = 16;
OriginalTranscript original_transcript = 18;
}
message Word {
reserved 2, 3, 5;
string text = 1;
bool is_final = 4;
}
message Translation {
bool is_final = 1;
string lang = 2;
repeated string texts = 3;
}
message OriginalTranscript {
string culture = 1;
repeated Word words = 2;
}
Text 消息类型及其字段说明
| 字段名称 | 类型 | 含义 |
|---|---|---|
uid | int64 | 文本所对应的用户 ID。 |
time | int64 | 该句段转写的起始时间。仅在 isFinal 为 true 时有值,其他时候为 0。 |
words | repeated | 转写结果的数组,详见 Word 消息类型。 |
duration_ms | int32 | 转写文本的时长,单位为毫秒。 |
data_type | string | 数据类型:
|
trans | repeated | 翻译结果的数组,详见 Translation 消息类型。 |
culture | string | 转写的源语言。 |
text_ts | int64 | 转写结果的时间戳,持续递增,用于实时翻译时原文和译文的对齐。 |
original_transcript | OriginalTranscript | 转写后的文本,用于翻译,详见 OriginalTranscript。 |
Word 消息类型及其字段说明
| 字段名称 | 类型 | 含义 |
|---|---|---|
text | string | 转写的结果。 |
is_final | bool | 该句是否为转写的最终结果:
true 时表明转写引擎认为该句的文字转写结果已经确定,无需再进行修改,但并不代表这句话在语义上已经结束。 |
Translation 消息类型及其字段说明
| 字段名称 | 类型 | 含义 |
|---|---|---|
is_final | bool | 该句是否为翻译的最终结果:
true 时表明翻译引擎认为该句的翻译结果已经确定,无需再进行修改,但并不代表这句话在语义上已经结束。 |
lang | string | 翻译的目标语言。 |
texts | repeated | 翻译的结果。 |
OriginalTranscript 消息类型及其字段说明
| 字段名称 | 类型 | 含义 |
|---|---|---|
culture | string | 转写的源语言。 |
words | repeated | 转写结果的数组,详见 Word 消息类型。 |
示例数据
本节提供 transcribe 和 translate 两种数据类型的示例。
transcribe
- protobuf
- json
time: 1753359518654
words {
text: "Hello, how are you?"
is_final: true
}
duration_ms: 770
data_type: "transcribe"
culture: "en-US"
text_ts: 1753359520754
{
"transcript": {
"uid": 222,
"language": "zh-CN",
"text": "北选手",
"isFinal": false,
"offset": 1751438272384,
"duration": 760,
"textTs": 1751438273939
}
}
translate
- protobuf
- json
time: 1753359518654
duration_ms: 770
data_type: "translate"
trans {
is_final: true
lang: "es-ES"
texts: "Hola, ¿cómo estás? "
}
text_ts: 1753359520754
original_transcript {
culture: "en-US"
words {
text: "Hello, how are you?"
is_final: true
}
}
{
"translation": {
"uid": 222,
"isFinal": true,
"offset": 1751438270274,
"duration": 1320,
"textTs": 1751438272825,
"results0": {
"language": "ja-JP",
"texts": [
"4月13日。 "
]
},
"original_transcript": {
"language": "zh-CN",
"text": "4月13日。"
}
}
}