实时字幕
与对话式智能体进行实时互动时,你可能需要实时字幕来显示你与智能体的对话内容。本文介绍如何在你的 App 中实现实时字幕功能。
技术原理
声网提供一套灵活可扩展、标准化的对话式 AI 引擎客户端组件(以下简称组件)。该组件支持 iOS、Android、Web 平台,封装了多个场景化 API,你只需要调用这些 API 即可结合声网实时互动 (RTC) SDK 和实时消息 (RTM) SDK 的能力实现以下功能:
组件通过 onTranscriptionUpdated
回调接收字幕转录内容,支持监听不同类型的字幕转录信息:
- 智能体字幕:智能体的语音转录内容,包括实时更新和最终结果。
- 用户字幕:用户的语音转录内容,支持实时显示和状态管理。
- 字幕转录状态:支持进行中、完成、中断等不同状态的处理。
以下为使用组件实现实时字幕功能的工作流程图:
前提条件
开始前,请确保完成以下准备工作:
- 已集成 RTC v4.5.1 及以上版本 SDK,且在 App 中实现了基本的实时音视频功能、获取了相关设备的使用权限。请参考实现音视频互动。
- 已在控制台为项目启用 RTM 服务,并在 App 中实现了基本的实时消息功能。请参考实现收发消息。
- 已参考实现对话式智能体实现与智能体对话的基本逻辑。
- 确保 RTC 可用、RTM 已登录,且 RTC 和 RTM 实例的生命周期大于组件的生命周期。组件内部不负责维护 RTC,RTM 的初始化、生命周期以及鉴权/登录状态的逻辑。
实现方法
集成组件
- Android
- iOS
- Web
初始化组件
为 RTC 和 RTM 实例创建配置对象,并设置转录字幕渲染模式,之后创建组件实例:
- Android
- iOS
- Web
// 为 RTC 和 RTM 实例创建配置对象
val config = ConversationalAIAPIConfig(
rtcEngine = rtcEngineInstance,
rtmClient = rtmClientInstance,
/**
* 设置转录字幕渲染模式,可选值:
* - `TranscriptionRenderMode.Word`:逐字渲染模式,即回调接收的字幕内容将逐字渲染到 UI 上。
* - `TranscriptionRenderMode.Text`:逐句渲染模式,即回调接收的字幕内容将全量渲染到 UI 上。
*/
renderMode = TranscriptionRenderMode.Word,
enableLog = true
)
// 创建组件实例
val api = ConversationalAIAPIImpl(config)
/// 为 RTC 和 RTM 实例创建配置对象
let config = ConversationalAIAPIConfig(
rtcEngine: rtcEngine,
rtmEngine: rtmEngine,
/**
* 设置转录字幕渲染模式,可选值:
* - `.words`:逐字渲染模式,即回调接收的字幕内容将逐字渲染到 UI 上。
* - `.text`:逐句渲染模式,即回调接收的字幕内容将全量渲染到 UI 上。
*/
renderMode: .words,
enableLog: true
)
/// 创建组件实例
convoAIAPI = ConversationalAIAPIImpl(config: config)
// 初始化组件
ConversationalAIAPI.init({
rtcEngine,
rtmEngine,
/**
* 设置转录字幕渲染模式,可选值:
* - `ESubtitleHelperMode.WORD`:逐字渲染模式,即回调接收的字幕内容将逐字渲染到 UI 上。
* - `ESubtitleHelperMode.TEXT`:逐句渲染模式,即回调接收的字幕内容将全量渲染到 UI 上。
*
* 不指定则自动根据消息判断模式,或手动指定模式
*/
renderMode: ESubtitleHelperMode.WORD,
})
// 获取 API 实例(单例)
const conversationalAIAPI = ConversationalAIAPI.getInstance()
注册字幕回调
调用 addHandler
方法,注册并实现字幕转录回调:
- Android
- iOS
- Web
// 注册回调
api.addHandler(object : IConversationalAIAPIEventHandler {
// 监听转录内容更新,用于实时显示字幕转录内容
override fun onTranscriptionUpdated(agentUserId: String, transcription: Transcription) {
when (transcription.type) {
TranscriptionType.AGENT -> {
// AI agent的转录内容
when (transcription.status) {
TranscriptionStatus.IN_PROGRESS -> {
// 转录正在进行中,实时更新显示
updateAgentTranscription(transcription.text, false)
}
TranscriptionStatus.END -> {
// 转录完成,最终显示
updateAgentTranscription(transcription.text, true)
}
TranscriptionStatus.INTERRUPTED -> {
// 转录被中断
updateAgentTranscription(transcription.text, true)
Log.d("Transcription", "Agent transcription interrupted")
}
}
}
TranscriptionType.USER -> {
// 用户的转录内容
when (transcription.status) {
TranscriptionStatus.IN_PROGRESS -> {
// 用户正在说话,实时显示
updateUserTranscription(transcription.text, false)
}
TranscriptionStatus.END -> {
// 用户说话完成
updateUserTranscription(transcription.text, true)
}
TranscriptionStatus.INTERRUPTED -> {
// 用户说话被中断
updateUserTranscription(transcription.text, true)
Log.d("Transcription", "User transcription interrupted")
}
}
}
}
}
})
// 让你的类遵循 ConversationalAIAPIEventHandler 协议
class ConversationViewController: UIViewController, ConversationalAIAPIEventHandler {
// 用于去重的转录缓存
private var transcriptionCache: [Int: String] = [:]
func onTranscriptionUpdated(agentUserId: String, transcription: Transcription) {
// 监听转录内容更新,用于实时显示字幕转录内容
DispatchQueue.main.async {
self.handleTranscriptionUpdate(transcription)
}
}
private func handleTranscriptionUpdate(_ transcription: Transcription) {
// 去重处理:避免重复显示相同内容
let cacheKey = transcription.turnId
if transcriptionCache[cacheKey] == transcription.text {
return
}
transcriptionCache[cacheKey] = transcription.text
switch transcription.type {
case .agent:
// AI agent的转录内容
switch transcription.status {
case .inProgress:
// 转录正在进行中,实时更新显示
agentResponseLabel.text = transcription.text
case .end:
// 转录完成,最终显示
agentResponseLabel.text = transcription.text
transcriptionCache.removeValue(forKey: cacheKey)
case .interrupted:
// 转录被中断
agentResponseLabel.text = transcription.text
transcriptionCache.removeValue(forKey: cacheKey)
print("Agent transcription interrupted")
}
case .user:
// 用户的转录内容
switch transcription.status {
case .inProgress:
// 用户正在说话,实时显示
userInputLabel.text = transcription.text
case .end:
// 用户说话完成
userInputLabel.text = transcription.text
transcriptionCache.removeValue(forKey: cacheKey)
case .interrupted:
// 用户说话被中断
userInputLabel.text = transcription.text
transcriptionCache.removeValue(forKey: cacheKey)
print("User transcription interrupted")
}
}
}
}
// 注册事件回调
convoAIAPI.addHandler(handler: self)
// 监听转录内容更新,用于实时显示字幕转录内容
conversationalAIAPI.on(EConversationalAIAPIEvents.TRANSCRIPTION_UPDATED, onTextChanged)
实现字幕数据回调接口
你需要自行实现字幕处理逻辑,根据转录内容更新 UI 显示。
- Android
- iOS
- Web
将你实现的字幕 UI 模块继承自 IConversationSubtitleCallback
接口, 并实现 onTranscriptionUpdated
方法, 在该方法内处理字幕渲染至 UI 的逻辑。
class CovMessageListView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr), IConversationSubtitleCallback {
override fun onTranscriptionUpdated(subtitle: SubtitleMessage) {
// 这里实现 UI 渲染逻辑
}
}
将你实现的字幕 UI 模块实现 ConversationSubtitleDelegate
协议, 并实现 onTranscriptionUpdated
方法, 在该方法内处理字幕渲染至 UI 的逻辑:
extension ChatViewController: ConversationSubtitleDelegate {
public func onTranscriptionUpdated(subtitle: SubtitleMessage) {
// 这里实现 UI 渲染逻辑
}
}
将你实现的字幕 UI 模块接收客户端组件的字幕数据回调,并实现一个简单的 React JS 组件来展示这些消息:
- 所有
TRANSCRIPTION_UPDATED
事件回调均返回完整聊天对话列表。每次均有可能更新最近的几条对话字幕,请根据完整的列表渲染 UI。 - 推荐使用状态管理库如
zustand
、redux
等来代替React.useState
管理状态,以便在组件间共享状态。
import * as React from "react"
import {
type IUserTranscription,
type IAgentTranscription,
type ISubtitleHelperItem,
EConversationalAIAPIEvents,
} from "@/conversational-ai-api/type"
import { ConversationalAIAPI } from "@/conversational-ai-api"
// 监听转录内容更新,用于实时显示字幕转录内容
export const ChatHistory = () => {
const [chatHistory, setChatHistory] = React.useState<
ISubtitleHelperItem<Partial<IUserTranscription | IAgentTranscription>>[]
>([])
const conversationalAIAPI = ConversationalAIAPI.getInstance()
conversationalAIAPI.on(
EConversationalAIAPIEvents.TRANSCRIPTION_UPDATED,
setChatHistory
)
return (
<>
{chatHistory.map((message) => (
<div key={`${message.uid}-${message.turn_id}`}>
{message.uid}: {message.text}
</div>
))}
</>
)
}
订阅频道消息
字幕转录内容通过 RTM 频道消息传递,你需要在开始智能体会话前订阅频道消息,以接收字幕数据。
- Android
- iOS
- Web
在开始智能体会话前调用 subscribeMessage
订阅频道消息:
api.subscribeMessage("channelName") { error ->
if (error != null) {
// 处理错误
}
}
在开始智能体会话前调用 subscribeMessage
订阅频道消息:
convoAIAPI.subscribeMessage(channelName: channelName) { error in
if let error = error {
print("订阅失败: \(error.message)")
} else {
print("订阅成功")
}
}
conversationalAIAPI.subscribeMessage(channel_name)
智能体加入频道
调用 POST 创建对话式智能体接口,并完成以下参数设置:
advanced_features.enable_rtm: true
—— (必选)启动 RTM 服务parameters.data_channel: "rtm"
—— (必选)开启 RTM 数据传输通道parameters.enable_metrics: true
—— (按需开启)接收智能体性能数据parameters.enable_error_message: true
—— (按需开启)接收智能体错误事件
调用成功后,智能体会加入指定 RTC 频道,用户可以开始与智能体互动。
取消订阅频道消息
每次智能体会话结束后,你需要取消订阅频道消息,以释放字幕相关资源。
- Android
- iOS
- Web
api.unsubscribeMessage("channelName") { error ->
if (error != null) {
// 处理错误
}
}
/// 取消订阅频道消息
convoAIAPI.unsubscribeMessage(channelName: channelName) { error in
if let error = error {
print("取消订阅失败: \(error.message)")
} else {
print("取消订阅成功")
}
}
conversationalAIAPI.unsubscribeMessage(channel_name)
销毁组件实例
结束 AI 对话场景后或关闭 App 前,你需要销毁组件实例,以释放组件的所有资源。
- Android
- iOS
- Web
api.destroy()
convoAIAPI.destroy()
conversationalAIAPI.destroy()
参考信息
示例项目
声网提供了开源的实时字幕示例项目供你参考,你可以前往下载或查看其中的源代码。
组件结构
客户端组件文件夹的结构和各文件作用如下:
以下文件和文件夹即为集成客户端组件所需全部内容,无需拷贝其他文件。
- Android
- iOS
- Web
IConversationalAIAPI.kt
— API 接口及相关数据结构和枚举ConversationalAIAPIImpl.kt
— ConversationalAI API 主要实现逻辑ConversationalAIUtils.kt
— 工具函数与事件回调管理subRender/
v3/
— 字幕部分模块TranscriptionController.kt
— 字幕控制器MessageParser.kt
— 消息解析器
ConversationalAIAPI.swift
— API 接口及相关数据结构和枚举ConversationalAIAPIImpl.swift
— ConversationalAI API 主要实现逻辑Transcription/
TranscriptionController.swift
— 字幕控制器
index.ts
— API 类type.ts
— API 接口及相关数据结构和枚举utils/index.ts
— API 工具函数utils/events.ts
— 事件管理类,可以拓展该类以轻松实现事件监听和播报utils/sub-render.ts
— 字幕部分模块