即时通信 IM Android&iOS&Windows&Flutter

文字转语音和音色克隆是即时通信 IM SDK 提供的 AI 能力,主要包含以下功能。

相关产品
即时通信 IM 产品页,含版本规格、价格与购买入口。

一、功能描述

·完整文本转语音:将文本内容转换为语音音频文件,支持指定音色和音频格式。

·流式文本实时转语音:对 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(), }; }}

赞 (0)
上一篇 4天前
下一篇 4天前