From 087fd5d547aaee897e4381c6b81872e86c6f649f Mon Sep 17 00:00:00 2001 From: rainv123 <2148537152@qq.com> Date: Mon, 13 Oct 2025 14:17:53 +0800 Subject: [PATCH 01/24] =?UTF-8?q?update:=E9=9F=B3=E8=89=B2=E5=85=8B?= =?UTF-8?q?=E9=9A=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/VoiceCloneController.java | 4 +- .../voiceclone/service/VoiceCloneService.java | 7 ++ .../service/impl/VoiceCloneServiceImpl.java | 108 ++++++++++++++++++ 3 files changed, 118 insertions(+), 1 deletion(-) diff --git a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/controller/VoiceCloneController.java b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/controller/VoiceCloneController.java index 5e166b65..21c6eda3 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/controller/VoiceCloneController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/controller/VoiceCloneController.java @@ -175,7 +175,9 @@ public class VoiceCloneController { public Result cloneAudio(@RequestBody Map params) { String cloneId = params.get("cloneId"); checkPermission(cloneId); - return new Result(); + // 调用服务层进行语音克隆训练 + String result = voiceCloneService.cloneAudio(cloneId); + return new Result().ok(result); } private void checkPermission(String id) { diff --git a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/VoiceCloneService.java b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/VoiceCloneService.java index bdfd5ed0..750f02e6 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/VoiceCloneService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/VoiceCloneService.java @@ -65,4 +65,11 @@ public interface VoiceCloneService extends BaseService { * 获取音频数据 */ byte[] getVoiceData(String id); + + /** + * 克隆音频,调用火山引擎进行语音复刻训练 + * @param cloneId 语音克隆记录ID + * @return 训练结果消息 + */ + String cloneAudio(String cloneId); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java index 32b3c88f..cf66be20 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java @@ -1,7 +1,14 @@ package xiaozhi.modules.voiceclone.service.impl; +import java.io.IOException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.URI; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -12,14 +19,17 @@ import org.springframework.web.multipart.MultipartFile; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import xiaozhi.common.page.PageData; import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.utils.ConvertUtils; 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.service.ModelConfigService; import xiaozhi.modules.sys.service.SysUserService; import xiaozhi.modules.voiceclone.dao.VoiceCloneDao; @@ -28,6 +38,7 @@ import xiaozhi.modules.voiceclone.dto.VoiceCloneResponseDTO; import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity; import xiaozhi.modules.voiceclone.service.VoiceCloneService; +@Slf4j @Service @RequiredArgsConstructor public class VoiceCloneServiceImpl extends BaseServiceImpl @@ -35,6 +46,7 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl page(Map params) { @@ -207,4 +219,100 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl config = modelConfig.getConfigJson(); + String appid = (String) config.get("appid"); + String accessToken = (String) config.get("access_token"); + + if (StringUtils.isAnyBlank(appid, accessToken)) { + throw new RenException("火山引擎配置不完整"); + } + + String audioBase64 = Base64.getEncoder().encodeToString(entity.getVoice()); + Map reqBody = new HashMap<>(); + reqBody.put("appid", appid); + reqBody.put("name", entity.getName()); + List> audios = new ArrayList<>(); + Map audioMap = new HashMap<>(); + audioMap.put("audio_bytes", audioBase64); + audioMap.put("audio_format", "wav"); + audios.add(audioMap); + reqBody.put("audios", audios); + reqBody.put("source", 2); + reqBody.put("language", 0); + reqBody.put("model_type", 1); + reqBody.put("speaker_id", entity.getName()); + + String apiUrl = "https://openspeech.bytedance.com/api/v1/mega_tts/audio/upload"; + + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(apiUrl)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer;" + accessToken) + .header("Resource-Id", "volc.megatts.voiceclone") + .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(reqBody))) + .build(); + // >>> 打印调试信息 + System.out.println(">>> 请求地址 = " + apiUrl); + System.out.println(">>> AppID = " + appid); + System.out.println(">>> Token = " + accessToken); + + + /* ===== 真正发请求 ===== */ + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + System.out.println(">>> HTTP status = " + response.statusCode()); + System.out.println(">>> response body = " + response.body()); + + if (response.statusCode() == 200) { + Map rsp = objectMapper.readValue(response.body(), Map.class); + String status = (String) rsp.get("status"); + String spkId = (String) rsp.get("spk_id"); + if ("success".equals(status) && StringUtils.isNotBlank(spkId)) { + entity.setTrainStatus(2); + entity.setVoiceId(spkId); + entity.setTrainError(null); + baseDao.updateById(entity); + return "训练成功,声音ID: " + spkId; + } else { + String msg = (String) rsp.getOrDefault("message", "训练失败"); + throw new RenException(msg); + } + } else { + System.out.println(">>> 请求失败 HTTP = " + response.statusCode()); + System.out.println(">>> 失败 body = " + response.body()); + 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()); + } + } } From bec32bd085212f945df3984cfb7c7b2ab2c1905a Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Mon, 13 Oct 2025 15:26:26 +0800 Subject: [PATCH 02/24] =?UTF-8?q?update:=E6=8A=8A=E4=BC=A0=E9=80=92name?= =?UTF-8?q?=E6=94=B9=E6=88=90=E4=BC=A0=E9=80=92speaker=5Fid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/VoiceCloneController.java | 4 +- .../voiceclone/service/VoiceCloneService.java | 12 +- .../service/impl/VoiceCloneServiceImpl.java | 132 ++++++++++-------- 3 files changed, 84 insertions(+), 64 deletions(-) diff --git a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/controller/VoiceCloneController.java b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/controller/VoiceCloneController.java index 21c6eda3..90435e11 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/controller/VoiceCloneController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/controller/VoiceCloneController.java @@ -176,8 +176,8 @@ public class VoiceCloneController { String cloneId = params.get("cloneId"); checkPermission(cloneId); // 调用服务层进行语音克隆训练 - String result = voiceCloneService.cloneAudio(cloneId); - return new Result().ok(result); + voiceCloneService.cloneAudio(cloneId); + return new Result(); } private void checkPermission(String id) { diff --git a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/VoiceCloneService.java b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/VoiceCloneService.java index 750f02e6..7968bd73 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/VoiceCloneService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/VoiceCloneService.java @@ -35,17 +35,17 @@ public interface VoiceCloneService extends BaseService { * 根据用户ID查询声音克隆列表 */ List getByUserId(Long userId); - + /** * 分页查询带模型名称和用户名称的声音克隆列表 */ PageData pageWithNames(Map params); - + /** * 根据ID查询带模型名称和用户名称的声音克隆信息 */ VoiceCloneResponseDTO getByIdWithNames(String id); - + /** * 根据用户ID查询带模型名称的声音克隆列表 */ @@ -65,11 +65,11 @@ public interface VoiceCloneService extends BaseService { * 获取音频数据 */ byte[] getVoiceData(String id); - + /** * 克隆音频,调用火山引擎进行语音复刻训练 + * * @param cloneId 语音克隆记录ID - * @return 训练结果消息 */ - String cloneAudio(String cloneId); + void cloneAudio(String cloneId); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java index cf66be20..e1e9877e 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java @@ -1,10 +1,9 @@ package xiaozhi.modules.voiceclone.service.impl; -import java.io.IOException; +import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; -import java.net.URI; import java.util.ArrayList; import java.util.Arrays; 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.metadata.IPage; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import xiaozhi.common.exception.ErrorCode; +import xiaozhi.common.exception.RenException; import xiaozhi.common.page.PageData; import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.utils.ConvertUtils; 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.service.ModelConfigService; import xiaozhi.modules.sys.service.SysUserService; @@ -221,34 +221,59 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl 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) { - throw new RenException("火山引擎配置未找到"); - } - Map config = modelConfig.getConfigJson(); + /** + * 调用火山引擎进行语音复刻训练 + * + * @param config 模型配置 + * @param entity 语音克隆记录实体 + * @throws Exception + */ + private void huoshanClone(Map config, VoiceCloneEntity entity) throws Exception { String appid = (String) config.get("appid"); String accessToken = (String) config.get("access_token"); if (StringUtils.isAnyBlank(appid, accessToken)) { - throw new RenException("火山引擎配置不完整"); + throw new RenException("火山引擎缺少appid或access_token"); } String audioBase64 = Base64.getEncoder().encodeToString(entity.getVoice()); Map reqBody = new HashMap<>(); reqBody.put("appid", appid); - reqBody.put("name", entity.getName()); List> audios = new ArrayList<>(); Map audioMap = new HashMap<>(); audioMap.put("audio_bytes", audioBase64); @@ -258,7 +283,7 @@ public String cloneAudio(String cloneId) { reqBody.put("source", 2); reqBody.put("language", 0); 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"; @@ -267,52 +292,47 @@ public String cloneAudio(String cloneId) { .uri(URI.create(apiUrl)) .header("Content-Type", "application/json") .header("Authorization", "Bearer;" + accessToken) - .header("Resource-Id", "volc.megatts.voiceclone") + .header("Resource-Id", "seed-icl-1.0") .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(reqBody))) .build(); - // >>> 打印调试信息 - System.out.println(">>> 请求地址 = " + apiUrl); - System.out.println(">>> AppID = " + appid); - System.out.println(">>> Token = " + accessToken); - - - /* ===== 真正发请求 ===== */ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(">>> HTTP status = " + response.statusCode()); System.out.println(">>> response body = " + response.body()); if (response.statusCode() == 200) { - Map rsp = objectMapper.readValue(response.body(), Map.class); - String status = (String) rsp.get("status"); - String spkId = (String) rsp.get("spk_id"); - if ("success".equals(status) && StringUtils.isNotBlank(spkId)) { - entity.setTrainStatus(2); - entity.setVoiceId(spkId); - entity.setTrainError(null); - baseDao.updateById(entity); - return "训练成功,声音ID: " + spkId; + Map rsp = objectMapper.readValue(response.body(), + new TypeReference>() { + }); + + // 获取BaseResp对象 + Map baseResp = objectMapper.convertValue(rsp.get("BaseResp"), + new TypeReference>() { + }); + 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 { - String msg = (String) rsp.getOrDefault("message", "训练失败"); - throw new RenException(msg); + throw new RenException("响应格式错误,缺少BaseResp字段"); } } else { - System.out.println(">>> 请求失败 HTTP = " + response.statusCode()); - System.out.println(">>> 失败 body = " + response.body()); 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()); } - } } From 471ad864b6210bf54b19a7a0f8c4244bf5ea4c79 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Mon, 13 Oct 2025 15:31:49 +0800 Subject: [PATCH 03/24] =?UTF-8?q?update:=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modules/voiceclone/service/impl/VoiceCloneServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java index e1e9877e..6ccbc8a8 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java @@ -321,7 +321,7 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl Date: Tue, 14 Oct 2025 10:19:44 +0800 Subject: [PATCH 04/24] =?UTF-8?q?update:=E4=BF=AE=E6=94=B9=E9=9F=B3?= =?UTF-8?q?=E8=89=B2=E7=BC=96=E7=A0=81=E6=8E=92=E5=BA=8F=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xiaozhi/common/exception/ErrorCode.java | 8 ++++ .../modules/timbre/service/TimbreService.java | 8 ++++ .../service/impl/TimbreServiceImpl.java | 45 ++++++++++++++++++- .../service/impl/VoiceCloneServiceImpl.java | 37 ++++++++++++--- .../main/resources/i18n/messages.properties | 14 +++++- .../resources/i18n/messages_en_US.properties | 10 ++++- .../resources/i18n/messages_zh_CN.properties | 10 ++++- .../resources/i18n/messages_zh_TW.properties | 10 ++++- 8 files changed, 129 insertions(+), 13 deletions(-) diff --git a/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java b/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java index 573269b8..c8cc784a 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java +++ b/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java @@ -188,4 +188,12 @@ public interface ErrorCode { int VOICE_RESOURCE_ACCOUNT_EMPTY = 10148; // 归属账号不能为空 int VOICE_RESOURCE_DELETE_ID_EMPTY = 10149; // 删除的音色资源ID不能为空 int VOICE_RESOURCE_NO_PERMISSION = 10150; // 您没有权限操作该记录 + int VOICE_CLONE_AUDIO_NOT_UPLOADED = 10151; // 请先上传音频文件 + int VOICE_CLONE_MODEL_CONFIG_NOT_FOUND = 10152; // 模型配置未找到 + int VOICE_CLONE_MODEL_TYPE_NOT_FOUND = 10153; // 模型类型未找到 + int VOICE_CLONE_TRAINING_FAILED = 10154; // 训练失败 + int VOICE_CLONE_HUOSHAN_CONFIG_MISSING = 10155; // 火山引擎缺少配置 + int VOICE_CLONE_RESPONSE_FORMAT_ERROR = 10156; // 响应格式错误 + int VOICE_CLONE_REQUEST_FAILED = 10157; // 请求失败 + int VOICE_CLONE_PREFIX = 10158; // 复刻音色前缀 } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/timbre/service/TimbreService.java b/main/manager-api/src/main/java/xiaozhi/modules/timbre/service/TimbreService.java index eebd55be..424ad2b9 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/timbre/service/TimbreService.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/timbre/service/TimbreService.java @@ -64,4 +64,12 @@ public interface TimbreService extends BaseService { * @return 音色名称 */ String getTimbreNameById(String id); + + /** + * 检查音色编码(ttsVoice/speaker_id)是否已存在 + * + * @param ttsVoice 音色编码 + * @return 是否存在 + */ + boolean existsByTtsVoice(String ttsVoice); } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/timbre/service/impl/TimbreServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/timbre/service/impl/TimbreServiceImpl.java index 2ed5b1ab..ac26e30f 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/timbre/service/impl/TimbreServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/timbre/service/impl/TimbreServiceImpl.java @@ -1,9 +1,12 @@ package xiaozhi.modules.timbre.service.impl; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import xiaozhi.common.exception.ErrorCode; +import xiaozhi.common.utils.MessageUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; @@ -120,12 +123,39 @@ public class TimbreServiceImpl extends BaseServiceImpl if (StringUtils.isNotBlank(voiceName)) { queryWrapper.like("name", voiceName); } + List timbreEntities = timbreDao.selectList(queryWrapper); if (CollectionUtil.isEmpty(timbreEntities)) { return null; } - return ConvertUtils.sourceToTarget(timbreEntities, VoiceDTO.class); + // 自定义排序:音色编码以S_开头的排在最前面,其他按sort字段升序排序 + timbreEntities.sort((t1, t2) -> { + // 先判断是否以S_开头 + boolean isT1S_Start = StringUtils.isNotBlank(t1.getTtsVoice()) && t1.getTtsVoice().startsWith("S_"); + boolean isT2S_Start = StringUtils.isNotBlank(t2.getTtsVoice()) && t2.getTtsVoice().startsWith("S_"); + + if (isT1S_Start && !isT2S_Start) return -1; // t1以S_开头,排在前面 + if (!isT1S_Start && isT2S_Start) return 1; // t2以S_开头,排在前面 + + // 都以S_开头或都不以S_开头,则按sort字段升序排序 + return Long.compare(t1.getSort(), t2.getSort()); + }); + + // 排序完成后转换为DTO列表,并为复刻音色添加前缀 + List voiceDTOs = new ArrayList<>(); + for (TimbreEntity entity : timbreEntities) { + VoiceDTO dto = new VoiceDTO(); + dto.setId(entity.getId()); + // 对于音色编码以S_开头的复刻音色,在名称前加上"克隆:"前缀 + String name = entity.getName(); + if (StringUtils.isNotBlank(entity.getTtsVoice()) && entity.getTtsVoice().startsWith("S_") && !name.startsWith("克隆:")) { + name = MessageUtils.getMessage(ErrorCode.VOICE_CLONE_PREFIX) + name; + } + dto.setName(name); + voiceDTOs.add(dto); + } + return voiceDTOs; } /** @@ -158,4 +188,17 @@ public class TimbreServiceImpl extends BaseServiceImpl return null; } + + @Override + public boolean existsByTtsVoice(String ttsVoice) { + if (StringUtils.isBlank(ttsVoice)) { + return false; + } + + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("tts_voice", ttsVoice); + + // 检查数据库中是否存在相同的ttsVoice值 + return timbreDao.exists(queryWrapper); + } } \ No newline at end of file diff --git a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java index 6ccbc8a8..9ce6abb9 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java @@ -47,6 +47,7 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl page(Map params) { @@ -228,19 +229,19 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl config = modelConfig.getConfigJson(); String type = (String) config.get("type"); if (StringUtils.isBlank(type)) { - throw new RenException("模型类型未找到"); + throw new RenException(ErrorCode.VOICE_CLONE_MODEL_TYPE_NOT_FOUND); } if (type.equals("huoshan_double_stream")) { huoshanClone(config, entity); @@ -252,7 +253,7 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl Date: Tue, 14 Oct 2025 10:21:50 +0800 Subject: [PATCH 05/24] =?UTF-8?q?fix:=E8=A1=A5=E5=85=85=E9=9F=B3=E8=89=B2?= =?UTF-8?q?=E5=85=8B=E9=9A=86=E7=95=8C=E9=9D=A2=E5=BC=B9=E7=AA=97=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-web/src/i18n/en.js | 2 ++ main/manager-web/src/i18n/zh_CN.js | 2 ++ main/manager-web/src/i18n/zh_TW.js | 2 ++ main/manager-web/src/views/VoiceResourceManagement.vue | 6 +++--- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/main/manager-web/src/i18n/en.js b/main/manager-web/src/i18n/en.js index 00200256..2ddbe83c 100644 --- a/main/manager-web/src/i18n/en.js +++ b/main/manager-web/src/i18n/en.js @@ -1095,6 +1095,8 @@ export default { 'voiceClone.play': 'Play', 'voiceClone.pause': 'Pause', 'voiceClone.cancel': 'Cancel', + 'voiceClone.warning': 'Warning', + 'voiceClone.ok': 'OK', 'voiceClone.nextStep': 'Next', 'voiceClone.prevStep': 'Previous', 'voiceClone.upload': 'Upload Audio', diff --git a/main/manager-web/src/i18n/zh_CN.js b/main/manager-web/src/i18n/zh_CN.js index 678ee041..c3df3f0b 100644 --- a/main/manager-web/src/i18n/zh_CN.js +++ b/main/manager-web/src/i18n/zh_CN.js @@ -1095,6 +1095,8 @@ export default { 'voiceClone.play': '播放', 'voiceClone.pause': '暂停', 'voiceClone.cancel': '取消', + 'voiceClone.warning': '警告', + 'voiceClone.ok': '确定', 'voiceClone.nextStep': '下一步', 'voiceClone.prevStep': '上一步', 'voiceClone.upload': '上传音频', diff --git a/main/manager-web/src/i18n/zh_TW.js b/main/manager-web/src/i18n/zh_TW.js index 9a511402..5515a5f2 100644 --- a/main/manager-web/src/i18n/zh_TW.js +++ b/main/manager-web/src/i18n/zh_TW.js @@ -1095,6 +1095,8 @@ export default { 'voiceClone.play': '播放', 'voiceClone.pause': '暫停', 'voiceClone.cancel': '取消', + 'voiceClone.warning': '警告', + 'voiceClone.ok': '確定', 'voiceClone.nextStep': '下一步', 'voiceClone.prevStep': '上一步', 'voiceClone.upload': '上傳音頻', diff --git a/main/manager-web/src/views/VoiceResourceManagement.vue b/main/manager-web/src/views/VoiceResourceManagement.vue index efcba413..cce107e1 100644 --- a/main/manager-web/src/views/VoiceResourceManagement.vue +++ b/main/manager-web/src/views/VoiceResourceManagement.vue @@ -249,9 +249,9 @@ export default { } const itemCount = items.length; - this.$confirm(this.$t('voiceClone.confirmDelete', { count: itemCount }), 'Warning', { - confirmButtonText: 'OK', - cancelButtonText: 'Cancel', + this.$confirm(this.$t('voiceClone.confirmDelete', { count: itemCount }), this.$t('voiceClone.warning'), { + confirmButtonText: this.$t('voiceClone.ok'), + cancelButtonText: this.$t('voiceClone.cancel'), type: 'warning', distinguishCancelAndClose: true }).then(() => { From 56eb25f8e2661ca05cca4e91aa417b6fa74b7771 Mon Sep 17 00:00:00 2001 From: rainv123 <2148537152@qq.com> Date: Tue, 14 Oct 2025 10:59:54 +0800 Subject: [PATCH 06/24] =?UTF-8?q?fix:=E6=9B=B4=E6=AD=A3=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/i18n/messages_zh_CN.properties | 2 +- .../src/main/resources/i18n/messages_zh_TW.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties b/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties index 1a444d6d..85b5e2fe 100644 --- a/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties +++ b/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties @@ -164,4 +164,4 @@ 10155=\u706B\u5C71\u5F15\u64CE\u7F3A\u5C11\u914D\u7F6E 10156=\u54CD\u5E94\u683C\u5F0F\u9519\u8BEF\uFF0C\u7F3A\u5C11BaseResp\u5B57\u6BB5 10157=\u8BF7\u6C42\u5931\u8D25 -10158=\u8BF7\u6C42\u5931\u8D25\uFF0C\u72B6\u6001\u7801 \ No newline at end of file +10158=\u514B\u9686\u8BF7\u6C42: diff --git a/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties b/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties index 523bfa1f..22a753b5 100644 --- a/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties +++ b/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties @@ -164,4 +164,4 @@ 10155=\u706B\u5C71\u5F15\u64CE\u7F3A\u5C11appid\u6216access_token 10156=\u97FF\u61C9\u683C\u5F0F\u932F\u8AA4\uFF0C\u7F3A\u5C11BaseResp\u5B57\u6BB5 10157=\u8ACB\u6C42\u5931\u6557 -10158=\u590D\u8B58\u97F3\u8272\uFF1A\uFF0C\u72C0\u614B\u78BC \ No newline at end of file +10158=\u514b\u9686\u8a9e\u97f3: \ No newline at end of file From 3b69629a6122237b32fe9c1c6df9fa77edc130fb Mon Sep 17 00:00:00 2001 From: rainv123 <2148537152@qq.com> Date: Tue, 14 Oct 2025 11:04:49 +0800 Subject: [PATCH 07/24] =?UTF-8?q?fix:=E8=A1=A5=E5=85=85=E9=94=AE=E7=9A=84?= =?UTF-8?q?=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-web/src/i18n/en.js | 2 ++ main/manager-web/src/i18n/zh_CN.js | 2 ++ main/manager-web/src/i18n/zh_TW.js | 2 ++ 3 files changed, 6 insertions(+) diff --git a/main/manager-web/src/i18n/en.js b/main/manager-web/src/i18n/en.js index 2ddbe83c..70e4c96c 100644 --- a/main/manager-web/src/i18n/en.js +++ b/main/manager-web/src/i18n/en.js @@ -885,6 +885,8 @@ export default { 'ttsModel.unnamedVoice': 'Unnamed Voice', 'ttsModel.finishEditingFirst': 'Please finish current editing first', 'ttsModel.selectVoiceToDelete': 'Please select voices to delete', + 'ttsModel.warning': 'Warning', + 'ttsModel.confirmDeleteVoice': 'Are you sure to delete {count} voices?', // OTA Management Page Text 'otaManagement.firmwareManagement': 'Firmware Management', diff --git a/main/manager-web/src/i18n/zh_CN.js b/main/manager-web/src/i18n/zh_CN.js index c3df3f0b..bc8a0d06 100644 --- a/main/manager-web/src/i18n/zh_CN.js +++ b/main/manager-web/src/i18n/zh_CN.js @@ -885,6 +885,8 @@ export default { 'ttsModel.unnamedVoice': '未命名音色', 'ttsModel.finishEditingFirst': '请先完成当前编辑', 'ttsModel.selectVoiceToDelete': '请选择要删除的音色', + 'ttsModel.warning': '警告', + 'ttsModel.confirmDeleteVoice': '确定要删除{count}个音色吗?', // OTA管理页面文本 'otaManagement.firmwareManagement': '固件管理', diff --git a/main/manager-web/src/i18n/zh_TW.js b/main/manager-web/src/i18n/zh_TW.js index 5515a5f2..8c05836c 100644 --- a/main/manager-web/src/i18n/zh_TW.js +++ b/main/manager-web/src/i18n/zh_TW.js @@ -883,6 +883,8 @@ export default { 'ttsModel.unnamedVoice': '未命名音色', 'ttsModel.finishEditingFirst': '請先完成目前編輯', 'ttsModel.selectVoiceToDelete': '請選擇要刪除的音色', + 'ttsModel.warning': '警告', + 'ttsModel.confirmDeleteVoice': '確定要刪除{count}個音色嗎?', 'ttsModel.operationFailed': '操作失敗', 'ttsModel.operationClosed': '操作已關閉', From 174b9f589833330296c26f74fa2fbe2858d4ffa4 Mon Sep 17 00:00:00 2001 From: rainv123 <2148537152@qq.com> Date: Tue, 14 Oct 2025 15:05:02 +0800 Subject: [PATCH 08/24] =?UTF-8?q?updata:=E6=9B=B4=E6=96=B0=E7=81=AB?= =?UTF-8?q?=E5=B1=B1=E5=8F=8C=E5=90=91=E6=B5=81=E5=BC=8FTTS+=E5=A3=B0?= =?UTF-8?q?=E9=9F=B3=E5=85=8B=E9=9A=86=E9=85=8D=E7=BD=AE=E6=95=99=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/huoshan-streamTTS-voice-cloning.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/huoshan-streamTTS-voice-cloning.md b/docs/huoshan-streamTTS-voice-cloning.md index 34952f30..604a94dc 100644 --- a/docs/huoshan-streamTTS-voice-cloning.md +++ b/docs/huoshan-streamTTS-voice-cloning.md @@ -1,10 +1,20 @@ -# 火山双向流式TTS+声音克隆配置教程 -单模块部署下,使用火山引擎双向流式语音合成服务的同时进行声音克隆,支持WebSocket协议流式调用。 +# 火山双流式语音合成+音色克隆配置教程 +使用火山引擎双向流式语音合成服务的同时进行声音克隆,支持WebSocket协议流式调用。 ### 1.开通火山引擎服务 访问 https://console.volcengine.com/speech/app 在应用管理创建应用,勾选语音合成大模型和声音复刻大模型,左边列表点击声音复刻大模型后下滑获得App Id,Access Token,Cluster ID以及声音ID(S_xxxxx) ### 2.克隆音色 +#####2.1全模块部署 +1.填写配置。 +如果你是全模块部署,请在智控台页面模型配置的语音合成页面,找到“火山双流式语音合成”,点击修改,将你火山引擎的App Id,Access Token以及声音ID(S_xxxxx)填入,注意:把资源id(Resource-Id)改成```volc.megatts.defaul```或```seed-icl-1.0``` + +2.音色克隆。 +准备一段8-60之间的音频文件,点击音色克隆菜单的音色资源,新增音色资源。回到音色克隆页面,点击上传音频。上传好音频之后点击复刻,等1~2秒会返回结果。 +复刻成功后,在“火山双流式语音合成”的“音色资源”里可以看到你复刻好的声音,也可以在配置角色使用“火山双流式语音合成”时选择复刻的音色了。 + +#####2.2单模块部署 克隆音色请参照教程 https://github.com/104gogo/huoshan-voice-copy +1.音色克隆。 准备一段 10-30 秒的音频文件(.wav格式)添加到克隆的项目中,将平台获得的密钥填入```uploadAndStatus.py```和```tts_http_demo.py``` 在uploadAndStatus.py中,将 audio_path=修改成自己的.wav文件名称 @@ -19,11 +29,9 @@ python uploadAndStatus.py python tts_http_demo.py ``` 回到火山引擎控制台页面,刷新可以看到声音复刻详情的状态是复刻成功。 -### 3.填写配置文件 -将火山引擎服务申请到的密钥填入.config.yaml的HuoshanDoubleStreamTTS配置文件中 +2.填写配置文件(.config.yaml) 修改 resource_id的参数为``` volc.megatts.default``` -(参考官方文档 https://www.volcengine.com/docs/6561/1329505) speaker的参数填入声音ID(S_xxxxx) -启动服务,唤醒小智发出的声音是克隆的音色即成功。 +启动服务,唤醒小智发出的声音是克隆的音色即成功。 \ No newline at end of file From 55348ffe650055c0df7842d1f7e43f61ad3a3e28 Mon Sep 17 00:00:00 2001 From: rainv123 <2148537152@qq.com> Date: Tue, 14 Oct 2025 15:13:22 +0800 Subject: [PATCH 09/24] =?UTF-8?q?updata:=E6=9B=B4=E6=96=B0=E7=81=AB?= =?UTF-8?q?=E5=B1=B1=E5=8F=8C=E5=90=91=E6=B5=81=E5=BC=8FTTS+=E5=A3=B0?= =?UTF-8?q?=E9=9F=B3=E5=85=8B=E9=9A=86=E9=85=8D=E7=BD=AE=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/huoshan-streamTTS-voice-cloning.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/huoshan-streamTTS-voice-cloning.md b/docs/huoshan-streamTTS-voice-cloning.md index 604a94dc..8467bf1f 100644 --- a/docs/huoshan-streamTTS-voice-cloning.md +++ b/docs/huoshan-streamTTS-voice-cloning.md @@ -1,20 +1,24 @@ # 火山双流式语音合成+音色克隆配置教程 使用火山引擎双向流式语音合成服务的同时进行声音克隆,支持WebSocket协议流式调用。 -### 1.开通火山引擎服务 +## 1.开通火山引擎服务 访问 https://console.volcengine.com/speech/app 在应用管理创建应用,勾选语音合成大模型和声音复刻大模型,左边列表点击声音复刻大模型后下滑获得App Id,Access Token,Cluster ID以及声音ID(S_xxxxx) -### 2.克隆音色 -#####2.1全模块部署 +## 2.克隆音色 +###2.1全模块部署 1.填写配置。 -如果你是全模块部署,请在智控台页面模型配置的语音合成页面,找到“火山双流式语音合成”,点击修改,将你火山引擎的App Id,Access Token以及声音ID(S_xxxxx)填入,注意:把资源id(Resource-Id)改成```volc.megatts.defaul```或```seed-icl-1.0``` + +如果你是全模块部署,请在智控台页面模型配置的语音合成页面,找到“火山双流式语音合成”,点击修改,将你火山引擎的App Id,Access Token以及声音ID(S_xxxxx)填进去, +注意:把资源id(Resource-Id)改成```volc.megatts.defaul```或```seed-icl-1.0``` 2.音色克隆。 + 准备一段8-60之间的音频文件,点击音色克隆菜单的音色资源,新增音色资源。回到音色克隆页面,点击上传音频。上传好音频之后点击复刻,等1~2秒会返回结果。 复刻成功后,在“火山双流式语音合成”的“音色资源”里可以看到你复刻好的声音,也可以在配置角色使用“火山双流式语音合成”时选择复刻的音色了。 -#####2.2单模块部署 +###2.2单模块部署 克隆音色请参照教程 https://github.com/104gogo/huoshan-voice-copy 1.音色克隆。 + 准备一段 10-30 秒的音频文件(.wav格式)添加到克隆的项目中,将平台获得的密钥填入```uploadAndStatus.py```和```tts_http_demo.py``` 在uploadAndStatus.py中,将 audio_path=修改成自己的.wav文件名称 @@ -31,7 +35,8 @@ python tts_http_demo.py 回到火山引擎控制台页面,刷新可以看到声音复刻详情的状态是复刻成功。 2.填写配置文件(.config.yaml) -修改 resource_id的参数为``` volc.megatts.default``` + +注意:修改 resource_id的参数为``` volc.megatts.default``` speaker的参数填入声音ID(S_xxxxx) 启动服务,唤醒小智发出的声音是克隆的音色即成功。 \ No newline at end of file From afc142b397bee5a112aea63c80e20e4864450db2 Mon Sep 17 00:00:00 2001 From: rainv123 <2148537152@qq.com> Date: Tue, 14 Oct 2025 15:16:51 +0800 Subject: [PATCH 10/24] =?UTF-8?q?update:=E6=9B=B4=E6=96=B0=E7=81=AB?= =?UTF-8?q?=E5=B1=B1=E5=8F=8C=E6=B5=81=E5=BC=8F=E8=AF=AD=E9=9F=B3=E5=90=88?= =?UTF-8?q?=E6=88=90+=E9=9F=B3=E8=89=B2=E5=85=8B=E9=9A=86=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/huoshan-streamTTS-voice-cloning.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/huoshan-streamTTS-voice-cloning.md b/docs/huoshan-streamTTS-voice-cloning.md index 8467bf1f..4b242145 100644 --- a/docs/huoshan-streamTTS-voice-cloning.md +++ b/docs/huoshan-streamTTS-voice-cloning.md @@ -2,23 +2,20 @@ 使用火山引擎双向流式语音合成服务的同时进行声音克隆,支持WebSocket协议流式调用。 ## 1.开通火山引擎服务 访问 https://console.volcengine.com/speech/app 在应用管理创建应用,勾选语音合成大模型和声音复刻大模型,左边列表点击声音复刻大模型后下滑获得App Id,Access Token,Cluster ID以及声音ID(S_xxxxx) -## 2.克隆音色 -###2.1全模块部署 -1.填写配置。 +##2.全模块部署 +1.填写配置。 如果你是全模块部署,请在智控台页面模型配置的语音合成页面,找到“火山双流式语音合成”,点击修改,将你火山引擎的App Id,Access Token以及声音ID(S_xxxxx)填进去, 注意:把资源id(Resource-Id)改成```volc.megatts.defaul```或```seed-icl-1.0``` 2.音色克隆。 - 准备一段8-60之间的音频文件,点击音色克隆菜单的音色资源,新增音色资源。回到音色克隆页面,点击上传音频。上传好音频之后点击复刻,等1~2秒会返回结果。 复刻成功后,在“火山双流式语音合成”的“音色资源”里可以看到你复刻好的声音,也可以在配置角色使用“火山双流式语音合成”时选择复刻的音色了。 -###2.2单模块部署 +###3.单模块部署 克隆音色请参照教程 https://github.com/104gogo/huoshan-voice-copy 1.音色克隆。 - 准备一段 10-30 秒的音频文件(.wav格式)添加到克隆的项目中,将平台获得的密钥填入```uploadAndStatus.py```和```tts_http_demo.py``` 在uploadAndStatus.py中,将 audio_path=修改成自己的.wav文件名称 From c449d29de0182255422c35b6a7adbcf49521ef31 Mon Sep 17 00:00:00 2001 From: rainv123 <2148537152@qq.com> Date: Tue, 14 Oct 2025 15:23:48 +0800 Subject: [PATCH 11/24] =?UTF-8?q?Update=EF=BC=9A=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E7=81=AB=E5=B1=B1=E5=8F=8C=E6=B5=81=E5=BC=8F=E8=AF=AD=E9=9F=B3?= =?UTF-8?q?=E5=90=88=E6=88=90+=E9=9F=B3=E8=89=B2=E5=85=8B=E9=9A=86?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/huoshan-streamTTS-voice-cloning.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/huoshan-streamTTS-voice-cloning.md b/docs/huoshan-streamTTS-voice-cloning.md index 4b242145..44af3fb3 100644 --- a/docs/huoshan-streamTTS-voice-cloning.md +++ b/docs/huoshan-streamTTS-voice-cloning.md @@ -3,16 +3,16 @@ ## 1.开通火山引擎服务 访问 https://console.volcengine.com/speech/app 在应用管理创建应用,勾选语音合成大模型和声音复刻大模型,左边列表点击声音复刻大模型后下滑获得App Id,Access Token,Cluster ID以及声音ID(S_xxxxx) -##2.全模块部署 +### 2.全模块部署 1.填写配置。 -如果你是全模块部署,请在智控台页面模型配置的语音合成页面,找到“火山双流式语音合成”,点击修改,将你火山引擎的App Id,Access Token以及声音ID(S_xxxxx)填进去, -注意:把资源id(Resource-Id)改成```volc.megatts.defaul```或```seed-icl-1.0``` +如果你是全模块部署,请在智控台页面模型配置的语音合成页面,找到“火山双流式语音合成”,点击修改,将你火山引擎的App Id,Access Token以及声音ID(S_xxxxx)填进去。 +#####注意:把资源id(Resource-Id)改成```volc.megatts.defaul```或```seed-icl-1.0``` 2.音色克隆。 准备一段8-60之间的音频文件,点击音色克隆菜单的音色资源,新增音色资源。回到音色克隆页面,点击上传音频。上传好音频之后点击复刻,等1~2秒会返回结果。 复刻成功后,在“火山双流式语音合成”的“音色资源”里可以看到你复刻好的声音,也可以在配置角色使用“火山双流式语音合成”时选择复刻的音色了。 -###3.单模块部署 +### 3.单模块部署 克隆音色请参照教程 https://github.com/104gogo/huoshan-voice-copy 1.音色克隆。 @@ -31,9 +31,9 @@ python tts_http_demo.py ``` 回到火山引擎控制台页面,刷新可以看到声音复刻详情的状态是复刻成功。 -2.填写配置文件(.config.yaml) +2.填写配置文件。(.config.yaml) 注意:修改 resource_id的参数为``` volc.megatts.default``` speaker的参数填入声音ID(S_xxxxx) -启动服务,唤醒小智发出的声音是克隆的音色即成功。 \ No newline at end of file +启动服务,唤醒小智发出的声音是克隆的音色即成功。 From a033b56b8dd283a304b6f7b13b58c516661eef53 Mon Sep 17 00:00:00 2001 From: rainv123 <2148537152@qq.com> Date: Tue, 14 Oct 2025 15:51:34 +0800 Subject: [PATCH 12/24] =?UTF-8?q?Update=EF=BC=9A=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/i18n/messages_zh_CN.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties b/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties index 85b5e2fe..902e2a50 100644 --- a/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties +++ b/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties @@ -164,4 +164,4 @@ 10155=\u706B\u5C71\u5F15\u64CE\u7F3A\u5C11\u914D\u7F6E 10156=\u54CD\u5E94\u683C\u5F0F\u9519\u8BEF\uFF0C\u7F3A\u5C11BaseResp\u5B57\u6BB5 10157=\u8BF7\u6C42\u5931\u8D25 -10158=\u514B\u9686\u8BF7\u6C42: +10158=\u514b\u9686\u8272\u97f3: From d451fe7a0bda2fdb6b08e69f9d4611efb22cb248 Mon Sep 17 00:00:00 2001 From: rainv123 <2148537152@qq.com> Date: Fri, 17 Oct 2025 09:22:08 +0800 Subject: [PATCH 13/24] =?UTF-8?q?uptate:=E4=BF=AE=E6=94=B9=E8=AE=AD?= =?UTF-8?q?=E7=BB=83=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/views/VoiceCloneManagement.vue | 120 ++++++++++++++++-- 1 file changed, 110 insertions(+), 10 deletions(-) diff --git a/main/manager-web/src/views/VoiceCloneManagement.vue b/main/manager-web/src/views/VoiceCloneManagement.vue index a0e68ebc..138f8612 100644 --- a/main/manager-web/src/views/VoiceCloneManagement.vue +++ b/main/manager-web/src/views/VoiceCloneManagement.vue @@ -36,7 +36,9 @@ @@ -226,8 +228,6 @@ export default { switch (row.trainStatus) { case 0: return this.$t('voiceClone.waitingTraining'); - case 1: - return this.$t('voiceClone.training'); case 2: return this.$t('voiceClone.trainSuccess'); case 3: @@ -236,19 +236,90 @@ export default { return ''; } }, + // 获取状态按钮样式 + getStatusButtonClass(row) { + if (!row.hasVoice || row.trainStatus === 0) { + return 'status-waiting'; + } else if (row.trainStatus === 2) { + return 'status-success'; + } else if (row.trainStatus === 3) { + return 'status-failed'; + } + return ''; + }, // 处理复刻操作 handleClone(row) { + // 防止重复提交 + if (row._submitting) { + return; + } + row._submitting = true; + const params = { cloneId: row.id }; - Api.voiceClone.cloneAudio(params, (res) => { - res = res.data; - if (res.code === 0) { - this.$message.success(this.$t('message.success')); - } else { - this.$message.error(res.msg || this.$t('message.error')); + try { + Api.voiceClone.cloneAudio(params, (res) => { + try { + res = res.data; + if (res.code === 0) { + this.$message.success(this.$t('message.success')); + } else { + // 出错时更新状态为训练失败 + console.log('API返回错误,更新状态为训练失败'); + this.updateRowStatus(row, 3); + this.$message.error(res.msg || this.$t('message.error')); + // 检查是否包含403错误 + if (res.msg && res.msg.includes('状态码: 403')) { + console.error('权限错误: 403'); + } + } + } catch (error) { + console.error('处理响应时出错:', error); + // 出错时更新状态为训练失败 + this.updateRowStatus(row, 3); + this.$message.error('处理响应时出错'); + } finally { + row._submitting = false; + } + }, (error) => { + console.error('API调用失败:', error); + // 请求失败时更新状态为训练失败 + this.updateRowStatus(row, 3); + this.$message.error('请求失败'); + row._submitting = false; + }); + } catch (error) { + console.error('调用API时出错:', error); + // 出错时更新状态为训练失败 + this.updateRowStatus(row, 3); + this.$message.error('调用API时出错'); + row._submitting = false; + } + }, + + // 更新行状态并触发视图更新 + updateRowStatus(row, status) { + // 在Vue中直接修改数组中的对象属性可能不会触发视图更新 + // 找到对应行的索引 + const index = this.voiceCloneList.findIndex(item => item.id === row.id); + if (index !== -1) { + // 使用Vue.set来确保响应式更新 + this.$set(this.voiceCloneList, index, { + ...this.voiceCloneList[index], + trainStatus: status + }); + // 强制表格重新渲染 + if (this.$refs.paramsTable) { + this.$refs.paramsTable.doLayout(); } - }); + } else { + // 如果找不到索引,直接更新row对象 + row.trainStatus = status; + // 强制整个表格重新渲染 + this.$forceUpdate(); + } + console.log('更新行状态:', row.id, '状态:', status); }, // 复刻成功后的回调 handleCloneSuccess() { @@ -619,6 +690,35 @@ export default { color: #5a64b5 !important; } +/* 状态按钮样式 */ +.status-button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 4px 10px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; +} + +.status-waiting { + background-color: #f5f7fa; + color: #909399; + border: 1px solid #e4e7ed; +} + +.status-success { + background-color: #f6ffed; + color: #52c41a; + border: 1px solid #b7eb8f; +} + +.status-failed { + background-color: #fff2f0; + color: #ff4d4f; + border: 1px solid #ffccc7; +} + .name-view { display: inline-flex; align-items: center; From 008456d7cd19f48144bf82261cb811017afb7603 Mon Sep 17 00:00:00 2001 From: rainv123 <2148537152@qq.com> Date: Fri, 17 Oct 2025 11:31:13 +0800 Subject: [PATCH 14/24] =?UTF-8?q?update:=E6=96=B0=E5=A2=9E=E9=9F=B3?= =?UTF-8?q?=E8=89=B2=E8=B5=84=E6=BA=90=E6=97=B6=E5=88=A4=E6=96=AD=E5=A3=B0?= =?UTF-8?q?=E9=9F=B3id=E6=98=AF=E5=90=A6=E5=B7=B2=E7=BB=8F=E5=AD=98?= =?UTF-8?q?=E5=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/xiaozhi/common/exception/ErrorCode.java | 1 + .../main/java/xiaozhi/common/utils/MessageUtils.java | 8 ++++---- .../controller/VoiceResourceController.java | 2 ++ .../service/impl/VoiceCloneServiceImpl.java | 11 +++++++++++ .../src/main/resources/i18n/messages_en_US.properties | 3 ++- .../src/main/resources/i18n/messages_zh_CN.properties | 1 + .../src/main/resources/i18n/messages_zh_TW.properties | 3 ++- 7 files changed, 23 insertions(+), 6 deletions(-) diff --git a/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java b/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java index c8cc784a..aca6b5d0 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java +++ b/main/manager-api/src/main/java/xiaozhi/common/exception/ErrorCode.java @@ -196,4 +196,5 @@ public interface ErrorCode { int VOICE_CLONE_RESPONSE_FORMAT_ERROR = 10156; // 响应格式错误 int VOICE_CLONE_REQUEST_FAILED = 10157; // 请求失败 int VOICE_CLONE_PREFIX = 10158; // 复刻音色前缀 + int VOICE_ID_ALREADY_EXISTS = 10159; // 音色ID已存在 } diff --git a/main/manager-api/src/main/java/xiaozhi/common/utils/MessageUtils.java b/main/manager-api/src/main/java/xiaozhi/common/utils/MessageUtils.java index c346bf3d..abcc2cb3 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/utils/MessageUtils.java +++ b/main/manager-api/src/main/java/xiaozhi/common/utils/MessageUtils.java @@ -11,15 +11,15 @@ import org.springframework.context.i18n.LocaleContextHolder; public class MessageUtils { private static MessageSource messageSource; - static { - messageSource = (MessageSource) SpringContextUtils.getBean("messageSource"); - } - public static String getMessage(int code) { return getMessage(code, new String[0]); } public static String getMessage(int code, String... params) { + if (messageSource == null) { + // 延迟初始化,确保Spring上下文已完全初始化 + messageSource = (MessageSource) SpringContextUtils.getBean("messageSource"); + } return messageSource.getMessage(code + "", params, LocaleContextHolder.getLocale()); } } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/controller/VoiceResourceController.java b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/controller/VoiceResourceController.java index eff10c18..a4cfa479 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/controller/VoiceResourceController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/controller/VoiceResourceController.java @@ -80,6 +80,8 @@ public class VoiceResourceController { try { voiceCloneService.save(dto); return new Result(); + } catch (xiaozhi.common.exception.RenException e) { + return new Result().error(e.getCode(), e.getMsg()); } catch (RuntimeException e) { return new Result().error(ErrorCode.ADD_DATA_FAILED, e.getMessage()); } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java index 9ce6abb9..0b1f38b5 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java @@ -74,6 +74,17 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl wrapper = new QueryWrapper<>(); + wrapper.eq("voice_id", voiceId); + wrapper.eq("model_id", dto.getModelId()); + Long count = baseDao.selectCount(wrapper); + if (count > 0) { + throw new RenException(ErrorCode.VOICE_ID_ALREADY_EXISTS, "音色ID " + voiceId + " 已存在"); + } + } + // 遍历选择的音色ID,为每个音色ID创建一条记录 int index = 0; String namePrefix = DateUtils.format(new java.util.Date(), "MMddHHmm"); diff --git a/main/manager-api/src/main/resources/i18n/messages_en_US.properties b/main/manager-api/src/main/resources/i18n/messages_en_US.properties index ffc7a2cb..a052320a 100644 --- a/main/manager-api/src/main/resources/i18n/messages_en_US.properties +++ b/main/manager-api/src/main/resources/i18n/messages_en_US.properties @@ -164,4 +164,5 @@ 10155=Huoshan Engine configuration missing 10156=Response format error, missing BaseResp field 10157=Request failed -10158=Clone Voice: \ No newline at end of file +10158=Clone Voice: +10159=Voice ID already exists \ No newline at end of file diff --git a/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties b/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties index 902e2a50..65afe8b0 100644 --- a/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties +++ b/main/manager-api/src/main/resources/i18n/messages_zh_CN.properties @@ -165,3 +165,4 @@ 10156=\u54CD\u5E94\u683C\u5F0F\u9519\u8BEF\uFF0C\u7F3A\u5C11BaseResp\u5B57\u6BB5 10157=\u8BF7\u6C42\u5931\u8D25 10158=\u514b\u9686\u8272\u97f3: +10159=\u97F3\u8272ID\u5DF2\u5B58\u5728 diff --git a/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties b/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties index 22a753b5..4182f677 100644 --- a/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties +++ b/main/manager-api/src/main/resources/i18n/messages_zh_TW.properties @@ -164,4 +164,5 @@ 10155=\u706B\u5C71\u5F15\u64CE\u7F3A\u5C11appid\u6216access_token 10156=\u97FF\u61C9\u683C\u5F0F\u932F\u8AA4\uFF0C\u7F3A\u5C11BaseResp\u5B57\u6BB5 10157=\u8ACB\u6C42\u5931\u6557 -10158=\u514b\u9686\u8a9e\u97f3: \ No newline at end of file +10158=\u514b\u9686\u8a9e\u97f3: +10159=\u97F3\u8272ID\u5DF2\u5B58\u5728 \ No newline at end of file From f8b1e8199c723942b8018fb7c1a6079020edfa83 Mon Sep 17 00:00:00 2001 From: 3030332422 <3030332422@qq.com> Date: Fri, 17 Oct 2025 15:56:33 +0800 Subject: [PATCH 15/24] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E5=A4=8D=E5=88=BB?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../voiceclone/service/impl/VoiceCloneServiceImpl.java | 3 --- main/manager-web/src/views/VoiceCloneManagement.vue | 9 +++------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java index 0b1f38b5..20c1e44f 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java @@ -257,14 +257,11 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl { console.error('API调用失败:', error); - // 请求失败时更新状态为训练失败 - this.updateRowStatus(row, 3); this.$message.error('请求失败'); row._submitting = false; + this.fetchVoiceCloneList(); }); } catch (error) { console.error('调用API时出错:', error); - // 出错时更新状态为训练失败 - this.updateRowStatus(row, 3); this.$message.error('调用API时出错'); row._submitting = false; + this.fetchVoiceCloneList(); } }, From af02d537181b0ca8723d426d53ce9a937553a9b6 Mon Sep 17 00:00:00 2001 From: 3030332422 <3030332422@qq.com> Date: Fri, 17 Oct 2025 17:08:41 +0800 Subject: [PATCH 16/24] =?UTF-8?q?update:=E4=BF=AE=E5=A4=8D=E5=A4=8D?= =?UTF-8?q?=E5=88=BB=E5=A4=B1=E8=B4=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../voiceclone/service/impl/VoiceCloneServiceImpl.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java index 20c1e44f..a4a9932c 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/service/impl/VoiceCloneServiceImpl.java @@ -257,11 +257,17 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl Date: Fri, 17 Oct 2025 17:55:56 +0800 Subject: [PATCH 17/24] =?UTF-8?q?update:=E5=A2=9E=E5=8A=A0=E9=9F=B3?= =?UTF-8?q?=E8=89=B2=E5=85=8B=E9=9A=86=E9=A1=B5=E9=9D=A2=E8=AF=A6=E6=83=85?= =?UTF-8?q?=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/manager-web/src/i18n/en.js | 1 + main/manager-web/src/i18n/zh_CN.js | 1 + main/manager-web/src/i18n/zh_TW.js | 1 + .../src/views/VoiceCloneManagement.vue | 130 ++++++++++++++++-- 4 files changed, 125 insertions(+), 8 deletions(-) diff --git a/main/manager-web/src/i18n/en.js b/main/manager-web/src/i18n/en.js index 70e4c96c..77028d69 100644 --- a/main/manager-web/src/i18n/en.js +++ b/main/manager-web/src/i18n/en.js @@ -1112,4 +1112,5 @@ export default { 'voiceClone.updateNameSuccess': 'Name updated successfully', 'voiceClone.updateNameFailed': 'Failed to update name', 'voiceClone.playFailed': 'Play failed', + 'voiceClone.Details': 'Details', } \ No newline at end of file diff --git a/main/manager-web/src/i18n/zh_CN.js b/main/manager-web/src/i18n/zh_CN.js index bc8a0d06..c063bb4d 100644 --- a/main/manager-web/src/i18n/zh_CN.js +++ b/main/manager-web/src/i18n/zh_CN.js @@ -1112,4 +1112,5 @@ export default { 'voiceClone.updateNameSuccess': '名称更新成功', 'voiceClone.updateNameFailed': '名称更新失败', 'voiceClone.playFailed': '播放失败', + 'voiceClone.Details': '详情', } \ No newline at end of file diff --git a/main/manager-web/src/i18n/zh_TW.js b/main/manager-web/src/i18n/zh_TW.js index 8c05836c..07a82737 100644 --- a/main/manager-web/src/i18n/zh_TW.js +++ b/main/manager-web/src/i18n/zh_TW.js @@ -1112,4 +1112,5 @@ export default { 'voiceClone.updateNameSuccess': '名稱更新成功', 'voiceClone.updateNameFailed': '名稱更新失敗', 'voiceClone.playFailed': '播放失敗', + 'voiceClone.Details': '詳情', } \ No newline at end of file diff --git a/main/manager-web/src/views/VoiceCloneManagement.vue b/main/manager-web/src/views/VoiceCloneManagement.vue index f1ce9940..04a4fe19 100644 --- a/main/manager-web/src/views/VoiceCloneManagement.vue +++ b/main/manager-web/src/views/VoiceCloneManagement.vue @@ -41,6 +41,17 @@ + + + + + - + - +