diff --git a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java index febe12ff..3ae49919 100644 --- a/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java +++ b/main/manager-api/src/main/java/xiaozhi/common/constant/Constant.java @@ -146,6 +146,11 @@ public interface Constant { */ String MEMORY_NO_MEM = "Memory_nomem"; + /** + * 火山引擎双声道语音克隆 + */ + String VOICE_CLONE_HUOSHAN_DOUBLE_STREAM = "huoshan_double_stream"; + enum SysBaseParam { /** * ICP备案号 @@ -171,6 +176,38 @@ public interface Constant { } } + /** + * 训练状态 + */ + enum TrainStatus { + /** + * 未训练 + */ + NOT_TRAINED(0), + /** + * 训练中 + */ + TRAINING(1), + /** + * 已训练 + */ + TRAINED(2), + /** + * 训练失败 + */ + TRAIN_FAILED(3); + + private final int code; + + TrainStatus(int code) { + this.code = code; + } + + public int getCode() { + return code; + } + } + /** * 系统短信 */ 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 aca6b5d0..63b625b5 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 @@ -55,7 +55,7 @@ public interface ErrorCode { int OTA_DEVICE_NOT_FOUND = 10041; int OTA_DEVICE_NEED_BIND = 10042; - + // 新增错误编码 int DELETE_DATA_FAILED = 10043; int USER_NOT_LOGIN = 10044; @@ -66,7 +66,7 @@ public interface ErrorCode { int INVALID_LLM_TYPE = 10049; int TOKEN_GENERATE_ERROR = 10050; int RESOURCE_NOT_FOUND = 10051; - + // 新增错误编码 int DEFAULT_AGENT_NOT_FOUND = 10052; int AGENT_NOT_FOUND = 10053; @@ -89,7 +89,7 @@ public interface ErrorCode { int LLM_NOT_EXIST = 10092; // 设置的LLM不存在 int MODEL_REFERENCED_BY_AGENT = 10093; // 该模型配置已被智能体引用,无法删除 int LLM_REFERENCED_BY_INTENT = 10094; // 该LLM模型已被意图识别配置引用,无法删除 - + // 登录相关错误码 int ADD_DATA_FAILED = 10065; // 新增数据失败 int UPDATE_DATA_FAILED = 10066; // 修改数据失败 @@ -102,17 +102,17 @@ public interface ErrorCode { int RETRIEVE_PASSWORD_DISABLED = 10073; // 未开启找回密码功能 int PHONE_FORMAT_ERROR = 10074; // 手机号码格式不正确 int SMS_CODE_ERROR = 10075; // 手机验证码错误 - + // 字典类型相关错误码 int DICT_TYPE_NOT_EXIST = 10076; // 字典类型不存在 int DICT_TYPE_DUPLICATE = 10077; // 字典类型编码重复 - + // 资源处理相关错误码 int RESOURCE_READ_ERROR = 10078; // 读取资源失败 - + // 智能体相关错误码 int LLM_INTENT_PARAMS_MISMATCH = 10079; // LLM大模型和Intent意图识别,选择参数不匹配 - + // 声纹相关错误码 int VOICEPRINT_ALREADY_REGISTERED = 10080; // 此声音声纹已经注册 int VOICEPRINT_DELETE_ERROR = 10081; // 删除声纹出现错误 @@ -126,12 +126,12 @@ public interface ErrorCode { int VOICEPRINT_UNREGISTER_REQUEST_ERROR = 10089; // 声纹注销请求失败 int VOICEPRINT_UNREGISTER_PROCESS_ERROR = 10090; // 声纹注销处理失败 int VOICEPRINT_IDENTIFY_REQUEST_ERROR = 10091; // 声纹识别请求失败 - + // 服务端管理相关错误码 int INVALID_SERVER_ACTION = 10095; // 无效服务端操作 int SERVER_WEBSOCKET_NOT_CONFIGURED = 10096; // 未配置服务端WebSocket地址 int TARGET_WEBSOCKET_NOT_EXIST = 10097; // 目标WebSocket地址不存在 - + // 参数验证相关错误码 int WEBSOCKET_URLS_EMPTY = 10098; // WebSocket地址列表不能为空 int WEBSOCKET_URL_LOCALHOST = 10099; // WebSocket地址不能使用localhost或127.0.0.1 @@ -175,7 +175,7 @@ public interface ErrorCode { int DOWNLOAD_LINK_INVALID = 10137; // 下载链接无效 int CHAT_ROLE_USER = 10138; // 用户角色 int CHAT_ROLE_AGENT = 10139; // 智能体角色 - + // 声音克隆相关错误码 int VOICE_CLONE_AUDIO_EMPTY = 10140; // 音频文件不能为空 int VOICE_CLONE_NOT_AUDIO_FILE = 10141; // 只支持音频文件 @@ -197,4 +197,5 @@ public interface ErrorCode { int VOICE_CLONE_REQUEST_FAILED = 10157; // 请求失败 int VOICE_CLONE_PREFIX = 10158; // 复刻音色前缀 int VOICE_ID_ALREADY_EXISTS = 10159; // 音色ID已存在 + int VOICE_CLONE_HUOSHAN_VOICE_ID_ERROR = 10160; // 火山引擎音色ID格式错误 } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java index 72b8ea4d..3556877d 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java @@ -42,6 +42,7 @@ import xiaozhi.modules.agent.service.AgentTemplateService; import xiaozhi.modules.agent.vo.AgentInfoVO; import xiaozhi.modules.device.service.DeviceService; import xiaozhi.modules.model.dto.ModelProviderDTO; +import xiaozhi.modules.model.dto.VoiceDTO; import xiaozhi.modules.model.entity.ModelConfigEntity; import xiaozhi.modules.model.service.ModelConfigService; import xiaozhi.modules.model.service.ModelProviderService; @@ -373,6 +374,22 @@ public class AgentServiceImpl extends BaseServiceImpl imp entity.setLlmModelId(template.getLlmModelId()); entity.setVllmModelId(template.getVllmModelId()); entity.setTtsModelId(template.getTtsModelId()); + + if (template.getTtsVoiceId() == null && template.getTtsModelId() != null) { + ModelConfigEntity ttsModel = modelConfigService.selectById(template.getTtsModelId()); + if (ttsModel != null && ttsModel.getConfigJson() != null) { + Map config = ttsModel.getConfigJson(); + String voice = (String) config.get("voice"); + if (StringUtils.isBlank(voice)) { + voice = (String) config.get("speaker"); + } + VoiceDTO timbre = timbreModelService.getByVoiceCode(template.getTtsModelId(), voice); + if (timbre != null) { + template.setTtsVoiceId(timbre.getId()); + } + } + } + entity.setTtsVoiceId(template.getTtsVoiceId()); entity.setMemModelId(template.getMemModelId()); entity.setIntentModelId(template.getIntentModelId()); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java index dcee2c2b..a34c73dd 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java @@ -38,6 +38,8 @@ import xiaozhi.modules.sys.dto.SysParamsDTO; import xiaozhi.modules.sys.service.SysParamsService; import xiaozhi.modules.timbre.service.TimbreService; import xiaozhi.modules.timbre.vo.TimbreDetailsVO; +import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity; +import xiaozhi.modules.voiceclone.service.VoiceCloneService; @Service @AllArgsConstructor @@ -51,6 +53,7 @@ public class ConfigServiceImpl implements ConfigService { private final TimbreService timbreService; private final AgentPluginMappingService agentPluginMappingService; private final AgentMcpAccessPointService agentMcpAccessPointService; + private final VoiceCloneService cloneVoiceService; private final AgentVoicePrintDao agentVoicePrintDao; @Override @@ -124,6 +127,11 @@ public class ConfigServiceImpl implements ConfigService { voice = timbre.getTtsVoice(); referenceAudio = timbre.getReferenceAudio(); referenceText = timbre.getReferenceText(); + } else { + VoiceCloneEntity voice_print = cloneVoiceService.selectById(agent.getTtsVoiceId()); + if (voice_print != null) { + voice = voice_print.getVoiceId(); + } } // 构建返回数据 Map result = new HashMap<>(); @@ -395,7 +403,7 @@ public class ConfigServiceImpl implements ConfigService { // 火山引擎声音克隆需要替换resource_id Map map = (Map) model.getConfigJson(); - if ("huoshan_double_stream".equals(map.get("type"))) { + if (Constant.VOICE_CLONE_HUOSHAN_DOUBLE_STREAM.equals(map.get("type"))) { // 如果voice是”S_“开头的,使用seed-icl-1.0 if (voice != null && voice.startsWith("S_")) { map.put("resource_id", "seed-icl-1.0"); 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 424ad2b9..d8025b3c 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,12 +64,13 @@ public interface TimbreService extends BaseService { * @return 音色名称 */ String getTimbreNameById(String id); - + /** - * 检查音色编码(ttsVoice/speaker_id)是否已存在 + * 根据音色编码获取音色信息 * - * @param ttsVoice 音色编码 - * @return 是否存在 + * @param ttsModelId 音色模型ID + * @param voiceCode 音色编码 + * @return 音色信息 */ - boolean existsByTtsVoice(String ttsVoice); + VoiceDTO getByVoiceCode(String ttsModelId, String voiceCode); } \ 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 ac26e30f..8ebb2155 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 @@ -5,8 +5,7 @@ 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 java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; @@ -18,18 +17,23 @@ import com.baomidou.mybatisplus.core.metadata.IPage; import cn.hutool.core.collection.CollectionUtil; import lombok.AllArgsConstructor; import xiaozhi.common.constant.Constant; +import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.page.PageData; import xiaozhi.common.redis.RedisKeys; import xiaozhi.common.redis.RedisUtils; import xiaozhi.common.service.impl.BaseServiceImpl; import xiaozhi.common.utils.ConvertUtils; +import xiaozhi.common.utils.MessageUtils; import xiaozhi.modules.model.dto.VoiceDTO; +import xiaozhi.modules.security.user.SecurityUser; import xiaozhi.modules.timbre.dao.TimbreDao; import xiaozhi.modules.timbre.dto.TimbreDataDTO; import xiaozhi.modules.timbre.dto.TimbrePageDTO; import xiaozhi.modules.timbre.entity.TimbreEntity; import xiaozhi.modules.timbre.service.TimbreService; import xiaozhi.modules.timbre.vo.TimbreDetailsVO; +import xiaozhi.modules.voiceclone.dao.VoiceCloneDao; +import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity; /** * 音色的业务层的实现 @@ -42,6 +46,7 @@ import xiaozhi.modules.timbre.vo.TimbreDetailsVO; public class TimbreServiceImpl extends BaseServiceImpl implements TimbreService { private final TimbreDao timbreDao; + private final VoiceCloneDao voiceCloneDao; private final RedisUtils redisUtils; @Override @@ -123,39 +128,31 @@ public class TimbreServiceImpl extends BaseServiceImpl if (StringUtils.isNotBlank(voiceName)) { queryWrapper.like("name", voiceName); } - List timbreEntities = timbreDao.selectList(queryWrapper); - if (CollectionUtil.isEmpty(timbreEntities)) { - return null; + if (timbreEntities == null) { + timbreEntities = new ArrayList<>(); } + List voiceDTOs = timbreEntities.stream() + .map(entity -> new VoiceDTO(entity.getId(), entity.getName())) + .collect(Collectors.toList()); - // 自定义排序:音色编码以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; + // 获取当前登录用户ID + Long currentUserId = SecurityUser.getUser().getId(); + if (currentUserId != null) { + // 查询用户的所有克隆音色记录 + List cloneEntities = voiceCloneDao.getTrainSuccess(ttsModelId, currentUserId); + for (VoiceDTO entity : cloneEntities) { + // 只添加训练成功的克隆音色,且模型ID匹配 + VoiceDTO voiceDTO = new VoiceDTO(); + voiceDTO.setId(entity.getId()); + voiceDTO.setName(MessageUtils.getMessage(ErrorCode.VOICE_CLONE_PREFIX) + entity.getName()); + redisUtils.set(RedisKeys.getTimbreNameById(voiceDTO.getId()), voiceDTO.getName(), + RedisUtils.NOT_EXPIRE); + voiceDTOs.add(0, voiceDTO); } - dto.setName(name); - voiceDTOs.add(dto); } - return voiceDTOs; + + return CollectionUtil.isEmpty(voiceDTOs) ? null : voiceDTOs; } /** @@ -184,21 +181,30 @@ public class TimbreServiceImpl extends BaseServiceImpl redisUtils.set(RedisKeys.getTimbreNameById(id), name); } return name; + } else { + VoiceCloneEntity cloneEntity = voiceCloneDao.selectById(id); + if (cloneEntity != null) { + String name = MessageUtils.getMessage(ErrorCode.VOICE_CLONE_PREFIX) + cloneEntity.getName(); + redisUtils.set(RedisKeys.getTimbreNameById(id), name); + return name; + } } return null; } @Override - public boolean existsByTtsVoice(String ttsVoice) { - if (StringUtils.isBlank(ttsVoice)) { - return false; + public VoiceDTO getByVoiceCode(String ttsModelId, String voiceCode) { + if (StringUtils.isBlank(voiceCode)) { + return null; } - QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.eq("tts_voice", ttsVoice); - - // 检查数据库中是否存在相同的ttsVoice值 - return timbreDao.exists(queryWrapper); + queryWrapper.eq("tts_model_id", ttsModelId); + queryWrapper.eq("tts_voice", voiceCode); + List list = timbreDao.selectList(queryWrapper); + if (list.isEmpty()) { + return null; + } + return new VoiceDTO(list.get(0).getId(), list.get(0).getName()); } } \ No newline at end of file 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 90435e11..b7de9332 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 @@ -116,6 +116,7 @@ public class VoiceCloneController { checkPermission(id); voiceCloneService.updateName(id, name); + redisUtils.delete(RedisKeys.getTimbreNameById(id)); return new Result(); } catch (Exception e) { return new Result().error(ErrorCode.UPDATE_DATA_FAILED, "更新失败: " + e.getMessage()); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/dao/VoiceCloneDao.java b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/dao/VoiceCloneDao.java index f866c46f..08e0e012 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/dao/VoiceCloneDao.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/voiceclone/dao/VoiceCloneDao.java @@ -1,9 +1,12 @@ package xiaozhi.modules.voiceclone.dao; +import java.util.List; + import org.apache.ibatis.annotations.Mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import xiaozhi.modules.model.dto.VoiceDTO; import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity; /** @@ -11,5 +14,13 @@ import xiaozhi.modules.voiceclone.entity.VoiceCloneEntity; */ @Mapper public interface VoiceCloneDao extends BaseMapper { + /** + * 获取用户训练成功的音色列表 + * + * @param modelId 模型ID + * @param userId 用户ID + * @return 训练成功的音色列表 + */ + List getTrainSuccess(String modelId, Long userId); } 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 7968bd73..ea8fd61d 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 @@ -33,6 +33,9 @@ public interface VoiceCloneService extends BaseService { /** * 根据用户ID查询声音克隆列表 + * + * @param userId 用户ID + * @return 声音克隆列表 */ List getByUserId(Long userId); 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 89064a53..bc13d2aa 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 @@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import xiaozhi.common.constant.Constant; import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.RenException; import xiaozhi.common.page.PageData; @@ -47,7 +48,6 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl page(Map params) { @@ -74,8 +74,27 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl config = modelConfig.getConfigJson(); + String type = (String) config.get("type"); + if (StringUtils.isBlank(type)) { + throw new RenException(ErrorCode.VOICE_CLONE_MODEL_TYPE_NOT_FOUND); + } + // 检查Voice ID是否已经被使用 for (String voiceId : dto.getVoiceIds()) { + if (StringUtils.isBlank(voiceId)) { + continue; + } + if (Constant.VOICE_CLONE_HUOSHAN_DOUBLE_STREAM.equals(type)) { + if (voiceId.indexOf("S_") == -1) { + throw new RenException(ErrorCode.VOICE_CLONE_HUOSHAN_VOICE_ID_ERROR); + } + } + QueryWrapper wrapper = new QueryWrapper<>(); wrapper.eq("voice_id", voiceId); wrapper.eq("model_id", dto.getModelId()); @@ -254,7 +273,7 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl + + + + + \ No newline at end of file diff --git a/main/manager-web/src/i18n/en.js b/main/manager-web/src/i18n/en.js index f6f64086..b3043d5c 100644 --- a/main/manager-web/src/i18n/en.js +++ b/main/manager-web/src/i18n/en.js @@ -876,6 +876,8 @@ export default { 'ttsModel.operationClosed': 'Operation closed', 'ttsModel.edit': 'Edit', 'ttsModel.delete': 'Delete', + 'ttsModel.deleteVoiceSuccess': 'Successfully deleted {count} voices', + 'ttsModel.deleteFailed': 'Failed to delete voices', 'ttsModel.save': 'Save', 'ttsModel.deselectAll': 'Deselect All', 'ttsModel.selectAll': 'Select All', diff --git a/main/manager-web/src/i18n/zh_CN.js b/main/manager-web/src/i18n/zh_CN.js index 0e8e70a5..e19b3473 100644 --- a/main/manager-web/src/i18n/zh_CN.js +++ b/main/manager-web/src/i18n/zh_CN.js @@ -876,6 +876,8 @@ export default { 'ttsModel.operationClosed': '操作已关闭', 'ttsModel.edit': '编辑', 'ttsModel.delete': '删除', + 'ttsModel.deleteVoiceSuccess': '成功删除{count}个音色', + 'ttsModel.deleteFailed': '删除音色失败', 'ttsModel.save': '保存', 'ttsModel.deselectAll': '取消全选', 'ttsModel.selectAll': '全选', diff --git a/main/manager-web/src/i18n/zh_TW.js b/main/manager-web/src/i18n/zh_TW.js index dffa61d2..dbf61788 100644 --- a/main/manager-web/src/i18n/zh_TW.js +++ b/main/manager-web/src/i18n/zh_TW.js @@ -874,6 +874,8 @@ export default { 'ttsModel.operation': '操作', 'ttsModel.edit': '編輯', 'ttsModel.delete': '刪除', + 'ttsModel.deleteVoiceSuccess': '成功刪除{count}個音色', + 'ttsModel.deleteFailed': '刪除音色失敗', 'ttsModel.save': '保存', 'ttsModel.deselectAll': '取消全選', 'ttsModel.selectAll': '全選',