diff --git a/docs/huoshan-streamTTS-voice-cloning.md b/docs/huoshan-streamTTS-voice-cloning.md index 34952f30..d831a0d0 100644 --- a/docs/huoshan-streamTTS-voice-cloning.md +++ b/docs/huoshan-streamTTS-voice-cloning.md @@ -1,29 +1,56 @@ -# 火山双向流式TTS+声音克隆配置教程 -单模块部署下,使用火山引擎双向流式语音合成服务的同时进行声音克隆,支持WebSocket协议流式调用。 +# 智控台 火山双流式语音合成+音色克隆配置教程 + +本教程分为3个阶段:准备阶段、配置阶段、克隆阶段、使用阶段。主要是介绍通过智控台配置火山双流式语音合成+音色克隆的过程。 + +## 第一阶段:准备阶段 +超级管理员先预先把火山引擎服务开通好,获取到App Id,Access Token。默认火上引擎会赠送一个音色资源。这个音色资源需要把它复制到本项目里。 + +如果你想克隆多个音色,需要购买开通多个音色资源。只要把每个音色资源的声音ID(S_xxxxx)复制到本项目。然后分配给系统的账号使用即可。以下是详细步骤: + ### 1.开通火山引擎服务 -访问 https://console.volcengine.com/speech/app 在应用管理创建应用,勾选语音合成大模型和声音复刻大模型,左边列表点击声音复刻大模型后下滑获得App Id,Access Token,Cluster ID以及声音ID(S_xxxxx) -### 2.克隆音色 -克隆音色请参照教程 https://github.com/104gogo/huoshan-voice-copy +访问 https://console.volcengine.com/speech/app 在应用管理创建应用,勾选语音合成大模型和声音复刻大模型。 -准备一段 10-30 秒的音频文件(.wav格式)添加到克隆的项目中,将平台获得的密钥填入```uploadAndStatus.py```和```tts_http_demo.py``` +### 2.获取音色资源ID +访问 https://console.volcengine.com/speech/service/9999 复制三项内容,分别是App Id,Access Token以及声音ID(S_xxxxx)。如图 -在uploadAndStatus.py中,将 audio_path=修改成自己的.wav文件名称 -```python -train(appid=appid, token=token, audio_path=r".\audios\xiaohe.wav", spk_id=spk_id) -``` +![获取音色资源](images/image-clone-integration-01.png) -运行以下命令生成test_submit.mp3,点击播放试听克隆效果 +## 第二阶段:配置火山引擎服务 -```python -python uploadAndStatus.py -python tts_http_demo.py -``` -回到火山引擎控制台页面,刷新可以看到声音复刻详情的状态是复刻成功。 -### 3.填写配置文件 -将火山引擎服务申请到的密钥填入.config.yaml的HuoshanDoubleStreamTTS配置文件中 +### 1.填写火山引擎配置 -修改 resource_id的参数为``` volc.megatts.default``` -(参考官方文档 https://www.volcengine.com/docs/6561/1329505) -speaker的参数填入声音ID(S_xxxxx) +使用超级管理员账号登录智控台,点击顶部【模型配置】,再点击模型配置页面左侧的【语音合成】,搜索找到“火山双流式语音合成”,点击修改,将你火山引擎的`App Id`填入到【应用ID】字段里,将`Access Token`填入到【访问令牌】字段里。然后保存。 -启动服务,唤醒小智发出的声音是克隆的音色即成功。 +### 2.将音色资源ID分配给系统账号 + +使用超级管理员账号登录智控台,点击顶部【音色克隆】、【音色资源】。 + +点击新增按钮,在【平台名称】选择“火山双流式语音合成”; + +在【音色资源ID】填入你火山引擎的声音资源ID(S_xxxxx),填入后按回车; + +在【归属账号】选择你要分配给的系统账号,你可以分配给你自己。然后点击保存 + +## 第三阶段:克隆阶段 + +如果登录后,点击顶部【音色克隆】》【音色克隆】,显示【您的账号暂无音色资源请联系管理员分配音色资源】,说明你在第二阶段还没有把音色资源ID分配给这个账号。那就是回到第二阶段,分配音色资源给对应的账号。 + +如果登录后,点击顶部【音色克隆】》【音色克隆】,能看到对应的音色列表。请继续。 + +在列表里会看到对应的音色列表。选择其中一个音色资源,点击【上传音频】按钮。上传后,可以试听一下声音或者截取某段声音。确认后点击【上传音频】按钮。 +![上传音频](images/image-clone-integration-02.png) + +上传音频后,在列表里会看到对应的音色会变成“待复刻”状态。点击【立即复刻】按钮。等1~2秒会返回结果。 + +如果复刻失败,请将鼠标放到“错误信息”图标上,会显示失败的原因。 + +如果复刻成功,在列表里会看到对应的音色会变成“训练成功”状态。此时你可以点击【声音名称】栏的修改按钮,修改音色资源的名称,方便后期选择使用。 + +## 第三阶段:使用阶段 + +点击顶部【智能体管理】,选择任意一个智能体,点击【配置角色】按钮。 + +语音合成(TTS)选择“火山双流式语音合成”。在列表里,找到名字带有“克隆音色”的音色资源(如图),选择它,点击保存。 +![选择音色](images/image-clone-integration-03.png) + +接下来,可以唤醒小智和它对话。 diff --git a/docs/images/image-clone-integration-01.png b/docs/images/image-clone-integration-01.png new file mode 100644 index 00000000..7132ee7a Binary files /dev/null and b/docs/images/image-clone-integration-01.png differ diff --git a/docs/images/image-clone-integration-02.png b/docs/images/image-clone-integration-02.png new file mode 100644 index 00000000..80972df9 Binary files /dev/null and b/docs/images/image-clone-integration-02.png differ diff --git a/docs/images/image-clone-integration-03.png b/docs/images/image-clone-integration-03.png new file mode 100644 index 00000000..7f35e393 Binary files /dev/null and b/docs/images/image-clone-integration-03.png differ 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 573269b8..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; // 只支持音频文件 @@ -188,4 +188,14 @@ 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; // 复刻音色前缀 + 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/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/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 1b0181bc..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<>(); @@ -392,6 +400,15 @@ public class ConfigServiceImpl implements ConfigService { ((Map) model.getConfigJson()).put("ref_audio", referenceAudio); if (referenceText != null) ((Map) model.getConfigJson()).put("ref_text", referenceText); + + // 火山引擎声音克隆需要替换resource_id + Map map = (Map) model.getConfigJson(); + 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"); + } + } } // 如果是Intent类型,且type=intent_llm,则给他添加附加模型 if ("Intent".equals(modelTypes[i])) { 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..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,4 +64,13 @@ public interface TimbreService extends BaseService { * @return 音色名称 */ String getTimbreNameById(String id); + + /** + * 根据音色编码获取音色信息 + * + * @param ttsModelId 音色模型ID + * @param voiceCode 音色编码 + * @return 音色信息 + */ + 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 2ed5b1ab..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 @@ -1,9 +1,11 @@ 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 java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; @@ -15,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; /** * 音色的业务层的实现 @@ -39,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 @@ -121,11 +129,30 @@ public class TimbreServiceImpl extends BaseServiceImpl 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()); + + // 获取当前登录用户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); + } } - return ConvertUtils.sourceToTarget(timbreEntities, VoiceDTO.class); + return CollectionUtil.isEmpty(voiceDTOs) ? null : voiceDTOs; } /** @@ -154,8 +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 VoiceDTO getByVoiceCode(String ttsModelId, String voiceCode) { + if (StringUtils.isBlank(voiceCode)) { + return null; + } + QueryWrapper queryWrapper = new 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 5e166b65..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()); @@ -175,6 +176,8 @@ public class VoiceCloneController { public Result cloneAudio(@RequestBody Map params) { String cloneId = params.get("cloneId"); checkPermission(cloneId); + // 调用服务层进行语音克隆训练 + voiceCloneService.cloneAudio(cloneId); return new Result(); } 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/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 bdfd5ed0..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,19 +33,22 @@ public interface VoiceCloneService extends BaseService { /** * 根据用户ID查询声音克隆列表 + * + * @param userId 用户ID + * @return 声音克隆列表 */ List getByUserId(Long userId); - + /** * 分页查询带模型名称和用户名称的声音克隆列表 */ PageData pageWithNames(Map params); - + /** * 根据ID查询带模型名称和用户名称的声音克隆信息 */ VoiceCloneResponseDTO getByIdWithNames(String id); - + /** * 根据用户ID查询带模型名称的声音克隆列表 */ @@ -65,4 +68,11 @@ public interface VoiceCloneService extends BaseService { * 获取音频数据 */ byte[] getVoiceData(String id); + + /** + * 克隆音频,调用火山引擎进行语音复刻训练 + * + * @param cloneId 语音克隆记录ID + */ + 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 32b3c88f..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 @@ -1,7 +1,13 @@ package xiaozhi.modules.voiceclone.service.impl; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; 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 +18,19 @@ 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.constant.Constant; +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; import xiaozhi.modules.voiceclone.dao.VoiceCloneDao; @@ -28,6 +39,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 +47,7 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl page(Map params) { @@ -61,6 +74,36 @@ 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()); + 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"); @@ -207,4 +250,125 @@ 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); + } + if (Constant.VOICE_CLONE_HUOSHAN_DOUBLE_STREAM.equals(type)) { + huoshanClone(config, entity); + } + } catch (RenException re) { + entity.setTrainStatus(3); + entity.setTrainError(re.getMsg()); + baseDao.updateById(entity); + throw re; + } catch (Exception e) { + e.printStackTrace(); + entity.setTrainStatus(3); + entity.setTrainError(e.getMessage()); + baseDao.updateById(entity); + throw new RenException(ErrorCode.VOICE_CLONE_TRAINING_FAILED, e.getMessage()); + } + } + + /** + * 调用火山引擎进行语音复刻训练 + * + * @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(ErrorCode.VOICE_CLONE_HUOSHAN_CONFIG_MISSING); + } + + String audioBase64 = Base64.getEncoder().encodeToString(entity.getVoice()); + Map reqBody = new HashMap<>(); + reqBody.put("appid", appid); + 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.getVoiceId()); + + 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", "seed-icl-1.0") + .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(reqBody))) + .build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + System.out.println(">>> HTTP status = " + response.statusCode()); + System.out.println(">>> response body = " + response.body()); + + 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(""); + baseDao.updateById(entity); + } else { + // 失败时使用StatusMessage作为错误信息 + String errorMsg = StringUtils.isNotBlank(statusMessage) ? statusMessage : "训练失败"; + throw new RenException(errorMsg); + } + } else { + String errorMsg = objectMapper.convertValue(rsp.get("message"), + new TypeReference() { + }); + if (StringUtils.isNotBlank(errorMsg)) { + throw new RenException(errorMsg); + } + throw new RenException(ErrorCode.VOICE_CLONE_RESPONSE_FORMAT_ERROR); + } + } } diff --git a/main/manager-api/src/main/resources/db/changelog/202510071521.sql b/main/manager-api/src/main/resources/db/changelog/202510071522.sql similarity index 71% rename from main/manager-api/src/main/resources/db/changelog/202510071521.sql rename to main/manager-api/src/main/resources/db/changelog/202510071522.sql index 06ffa006..f53d4227 100644 --- a/main/manager-api/src/main/resources/db/changelog/202510071521.sql +++ b/main/manager-api/src/main/resources/db/changelog/202510071522.sql @@ -11,5 +11,9 @@ CREATE TABLE `ai_voice_clone` ( `train_error` VARCHAR(255) COMMENT '训练错误原因', `creator` BIGINT COMMENT '创建者 ID', `create_date` DATETIME COMMENT '创建时间', - PRIMARY KEY (`id`) + PRIMARY KEY (`id`), + INDEX idx_ai_voice_clone_user_id_model_id_train_status (model_id,user_id, train_status), + INDEX idx_ai_voice_clone_voice_id (voice_id), + INDEX idx_ai_voice_clone_user_id (user_id), + INDEX idx_ai_voice_clone_model_id_voice_id (model_id, voice_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='声音克隆表'; \ No newline at end of file diff --git a/main/manager-api/src/main/resources/db/changelog/202510191042.sql b/main/manager-api/src/main/resources/db/changelog/202510191042.sql new file mode 100644 index 00000000..1511645b --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202510191042.sql @@ -0,0 +1,2 @@ +UPDATE `ai_model_config` SET model_code = 'XunfeiSparkLLM' WHERE id = 'LLM_XunfeiSparkLLM'; +UPDATE `ai_model_config` SET model_code = 'XunfeiStreamASR' WHERE id = 'ASR_XunfeiStream'; \ No newline at end of file diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml index b3845b4d..b6b88bf3 100755 --- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml @@ -389,9 +389,16 @@ databaseChangeLog: encoding: utf8 path: classpath:db/changelog/202509220958.sql - changeSet: - id: 202510071521 + id: 202510071522 author: hrz changes: - sqlFile: encoding: utf8 - path: classpath:db/changelog/202510071521.sql + path: classpath:db/changelog/202510071522.sql + - changeSet: + id: 202510191042 + author: hrz + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202510191042.sql diff --git a/main/manager-api/src/main/resources/i18n/messages.properties b/main/manager-api/src/main/resources/i18n/messages.properties index 816a1a8b..45eff87b 100644 --- a/main/manager-api/src/main/resources/i18n/messages.properties +++ b/main/manager-api/src/main/resources/i18n/messages.properties @@ -135,7 +135,9 @@ 10124=\u60A8\u7684mqtt\u5BC6\u94A5\u957F\u5EA6\u4E0D\u5B89\u5168\uFF0Cmqtt\u5BC6\u94A5\u5FC5\u987B\u540C\u65F6\u5305\u542B\u5927\u5C0F\u5199\u5B57\u6BCD 10125=\u60A8\u7684mqtt\u5BC6\u94A5\u5305\u542B\u5F31\u5BC6\u7801 10128=\u5B57\u5178\u6807\u7B7E\u91CD\u590D -10129=modelType\u548CprovideCode\u4E0D\u80FD\u4E3A\u7A7A +10129=SM2\u5BC6\u94A5\u672A\u914D\u7F6E +10130=SM2\u89E3\u5BC6\u5931\u8D25 +10131=modelType\u548CprovideCode\u4E0D\u80FD\u4E3A\u7A7A 10132=\u6CA1\u6709\u6743\u9650\u67E5\u770B\u8BE5\u667A\u80FD\u4F53\u7684\u804A\u5929\u8BB0\u5F55 10133=\u4F1A\u8BDDID\u4E0D\u80FD\u4E3A\u7A7A 10134=\u667A\u80FD\u4F53ID\u4E0D\u80FD\u4E3A\u7A7A @@ -154,4 +156,14 @@ 10147=\u97F3\u8272ID\u4E0D\u80FD\u4E3A\u7A7A 10148=\u5F52\u5C5E\u8D26\u53F7\u4E0D\u80FD\u4E3A\u7A7A 10149=\u5220\u9664\u7684\u97F3\u8272\u8D44\u6E90ID\u4E0D\u80FD\u4E3A\u7A7A -10150=\u60A8\u6CA1\u6709\u6743\u9650\u64CD\u4F5C\u8BE5\u8BB0\u5F55 \ No newline at end of file +10150=\u60A8\u6CA1\u6709\u6743\u9650\u64CD\u4F5C\u8BE5\u8BB0\u5F55 +10151=\u8BF7\u5148\u4E0A\u4F20\u97F3\u9891\u6587\u4EF6 +10152=\u6A21\u578B\u914D\u7F6E\u672A\u627E\u5230 +10153=\u6A21\u578B\u7C7B\u578B\u672A\u627E\u5230 +10154=\u8BAD\u7EC3\u5931\u8D25 +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{0} +10159=\u97F3\u8272ID\u5DF2\u5B58\u5728 +10160=\u706B\u5C71\u5F15\u64CE\u97F3\u8272ID\u683C\u5F0F\u9519\u8BEF\uFF0C\u5FC5\u987B\u4EE5S_\u5F00\u5934 \ No newline at end of file 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 d338896c..98cd91db 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 @@ -156,4 +156,14 @@ 10147=Voice ID cannot be empty 10148=Owner account cannot be empty 10149=Voice resource ID to delete cannot be empty -10150=You do not have permission to operate this record \ No newline at end of file +10150=You do not have permission to operate this record +10151=Please upload audio file first +10152=Model configuration not found +10153=Model type not found +10154=Training failed: {0} +10155=Huoshan Engine configuration missing +10156=Response format error, missing BaseResp field +10157=Request failed +10158=Clone Voice: +10159=Voice ID already exists +10160=Huoshan Engine voice ID format error, must start with S_ \ 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 175de703..563ee766 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 @@ -156,4 +156,14 @@ 10147=\u97F3\u8272ID\u4E0D\u80FD\u4E3A\u7A7A 10148=\u5F52\u5C5E\u8D26\u53F7\u4E0D\u80FD\u4E3A\u7A7A 10149=\u5220\u9664\u7684\u97F3\u8272\u8D44\u6E90ID\u4E0D\u80FD\u4E3A\u7A7A -10150=\u60A8\u6CA1\u6709\u6743\u9650\u64CD\u4F5C\u8BE5\u8BB0\u5F55 \ No newline at end of file +10150=\u60A8\u6CA1\u6709\u6743\u9650\u64CD\u4F5C\u8BE5\u8BB0\u5F55 +10151=\u8BF7\u5148\u4E0A\u4F20\u97F3\u9891\u6587\u4EF6 +10152=\u6A21\u578B\u914D\u7F6E\u672A\u627E\u5230 +10153=\u6A21\u578B\u7C7B\u578B\u672A\u627E\u5230 +10154=\u8BAD\u7EC3\u5931\u8D25 +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\u8272\u97F3: +10159=\u97F3\u8272ID\u5DF2\u5B58\u5728 +10160=\u706B\u5C71\u5F15\u64CE\u97F3\u8272ID\u683C\u5F0F\u9519\u8BEF\uFF0C\u5FC5\u987B\u4EE5S_\u5F00\u5934 \ No newline at end of file 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 3ed34742..4f7ebabf 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 @@ -156,4 +156,14 @@ 10147=\u97F3\u8272ID\u4E0D\u80FD\u70BA\u7A7A 10148=\u6B78\u5C6C\u5E33\u865F\u4E0D\u80FD\u70BA\u7A7A 10149=\u522A\u9664\u7684\u97F3\u8272\u8CC7\u6E90ID\u4E0D\u80FD\u70BA\u7A7A -10150=\u60A8\u6C92\u6709\u6B0A\u9650\u64CD\u4F5C\u8A72\u8A18\u9304 \ No newline at end of file +10150=\u60A8\u6C92\u6709\u6B0A\u9650\u64CD\u4F5C\u8A72\u8A18\u9304 +10151=\u8ACB\u5148\u4E0A\u50B3\u97F3\u983B\u6587\u4EF6 +10152=\u6A21\u578B\u914D\u7F6E\u672A\u627E\u5230 +10153=\u6A21\u578B\u985E\u578B\u672A\u627E\u5230 +10154=\u6A21\u578B\u8AB2\u7DF4\u5931\u6557: {0} +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: +10159=\u97F3\u8272ID\u5DF2\u5B58\u5728 +10160=\u706B\u5C71\u5F15\u64CE\u97F3\u8272ID\u683C\u5F0F\u932F\u8AA4\uFF0C\u5FC5\u9808\u4EE5S_\u958B\u982D \ No newline at end of file diff --git a/main/manager-api/src/main/resources/mapper/voiceclone/VoiceCloneDao.xml b/main/manager-api/src/main/resources/mapper/voiceclone/VoiceCloneDao.xml new file mode 100644 index 00000000..d7658592 --- /dev/null +++ b/main/manager-api/src/main/resources/mapper/voiceclone/VoiceCloneDao.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/main/manager-web/src/apis/module/voiceClone.js b/main/manager-web/src/apis/module/voiceClone.js index ac661f2c..89156974 100644 --- a/main/manager-web/src/apis/module/voiceClone.js +++ b/main/manager-web/src/apis/module/voiceClone.js @@ -79,7 +79,7 @@ export default { }, // 复刻音频 - cloneAudio(params, callback) { + cloneAudio(params, callback, errorCallback) { RequestService.sendRequest() .url(`${getServiceUrl()}/voiceClone/cloneAudio`) .method('POST') @@ -88,10 +88,19 @@ export default { RequestService.clearRequestTime(); callback(res); }) + .fail((res) => { + // 业务失败回调 + RequestService.clearRequestTime(); + if (errorCallback) { + errorCallback(res); + } else { + callback(res); + } + }) .networkFail((err) => { console.error('上传失败:', err); RequestService.reAjaxFun(() => { - this.cloneAudio(params, callback); + this.cloneAudio(params, callback, errorCallback); }); }).send(); } diff --git a/main/manager-web/src/i18n/en.js b/main/manager-web/src/i18n/en.js index 00200256..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', @@ -885,6 +887,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', @@ -1019,7 +1023,7 @@ export default { 'warning': 'Warning', 'info': 'Info', 'common.networkError': 'Network request failed', - + // SM2 Encryption 'sm2.publicKeyNotConfigured': 'SM2 public key not configured, please contact administrator', 'sm2.encryptionFailed': 'Password encryption failed', @@ -1095,6 +1099,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', @@ -1108,4 +1114,5 @@ export default { 'voiceClone.updateNameSuccess': 'Name updated successfully', 'voiceClone.updateNameFailed': 'Failed to update name', 'voiceClone.playFailed': 'Play failed', + 'voiceClone.Details': 'Error 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 678ee041..e19b3473 100644 --- a/main/manager-web/src/i18n/zh_CN.js +++ b/main/manager-web/src/i18n/zh_CN.js @@ -466,7 +466,7 @@ export default { 'chatHistory.downloadCurrentSession': '下载本会话聊天记录', 'chatHistory.downloadCurrentWithPreviousSessions': '下载本会话及前20条会话聊天记录', 'chatHistory.downloadLinkFailed': '获取下载链接失败', - + 'cache.status': '缓存状态', 'cache.cdnEnabled': 'CDN模式已启用', 'cache.cdnDisabled': 'CDN模式已禁用', @@ -876,6 +876,8 @@ export default { 'ttsModel.operationClosed': '操作已关闭', 'ttsModel.edit': '编辑', 'ttsModel.delete': '删除', + 'ttsModel.deleteVoiceSuccess': '成功删除{count}个音色', + 'ttsModel.deleteFailed': '删除音色失败', 'ttsModel.save': '保存', 'ttsModel.deselectAll': '取消全选', 'ttsModel.selectAll': '全选', @@ -885,6 +887,8 @@ export default { 'ttsModel.unnamedVoice': '未命名音色', 'ttsModel.finishEditingFirst': '请先完成当前编辑', 'ttsModel.selectVoiceToDelete': '请选择要删除的音色', + 'ttsModel.warning': '警告', + 'ttsModel.confirmDeleteVoice': '确定要删除{count}个音色吗?', // OTA管理页面文本 'otaManagement.firmwareManagement': '固件管理', @@ -1019,7 +1023,7 @@ export default { 'warning': '警告', 'info': '提示', 'common.networkError': '网络请求失败', - + // SM2加密相关错误消息 'sm2.publicKeyNotConfigured': 'SM2公钥未配置,请联系管理员', 'sm2.encryptionFailed': '密码加密失败', @@ -1095,6 +1099,8 @@ export default { 'voiceClone.play': '播放', 'voiceClone.pause': '暂停', 'voiceClone.cancel': '取消', + 'voiceClone.warning': '警告', + 'voiceClone.ok': '确定', 'voiceClone.nextStep': '下一步', 'voiceClone.prevStep': '上一步', 'voiceClone.upload': '上传音频', @@ -1108,4 +1114,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 9a511402..dbf61788 100644 --- a/main/manager-web/src/i18n/zh_TW.js +++ b/main/manager-web/src/i18n/zh_TW.js @@ -16,7 +16,7 @@ export default { 'header.paramDictionary': '參數字典', 'header.paramManagement': '參數管理', 'header.dictManagement': '字典管理', - 'header.agentTemplate': '預設角色模板', + 'header.agentTemplate': '預設角色模板', 'header.searchHistory': '搜索歷史', 'header.clearHistory': '清空歷史', 'header.providerManagement': '字段管理', @@ -874,6 +874,8 @@ export default { 'ttsModel.operation': '操作', 'ttsModel.edit': '編輯', 'ttsModel.delete': '刪除', + 'ttsModel.deleteVoiceSuccess': '成功刪除{count}個音色', + 'ttsModel.deleteFailed': '刪除音色失敗', 'ttsModel.save': '保存', 'ttsModel.deselectAll': '取消全選', 'ttsModel.selectAll': '全選', @@ -883,6 +885,8 @@ export default { 'ttsModel.unnamedVoice': '未命名音色', 'ttsModel.finishEditingFirst': '請先完成目前編輯', 'ttsModel.selectVoiceToDelete': '請選擇要刪除的音色', + 'ttsModel.warning': '警告', + 'ttsModel.confirmDeleteVoice': '確定要刪除{count}個音色嗎?', 'ttsModel.operationFailed': '操作失敗', 'ttsModel.operationClosed': '操作已關閉', @@ -1095,6 +1099,8 @@ export default { 'voiceClone.play': '播放', 'voiceClone.pause': '暫停', 'voiceClone.cancel': '取消', + 'voiceClone.warning': '警告', + 'voiceClone.ok': '確定', 'voiceClone.nextStep': '下一步', 'voiceClone.prevStep': '上一步', 'voiceClone.upload': '上傳音頻', @@ -1108,4 +1114,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 a0e68ebc..ad64fd3a 100644 --- a/main/manager-web/src/views/VoiceCloneManagement.vue +++ b/main/manager-web/src/views/VoiceCloneManagement.vue @@ -36,9 +36,22 @@ + + + + +