mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-24 08:03:53 +08:00
update:把传递name改成传递speaker_id
This commit is contained in:
+2
-2
@@ -176,8 +176,8 @@ public class VoiceCloneController {
|
|||||||
String cloneId = params.get("cloneId");
|
String cloneId = params.get("cloneId");
|
||||||
checkPermission(cloneId);
|
checkPermission(cloneId);
|
||||||
// 调用服务层进行语音克隆训练
|
// 调用服务层进行语音克隆训练
|
||||||
String result = voiceCloneService.cloneAudio(cloneId);
|
voiceCloneService.cloneAudio(cloneId);
|
||||||
return new Result<String>().ok(result);
|
return new Result<String>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkPermission(String id) {
|
private void checkPermission(String id) {
|
||||||
|
|||||||
+6
-6
@@ -35,17 +35,17 @@ public interface VoiceCloneService extends BaseService<VoiceCloneEntity> {
|
|||||||
* 根据用户ID查询声音克隆列表
|
* 根据用户ID查询声音克隆列表
|
||||||
*/
|
*/
|
||||||
List<VoiceCloneEntity> getByUserId(Long userId);
|
List<VoiceCloneEntity> getByUserId(Long userId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询带模型名称和用户名称的声音克隆列表
|
* 分页查询带模型名称和用户名称的声音克隆列表
|
||||||
*/
|
*/
|
||||||
PageData<VoiceCloneResponseDTO> pageWithNames(Map<String, Object> params);
|
PageData<VoiceCloneResponseDTO> pageWithNames(Map<String, Object> params);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据ID查询带模型名称和用户名称的声音克隆信息
|
* 根据ID查询带模型名称和用户名称的声音克隆信息
|
||||||
*/
|
*/
|
||||||
VoiceCloneResponseDTO getByIdWithNames(String id);
|
VoiceCloneResponseDTO getByIdWithNames(String id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据用户ID查询带模型名称的声音克隆列表
|
* 根据用户ID查询带模型名称的声音克隆列表
|
||||||
*/
|
*/
|
||||||
@@ -65,11 +65,11 @@ public interface VoiceCloneService extends BaseService<VoiceCloneEntity> {
|
|||||||
* 获取音频数据
|
* 获取音频数据
|
||||||
*/
|
*/
|
||||||
byte[] getVoiceData(String id);
|
byte[] getVoiceData(String id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 克隆音频,调用火山引擎进行语音复刻训练
|
* 克隆音频,调用火山引擎进行语音复刻训练
|
||||||
|
*
|
||||||
* @param cloneId 语音克隆记录ID
|
* @param cloneId 语音克隆记录ID
|
||||||
* @return 训练结果消息
|
|
||||||
*/
|
*/
|
||||||
String cloneAudio(String cloneId);
|
void cloneAudio(String cloneId);
|
||||||
}
|
}
|
||||||
|
|||||||
+76
-56
@@ -1,10 +1,9 @@
|
|||||||
package xiaozhi.modules.voiceclone.service.impl;
|
package xiaozhi.modules.voiceclone.service.impl;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
import java.net.URI;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
@@ -19,16 +18,17 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.common.utils.DateUtils;
|
import xiaozhi.common.utils.DateUtils;
|
||||||
import xiaozhi.common.exception.ErrorCode;
|
|
||||||
import xiaozhi.common.exception.RenException;
|
|
||||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||||
import xiaozhi.modules.model.service.ModelConfigService;
|
import xiaozhi.modules.model.service.ModelConfigService;
|
||||||
import xiaozhi.modules.sys.service.SysUserService;
|
import xiaozhi.modules.sys.service.SysUserService;
|
||||||
@@ -221,34 +221,59 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl<VoiceCloneDao, VoiceC
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public String cloneAudio(String cloneId) {
|
public void cloneAudio(String cloneId) {
|
||||||
VoiceCloneEntity entity = baseDao.selectById(cloneId);
|
VoiceCloneEntity entity = baseDao.selectById(cloneId);
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
throw new RenException(ErrorCode.VOICE_CLONE_RECORD_NOT_EXIST);
|
throw new RenException(ErrorCode.VOICE_CLONE_RECORD_NOT_EXIST);
|
||||||
}
|
}
|
||||||
if (entity.getVoice() == null || entity.getVoice().length == 0) {
|
if (entity.getVoice() == null || entity.getVoice().length == 0) {
|
||||||
throw new RenException("请先上传音频文件");
|
throw new RenException("请先上传音频文件");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
ModelConfigEntity modelConfig = modelConfigService.getModelByIdFromCache(entity.getModelId());
|
||||||
|
if (modelConfig == null || modelConfig.getConfigJson() == null) {
|
||||||
|
throw new RenException("模型配置未找到");
|
||||||
|
}
|
||||||
|
Map<String, Object> config = modelConfig.getConfigJson();
|
||||||
|
String type = (String) config.get("type");
|
||||||
|
if (StringUtils.isBlank(type)) {
|
||||||
|
throw new RenException("模型类型未找到");
|
||||||
|
}
|
||||||
|
if (type.equals("huoshan_double_stream")) {
|
||||||
|
huoshanClone(config, entity);
|
||||||
|
}
|
||||||
|
} catch (RenException re) {
|
||||||
|
throw re;
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
entity.setTrainStatus(3);
|
||||||
|
entity.setTrainError(e.getMessage());
|
||||||
|
baseDao.updateById(entity);
|
||||||
|
throw new RenException("语音克隆失败: " + e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
/**
|
||||||
ModelConfigEntity modelConfig =
|
* 调用火山引擎进行语音复刻训练
|
||||||
modelConfigService.getModelByIdFromCache("TTS_HuoshanDoubleStreamTTS");
|
*
|
||||||
if (modelConfig == null || modelConfig.getConfigJson() == null) {
|
* @param config 模型配置
|
||||||
throw new RenException("火山引擎配置未找到");
|
* @param entity 语音克隆记录实体
|
||||||
}
|
* @throws Exception
|
||||||
Map<String, Object> config = modelConfig.getConfigJson();
|
*/
|
||||||
|
private void huoshanClone(Map<String, Object> config, VoiceCloneEntity entity) throws Exception {
|
||||||
String appid = (String) config.get("appid");
|
String appid = (String) config.get("appid");
|
||||||
String accessToken = (String) config.get("access_token");
|
String accessToken = (String) config.get("access_token");
|
||||||
|
|
||||||
if (StringUtils.isAnyBlank(appid, accessToken)) {
|
if (StringUtils.isAnyBlank(appid, accessToken)) {
|
||||||
throw new RenException("火山引擎配置不完整");
|
throw new RenException("火山引擎缺少appid或access_token");
|
||||||
}
|
}
|
||||||
|
|
||||||
String audioBase64 = Base64.getEncoder().encodeToString(entity.getVoice());
|
String audioBase64 = Base64.getEncoder().encodeToString(entity.getVoice());
|
||||||
Map<String, Object> reqBody = new HashMap<>();
|
Map<String, Object> reqBody = new HashMap<>();
|
||||||
reqBody.put("appid", appid);
|
reqBody.put("appid", appid);
|
||||||
reqBody.put("name", entity.getName());
|
|
||||||
List<Map<String, String>> audios = new ArrayList<>();
|
List<Map<String, String>> audios = new ArrayList<>();
|
||||||
Map<String, String> audioMap = new HashMap<>();
|
Map<String, String> audioMap = new HashMap<>();
|
||||||
audioMap.put("audio_bytes", audioBase64);
|
audioMap.put("audio_bytes", audioBase64);
|
||||||
@@ -258,7 +283,7 @@ public String cloneAudio(String cloneId) {
|
|||||||
reqBody.put("source", 2);
|
reqBody.put("source", 2);
|
||||||
reqBody.put("language", 0);
|
reqBody.put("language", 0);
|
||||||
reqBody.put("model_type", 1);
|
reqBody.put("model_type", 1);
|
||||||
reqBody.put("speaker_id", entity.getName());
|
reqBody.put("speaker_id", entity.getVoiceId());
|
||||||
|
|
||||||
String apiUrl = "https://openspeech.bytedance.com/api/v1/mega_tts/audio/upload";
|
String apiUrl = "https://openspeech.bytedance.com/api/v1/mega_tts/audio/upload";
|
||||||
|
|
||||||
@@ -267,52 +292,47 @@ public String cloneAudio(String cloneId) {
|
|||||||
.uri(URI.create(apiUrl))
|
.uri(URI.create(apiUrl))
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.header("Authorization", "Bearer;" + accessToken)
|
.header("Authorization", "Bearer;" + accessToken)
|
||||||
.header("Resource-Id", "volc.megatts.voiceclone")
|
.header("Resource-Id", "seed-icl-1.0")
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(reqBody)))
|
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(reqBody)))
|
||||||
.build();
|
.build();
|
||||||
// >>> 打印调试信息
|
|
||||||
System.out.println(">>> 请求地址 = " + apiUrl);
|
|
||||||
System.out.println(">>> AppID = " + appid);
|
|
||||||
System.out.println(">>> Token = " + accessToken);
|
|
||||||
|
|
||||||
|
|
||||||
/* ===== 真正发请求 ===== */
|
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
|
||||||
System.out.println(">>> HTTP status = " + response.statusCode());
|
System.out.println(">>> HTTP status = " + response.statusCode());
|
||||||
System.out.println(">>> response body = " + response.body());
|
System.out.println(">>> response body = " + response.body());
|
||||||
|
|
||||||
if (response.statusCode() == 200) {
|
if (response.statusCode() == 200) {
|
||||||
Map<String, Object> rsp = objectMapper.readValue(response.body(), Map.class);
|
Map<String, Object> rsp = objectMapper.readValue(response.body(),
|
||||||
String status = (String) rsp.get("status");
|
new TypeReference<Map<String, Object>>() {
|
||||||
String spkId = (String) rsp.get("spk_id");
|
});
|
||||||
if ("success".equals(status) && StringUtils.isNotBlank(spkId)) {
|
|
||||||
entity.setTrainStatus(2);
|
// 获取BaseResp对象
|
||||||
entity.setVoiceId(spkId);
|
Map<String, Object> baseResp = objectMapper.convertValue(rsp.get("BaseResp"),
|
||||||
entity.setTrainError(null);
|
new TypeReference<Map<String, Object>>() {
|
||||||
baseDao.updateById(entity);
|
});
|
||||||
return "训练成功,声音ID: " + spkId;
|
if (baseResp != null) {
|
||||||
|
Integer statusCode = objectMapper.convertValue(baseResp.get("StatusCode"), Integer.class);
|
||||||
|
String statusMessage = objectMapper.convertValue(baseResp.getOrDefault("StatusMessage", ""),
|
||||||
|
String.class);
|
||||||
|
|
||||||
|
// 获取speaker_id
|
||||||
|
String speakerId = objectMapper.convertValue(rsp.get("speaker_id"), String.class);
|
||||||
|
|
||||||
|
// StatusCode == 0 表示成功
|
||||||
|
if (statusCode != null && statusCode == 0 && StringUtils.isNotBlank(speakerId)) {
|
||||||
|
entity.setTrainStatus(2);
|
||||||
|
entity.setVoiceId(speakerId);
|
||||||
|
entity.setTrainError(null);
|
||||||
|
baseDao.updateById(entity);
|
||||||
|
} else {
|
||||||
|
// 失败时使用StatusMessage作为错误信息
|
||||||
|
String errorMsg = StringUtils.isNotBlank(statusMessage) ? statusMessage : "训练失败";
|
||||||
|
throw new RenException(errorMsg);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
String msg = (String) rsp.getOrDefault("message", "训练失败");
|
throw new RenException("响应格式错误,缺少BaseResp字段");
|
||||||
throw new RenException(msg);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
System.out.println(">>> 请求失败 HTTP = " + response.statusCode());
|
|
||||||
System.out.println(">>> 失败 body = " + response.body());
|
|
||||||
throw new RenException("请求失败,状态码: " + response.statusCode());
|
throw new RenException("请求失败,状态码: " + response.statusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (RenException re) {
|
|
||||||
// 自己抛的业务异常继续往外抛
|
|
||||||
throw re;
|
|
||||||
} catch (Exception e) {
|
|
||||||
/* ===== 任何其他异常(空指针、IO、JSON、404...)全部打印 ===== */
|
|
||||||
System.out.println(">>> 语音克隆异常: " + e.getMessage());
|
|
||||||
e.printStackTrace(); // 详细堆栈
|
|
||||||
entity.setTrainStatus(3);
|
|
||||||
entity.setTrainError(e.getMessage());
|
|
||||||
baseDao.updateById(entity);
|
|
||||||
throw new RenException("语音克隆失败: " + e.getMessage());
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user