一、功能描述
·完整文本转语音:将文本内容转换为语音音频文件,支持指定音色和音频格式。
·流式文本实时转语音:对 IM 流式消息边生成文本、边合成语音,应用接收音频包后解码播放。
·音色克隆:基于一段参考音频,克隆出自定义音色,后续可在 TTS 中使用。
·自定义音色列表拉取:获取当前用户已克隆的所有自定义音色。
·自定义音色删除:删除不再需要的克隆音色。
注意:说明:
二、效果展示
| 文字转语音 | 音色克隆 |
| | |
三、文字转语音(convertTextToVoice)
将文本内容转换为语音音频,返回音频文件的下载 URL。
请求参数:
| 参数 | 类型 | 必填 | 说明 |
| text | String | 是 | 需要转语音的文字内容(长度 1-255)。 |
| voiceId | String | 否 | TTS 音色 ID,可使用系统预置音色或克隆的自定义音色 ID。 |
| audioFormat | String | 否 | 音频格式,支持 pcm、wav。 |
| language | String | 否 | 语言代码(参考 ISO 639-1),如 zh、en 等。 |
返回参数:
| 参数 | 类型 | 说明 |
| audioUrl | String | 生成的音频文件下载地址。 |
| requestId | String | 唯一请求 ID。 |
示例代码:
JavaSwiftC++Dartpublic void convertTextToVoice(String text, String voiceId, String audioFormat) { try { JSONObject params = new JSONObject(); params.put("text", text); // 选填参数 if (voiceId != null && !voiceId.isEmpty()) { params.put("voiceId", voiceId); } if (audioFormat != null && !audioFormat.isEmpty()) { params.put("audioFormat", audioFormat); } String jsonString = params.toString(); V2TIMManager.getInstance().callExperimentalAPI( "convertTextToVoice", jsonString, new V2TIMValueCallback<Object>() { @Override public void onSuccess(Object result) { if (result instanceof HashMap) { HashMap<String, Object> map = (HashMap<String, Object>) result; String audioUrl = (String) map.get("audioUrl"); String requestId = (String) map.get("requestId"); Log.i("TTS", "Success – audio_url: " + audioUrl + ", request_id: " + requestId); } } @Override public void onError(int code, String desc) { Log.e("TTS", "Failed – code: " + code + ", message: " + desc); } } ); } catch (Exception e) { Log.e("TTS", "Failed to serialize parameters: " + e.getMessage()); }}func convertTextToVoice(text: String, voiceId: String? = nil, audioFormat: String? = nil) { var params: [String: Any] = [ "text": text ] // 选填参数 if let voiceId = voiceId, !voiceId.isEmpty { params["voiceId"] = voiceId } if let audioFormat = audioFormat, !audioFormat.isEmpty { params["audioFormat"] = audioFormat } guard let jsonData = try? JSONSerialization.data(withJSONObject: params), let jsonString = String(data: jsonData, encoding: .utf8) else { print("Failed to serialize parameters") return } V2TIMManager.sharedInstance().callExperimentalAPI( api: "convertTextToVoice", param: jsonString as NSObject, succ: { result in if let dict = result as? [String: Any] { let audioUrl = dict["audioUrl"] as? String ?? "" let requestId = dict["requestId"] as? String ?? "" print("Success – audio_url: \\(audioUrl), request_id: \\(requestId)") } }, fail: { code, message in print("Failed – code: \\(code), message: \\(message ?? "")") } )}void ConvertTextToVoice() { json::Object json_param; json_param[kTIMRequestInternalOperation] = kTIMInternalOperationConvertTextToVoice; // 需要转语音的文字内容(必填,长度 1-255) json_param[kTIMRequestConvertTextToVoiceText] = "你好,这是一段测试文字转语音的内容"; // TTS 声音 ID(选填) // json_param[kTIMRequestConvertTextToVoiceVoiceId] = ""; // 音频格式: pcm wav(选填) json_param[kTIMRequestConvertTextToVoiceAudioFormat] = "wav"; // 语言代码(选填) json_param[kTIMRequestConvertTextToVoiceLanguage] = ""; std::string json_parameter = json::Serialize(json_param); int ret = callExperimentalAPI(json_parameter.c_str(), [](int32_t code, const char* desc, const char* json_params, const void* user_data) { printf("\\nConvertTextToVoice|code:%d|desc:%s\\n", code, desc); if (code == 0 && json_params) { json::Value json = json::Deserialize(json_params); if (json.HasKey(kTIMResponseConvertTextToVoiceAudioUrl)) { std::string audio_url = json[kTIMResponseConvertTextToVoiceAudioUrl]; Printf("audio_url: %s\\n", audio_url.c_str()); } if (json.HasKey(kTIMResponseConvertTextToVoiceRequestId)) { std::string request_id = json[kTIMResponseConvertTextToVoiceRequestId]; Printf("request_id: %s\\n", request_id.c_str()); } } }, nullptr); Printf("ConvertTextToVoice|ret:%d\\n", ret);}Future<Map<String, dynamic>> convertTextToVoice({ required String text, String? voiceId, String audioFormat = 'wav', String language = '',}) async { Map<String, dynamic> param = { 'request_convert_text_to_voice_text': text, 'request_convert_text_to_voice_audio_format': audioFormat, 'request_convert_text_to_voice_language': language, }; if (voiceId != null && voiceId.isNotEmpty) { param['request_convert_text_to_voice_voice_id'] = voiceId; } final result = await TencentImSDKPlugin.v2TIMManager.callExperimentalAPI( api: 'internal_operation_convert_text_to_voice', param: param, ); if (result.code == 0 && result.data != null) { final dataMap = result.data as Map<String, dynamic>; final audioUrl = dataMap['response_convert_text_to_voice_audio_url'] as String?; final requestId = dataMap['response_convert_text_to_voice_request_id'] as String?; if (audioUrl != null && audioUrl.isNotEmpty) { return { 'success': true, 'audioUrl': audioUrl, 'requestId': requestId, }; } } return { 'success': false, 'code': result.code ?? -1, 'message': result.desc ?? 'Convert text to voice failed', };}
四、流式文本实时转语音(流式消息 TTS)
对 IM 流式消息边生成文本、边合成语音,适用于 AI 回复实时播报。与 convertTextToVoice 返回完整音频文件不同,本功能持续返回音频包,由应用解码播放。
五、接口说明
以下接口是流式消息的成员函数:通过消息的 streamElem(V2TIMStreamElem)调用,作用于该条消息:
| 接口 | 用途 | 请求参数 | 成功结果 |
| startTTS (Java / Objective-C) | 对该流式消息开始语音合成。 | voiceFormat:声音参数,可选,见下表。 | 启动成功确认,不返回音频。 |
| stopTTS (Java / Objective-C) | 停止语音合成。 | 无。 | 停止成功确认。 |
声音参数 V2TIMTTSVoiceFormat:
| 参数 | 类型 | 说明 |
| voiceID | String | 音色 ID,可使用系统预置音色或克隆的自定义音色 ID,空值使用默认音色。 |
| speed | Float | 语速,范围 [0.5, 2.0],默认 1.0 |
音频通过高级消息监听的 onRecvTTSResponse (Java / Objective-C) 返回:
| 回调字段 | 类型 | 说明 |
| msgID | String | 所属消息 ID;只处理当前消息的音频,过滤停止后的在途回调。 |
| audioDataList | Array | 本次下发的 Opus 音频包,按列表顺序解码播放;不是累计音频。 |
| isFinished | Boolean | 音频流是否结束;先处理本次音频包,再处理结束标记。 |
六、调用流程
1. 注册高级消息监听,对目标流式消息调用 startTTS,并记录其 msgID。
2. 只处理当前 msgID 的回调,按顺序解码播放 audioDataList。
3. 收到 isFinished = true 后,播放完队列中剩余的音频即播报结束。
4. 打断时先解除 msgID 绑定并停止播放,再调用 stopTTS。
七、示例代码
以下代码直接调用 IM SDK,按顺序使用,变量和监听器由应用持有。player 为应用实现的 Opus 解码播放器:enqueue 按顺序解码播放,finish 表示音频输入结束,stop 立即停播并清空队列。
1. 注册监听并处理音频
JavaSwiftimport com.tencent.imsdk.v2.*;// 以下字段放在会话对象中,回调在主线程执行。private String activeMsgID;private V2TIMStreamElem activeElem;private final V2TIMAdvancedMsgListener ttsListener = new V2TIMAdvancedMsgListener() { @Override public void onRecvTTSResponse(V2TIMTTSResponse response) { if (activeMsgID == null || !activeMsgID.equals(response.getMsgID())) { return; } if (response.getAudioDataList() != null) { for (byte[] packet : response.getAudioDataList()) { player.enqueue(packet); } } if (response.isFinished()) { player.finish(); activeMsgID = null; activeElem = null; } }};// 开始前注册,页面退出时调用 removeAdvancedMsgListener(ttsListener)。V2TIMManager.getMessageManager().addAdvancedMsgListener(ttsListener);import ImSDK_Plus// 以下成员放在应用持有的 NSObject 子类中,并遵循 V2TIMAdvancedMsgListener。private var activeMsgID: String?private var activeElem: V2TIMStreamElem?func onRecvTTSResponse(response: V2TIMTTSResponse) { guard let id = activeMsgID, response.msgID == id else { return } for packet in response.audioDataList ?? [] { player.enqueue(packet) } if response.isFinished { player.finish() activeMsgID = nil activeElem = nil }}// 开始前注册,页面退出时调用 removeAdvancedMsgListener(listener:)。V2TIMManager.sharedInstance().addAdvancedMsgListener(listener: self)
2. 开始语音合成
JavaSwiftV2TIMStreamElem elem = message.getStreamElem();if (elem != null) { activeMsgID = message.getMsgID(); activeElem = elem; V2TIMTTSVoiceFormat format = new V2TIMTTSVoiceFormat(); format.setVoiceID(""); // 空值使用默认音色 format.setSpeed(1.0f); // 范围 [0.5, 2.0] elem.startTTS(format, new V2TIMCallback() { @Override public void onSuccess() {} @Override public void onError(int code, String desc) { // streamID 未就绪时可在 onRecvMessageModified 中重试 activeMsgID = null; activeElem = null; System.err.println("startTTS failed: " + code + ", " + desc); } });}if let elem = message.streamElem { activeMsgID = message.msgID activeElem = elem let format = V2TIMTTSVoiceFormat() format.voiceID = "" // 空值使用默认音色 format.speed = 1.0 // 范围 [0.5, 2.0] elem.startTTS(voiceFormat: format, succ: nil, fail: { [weak self] code, desc in // streamID 未就绪时可在 onRecvMessageModified 中重试 self?.activeMsgID = nil self?.activeElem = nil print("startTTS failed: \\(code), \\(desc ?? "")") })}
3. 停止语音合成
JavaSwiftV2TIMStreamElem elem = activeElem;activeMsgID = null; // 先解除绑定,丢弃在途音频activeElem = null;player.stop();if (elem != null) { elem.stopTTS(new V2TIMCallback() { @Override public void onSuccess() {} @Override public void onError(int code, String desc) { System.err.println("stopTTS failed: " + code + ", " + desc); } });}let elem = activeElemactiveMsgID = nil // 先解除绑定,丢弃在途音频activeElem = nilplayer.stop()elem?.stopTTS(succ: nil, fail: { code, desc in print("stopTTS failed: \\(code), \\(desc ?? "")")})
八、结果处理与异常收尾
·isFinished = true 只表示音频流结束,不代表播放完成,也不代表合成成功;超时、断连时同样会收到。
·停止后仍可能收到在途音频,按 msgID 过滤丢弃。
·失败或退出时停止播放并移除监听。
九、音色克隆(voiceClone)
基于参考音频克隆出自定义音色,克隆成功后返回音色 ID,可用于后续 TTS 调用。
请求参数:
| 参数 | 类型 | 必填 | 说明 |
| audioUrl | String | 是 | 参考音频 URL(要求 16k 单声道 wav 格式,时长 10-18 秒)。 |
| voiceName | String | 是 | 声音克隆的名称。 |
| promptText | String | 否 | 参考音频对应的文本内容。 |
| language | String | 否 | 语言代码(参考 ISO 639-1)。 |
返回参数:
| 参数 | 类型 | 说明 |
| voiceId | String | 克隆出的音色 ID,可用于 TTS 接口的 voiceId 参数。 |
| requestId | String | 唯一请求 ID。 |
注意:说明:
示例代码:
JavaSwiftC++DartJSONObject params = new JSONObject();params.put("audioUrl", "https://example.com/voice.wav");params.put("voiceName", "MyVoice");params.put("promptText", ""); // 选填V2TIMManager.getInstance().callExperimentalAPI("voiceClone", params.toString(), new V2TIMValueCallback<Object>() { @Override public void onSuccess(Object result) { try { JSONObject resultJson = new JSONObject((String) result); String voiceId = resultJson.optString("voiceId"); Log.d("TAG", "voiceId: " + voiceId); } catch (JSONException e) { Log.e("TAG", "parse error"); } } @Override public void onError(int code, String desc) { Log.e("TAG", "error: " + code + ", " + desc); } });let params: [String: Any] = [ "audioUrl": "https://example.com/voice.wav", "voiceName": "MyVoice", "promptText": "" // 选填]guard let jsonData = try? JSONSerialization.data(withJSONObject: params) else { print("Failed to serialize parameters"); return }guard let jsonString = String(data: jsonData, encoding: .utf8) else { print("Failed to convert to string"); return }V2TIMManager.sharedInstance().callExperimentalAPI( api: "voiceClone", param: jsonString as NSObject, succ: { result in guard let resultString = result as? String, let resultData = resultString.data(using: .utf8), let resultDict = try? JSONSerialization.jsonObject(with: resultData) as? [String: Any], let voiceId = resultDict["voiceId"] as? String else { print("parse error") return } print("voiceId: \\(voiceId)") }, fail: { code, desc in print("error: \\(code), \\(desc ?? "")") })void VoiceClone() { json::Object json_param; json_param[kTIMRequestInternalOperation] = kTIMInternalOperationVoiceClone; json_param[kTIMRequestVoiceCloneVoiceName] = "MyVoice"; // 必填 json_param[kTIMRequestVoiceCloneAudioUrl] = "https://example.com/voice.wav"; // 必填 json_param[kTIMRequestVoiceClonePromptText] = ""; // 选填 json_param[kTIMRequestVoiceCloneLanguage] = ""; // 选填 std::string param_str = json::Serialize(json_param); int ret = callExperimentalAPI(param_str.c_str(), [](int32_t code, const char* desc, const char* json_params, const void* user_data) { if (code != TIM_SUCC) { printf("voiceClone failed: %d, %s\\n", code, desc); return; } json::Value json_value = json::Deserialize(json_params); std::string voice_id = Json2String(json_value, kTIMResponseVoiceCloneVoiceId, ""); std::string request_id = Json2String(json_value, kTIMResponseVoiceCloneRequestId, ""); printf("voiceClone success: voiceId=%s, requestId=%s\\n", voice_id.c_str(), request_id.c_str()); }, nullptr);}Future<Map<String, dynamic>> voiceClone({ required String audioUrl, required String voiceName, String promptText = '', String language = '',}) async { Map<String, dynamic> param = { 'request_voice_clone_audio_url': audioUrl, 'request_voice_clone_voice_name': voiceName, 'request_voice_clone_prompt_text': promptText, 'request_voice_clone_language': language, }; final result = await TencentImSDKPlugin.v2TIMManager.callExperimentalAPI( api: 'internal_operation_voice_clone', param: param, ); if (result.code == 0 && result.data != null) { final dataMap = result.data as Map<String, dynamic>; final voiceId = dataMap['response_voice_clone_voice_id'] as String?; final requestId = dataMap['response_voice_clone_request_id'] as String?; if (voiceId != null && voiceId.isNotEmpty) { return { 'success': true, 'voiceId': voiceId, 'requestId': requestId, }; } } return { 'success': false, 'code': result.code ?? -1, 'message': result.desc ?? 'Voice clone failed', };}
十、获取自定义音色列表(getCustomVoiceList)
获取当前用户已克隆的所有自定义音色。
请求参数:无需额外参数。
返回参数:
| 参数 | 类型 | 说明 |
| voiceList | Array | 自定义音色列表,每个元素包含 voice_id(音色 ID)和 name(音色名称)。 |
示例代码:
JavaSwiftC++DartV2TIMManager.getInstance().callExperimentalAPI( "getCustomVoiceList", null, new V2TIMValueCallback<Object>() { @Override public void onSuccess(Object result) { List<Map<String, String>> voiceList = (List<Map<String, String>>) result; for (Map<String, String> item : voiceList) { String voiceId = item.get("voice_id"); String name = item.get("name"); Log.d(TAG, "Voice ID: " + voiceId + ", Name: " + name); } } @Override public void onError(int code, String desc) { Log.e(TAG, "getCustomVoiceList failed: " + code + ", " + desc); } });V2TIMManager.sharedInstance().callExperimentalAPI( api: "getCustomVoiceList", param: nil, succ: { result in // 解析返回的音色列表 print("getCustomVoiceList success: \\(result ?? "")") }, fail: { code, desc in print("error: \\(code), \\(desc ?? "")") })void GetCustomVoiceList() { json::Object json_param; json_param[kTIMRequestInternalOperation] = kTIMInternalOperationGetCustomVoiceList; std::string json_parameters = json::Serialize(json_param); int ret = callExperimentalAPI(json_parameters.c_str(), [](int32_t code, const char* desc, const char* json_params, const void* user_data) { printf("GetCustomVoiceList: code=%d desc=%s\\n", code, desc); if (code == 0 && json_params) { json::Object result = json::Deserialize(json_params); if (result.HasKey(kTIMResponseGetCustomVoiceListVoiceList)) { json::Array voice_list = result[kTIMResponseGetCustomVoiceListVoiceList]; printf(" voice_list count = %zu\\n", voice_list.size()); for (auto& item : voice_list) { json::Object voice_item = item; std::string voice_id = voice_item[kTIMCustomVoiceItemVoiceId]; std::string name = voice_item[kTIMCustomVoiceItemName]; Printf(" voice_id = %s, name = %s\\n", voice_id.c_str(), name.c_str()); } } } }, nullptr);}Future<Map<String, dynamic>> getCustomVoiceList() async { final result = await TencentImSDKPlugin.v2TIMManager.callExperimentalAPI( api: 'internal_operation_get_custom_voice_list', param: {}, ); if (result.code == 0 && result.data != null) { final dataMap = result.data as Map<String, dynamic>; final voiceList = dataMap['response_get_custom_voice_list_voice_list'] as List?; if (voiceList != null) { // Parse voice list final voices = voiceList.map((item) { final voiceMap = item as Map<String, dynamic>; return { 'voiceId': voiceMap['custom_voice_item_voice_id'] as String?, 'name': voiceMap['custom_voice_item_name'] as String?, }; }).toList(); return { 'success': true, 'voiceList': voices, }; } } return { 'success': false, 'code': result.code ?? -1, 'message': result.desc ?? 'Get custom voice list failed', };}
十一、删除自定义音色(deleteCustomVoice)
删除指定的克隆音色。
请求参数:
| 参数 | 类型 | 必填 | 说明 |
| voiceId | String | 是 | 要删除的音色 ID。 |
示例代码:
JavaSwiftC++DartJSONObject params = new JSONObject();params.put("voiceId", "voice_xxx");V2TIMManager.getInstance().callExperimentalAPI("deleteCustomVoice", params.toString(), new V2TIMValueCallback<Object>() { @Override public void onSuccess(Object result) { Log.d("TAG", "delete success"); } @Override public void onError(int code, String desc) { Log.e("TAG", "error: " + code + ", " + desc); } });let params: [String: Any] = ["voiceId": "voice_xxx"]guard let jsonData = try? JSONSerialization.data(withJSONObject: params) else { print("Failed to serialize parameters"); return }guard let jsonString = String(data: jsonData, encoding: .utf8) else { print("Failed to convert to string"); return }V2TIMManager.sharedInstance().callExperimentalAPI( api: "deleteCustomVoice", param: jsonString as NSObject, succ: { _ in print("delete success") }, fail: { code, desc in print("error: \\(code), \\(desc ?? "")") })void CallDeleteCustomVoice() { json::Object json_param; json_param[kTIMRequestInternalOperation] = kTIMInternalOperationDeleteCustomVoice; json_param[kTIMRequestDeleteCustomVoiceVoiceId] = "voice_xxx"; // 必填 std::string param_str = json::Serialize(json_param); callExperimentalAPI(param_str.c_str(), [](int32_t code, const char* desc, const char* json_params, const void* user_data) { if (code != TIM_SUCC) { printf("deleteCustomVoice failed: %d, %s\\n", code, desc); return; } printf("deleteCustomVoice success\\n"); }, nullptr);}Future<Map<String, dynamic>> deleteCustomVoice({ required String voiceId,}) async { try { Map<String, dynamic> param = { 'request_delete_custom_voice_voice_id': voiceId, }; final result = await TencentImSDKPlugin.v2TIMManager.callExperimentalAPI( api: 'internal_operation_delete_custom_voice', param: param, ); if (result.code == 0) { return { 'success': true, }; } return { 'success': false, 'code': result.code ?? -1, 'message': result.desc ?? 'Delete custom voice failed', }; } catch (e) { return { 'success': false, 'code': -1, 'message': e.toString(), }; }}