展示字幕
本文介绍如何展示用户在与智能体进行语音通话的过程中的字幕。如下:
- 用户说话内容:流式展示用户正在说的话(语音识别(ASR)的实时结果)
- 智能体说话内容:流式展示智能体输出的内容(大语言模型(LLM)实时的输出结果)
前提条件
已按照 快速开始 文档集成 ZEGO Express SDK 和 AI Agent 并实现基本的语音通话功能。
快速实现
用户与智能体进行语音对话期间,AI Agent 服务端通过 RTC 房间自定义消息下发 ASR 识别文本和 LLM 回答的文本。客户端可以监听房间自定义消息,解析对应的状态事件来渲染 UI 。
RTC 房间自定义消息的处理流程如下:
flowchart TD
Start([开始]) --> Init[实现onRecvExperimentalAPI回调并初始化字幕UI组件]
Init --> ParseMessage[解析RTC房间自定义消息]
ParseMessage --> |Cmd=3| ProcessASR[处理ASR文本]
ParseMessage --> |Cmd=4| ProcessLLM[处理LLM文本]
ProcessASR --> UpdateSubtitles1[更新用户字幕]
ProcessLLM --> UpdateSubtitles2[更新智能体字幕]
UpdateSubtitles1 --> HandleEndFlags[消息结束后清理消息缓存]
UpdateSubtitles2 --> HandleEndFlags[消息结束后清理消息缓存]
HandleEndFlags --> End([结束])
监听房间自定义消息
客户端可通过监听 onRecvExperimentalAPI
回调获取 method
为 liveroom.room.on_recive_room_channel_message
的房间自定义消息。以下是监听回调的示例代码:
ZegoExpressEngine.getEngine().setEventHandler(new IZegoEventHandler() {
@Override
public void onRecvExperimentalAPI(String content) {
super.onRecvExperimentalAPI(content);
try {
// 第一步:将 content 解析为 JSONObject
JSONObject json = new JSONObject(content);
// 第二步:检查 method 字段的值
if (json.has("method") && json.getString("method")
.equals("liveroom.room.on_recive_room_channel_message")) {
// 第三步:获取 params 并解析
JSONObject paramsObject = json.getJSONObject("params");
String msgContent = paramsObject.getString("msg_content");
// JSON 字符串示例:"{\"Timestamp\":1745224717,\"SeqId\":1467995418,\"Round\":2132219714,\"Cmd\":3,\"Data\":{\"MessageId\":\"2135894567\",\"Text\":\"你\",\"EndFlag\":false}}"
// 将 JSON 字符串解析为 AudioChatMessage 对象
AudioChatMessage chatMessage = gson.fromJson(msgContent, AudioChatMessage.class);
if (chatMessage.cmd == 3) {
updateASRChatMessage(chatMessage);
} else if (chatMessage.cmd == 4) {
addOrUpdateLLMChatMessage(chatMessage);
}
}
} catch (JSONException e) {
e.printStackTrace();
}
}
});
/**
* 语音聊天界面,接收到的后台服务器发过来的 房间内的聊天消息的结构体
*/
public static class AudioChatMessage {
@SerializedName("Timestamp")
public long timestamp;
@SerializedName("SeqId")
public Number seqId;
@SerializedName("Round")
public Number round;
@SerializedName("Cmd")
public Number cmd;
@SerializedName("Data")
public Data data;
public static class Data {
@SerializedName("SpeakStatus")
public Number speakStatus;
@SerializedName("Text")
public String text;
@SerializedName("MessageId")
public String messageId;
@SerializedName("EndFlag")
public boolean endFlag;
}
}
1
客户端可以通过实现ZegoEventHandler协议,监听 onRecvExperimentalAPI
回调获取 method
为 liveroom.room.on_recive_room_channel_message
的房间自定义消息。以下是监听回调的示例代码:
YourService.h/m
YourViewController.h/m
// 实现ZegoEventHandler协议
@interface YourService () <ZegoEventHandler>
@property (nonatomic, strong) YourViewController *youViewController;
@end
@implementation YourService
// 处理express onRecvExperimentalAPI接收到的消息
- (void)onRecvExperimentalAPI:(NSString *)content {
// 转发给view解析消息内容
[self.youViewController handleExpressExperimentalAPIContent:content];
}
@end // YourService implementation
1
// 在头文件中实现ZegoEventHandler协议
@interface YourViewController () <ZegoEventHandler>
@end
@implementation YourViewController
// 解析自定义信令消息
- (void)handleExpressExperimentalAPIContent:(NSString *)content {
// 解析JSON内容
NSError *error;
NSData *jsonData = [content dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *contentDict = [NSJSONSerialization JSONObjectWithData:jsonData
options:NSJSONReadingMutableContainers
error:&error];
if (error || !contentDict) {
NSLog(@"JSON解析失败: %@", error);
return;
}
// 检查是否为房间消息
NSString *method = contentDict[@"method"];
if (![method isEqualToString:@"liveroom.room.on_recive_room_channel_message"]) {
return;
}
// 获取消息参数
NSDictionary *params = contentDict[@"params"];
if (!params) {
return;
}
NSString *msgContent = params[@"msg_content"];
NSString *sendIdName = params[@"send_idname"];
NSString *sendNickname = params[@"send_nickname"];
NSString *roomId = params[@"roomid"];
if (!msgContent || !sendIdName || !roomId) {
NSLog(@"parseExperimentalAPIContent 参数不完整: msgContent=%@, sendIdName=%@, roomId=%@",
msgContent, sendIdName, roomId);
return;
}
// JSON 字符串示例:"{\"Timestamp\":1745224717,\"SeqId\":1467995418,\"Round\":2132219714,\"Cmd\":3,\"Data\":{\"MessageId\":\"2135894567\",\"Text\":\"你\",\"EndFlag\":false}}"
// 解析消息内容
[self handleMessageContent:msgContent userID:sendIdName userName:sendNickname ?: @""];
}
// 处理消息内容
- (void)handleMessageContent:(NSString *)command userID:(NSString *)userID userName:(NSString *)userName{
NSDictionary* msgDict = [self dictFromJson:command];
if (!msgDict) {
return;
}
// 解析基本信息
Number cmd = [msgDict[@"Cmd"] intValue];
int64_t seqId = [msgDict[@"SeqId"] longLongValue];
int64_t round = [msgDict[@"Round"] longLongValue];
int64_t timestamp = [msgDict[@"Timestamp"] longLongValue];
NSDictionary *data = msgDict[@"Data"];
// 根据命令类型处理消息
switch (cmd) {
case 3: // ASR文本
[self handleAsrText:data seqId:seqId round:round timestamp:timestamp];
break;
case 4: // LLM文本
[self handleLlmText:data seqId:seqId round:round timestamp:timestamp];
break;
}
}
@end // YourViewController implementation
1
客户端可通过监听 recvExperimentalAPI
回调获取 method
为 onRecvRoomChannelMessage
的房间自定义消息。以下是监听回调的示例代码:
zg.on("recvExperimentalAPI", (result) => {
const { method, content } = result;
if (method === "onRecvRoomChannelMessage") {
try {
// 解析消息
const recvMsg = JSON.parse(content.msgContent);
const { Cmd, SeqId, Data, Round } = recvMsg;
} catch (error) {
console.error("解析消息失败:", error);
}
}
});
// 启用 onRecvRoomChannelMessage 实验性 API
zg.callExperimentalAPI({ method: "onRecvRoomChannelMessage", params: {} });
1
房间自定义消息协议
房间自定义消息的各字段说明如下:
字段 | 类型 | 描述 |
---|
Timestamp | Number | 时间戳,秒级别 |
SeqId | Number | 包序列号,可能乱序,请根据序列号对消息进行排序。极端情况下 Id 可能不连续。 |
Round | Number | 对话轮次,每次用户主动说话轮次增加 |
Cmd | Number | 3: 语音识别(ASR)的文本 4: LLM 文本 |
Data | Object | 具体内容,各Cmd对应不同Data |
Cmd 不同对应的 Data 也不同,具体如下:
处理逻辑
根据 Cmd 字段判断消息类型,并根据 Data 字段获取消息内容。
使用字幕组件
您还可以直接下载字幕处理类源码到您的项目中直接使用。
您还可以直接下载字幕组件源码到您的项目中直接使用。
如果您是 Vue 项目,可以直接下载字幕处理hook到您的项目中直接使用。
注意事项
- 消息排序处理:通过房间自定义消息收到的数据可能会乱序,需要根据 SeqId 字段进行排序。
- 流式文本处理:
- ASR 文本每次下发的是全量文本,同一个 MessageId 的消息需要完全替换之前的内容。
- LLM 文本每次下发的是增量文本,同一个 MessageId 的消息需要在排序后累加显示。
- 内存管理:请及时清理已完成的消息缓存,特别是当用户进行长时间对话时。