功能描述
语音转文字功能支持三种使用场景:将发送出去的语音消息转文字、将本地录制的完整音频转文字,以及在录音过程中实时上传音频并接收识别结果(实时 ASR)。
说明:
为了方便用户测试功能,该功能提供免费试用,详细参见 免费试用说明。
语音消息转文字仅增强版 7.4 及以上版本支持。
录音转文字仅增强版 8.8 及以上版本支持。
实时语音转文字仅增强版 9.1.7822 及以上版本支持。
效果展示
您可以使用本功能,实现如下图所示的转文字效果:
语音消息转文字 | 录音转文字 | 实时语音转文字 |
![]() | ![]() | ![]() |
语音消息转文字
接口说明
接口参数说明如下:
入参 | 含义 | 说明 |
language | 识别的目标语言 | 如果您的主流用户使用中、英文居多,language 参数可传空字符串,此时我们默认使用中英模型识别。 如果您想指定识别的目标语言,可以将其设置成指定值。目前支持的语言请参见 语言支持。 |
callback | 识别结果回调 | 其中的 result 为识别的文本。 |
注意:
识别的语音消息时长不可以超过 60s(60.00001s 也不行),否则会识别失败。
识别的语音消息音频需要设置为 16k 采样率,否则可能识别失败。
示例代码如下:
// 获取到语音消息的 V2TIMMessage 对象V2TIMMessage msg = messageList.get(0);if (msg.elemType == V2TIM_ELEM_TYPE_SOUND) {// 从 V2TIMMessage 中获取到 soundElemV2TIMSoundElem soundElem = msg.getSoundElem();// 调用语音转文字,默认使用中英文识别模型soundElem.convertVoiceToText("",new V2TIMValueCallback<String>() {@Overridepublic void onError(int code, String desc) {TUIChatUtils.callbackOnError(callBack, TAG, code, desc);String str = "convertVoiceToText failed, code: " + code + " desc: " + desc;ToastUtil.show(str,true, 1);}@Overridepublic void onSuccess(String result) {// 识别成功,result 为识别结果String str = "convertVoiceToText succeed, result: " + result;ToastUtil.show(str, true, 1);}});}
// 获取到语音消息的 V2TIMMessage 对象let msg = messageList[0]// 检查消息类型是否为语音if msg.elemType == .ELEM_TYPE_SOUND, let soundElem = msg.soundElem {// 调用语音转文字,默认使用中英文识别模型soundElem.convertVoiceToText(language: "") { code, desc, result in// 识别成功,result 为识别结果print("convertVoiceToText, code: \\(code), desc: \\(desc ?? ""), result: \\(result ?? "")")}}
// 获取到语音消息的 V2TIMMessage 对象V2TIMMessage *msg = messageList[0];if (msg.elemType == V2TIM_ELEM_TYPE_SOUND) {// 从 V2TIMMessage 中获取到 soundElemV2TIMSoundElem *soundElem = msg.soundElem;// 调用语音转文字,默认使用中英文识别模型[soundElem convertVoiceToText:@"" completion:^(int code, NSString *desc, NSString *result) {// 识别成功,result 为识别结果NSLog(@"convertVoiceToText, code: %d, desc: %@, result: %@", code, desc, result);}];}
template <class T>class ValueCallback final : public V2TIMValueCallback<T> {public:using SuccessCallback = std::function<void(const T&)>;using ErrorCallback = std::function<void(int, const V2TIMString&)>;ValueCallback() = default;~ValueCallback() override = default;void SetCallback(SuccessCallback success_callback, ErrorCallback error_callback) {success_callback_ = std::move(success_callback);error_callback_ = std::move(error_callback);}void OnSuccess(const T& value) override {if (success_callback_) {success_callback_(value);}}void OnError(int error_code, const V2TIMString& error_message) override {if (error_callback_) {error_callback_(error_code, error_message);}}private:SuccessCallback success_callback_;ErrorCallback error_callback_;};auto callback = new ValueCallback<V2TIMString>{};callback->SetCallback([=](const V2TIMString& result) {// 语音转文字成功,result 为转换结果delete callback;},[=](int error_code, const V2TIMString& error_message) {// 语音转文字失败delete callback;});// 获取到语音消息的 V2TIMMessage 对象V2TIMMessage *msg = messageList[0];// 从 V2TIMMessage 中获取到 soundElemV2TIMElem *elem = msg->elemList[0];if (elem->elemType == V2TIM_ELEM_TYPE_SOUND) {V2TIMSoundElem *sound_elem = (V2TIMSoundElem *)elem;// 调用语音转文字,默认使用中英文识别模型sound_elem->ConvertVoiceToText("", callback);}
录音转文字
录音上传云端
接口参数说明如下:
入参 | 含义 | 说明 |
filePath | 文件路径 | 录音文件本地存储路径。 |
fileType | 文件类型 | 0:Unknown; 1:Image; 2:Video; 3:Audio; 4:log。 |
示例代码如下:
try {JSONObject uploadParam = new JSONObject();uploadParam.put("filePath", recordPath);uploadParam.put("fileType", 3); // 0: Unknown, 1: Image, 2: Video, 3: Audio, 4: LogV2TIMManager.getInstance().callExperimentalAPI("uploadFile",uploadParam.toString(),new V2TIMValueCallback<Object>() {@Overridepublic void onSuccess(Object result) {if (result instanceof String) {String fileUrl = (String) result;System.out.println("Upload successful, URL: " + fileUrl);}}@Overridepublic void onError(int code, String desc) {System.err.println("Upload failed, code: " + code + ", desc: " + desc);}});} catch (JSONException e) {e.printStackTrace();}
let uploadParam: [String: Any] = ["filePath": recordPath,"fileType": 3 // 0: Unknown 1: Image 2: Video 3: Audio 4: log]if let uploadData = try? JSONSerialization.data(withJSONObject: uploadParam, options: .prettyPrinted),let uploadParamStr = String(data: uploadData, encoding: .utf8) {V2TIMManager.sharedInstance().callExperimentalAPI(api: "uploadFile",param: uploadParamStr as NSObject,succ: { result inif let fileUrl = result as? String, !fileUrl.isEmpty {print("Upload successful, URL: \\(fileUrl)")}},fail: { code, desc inprint("Upload failed, code: \\(code), desc: \\(String(describing: desc))")})}
云端语音转文字
接口参数说明如下:
入参 | 含义 | 说明 |
url | 语音文件 url | 语音文件上传云端后生成的 url 下载地址。 |
language | 文本的语言 | 如果不设置,默认会自动检测。 |
示例代码如下:
try {JSONObject convertParam = new JSONObject();convertParam.put("url", fileUrl);convertParam.put("language", ""); // Empty means auto-detectV2TIMManager.getInstance().callExperimentalAPI("convertVoiceToText",convertParam.toString(),new V2TIMValueCallback<Object>() {@Overridepublic void onSuccess(Object result) {if (result instanceof String) {String text = (String) result;System.out.println("Voice to text successful: " + text);}}@Overridepublic void onError(int code, String desc) {System.err.println("Voice to text failed, code: " + code + ", desc: " + desc);}});} catch (JSONException e) {e.printStackTrace();}
let convertParam: [String: Any] = ["url": fileUrl,"language": "" // Empty means auto-detect]if let convertData = try? JSONSerialization.data(withJSONObject: convertParam, options: .prettyPrinted),let convertParamStr = String(data: convertData, encoding: .utf8) {V2TIMManager.sharedInstance().callExperimentalAPI(api: "convertVoiceToText",param: convertParamStr as NSObject,succ: { result inif let text = result as? String {print("Voice to text successful: \\(text)")}},fail: { code, desc inprint("Voice to text failed, code: \\(code), desc: \\(String(describing: desc))")})}
实时语音转文字
录音时持续上传音频、实时返回文字,适用于“边说边出字”。无需先上传完整文件。
接口说明
通过
callExperimentalAPI 调用以下三个实验性接口完成识别:实验接口 | 用途 | 请求参数 | 成功结果 |
startRealtimeTranscriber | 创建实时识别会话 | language:识别语言,使用 ISO 639-1 两位语言码,例如 zh、en。取值格式与下文“语言支持”表不同。 | 当前会话的 sessionID 字符串。 |
feedRealtimeAudioData | 向会话上传一批 Opus 音频包(16 kHz / 单声道 / 裸 Opus) | sessionID:会话 ID;dataList:音频包数组,按采集先后顺序排列。 | 请求成功确认,不返回识别文本。 |
stopRealtimeTranscriber | 请求结束当前识别会话 | sessionID:会话 ID。 | 结束请求的成功确认,不代表最终识别结果已到达。 |
结果通过
onExperimentalNotify 返回,key 为 onRecvRealtimeTranscriberResponse:通知字段 | 类型 | 说明 |
sessionID | String | 结果所属的会话;只处理当前会话的结果,过滤其他会话或过期会话。 |
chunkIndex | Integer | 句子序号。同一序号的后续结果表示该句最新文本,应覆盖更新。 |
text | String | 该句最新识别文本;空文本不覆盖已有内容,但仍需处理结束标记。 |
isFinished | Boolean | 会话是否已结束; true 表示该会话结束,不是单句识别完成标记。 |
参数类型:
平台 | 实时接口参数 | dataList 元素 | 通知参数 |
Android(Java) | Map<String, Object> | byte[],原始 Opus packet | Map |
iOS(Swift / Objective-C) | NSDictionary | Data / NSData,原始 Opus packet | NSDictionary |
注意:
应用需自行采集并编码音频,SDK 仅接收 16 kHz、单声道的裸 Opus 音频包,不支持 PCM、WAV 或 Ogg 等封装格式。
SDK 内置限制:启动请求 30 秒内未成功即返回超时;单个会话最长 10 分钟,连续 60 秒无音频上传会自动结束。会话结束时均会收到
isFinished = true。调用
stopRealtimeTranscriber 后,该会话不再接受音频上传;对同一会话重复停止会返回错误。调用流程
1. 注册结果监听,创建会话并保存有效
sessionID;创建期间采集的音频先缓存。2. 按采集顺序分批上传,按
chunkIndex 覆盖更新文本、升序拼接。3. 停止采集,尾批上传回调到达后请求停止;停止成功后仍保留监听和会话 ID。
4. 收到
isFinished = true 后取最终文本、清理会话和监听;未收到时由收尾超时兜底。示例代码
以下代码直接调用 IM SDK,按顺序使用,变量和监听器由应用持有。
opusPackets 由编码器提供;录音、缓存和超时需自行实现。注册监听并处理识别结果
import com.tencent.imsdk.v2.V2TIMManager;import com.tencent.imsdk.v2.V2TIMSDKListener;import com.tencent.imsdk.v2.V2TIMValueCallback;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.TreeMap;// 以下字段放在会话对象中,调用及状态更新在同一线程执行。private String sessionID;private final TreeMap<Integer, String> sentences = new TreeMap<>();private final V2TIMSDKListener asrListener = new V2TIMSDKListener() {@Overridepublic void onExperimentalNotify(String key, Object param) {if (!"onRecvRealtimeTranscriberResponse".equals(key)|| !(param instanceof Map)) {return;}Map<?, ?> result = (Map<?, ?>) param;if (sessionID == null || !sessionID.equals(result.get("sessionID"))) {return;}Object rawIndex = result.get("chunkIndex");int index = rawIndex instanceof Number ? ((Number) rawIndex).intValue() : 0;Object rawText = result.get("text");String text = rawText instanceof String ? (String) rawText : "";if (!text.isEmpty()) {sentences.put(index, text);}StringBuilder fullText = new StringBuilder();for (String sentence : sentences.values()) {fullText.append(sentence);}System.out.println("ASR text: " + fullText);if (Boolean.TRUE.equals(result.get("isFinished"))) {System.out.println("ASR final text: " + fullText);sessionID = null;V2TIMManager.getInstance().removeIMSDKListener(this);}}};// 创建会话前注册。V2TIMManager.getInstance().addIMSDKListener(asrListener);
import Foundationimport ImSDK_Plus// 以下成员放在应用持有的 NSObject 子类中,并遵循 V2TIMSDKListener。private var sessionID: String?private var sentences: [Int: String] = [:]func onExperimentalNotify(key: String, param: NSObject?) {guard key == "onRecvRealtimeTranscriberResponse",let result = param as? [String: Any],let id = sessionID,result["sessionID"] as? String == id else { return }let index = (result["chunkIndex"] as? NSNumber)?.intValue ?? 0let text = result["text"] as? String ?? ""if !text.isEmpty {sentences[index] = text}let fullText = sentences.sorted { $0.key < $1.key }.map(\\.value).joined()print("ASR text: \\(fullText)")if (result["isFinished"] as? NSNumber)?.boolValue == true {print("ASR final text: \\(fullText)")sessionID = nilV2TIMManager.sharedInstance().removeIMSDKListener(listener: self)}}// 创建会话前注册,self 遵循 V2TIMSDKListener。V2TIMManager.sharedInstance().addIMSDKListener(listener: self)
#import <ImSDK_Plus/ImSDK_Plus.h>// 声明在应用持有的 NSObject<V2TIMSDKListener> 子类中。@property(nonatomic, copy) NSString *sessionID;@property(nonatomic, strong) NSMutableDictionary<NSNumber *, NSString *> *sentences;- (void)onExperimentalNotify:(NSString *)key param:(NSObject *)param {if (![key isEqualToString:@"onRecvRealtimeTranscriberResponse"]|| ![param isKindOfClass:NSDictionary.class]) {return;}NSDictionary *result = (NSDictionary *)param;if (!self.sessionID || ![self.sessionID isEqualToString:result[@"sessionID"]]) {return;}NSNumber *index = result[@"chunkIndex"] ?: @0;NSString *text = result[@"text"] ?: @"";if (text.length > 0) {self.sentences[index] = text;}NSArray<NSNumber *> *indices = [self.sentences.allKeyssortedArrayUsingSelector:@selector(compare:)];NSMutableString *fullText = [NSMutableString string];for (NSNumber *key in indices) {[fullText appendString:self.sentences[key]];}NSLog(@"ASR text: %@", fullText);if ([result[@"isFinished"] boolValue]) {NSLog(@"ASR final text: %@", fullText);self.sessionID = nil;[[V2TIMManager sharedInstance] removeIMSDKListener:self];}}// 创建会话前初始化并注册。self.sentences = [NSMutableDictionary dictionary];[[V2TIMManager sharedInstance] addIMSDKListener:self];
创建实时识别会话
旧会话结束后才能重新启动,并需重新注册监听、清空句子缓存。
sentences.clear();Map<String, Object> startParams = new HashMap<>();startParams.put("language", "zh");V2TIMManager.getInstance().callExperimentalAPI("startRealtimeTranscriber", startParams,new V2TIMValueCallback<Object>() {@Overridepublic void onSuccess(Object result) {String id = result instanceof String ? (String) result : "";if (id.isEmpty()) {V2TIMManager.getInstance().removeIMSDKListener(asrListener);System.err.println("ASR sessionID is empty");return;}sessionID = id;System.out.println("ASR started: " + id);}@Overridepublic void onError(int code, String desc) {V2TIMManager.getInstance().removeIMSDKListener(asrListener);System.err.println("ASR start failed: " + code + ", " + desc);}});
sentences.removeAll()V2TIMManager.sharedInstance().callExperimentalAPI(api: "startRealtimeTranscriber",param: ["language": "zh"] as NSDictionary,succ: { [weak self] result inguard let self else { return }guard let id = result as? String, !id.isEmpty else {V2TIMManager.sharedInstance().removeIMSDKListener(listener: self)print("ASR sessionID is empty")return}self.sessionID = idprint("ASR started: \\(id)")},fail: { [weak self] code, desc inif let self {V2TIMManager.sharedInstance().removeIMSDKListener(listener: self)}print("ASR start failed: \\(code), \\(desc ?? "")")})
[self.sentences removeAllObjects];[[V2TIMManager sharedInstance] callExperimentalAPI:@"startRealtimeTranscriber"param:@{@"language": @"zh"}succ:^(NSObject *result) {NSString *id = [result isKindOfClass:NSString.class] ? (NSString *)result : @"";if (id.length == 0) {[[V2TIMManager sharedInstance] removeIMSDKListener:self];NSLog(@"ASR sessionID is empty");return;}self.sessionID = id;NSLog(@"ASR started: %@", id);}fail:^(int code, NSString *desc) {[[V2TIMManager sharedInstance] removeIMSDKListener:self];NSLog(@"ASR start failed: %d, %@", code, desc);}];
上传 Opus 音频
取得有效
sessionID 后分批上传;尾批的成功或失败回调到达后,再请求停止。// opusPackets: List<byte[]>,每个元素为一个完整的 Opus packet。if (sessionID != null && !opusPackets.isEmpty()) {Map<String, Object> feedParams = new HashMap<>();feedParams.put("sessionID", sessionID);feedParams.put("dataList", opusPackets);V2TIMManager.getInstance().callExperimentalAPI("feedRealtimeAudioData", feedParams,new V2TIMValueCallback<Object>() {@Overridepublic void onSuccess(Object result) {}@Overridepublic void onError(int code, String desc) {System.err.println("ASR feed failed: " + code + ", " + desc);}});}
// opusPackets: [Data],每个元素为一个完整的 Opus packet。if let id = sessionID, !opusPackets.isEmpty {V2TIMManager.sharedInstance().callExperimentalAPI(api: "feedRealtimeAudioData",param: ["sessionID": id, "dataList": opusPackets] as NSDictionary,succ: { _ in},fail: { code, desc inprint("ASR feed failed: \\(code), \\(desc ?? "")")})}
// opusPackets: NSArray<NSData *>,每个元素为一个完整的 Opus packet。if (self.sessionID.length > 0 && opusPackets.count > 0) {[[V2TIMManager sharedInstance] callExperimentalAPI:@"feedRealtimeAudioData"param:@{@"sessionID": self.sessionID, @"dataList": opusPackets}succ:^(NSObject *result) {}fail:^(int code, NSString *desc) {NSLog(@"ASR feed failed: %d, %@", code, desc);}];}
请求停止并等待尾部结果
先停采集、上传尾批再调用本接口。成功回调中不清理
sessionID 或监听器,继续等待 isFinished。if (sessionID != null) {Map<String, Object> stopParams = new HashMap<>();stopParams.put("sessionID", sessionID);V2TIMManager.getInstance().callExperimentalAPI("stopRealtimeTranscriber", stopParams,new V2TIMValueCallback<Object>() {@Overridepublic void onSuccess(Object result) {System.out.println("ASR stop acknowledged; waiting for isFinished");}@Overridepublic void onError(int code, String desc) {System.err.println("ASR stop failed: " + code + ", " + desc);sessionID = null;V2TIMManager.getInstance().removeIMSDKListener(asrListener);}});}
if let id = sessionID {V2TIMManager.sharedInstance().callExperimentalAPI(api: "stopRealtimeTranscriber",param: ["sessionID": id] as NSDictionary,succ: { _ inprint("ASR stop acknowledged; waiting for isFinished")},fail: { [weak self] code, desc inprint("ASR stop failed: \\(code), \\(desc ?? "")")if let self {self.sessionID = nilV2TIMManager.sharedInstance().removeIMSDKListener(listener: self)}})}
if (self.sessionID.length > 0) {[[V2TIMManager sharedInstance] callExperimentalAPI:@"stopRealtimeTranscriber"param:@{@"sessionID": self.sessionID}succ:^(NSObject *result) {NSLog(@"ASR stop acknowledged; waiting for isFinished");}fail:^(int code, NSString *desc) {NSLog(@"ASR stop failed: %d, %@", code, desc);self.sessionID = nil;[[V2TIMManager sharedInstance] removeIMSDKListener:self];}];}
结果处理与异常收尾
停止成功不代表结果已全部返回,以
isFinished = true 为准,并设置收尾超时(如 800 ms)兜底。空 text 不覆盖已有文本。只处理当前
sessionID 的结果。若取消后启动回调才返回新的 sessionID,应立即停止该会话。失败或退出时停止采集并移除监听。
语言支持
目前支持识别的目标语言如下:
支持的语言 | 入参设置 |
中文普通话 | "zh (cmn-Hans-CN)" |
中文粤语 | "yue-Hant-HK" |
English | "en-US" |
日本语(日本) | "ja-JP" |


