From e82cc53bfb311c0bc42614605a92f3ce4305870d Mon Sep 17 00:00:00 2001 From: DaGou12138 <991623169@qq.com> Date: Fri, 6 Feb 2026 16:54:21 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E8=A7=84=E8=8C=83ai=5Ftt?= =?UTF-8?q?s=5Fvoice=E8=AF=AD=E8=A8=80=E7=B1=BB=E5=9E=8B=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/db/changelog/202602061650.sql | 27 +++++++++++++++++++ .../db/changelog/db.changelog-master.yaml | 7 +++++ 2 files changed, 34 insertions(+) create mode 100644 main/manager-api/src/main/resources/db/changelog/202602061650.sql diff --git a/main/manager-api/src/main/resources/db/changelog/202602061650.sql b/main/manager-api/src/main/resources/db/changelog/202602061650.sql new file mode 100644 index 00000000..e5e569a6 --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202602061650.sql @@ -0,0 +1,27 @@ +-- 统一规范ai_tts_voice语言类型数据 +UPDATE ai_tts_voice +SET languages = CASE + WHEN languages IN ('中文', '普通话') THEN '普通话' + WHEN languages IN ('中文及中英文混合', '中文、英文', '中文、美式英语') THEN '普通话、英语' + WHEN languages IN ('英式英文', '英式英语', '美式英语', '澳洲英语', '英文') THEN '英语' + WHEN languages = '日语' THEN '日语' + WHEN languages = '日语、西语' THEN '日语、西班牙语' + WHEN languages = '韩语' THEN '韩语' + WHEN languages = '中文(东北)及中英文混合' THEN '东北话、英语' + WHEN languages = '东北话' THEN '东北话' + WHEN languages = '天津话' THEN '天津话' + WHEN languages IN ('粤语', '中文-广东口音') THEN '粤语' + WHEN languages = '中文(粤语)及中英文混合' THEN '粤语、英语' + WHEN languages = '粤语及粤英混合' THEN '粤语、英语' + WHEN languages = '中文-北京口音、英文' THEN '北京话、英语' + WHEN languages = '中文-北京口音' THEN '北京话' + WHEN languages = '中文-青岛口音' THEN '青岛话' + WHEN languages = '中文-河南口音' THEN '河南话' + WHEN languages = '中文-广西口音' THEN '广西话' + WHEN languages = '辽宁' THEN '辽宁话' + WHEN languages = '陕西' THEN '陕西话' + WHEN languages = '中文-四川口音' THEN '四川话' + WHEN languages = '中文-台湾口音' THEN '台湾话' + WHEN languages = '中文-长沙口音' THEN '长沙话' + ELSE languages +END; \ 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 ded39ce1..e2ce2412 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 @@ -529,3 +529,10 @@ databaseChangeLog: - sqlFile: encoding: utf8 path: classpath:db/changelog/202602051125.sql + - changeSet: + id: 202602061650 + author: DaGou12138 + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202602061650.sql From 520e24a54e73f2b76922c2878edc5809f6595b2e Mon Sep 17 00:00:00 2001 From: DaGou12138 <991623169@qq.com> Date: Fri, 6 Feb 2026 17:12:47 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E8=A7=84=E8=8C=83ai=5Ftts=5Fvoice=E8=AF=AD=E8=A8=80=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E6=95=B0=E6=8D=AE=E8=AF=AD=E5=8F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ .../resources/db/changelog/202602061650.sql | 17 ++--------------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index 28a808bd..c5d58f2b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ __pycache__/ .idea/ *.py[cod] *$py.class +.vscode +.claude +AGENTS.md # C extensions *.so diff --git a/main/manager-api/src/main/resources/db/changelog/202602061650.sql b/main/manager-api/src/main/resources/db/changelog/202602061650.sql index e5e569a6..72a3d23b 100644 --- a/main/manager-api/src/main/resources/db/changelog/202602061650.sql +++ b/main/manager-api/src/main/resources/db/changelog/202602061650.sql @@ -1,27 +1,14 @@ -- 统一规范ai_tts_voice语言类型数据 UPDATE ai_tts_voice SET languages = CASE - WHEN languages IN ('中文', '普通话') THEN '普通话' - WHEN languages IN ('中文及中英文混合', '中文、英文', '中文、美式英语') THEN '普通话、英语' + WHEN languages IN ('中文', '普通话','东北话','天津话','中文-北京口音','中文-青岛口音','中文-河南口音','中文-广西口音','辽宁','陕西','中文-四川口音','中文-台湾口音','中文-长沙口音') THEN '普通话' + WHEN languages IN ('中文及中英文混合', '中文、英文', '中文、美式英语','中文-北京口音、英文','中文(东北)及中英文混合') THEN '普通话、英语' WHEN languages IN ('英式英文', '英式英语', '美式英语', '澳洲英语', '英文') THEN '英语' WHEN languages = '日语' THEN '日语' WHEN languages = '日语、西语' THEN '日语、西班牙语' WHEN languages = '韩语' THEN '韩语' - WHEN languages = '中文(东北)及中英文混合' THEN '东北话、英语' - WHEN languages = '东北话' THEN '东北话' - WHEN languages = '天津话' THEN '天津话' WHEN languages IN ('粤语', '中文-广东口音') THEN '粤语' WHEN languages = '中文(粤语)及中英文混合' THEN '粤语、英语' WHEN languages = '粤语及粤英混合' THEN '粤语、英语' - WHEN languages = '中文-北京口音、英文' THEN '北京话、英语' - WHEN languages = '中文-北京口音' THEN '北京话' - WHEN languages = '中文-青岛口音' THEN '青岛话' - WHEN languages = '中文-河南口音' THEN '河南话' - WHEN languages = '中文-广西口音' THEN '广西话' - WHEN languages = '辽宁' THEN '辽宁话' - WHEN languages = '陕西' THEN '陕西话' - WHEN languages = '中文-四川口音' THEN '四川话' - WHEN languages = '中文-台湾口音' THEN '台湾话' - WHEN languages = '中文-长沙口音' THEN '长沙话' ELSE languages END; \ No newline at end of file From 4ea3bea85cbc3dfb07c366bf38722f384a0faa48 Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Mon, 9 Feb 2026 15:28:31 +0800 Subject: [PATCH 3/5] =?UTF-8?q?update:=20=E5=A2=9E=E5=8A=A0TTS=E9=9F=B3?= =?UTF-8?q?=E8=89=B2=E8=AF=AD=E7=A7=8D=E9=80=89=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modules/agent/dto/AgentUpdateDTO.java | 3 + .../modules/agent/entity/AgentEntity.java | 3 + .../agent/entity/AgentTemplateEntity.java | 5 + .../agent/service/impl/AgentServiceImpl.java | 3 + .../service/impl/ConfigServiceImpl.java | 14 ++ .../xiaozhi/modules/model/dto/VoiceDTO.java | 5 + .../service/impl/TimbreServiceImpl.java | 2 + .../resources/db/changelog/202602061650.sql | 8 +- .../main/resources/mapper/agent/AgentDao.xml | 2 + main/manager-web/src/components/HeaderBar.vue | 4 +- main/manager-web/src/i18n/de.js | 1 + main/manager-web/src/i18n/en.js | 1 + main/manager-web/src/i18n/vi.js | 1 + main/manager-web/src/i18n/zh_CN.js | 1 + main/manager-web/src/i18n/zh_TW.js | 1 + main/manager-web/src/views/roleConfig.vue | 182 +++++++++++++----- main/xiaozhi-server/agent-base-prompt.txt | 4 + main/xiaozhi-server/config.yaml | 25 ++- main/xiaozhi-server/core/connection.py | 1 - .../core/handle/receiveAudioHandle.py | 5 - .../core/utils/prompt_manager.py | 10 + 21 files changed, 220 insertions(+), 61 deletions(-) diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java index ebb29fbd..d15a1df7 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java @@ -41,6 +41,9 @@ public class AgentUpdateDTO implements Serializable { @Schema(description = "音色标识", example = "voice_02", nullable = true) private String ttsVoiceId; + @Schema(description = "音色语言", example = "普通话", nullable = true) + private String ttsLanguage; + @Schema(description = "记忆模型标识", example = "mem_model_02", nullable = true) private String memModelId; diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentEntity.java index d92d49b8..7f63b688 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentEntity.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentEntity.java @@ -45,6 +45,9 @@ public class AgentEntity { @Schema(description = "音色标识") private String ttsVoiceId; + @Schema(description = "音色语言") + private String ttsLanguage; + @Schema(description = "记忆模型标识") private String memModelId; diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentTemplateEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentTemplateEntity.java index a7704e5b..ae9c3c40 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentTemplateEntity.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentTemplateEntity.java @@ -64,6 +64,11 @@ public class AgentTemplateEntity implements Serializable { */ private String ttsVoiceId; + /** + * 音色语言 + */ + private String ttsLanguage; + /** * 记忆模型标识 */ 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 5485e425..95a55434 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 @@ -282,6 +282,9 @@ public class AgentServiceImpl extends BaseServiceImpl imp if (dto.getTtsVoiceId() != null) { existingEntity.setTtsVoiceId(dto.getTtsVoiceId()); } + if (dto.getTtsLanguage() != null) { + existingEntity.setTtsLanguage(dto.getTtsLanguage()); + } if (dto.getMemModelId() != null) { existingEntity.setMemModelId(dto.getMemModelId()); } 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 49885614..9cc3bbfb 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 @@ -87,6 +87,7 @@ public class ConfigServiceImpl implements ConfigService { null, null, null, + null, agent.getVadModelId(), agent.getAsrModelId(), null, @@ -135,15 +136,24 @@ public class ConfigServiceImpl implements ConfigService { String voice = null; String referenceAudio = null; String referenceText = null; + String language = null; TimbreDetailsVO timbre = timbreService.get(agent.getTtsVoiceId()); if (timbre != null) { voice = timbre.getTtsVoice(); referenceAudio = timbre.getReferenceAudio(); referenceText = timbre.getReferenceText(); + // 优先使用用户选择的语言,如果没有则使用音色支持的第一个语言 + if (StringUtils.isNotBlank(agent.getTtsLanguage())) { + language = agent.getTtsLanguage(); + } else if (StringUtils.isNotBlank(timbre.getLanguages())) { + language = timbre.getLanguages().split("、")[0].trim(); + } } else { VoiceCloneEntity voice_print = cloneVoiceService.selectById(agent.getTtsVoiceId()); if (voice_print != null) { voice = voice_print.getVoiceId(); + // 优先使用用户选择的语言,如果没有则使用默认值 + language = StringUtils.isNotBlank(agent.getTtsLanguage()) ? agent.getTtsLanguage() : "普通话"; } } // 构建返回数据 @@ -208,6 +218,7 @@ public class ConfigServiceImpl implements ConfigService { voice, referenceAudio, referenceText, + language, agent.getVadModelId(), agent.getAsrModelId(), agent.getLlmModelId(), @@ -385,6 +396,7 @@ public class ConfigServiceImpl implements ConfigService { String voice, String referenceAudio, String referenceText, + String language, String vadModelId, String asrModelId, String llmModelId, @@ -423,6 +435,8 @@ public class ConfigServiceImpl implements ConfigService { ((Map) model.getConfigJson()).put("ref_audio", referenceAudio); if (referenceText != null) ((Map) model.getConfigJson()).put("ref_text", referenceText); + if (language != null) + ((Map) model.getConfigJson()).put("language", language); // 火山引擎声音克隆需要替换resource_id Map map = (Map) model.getConfigJson(); diff --git a/main/manager-api/src/main/java/xiaozhi/modules/model/dto/VoiceDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/model/dto/VoiceDTO.java index e69608a9..9bc4fa8f 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/model/dto/VoiceDTO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/model/dto/VoiceDTO.java @@ -23,6 +23,9 @@ public class VoiceDTO implements Serializable { @Schema(description = "音频播放地址") private String voiceDemo; + @Schema(description = "语言类型") + private String languages; + @Schema(description = "是否为克隆音色") private Boolean isClone; @@ -31,6 +34,7 @@ public class VoiceDTO implements Serializable { this.id = id; this.name = name; this.voiceDemo = null; + this.languages = null; this.isClone = false; // 默认不是克隆音色 } @@ -39,6 +43,7 @@ public class VoiceDTO implements Serializable { this.id = id; this.name = name; this.voiceDemo = voiceDemo; + this.languages = null; this.isClone = false; } 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 a1f018a8..165824f6 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 @@ -129,6 +129,7 @@ public class TimbreServiceImpl extends BaseServiceImpl .map(entity -> { VoiceDTO dto = new VoiceDTO(entity.getId(), entity.getName()); dto.setVoiceDemo(entity.getVoiceDemo()); + dto.setLanguages(entity.getLanguages()); // 设置语言类型 dto.setIsClone(false); // 设置为普通音色 return dto; }) @@ -146,6 +147,7 @@ public class TimbreServiceImpl extends BaseServiceImpl voiceDTO.setName(MessageUtils.getMessage(ErrorCode.VOICE_CLONE_PREFIX) + entity.getName()); // 保留从数据库查询到的voiceDemo字段 voiceDTO.setVoiceDemo(entity.getVoiceDemo()); + voiceDTO.setLanguages(entity.getLanguages()); voiceDTO.setIsClone(true); // 设置为克隆音色 redisUtils.set(RedisKeys.getTimbreNameById(voiceDTO.getId()), voiceDTO.getName(), RedisUtils.NOT_EXPIRE); diff --git a/main/manager-api/src/main/resources/db/changelog/202602061650.sql b/main/manager-api/src/main/resources/db/changelog/202602061650.sql index 72a3d23b..4341cf59 100644 --- a/main/manager-api/src/main/resources/db/changelog/202602061650.sql +++ b/main/manager-api/src/main/resources/db/changelog/202602061650.sql @@ -11,4 +11,10 @@ SET languages = CASE WHEN languages = '中文(粤语)及中英文混合' THEN '粤语、英语' WHEN languages = '粤语及粤英混合' THEN '粤语、英语' ELSE languages -END; \ No newline at end of file +END; + +-- 添加音色语言字段到 ai_agent 表 +ALTER TABLE `ai_agent` ADD COLUMN `tts_language` VARCHAR(50) NULL COMMENT '音色语言' AFTER `tts_voice_id`; + +-- 添加音色语言字段到 ai_agent_template 表 +ALTER TABLE `ai_agent_template` ADD COLUMN `tts_language` VARCHAR(50) NULL COMMENT '音色语言' AFTER `tts_voice_id`; diff --git a/main/manager-api/src/main/resources/mapper/agent/AgentDao.xml b/main/manager-api/src/main/resources/mapper/agent/AgentDao.xml index 439be096..95ca0246 100644 --- a/main/manager-api/src/main/resources/mapper/agent/AgentDao.xml +++ b/main/manager-api/src/main/resources/mapper/agent/AgentDao.xml @@ -16,6 +16,7 @@ + @@ -45,6 +46,7 @@ a.vllm_model_id AS vllmModelId, a.tts_model_id AS ttsModelId, a.tts_voice_id AS ttsVoiceId, + a.tts_language AS ttsLanguage, a.mem_model_id AS memModelId, a.intent_model_id AS intentModelId, COALESCE( diff --git a/main/manager-web/src/components/HeaderBar.vue b/main/manager-web/src/components/HeaderBar.vue index dc9c6fb5..ecdf7d89 100644 --- a/main/manager-web/src/components/HeaderBar.vue +++ b/main/manager-web/src/components/HeaderBar.vue @@ -871,8 +871,8 @@ export default { } .equipment-management { - width: 79px; - font-size: 9px; + min-width: 80px; + font-size: 10px; } } diff --git a/main/manager-web/src/i18n/de.js b/main/manager-web/src/i18n/de.js index 77f75f4d..bd511ccf 100644 --- a/main/manager-web/src/i18n/de.js +++ b/main/manager-web/src/i18n/de.js @@ -762,6 +762,7 @@ export default { 'roleConfig.memoryHis': 'Speicher', 'roleConfig.memory': 'Speicher', 'roleConfig.intent': 'Intent', + 'roleConfig.language': 'Sprache auswählen', 'roleConfig.voiceType': 'Stimmtyp', 'roleConfig.pleaseEnterContent': 'Bitte Inhalt eingeben', 'roleConfig.pleaseEnterLangCode': 'Bitte Sprachcode eingeben, z.B.: en_US', diff --git a/main/manager-web/src/i18n/en.js b/main/manager-web/src/i18n/en.js index 2ecabb97..5e184cd9 100644 --- a/main/manager-web/src/i18n/en.js +++ b/main/manager-web/src/i18n/en.js @@ -762,6 +762,7 @@ export default { 'roleConfig.memoryHis': 'Memory', 'roleConfig.memory': 'Memory Model', 'roleConfig.intent': 'Intent Recognition', + 'roleConfig.language': 'Select Language', 'roleConfig.voiceType': 'Voice Type', 'roleConfig.pleaseEnterContent': 'Please enter content', 'roleConfig.pleaseEnterLangCode': 'Please enter language code, e.g.: en_US', diff --git a/main/manager-web/src/i18n/vi.js b/main/manager-web/src/i18n/vi.js index 5fc17c24..8a124b35 100644 --- a/main/manager-web/src/i18n/vi.js +++ b/main/manager-web/src/i18n/vi.js @@ -762,6 +762,7 @@ export default { 'roleConfig.memoryHis': 'Bộ nhớ', 'roleConfig.memory': 'Mô hình bộ nhớ', 'roleConfig.intent': 'Nhận dạng ý định', + 'roleConfig.language': 'Chọn ngôn ngữ', 'roleConfig.voiceType': 'Loại giọng nói', 'roleConfig.pleaseEnterContent': 'Vui lòng nhập nội dung', 'roleConfig.pleaseEnterLangCode': 'Vui lòng nhập mã ngôn ngữ, ví dụ: en_US', diff --git a/main/manager-web/src/i18n/zh_CN.js b/main/manager-web/src/i18n/zh_CN.js index ef1c8511..2edf4f04 100644 --- a/main/manager-web/src/i18n/zh_CN.js +++ b/main/manager-web/src/i18n/zh_CN.js @@ -762,6 +762,7 @@ export default { 'roleConfig.memoryHis': '记忆', 'roleConfig.memory': '记忆模式', 'roleConfig.tts': '语音合成(TTS)', + 'roleConfig.language': '选择语言', 'roleConfig.voiceType': '声音音色(Voice)', 'roleConfig.pleaseEnterContent': '请输入内容', 'roleConfig.pleaseEnterLangCode': '请输入语言编码,如:zh_CN', diff --git a/main/manager-web/src/i18n/zh_TW.js b/main/manager-web/src/i18n/zh_TW.js index 6744f094..7af2fdba 100644 --- a/main/manager-web/src/i18n/zh_TW.js +++ b/main/manager-web/src/i18n/zh_TW.js @@ -762,6 +762,7 @@ export default { 'roleConfig.memoryHis': '記憶', 'roleConfig.memory': '記憶模式', 'roleConfig.intent': '意圖識別(Intent)', + 'roleConfig.language': '選擇語言', 'roleConfig.voiceType': '聲音音色(Voice)', 'roleConfig.pleaseEnterContent': '請輸入內容', 'roleConfig.pleaseEnterLangCode': '請輸入語言編碼,如:zh_TW', diff --git a/main/manager-web/src/views/roleConfig.vue b/main/manager-web/src/views/roleConfig.vue index 580b6989..4f4a0f8e 100644 --- a/main/manager-web/src/views/roleConfig.vue +++ b/main/manager-web/src/views/roleConfig.vue @@ -232,47 +232,71 @@ - - - -
+ + +
+ - {{ item.label }} - -
- - -
+ > + {{ item.label }} + +
+
+
+ +
+ @@ -357,6 +381,9 @@ export default { isPaused: false, currentAudio: null, currentPlayingVoiceId: null, + // 语言筛选相关状态 + languageOptions: [], // 语言选项列表 + selectedLanguage: '', // 当前选中的语言 // 功能状态 featureStatus: { vad: false, // 语言检测活动功能状态 @@ -378,6 +405,7 @@ export default { vllmModelId: this.form.model.vllmModelId, ttsModelId: this.form.model.ttsModelId, ttsVoiceId: this.form.ttsVoiceId, + ttsLanguage: this.selectedLanguage, chatHistoryConf: this.form.chatHistoryConf, memModelId: this.form.model.memModelId, intentModelId: this.form.model.intentModelId, @@ -592,32 +620,84 @@ export default { if (!modelId) { this.voiceOptions = []; this.voiceDetails = {}; + this.languageOptions = []; + this.selectedLanguage = ''; return; } Api.model.getModelVoices(modelId, "", ({ data }) => { if (data.code === 0 && data.data) { - this.voiceOptions = data.data.map((voice) => ({ - value: voice.id, - label: voice.name, - // 只保留后端实际返回的音频相关字段 - voiceDemo: voice.voiceDemo, - voice_demo: voice.voice_demo, - // 使用后端实际返回的 isClone 字段 - isClone: Boolean(voice.isClone), - // 保存训练状态字段 - train_status: voice.trainStatus, - })); - // 保存完整的音色信息,添加调试信息 + // 保存完整的音色信息 this.voiceDetails = data.data.reduce((acc, voice) => { acc[voice.id] = voice; return acc; }, {}); + + // 提取所有语言选项并去重 + const allLanguages = new Set(); + data.data.forEach(voice => { + if (voice.languages) { + const languagesArray = voice.languages.split('、').map(lang => lang.trim()).filter(lang => lang); + languagesArray.forEach(lang => allLanguages.add(lang)); + } + }); + + this.languageOptions = Array.from(allLanguages).map(lang => ({ + value: lang, + label: lang + })); + + // 使用后端返回的用户选择的语言,如果没有则使用第一个语言选项 + if (this.form.ttsLanguage && this.languageOptions.some(option => option.value === this.form.ttsLanguage)) { + this.selectedLanguage = this.form.ttsLanguage; + } else if (this.languageOptions.length > 0) { + this.selectedLanguage = this.languageOptions[0].value; + } + + // 根据选中的语言筛选音色 + this.filterVoicesByLanguage(); } else { this.voiceOptions = []; this.voiceDetails = {}; + this.languageOptions = []; + this.selectedLanguage = ''; } }); }, + + // 根据语言筛选音色 + filterVoicesByLanguage() { + if (!this.voiceDetails || Object.keys(this.voiceDetails).length === 0) { + this.voiceOptions = []; + return; + } + + const allVoices = Object.values(this.voiceDetails); + + // 根据选中的语言筛选音色 + const filteredVoices = allVoices.filter(voice => { + if (!voice.languages) return false; + const languagesArray = voice.languages.split('、').map(lang => lang.trim()).filter(lang => lang); + return languagesArray.includes(this.selectedLanguage); + }); + + this.voiceOptions = filteredVoices.map((voice) => ({ + value: voice.id, + label: voice.name, + voiceDemo: voice.voiceDemo, + voice_demo: voice.voice_demo, + isClone: Boolean(voice.isClone), + train_status: voice.trainStatus, + })); + + // 检查当前选中的音色是否支持当前语言,如果不支持则选择第一个 + const currentVoiceSupportsLanguage = this.form.ttsVoiceId && + filteredVoices.some(voice => voice.id === this.form.ttsVoiceId); + + if (!currentVoiceSupportsLanguage) { + this.form.ttsVoiceId = filteredVoices.length > 0 ? filteredVoices[0].id : ''; + } + }, + getFunctionDisplayChar(name) { if (!name || name.length === 0) return ""; diff --git a/main/xiaozhi-server/agent-base-prompt.txt b/main/xiaozhi-server/agent-base-prompt.txt index 7fd90d21..3aeb1f0d 100644 --- a/main/xiaozhi-server/agent-base-prompt.txt +++ b/main/xiaozhi-server/agent-base-prompt.txt @@ -2,6 +2,10 @@ {{base_prompt}} + +【语言规范】你必须使用{{language}}进行回复和交流。无论用户使用何种语言提问,你都应该用{{language}}来回答。 + + 【核心目标】你不是冰冷的机器!请敏锐感知用户情绪,作为懂人心的伙伴,用有温度的回应照亮对话。 - **情感融入:** diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 51701d15..81c5c0b1 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -733,6 +733,7 @@ TTS: type: edge voice: zh-CN-XiaoxiaoNeural output_dir: tmp/ + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 DoubaoTTS: # 定义TTS API类型 type: doubao @@ -751,6 +752,7 @@ TTS: speed_ratio: 1.0 volume_ratio: 1.0 pitch_ratio: 1.0 + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 #火山tts,支持双向流式tts HuoshanDoubleStreamTTS: type: huoshan_double_stream @@ -791,6 +793,7 @@ TTS: # mix_factor: 0.3 # - source_speaker: zh_male_ahu_conversation_wvae_bigtts # mix_factor: 0.4 + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 CosyVoiceSiliconflow: type: siliconflow # 硅基流动TTS @@ -800,6 +803,7 @@ TTS: output_dir: tmp/ access_token: 你的硅基流动API密钥 response_format: wav + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 CozeCnTTS: type: cozecn # COZECN TTS @@ -808,6 +812,7 @@ TTS: output_dir: tmp/ access_token: 你的coze web key response_format: wav + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 VolcesAiGatewayTTS: type: openai # 火山引擎 - 边缘大模型网关 @@ -822,6 +827,7 @@ TTS: voice: zh_male_shaonianzixin_moon_bigtts speed: 1 output_dir: tmp/ + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 FishSpeech: # 参照教程:https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/fish-speech-integration.md type: fishspeech @@ -843,6 +849,7 @@ TTS: rate: 44100 api_key: "你的api_key" api_url: "http://127.0.0.1:8080/v1/tts" + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 GPT_SOVITS_V2: # 定义TTS API类型 #启动tts方法: @@ -868,6 +875,7 @@ TTS: parallel_infer: true repetition_penalty: 1.35 aux_ref_audio_paths: [] + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 GPT_SOVITS_V3: # 定义TTS API类型 GPT-SoVITS-v3lora-20250228 #启动tts方法: @@ -887,6 +895,7 @@ TTS: inp_refs: [] sample_steps: 32 if_sr: false + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 MinimaxTTSHTTPStream: # Minimax流式语音合成服务 type: minimax_httpstream @@ -918,6 +927,7 @@ TTS: # voice_id: female-shaonv # weight: 1 # language_boost: auto + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 AliyunTTS: # 阿里云智能语音交互服务,需要先在阿里云平台开通服务,然后获取验证信息 # 平台地址:https://nls-portal.console.aliyun.com/ @@ -937,6 +947,7 @@ TTS: # volume: 50 # speech_rate: 0 # pitch_rate: 0 + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 AliyunStreamTTS: # 阿里云CosyVoice大模型流式文本语音合成 # 采用FlowingSpeechSynthesizer接口,支持更低延迟和更自然的语音质量 @@ -950,7 +961,7 @@ TTS: output_dir: tmp/ appkey: 你的阿里云智能语音交互服务项目Appkey token: 你的阿里云智能语音交互服务AccessToken,临时的24小时,要长期用下方的access_key_id,access_key_secret - voice: longxiaochun + voice: longxiaochun access_key_id: 你的阿里云账号access_key_id access_key_secret: 你的阿里云账号access_key_secret # 截至2025年7月21日大模型音色只有北京节点采用,其他节点暂不支持 @@ -960,6 +971,7 @@ TTS: # volume: 50 # 音量:0-100 # speech_rate: 0 # 语速:-500到500 # pitch_rate: 0 # 语调:-500到500 + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 TencentTTS: # 腾讯云智能语音交互服务,需要先在腾讯云平台开通服务 # appid、secret_id、secret_key申请地址:https://console.cloud.tencent.com/cam/capi @@ -971,6 +983,7 @@ TTS: secret_key: 你的腾讯云SecretKey region: ap-guangzhou voice: 101001 + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 TTS302AI: # 302AI语音合成服务,需要先在302平台创建账户充值,并获取密钥信息 @@ -985,6 +998,7 @@ TTS: voice: "zh_female_wanwanxiaohe_moon_bigtts" output_dir: tmp/ access_token: "你的302API密钥" + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 GizwitsTTS: type: doubao # 火山引擎作为基座,可以完全使用企业级火山引擎语音合成服务 @@ -996,6 +1010,7 @@ TTS: voice: "zh_female_wanwanxiaohe_moon_bigtts" output_dir: tmp/ access_token: "你的机智云API key" + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 ACGNTTS: #在线网址:https://acgn.ttson.cn/ #token购买:www.ttson.cn @@ -1013,6 +1028,7 @@ TTS: format: mp3 output_dir: tmp/ emotion: 1 + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 OpenAITTS: # openai官方文本转语音服务,可支持全球大多数语种 type: openai @@ -1028,6 +1044,7 @@ TTS: # 语速范围0.25-4.0 speed: 1 output_dir: tmp/ + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 CustomTTS: # 自定义的TTS接口服务,请求参数可自定义,可接入众多TTS服务 # 以本地部署的KokoroTTS为例 @@ -1050,6 +1067,7 @@ TTS: # Authorization: Bearer xxxx format: mp3 # 接口返回的音频格式 output_dir: tmp/ + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 LinkeraiTTS: type: linkerai api_url: https://tts.linkerai.cn/tts @@ -1061,6 +1079,7 @@ TTS: access_token: "U4YdYXVfpwWnk2t5Gp822zWPCuORyeJL" voice: "OUeAo1mhq6IBExi" output_dir: tmp/ + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 PaddleSpeechTTS: #百度飞浆 PaddleSpeech 支持本地离线部署 支持模型训练 #框架地址 https://www.paddlepaddle.org.cn/ @@ -1074,6 +1093,7 @@ TTS: speed: 1.0 # 语速,1.0 表示正常语速,>1 表示加快,<1 表示减慢 volume: 1.0 # 音量,1.0 表示正常音量,>1 表示增大,<1 表示减小 save_path: # 保存路径 + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 IndexStreamTTS: # 基于Index-TTS-vLLM项目的TTS接口服务 # 参照教程:https://github.com/Ksuriuri/index-tts-vllm/blob/master/README.md @@ -1083,6 +1103,7 @@ TTS: # 默认音色,如需其他音色可到项目assets文件夹下注册 voice: "jay_klee" output_dir: tmp/ + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 AliBLTTS: # 阿里百炼CosyVoice大模型流式文本语音合成 # 可在这里找到你的 api_key https://bailian.console.aliyun.com/?apiKey=1#/api-key @@ -1097,6 +1118,7 @@ TTS: # volume: 50 # 音量:0-100 # rate: 1 # 语速:0.5~2 # pitch: 1 # 语调:0.5~2 + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 XunFeiTTS: # 讯飞TTS服务 官方网站:https://www.xfyun.cn/ # 登录讯飞语音技术平台 https://console.xfyun.cn/app/myapp 创建相关应用 @@ -1118,3 +1140,4 @@ TTS: # volume: 50 # 音量:0-100 # speed: 50 # 语速:0-100 # pitch: 50 # 语调:0-100 + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py index 6e8f5a8b..3f917963 100644 --- a/main/xiaozhi-server/core/connection.py +++ b/main/xiaozhi-server/core/connection.py @@ -134,7 +134,6 @@ class ConnectionHandler: self.asr_audio = [] self.asr_audio_queue = queue.Queue() self.current_speaker = None # 存储当前说话人 - self.current_language_tag = None # 存储当前ASR识别的语言标签 # llm相关变量 self.dialogue = Dialogue() diff --git a/main/xiaozhi-server/core/handle/receiveAudioHandle.py b/main/xiaozhi-server/core/handle/receiveAudioHandle.py index df466df5..4f69aa99 100644 --- a/main/xiaozhi-server/core/handle/receiveAudioHandle.py +++ b/main/xiaozhi-server/core/handle/receiveAudioHandle.py @@ -67,11 +67,6 @@ async def startToChat(conn: "ConnectionHandler", text): conn.current_speaker = speaker_name else: conn.current_speaker = None - # 保存语种信息到连接对象 - if language_tag: - conn.current_language_tag = language_tag - else: - conn.current_language_tag = "zh" if conn.need_bind: await check_bind_device(conn) diff --git a/main/xiaozhi-server/core/utils/prompt_manager.py b/main/xiaozhi-server/core/utils/prompt_manager.py index f7171603..fbd3fda0 100644 --- a/main/xiaozhi-server/core/utils/prompt_manager.py +++ b/main/xiaozhi-server/core/utils/prompt_manager.py @@ -251,6 +251,15 @@ class PromptManager: or "" ) + # 获取TTS选择的语言,默认值为中文 + language = ( + self.config.get("TTS", {}) + .get(self.config.get("selected_module", {}).get("TTS", ""), {}) + .get("language") + or "中文" + ) + self.logger.bind(tag=TAG).debug(f"获取到选择的语言: {language}") + # 替换模板变量 template = Template(self.base_prompt_template) enhanced_prompt = template.render( @@ -265,6 +274,7 @@ class PromptManager: device_id=device_id, client_ip=client_ip, dynamic_context=self.context_data, + language=language, *args, **kwargs, ) From 7b020ba55c06dec1f34c201df09e19bfc95b2152 Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Fri, 27 Feb 2026 11:17:08 +0800 Subject: [PATCH 4/5] =?UTF-8?q?update:=20=E5=A2=9E=E5=8A=A0=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E4=BD=93=E7=8B=AC=E7=AB=8B=E9=9F=B3=E9=A2=91=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modules/agent/dto/AgentUpdateDTO.java | 10 + .../modules/agent/entity/AgentEntity.java | 10 + .../agent/entity/AgentTemplateEntity.java | 16 ++ .../agent/service/impl/AgentServiceImpl.java | 9 + .../service/impl/ConfigServiceImpl.java | 15 ++ .../resources/db/changelog/202602061650.sql | 36 ++- .../main/resources/mapper/agent/AgentDao.xml | 6 + .../src/components/TtsAdvancedSettings.vue | 240 ++++++++++++++++++ main/manager-web/src/i18n/de.js | 8 + main/manager-web/src/i18n/en.js | 8 + main/manager-web/src/i18n/vi.js | 8 + main/manager-web/src/i18n/zh_CN.js | 8 + main/manager-web/src/i18n/zh_TW.js | 8 + main/manager-web/src/views/roleConfig.vue | 101 +++++++- .../core/providers/tts/alibl_stream.py | 11 +- .../core/providers/tts/aliyun.py | 20 +- .../core/providers/tts/aliyun_stream.py | 23 +- .../xiaozhi-server/core/providers/tts/base.py | 9 +- .../core/providers/tts/doubao.py | 14 +- .../providers/tts/huoshan_double_stream.py | 19 +- .../core/providers/tts/minimax_httpstream.py | 22 +- .../core/providers/tts/paddle_speech.py | 9 + .../core/providers/tts/siliconflow.py | 8 + .../core/providers/tts/ttson.py | 9 + .../core/providers/tts/xunfei_stream.py | 11 +- main/xiaozhi-server/core/utils/tts.py | 29 ++- 26 files changed, 638 insertions(+), 29 deletions(-) create mode 100644 main/manager-web/src/components/TtsAdvancedSettings.vue diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java index d15a1df7..8f6dead8 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java @@ -1,6 +1,7 @@ package xiaozhi.modules.agent.dto; import java.io.Serializable; +import java.math.BigDecimal; import java.util.HashMap; import java.util.List; @@ -44,6 +45,15 @@ public class AgentUpdateDTO implements Serializable { @Schema(description = "音色语言", example = "普通话", nullable = true) private String ttsLanguage; + @Schema(description = "TTS音量", example = "50", nullable = true) + private Integer ttsVolume; + + @Schema(description = "TTS语速", example = "50", nullable = true) + private Integer ttsRate; + + @Schema(description = "TTS音调", example = "50", nullable = true) + private Integer ttsPitch; + @Schema(description = "记忆模型标识", example = "mem_model_02", nullable = true) private String memModelId; diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentEntity.java index 7f63b688..dff620ca 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentEntity.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentEntity.java @@ -1,5 +1,6 @@ package xiaozhi.modules.agent.entity; +import java.math.BigDecimal; import java.util.Date; import com.baomidou.mybatisplus.annotation.IdType; @@ -48,6 +49,15 @@ public class AgentEntity { @Schema(description = "音色语言") private String ttsLanguage; + @Schema(description = "TTS音量") + private Integer ttsVolume; + + @Schema(description = "TTS语速") + private Integer ttsRate; + + @Schema(description = "TTS音调") + private Integer ttsPitch; + @Schema(description = "记忆模型标识") private String memModelId; diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentTemplateEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentTemplateEntity.java index ae9c3c40..b06cda91 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentTemplateEntity.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentTemplateEntity.java @@ -1,6 +1,7 @@ package xiaozhi.modules.agent.entity; import java.io.Serializable; +import java.math.BigDecimal; import java.util.Date; import com.baomidou.mybatisplus.annotation.IdType; @@ -69,6 +70,21 @@ public class AgentTemplateEntity implements Serializable { */ private String ttsLanguage; + /** + * TTS音量 + */ + private Integer ttsVolume; + + /** + * TTS语速 + */ + private Integer ttsRate; + + /** + * TTS音调 + */ + private Integer ttsPitch; + /** * 记忆模型标识 */ 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 95a55434..9c423ff8 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 @@ -285,6 +285,15 @@ public class AgentServiceImpl extends BaseServiceImpl imp if (dto.getTtsLanguage() != null) { existingEntity.setTtsLanguage(dto.getTtsLanguage()); } + if (dto.getTtsVolume() != null) { + existingEntity.setTtsVolume(dto.getTtsVolume()); + } + if (dto.getTtsRate() != null) { + existingEntity.setTtsRate(dto.getTtsRate()); + } + if (dto.getTtsPitch() != null) { + existingEntity.setTtsPitch(dto.getTtsPitch()); + } if (dto.getMemModelId() != null) { existingEntity.setMemModelId(dto.getMemModelId()); } 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 9cc3bbfb..55f23c67 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 @@ -88,6 +88,9 @@ public class ConfigServiceImpl implements ConfigService { null, null, null, + null, + null, + null, agent.getVadModelId(), agent.getAsrModelId(), null, @@ -219,6 +222,9 @@ public class ConfigServiceImpl implements ConfigService { referenceAudio, referenceText, language, + agent.getTtsVolume(), + agent.getTtsRate(), + agent.getTtsPitch(), agent.getVadModelId(), agent.getAsrModelId(), agent.getLlmModelId(), @@ -397,6 +403,9 @@ public class ConfigServiceImpl implements ConfigService { String referenceAudio, String referenceText, String language, + Integer ttsVolume, + Integer ttsRate, + Integer ttsPitch, String vadModelId, String asrModelId, String llmModelId, @@ -437,6 +446,12 @@ public class ConfigServiceImpl implements ConfigService { ((Map) model.getConfigJson()).put("ref_text", referenceText); if (language != null) ((Map) model.getConfigJson()).put("language", language); + if (ttsVolume != null) + ((Map) model.getConfigJson()).put("ttsVolume", ttsVolume); + if (ttsRate != null) + ((Map) model.getConfigJson()).put("ttsRate", ttsRate); + if (ttsPitch != null) + ((Map) model.getConfigJson()).put("ttsPitch", ttsPitch); // 火山引擎声音克隆需要替换resource_id Map map = (Map) model.getConfigJson(); diff --git a/main/manager-api/src/main/resources/db/changelog/202602061650.sql b/main/manager-api/src/main/resources/db/changelog/202602061650.sql index 4341cf59..b60f489d 100644 --- a/main/manager-api/src/main/resources/db/changelog/202602061650.sql +++ b/main/manager-api/src/main/resources/db/changelog/202602061650.sql @@ -13,8 +13,36 @@ SET languages = CASE ELSE languages END; --- 添加音色语言字段到 ai_agent 表 -ALTER TABLE `ai_agent` ADD COLUMN `tts_language` VARCHAR(50) NULL COMMENT '音色语言' AFTER `tts_voice_id`; +-- 添加音色语言、音量、语速、音调字段到 ai_agent 表 +SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ai_agent' AND COLUMN_NAME = 'tts_language'); +SET @sql = IF(@col_exists = 0, 'ALTER TABLE `ai_agent` ADD COLUMN `tts_language` VARCHAR(50) NULL COMMENT ''音色语言'' AFTER `tts_voice_id`', 'SELECT ''Column tts_language already exists'' AS msg'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; --- 添加音色语言字段到 ai_agent_template 表 -ALTER TABLE `ai_agent_template` ADD COLUMN `tts_language` VARCHAR(50) NULL COMMENT '音色语言' AFTER `tts_voice_id`; +SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ai_agent' AND COLUMN_NAME = 'tts_volume'); +SET @sql = IF(@col_exists = 0, 'ALTER TABLE `ai_agent` ADD COLUMN `tts_volume` INT NULL COMMENT ''TTS音量'' AFTER `tts_language`', 'SELECT ''Column tts_volume already exists'' AS msg'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ai_agent' AND COLUMN_NAME = 'tts_rate'); +SET @sql = IF(@col_exists = 0, 'ALTER TABLE `ai_agent` ADD COLUMN `tts_rate` INT NULL COMMENT ''TTS语速'' AFTER `tts_volume`', 'SELECT ''Column tts_rate already exists'' AS msg'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ai_agent' AND COLUMN_NAME = 'tts_pitch'); +SET @sql = IF(@col_exists = 0, 'ALTER TABLE `ai_agent` ADD COLUMN `tts_pitch` INT NULL COMMENT ''TTS音调'' AFTER `tts_rate`', 'SELECT ''Column tts_pitch already exists'' AS msg'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- 添加音色语言、音量、语速、音调字段到 ai_agent_template 表 +SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ai_agent_template' AND COLUMN_NAME = 'tts_language'); +SET @sql = IF(@col_exists = 0, 'ALTER TABLE `ai_agent_template` ADD COLUMN `tts_language` VARCHAR(50) NULL COMMENT ''音色语言'' AFTER `tts_voice_id`', 'SELECT ''Column tts_language already exists'' AS msg'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ai_agent_template' AND COLUMN_NAME = 'tts_volume'); +SET @sql = IF(@col_exists = 0, 'ALTER TABLE `ai_agent_template` ADD COLUMN `tts_volume` INT NULL COMMENT ''TTS音量'' AFTER `tts_language`', 'SELECT ''Column tts_volume already exists'' AS msg'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ai_agent_template' AND COLUMN_NAME = 'tts_rate'); +SET @sql = IF(@col_exists = 0, 'ALTER TABLE `ai_agent_template` ADD COLUMN `tts_rate` INT NULL COMMENT ''TTS语速'' AFTER `tts_volume`', 'SELECT ''Column tts_rate already exists'' AS msg'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ai_agent_template' AND COLUMN_NAME = 'tts_pitch'); +SET @sql = IF(@col_exists = 0, 'ALTER TABLE `ai_agent_template` ADD COLUMN `tts_pitch` INT NULL COMMENT ''TTS音调'' AFTER `tts_rate`', 'SELECT ''Column tts_pitch already exists'' AS msg'); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; \ No newline at end of file diff --git a/main/manager-api/src/main/resources/mapper/agent/AgentDao.xml b/main/manager-api/src/main/resources/mapper/agent/AgentDao.xml index 95ca0246..9c417909 100644 --- a/main/manager-api/src/main/resources/mapper/agent/AgentDao.xml +++ b/main/manager-api/src/main/resources/mapper/agent/AgentDao.xml @@ -17,6 +17,9 @@ + + + @@ -47,6 +50,9 @@ a.tts_model_id AS ttsModelId, a.tts_voice_id AS ttsVoiceId, a.tts_language AS ttsLanguage, + a.tts_volume AS ttsVolume, + a.tts_rate AS ttsRate, + a.tts_pitch AS ttsPitch, a.mem_model_id AS memModelId, a.intent_model_id AS intentModelId, COALESCE( diff --git a/main/manager-web/src/components/TtsAdvancedSettings.vue b/main/manager-web/src/components/TtsAdvancedSettings.vue new file mode 100644 index 00000000..fe11b0ac --- /dev/null +++ b/main/manager-web/src/components/TtsAdvancedSettings.vue @@ -0,0 +1,240 @@ + + + + + + + diff --git a/main/manager-web/src/i18n/de.js b/main/manager-web/src/i18n/de.js index bd511ccf..eed67199 100644 --- a/main/manager-web/src/i18n/de.js +++ b/main/manager-web/src/i18n/de.js @@ -764,6 +764,14 @@ export default { 'roleConfig.intent': 'Intent', 'roleConfig.language': 'Sprache auswählen', 'roleConfig.voiceType': 'Stimmtyp', + 'roleConfig.ttsVolume': 'Lautstärke', + 'roleConfig.ttsRate': 'Geschwindigkeit', + 'roleConfig.ttsPitch': 'Tonhöhe', + 'roleConfig.ttsAdvanced': 'TTS-Parameter', + 'roleConfig.advancedSettings': 'Erweiterte Einstellungen', + 'roleConfig.volumeHint': '-100=Min, 0=Standard, 100=Max', + 'roleConfig.speedHint': '-100=Langsamste, 0=Standard, 100=Schnellste', + 'roleConfig.pitchHint': '-100=Niedrigste, 0=Standard, 100=Höchste', 'roleConfig.pleaseEnterContent': 'Bitte Inhalt eingeben', 'roleConfig.pleaseEnterLangCode': 'Bitte Sprachcode eingeben, z.B.: en_US', 'roleConfig.pleaseEnterLangName': 'Bitte Interaktionssprache eingeben, z.B.: Englisch', diff --git a/main/manager-web/src/i18n/en.js b/main/manager-web/src/i18n/en.js index 5e184cd9..ebb80c81 100644 --- a/main/manager-web/src/i18n/en.js +++ b/main/manager-web/src/i18n/en.js @@ -764,6 +764,14 @@ export default { 'roleConfig.intent': 'Intent Recognition', 'roleConfig.language': 'Select Language', 'roleConfig.voiceType': 'Voice Type', + 'roleConfig.ttsVolume': 'Volume', + 'roleConfig.ttsRate': 'Speed', + 'roleConfig.ttsPitch': 'Pitch', + 'roleConfig.ttsAdvanced': 'TTS Parameters', + 'roleConfig.advancedSettings': 'Advanced Settings', + 'roleConfig.volumeHint': '-100=Min, 0=Standard, 100=Max', + 'roleConfig.speedHint': '-100=Slowest, 0=Standard, 100=Fastest', + 'roleConfig.pitchHint': '-100=Lowest, 0=Standard, 100=Highest', 'roleConfig.pleaseEnterContent': 'Please enter content', 'roleConfig.pleaseEnterLangCode': 'Please enter language code, e.g.: en_US', 'roleConfig.pleaseEnterLangName': 'Please enter interaction language, e.g.: English', diff --git a/main/manager-web/src/i18n/vi.js b/main/manager-web/src/i18n/vi.js index 8a124b35..af6e7ab5 100644 --- a/main/manager-web/src/i18n/vi.js +++ b/main/manager-web/src/i18n/vi.js @@ -764,6 +764,14 @@ export default { 'roleConfig.intent': 'Nhận dạng ý định', 'roleConfig.language': 'Chọn ngôn ngữ', 'roleConfig.voiceType': 'Loại giọng nói', + 'roleConfig.ttsVolume': 'Âm lượng', + 'roleConfig.ttsRate': 'Tốc độ', + 'roleConfig.ttsPitch': 'Cao độ', + 'roleConfig.ttsAdvanced': 'Tham số TTS', + 'roleConfig.advancedSettings': 'Cài đặt nâng cao', + 'roleConfig.volumeHint': '-100=Tối thiểu, 0=Tiêu chuẩn, 100=Tối đa', + 'roleConfig.speedHint': '-100=Chậm nhất, 0=Tiêu chuẩn, 100=Nhanh nhất', + 'roleConfig.pitchHint': '-100=Thấp nhất, 0=Tiêu chuẩn, 100=Cao nhất', 'roleConfig.pleaseEnterContent': 'Vui lòng nhập nội dung', 'roleConfig.pleaseEnterLangCode': 'Vui lòng nhập mã ngôn ngữ, ví dụ: en_US', 'roleConfig.pleaseEnterLangName': 'Vui lòng nhập ngôn ngữ tương tác, ví dụ: Tiếng Anh', diff --git a/main/manager-web/src/i18n/zh_CN.js b/main/manager-web/src/i18n/zh_CN.js index 2edf4f04..520e1c27 100644 --- a/main/manager-web/src/i18n/zh_CN.js +++ b/main/manager-web/src/i18n/zh_CN.js @@ -764,6 +764,14 @@ export default { 'roleConfig.tts': '语音合成(TTS)', 'roleConfig.language': '选择语言', 'roleConfig.voiceType': '声音音色(Voice)', + 'roleConfig.ttsVolume': '音量', + 'roleConfig.ttsRate': '语速', + 'roleConfig.ttsPitch': '音调', + 'roleConfig.ttsAdvanced': 'TTS参数', + 'roleConfig.advancedSettings': '高级设置', + 'roleConfig.volumeHint': '-100=最小, 0=标准, 100=最大', + 'roleConfig.speedHint': '-100=最慢, 0=标准, 100=最快', + 'roleConfig.pitchHint': '-100=最低, 0=标准, 100=最高', 'roleConfig.pleaseEnterContent': '请输入内容', 'roleConfig.pleaseEnterLangCode': '请输入语言编码,如:zh_CN', 'roleConfig.pleaseEnterLangName': '请输入交互语种,如:中文', diff --git a/main/manager-web/src/i18n/zh_TW.js b/main/manager-web/src/i18n/zh_TW.js index 7af2fdba..ca86ef97 100644 --- a/main/manager-web/src/i18n/zh_TW.js +++ b/main/manager-web/src/i18n/zh_TW.js @@ -764,6 +764,14 @@ export default { 'roleConfig.intent': '意圖識別(Intent)', 'roleConfig.language': '選擇語言', 'roleConfig.voiceType': '聲音音色(Voice)', + 'roleConfig.ttsVolume': '音量', + 'roleConfig.ttsRate': '語速', + 'roleConfig.ttsPitch': '音調', + 'roleConfig.ttsAdvanced': 'TTS參數', + 'roleConfig.advancedSettings': '高級設置', + 'roleConfig.volumeHint': '-100=最小, 0=標準, 100=最大', + 'roleConfig.speedHint': '-100=最慢, 0=標準, 100=最快', + 'roleConfig.pitchHint': '-100=最低, 0=標準, 100=最高', 'roleConfig.pleaseEnterContent': '請輸入內容', 'roleConfig.pleaseEnterLangCode': '請輸入語言編碼,如:zh_TW', 'roleConfig.pleaseEnterLangName': '請輸入交互語種,如:繁體中文', diff --git a/main/manager-web/src/views/roleConfig.vue b/main/manager-web/src/views/roleConfig.vue index 4f4a0f8e..a22b997f 100644 --- a/main/manager-web/src/views/roleConfig.vue +++ b/main/manager-web/src/views/roleConfig.vue @@ -234,12 +234,12 @@
- +
- +
@@ -294,6 +294,13 @@
+ + {{ $t('roleConfig.advancedSettings') }} +
@@ -318,6 +325,11 @@ :providers="currentContextProviders" @confirm="handleUpdateContext" /> + @@ -327,20 +339,30 @@ import { getServiceUrl } from "@/apis/api"; import RequestService from "@/apis/httpRequest"; import FunctionDialog from "@/components/FunctionDialog.vue"; import ContextProviderDialog from "@/components/ContextProviderDialog.vue"; +import TtsAdvancedSettings from "@/components/TtsAdvancedSettings.vue"; import HeaderBar from "@/components/HeaderBar.vue"; import i18n from "@/i18n"; import featureManager from "@/utils/featureManager"; export default { name: "RoleConfigPage", - components: { HeaderBar, FunctionDialog, ContextProviderDialog }, + components: { HeaderBar, FunctionDialog, ContextProviderDialog, TtsAdvancedSettings }, data() { return { showContextProviderDialog: false, + showTtsAdvancedDialog: false, + ttsSettings: { + volume: 0, + speed: 0, + pitch: 0 + }, form: { agentCode: "", agentName: "", ttsVoiceId: "", + ttsVolume: null, + ttsRate: null, + ttsPitch: null, chatHistoryConf: 0, systemPrompt: "", summaryMemory: "", @@ -422,6 +444,17 @@ export default { }), contextProviders: this.currentContextProviders, }; + + // 只在用户设置了TTS参数时才传递(不为null/undefined) + if (this.form.ttsVolume !== null && this.form.ttsVolume !== undefined) { + configData.ttsVolume = this.form.ttsVolume; + } + if (this.form.ttsRate !== null && this.form.ttsRate !== undefined) { + configData.ttsRate = this.form.ttsRate; + } + if (this.form.ttsPitch !== null && this.form.ttsPitch !== undefined) { + configData.ttsPitch = this.form.ttsPitch; + } Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => { if (data.code === 0) { this.$message.success({ @@ -535,6 +568,14 @@ export default { intentModelId: data.data.intentModelId, }, }; + + // 同步TTS设置到ttsSettings + this.ttsSettings = { + volume: this.form.ttsVolume || 0, + speed: this.form.ttsRate || 0, + pitch: this.form.ttsPitch || 0 + }; + // 后端只给了最小映射:[{ id, agentId, pluginId }, ...] const savedMappings = data.data.functions || []; @@ -696,6 +737,13 @@ export default { if (!currentVoiceSupportsLanguage) { this.form.ttsVoiceId = filteredVoices.length > 0 ? filteredVoices[0].id : ''; } + + // 同步到ttsSettings(如果值为null,使用0作为显示默认值,但不修改form中的值) + this.ttsSettings = { + volume: this.form.ttsVolume !== null && this.form.ttsVolume !== undefined ? this.form.ttsVolume : 0, + speed: this.form.ttsRate !== null && this.form.ttsRate !== undefined ? this.form.ttsRate : 0, + pitch: this.form.ttsPitch !== null && this.form.ttsPitch !== undefined ? this.form.ttsPitch : 0 + }; }, getFunctionDisplayChar(name) { @@ -763,6 +811,16 @@ export default { openContextProviderDialog() { this.showContextProviderDialog = true; }, + openTtsAdvancedSettings() { + this.showTtsAdvancedDialog = true; + }, + handleTtsSettingsSave(settings) { + // 保存TTS设置 + this.ttsSettings = { ...settings }; + this.form.ttsVolume = settings.volume; + this.form.ttsRate = settings.speed; + this.form.ttsPitch = settings.pitch; + }, handleUpdateContext(providers) { this.currentContextProviders = providers; }, @@ -1316,6 +1374,15 @@ export default { margin-bottom: 0; } +.model-row .language-select-item { + flex: 0 0 35%; + max-width: 35%; +} + +.model-row .language-select-item .language-select { + width: 100%; +} + .model-row .el-form-item__label { font-size: 12px !important; color: #3d4566 !important; @@ -1491,4 +1558,30 @@ export default { text-decoration: underline; } } + +.slider-wrapper { + width: 100%; + padding-right: 12px; +} + +.slider-hint { + display: block; + font-size: 12px; + color: #909399; + margin-top: 4px; + line-height: 1.5; +} + +.tts-slider { + width: 100%; +} + +.tts-slider ::v-deep .el-slider__input { + width: 80px; +} + +.tts-slider ::v-deep .el-input__inner { + text-align: center; + padding: 0 8px; +} diff --git a/main/xiaozhi-server/core/providers/tts/alibl_stream.py b/main/xiaozhi-server/core/providers/tts/alibl_stream.py index c636d0cb..b276c855 100644 --- a/main/xiaozhi-server/core/providers/tts/alibl_stream.py +++ b/main/xiaozhi-server/core/providers/tts/alibl_stream.py @@ -6,9 +6,9 @@ import queue import asyncio import traceback import websockets + from asyncio import Task from config.logger import setup_logging -from core.utils import opus_encoder_utils from core.utils.tts import MarkdownCleaner from core.providers.tts.base import TTSProviderBase from core.providers.tts.dto.dto import SentenceType, ContentType, InterfaceType @@ -18,6 +18,12 @@ logger = setup_logging() class TTSProvider(TTSProviderBase): + TTS_PARAM_CONFIG = [ + ("ttsVolume", "volume", 0, 100, 50, int), + ("ttsRate", "rate", 0.5, 2.0, 1.0, lambda v: round(v, 1)), + ("ttsPitch", "pitch", 0.5, 2.0, 1.0, lambda v: round(v, 1)), + ] + def __init__(self, config, delete_audio_file): super().__init__(config, delete_audio_file) @@ -51,6 +57,9 @@ class TTSProvider(TTSProviderBase): pitch = config.get("pitch", "1.0") self.pitch = float(pitch) if pitch else 1.0 + # 应用百分比调整(如果存在),否则使用公有化配置 + self._apply_percentage_params(config) + self.header = { "Authorization": f"Bearer {self.api_key}", # "user-agent": "your_platform_info", // 可选 diff --git a/main/xiaozhi-server/core/providers/tts/aliyun.py b/main/xiaozhi-server/core/providers/tts/aliyun.py index e97c13e4..05ea2031 100644 --- a/main/xiaozhi-server/core/providers/tts/aliyun.py +++ b/main/xiaozhi-server/core/providers/tts/aliyun.py @@ -1,15 +1,17 @@ import uuid import json import hmac +import time import hashlib import base64 import requests -from datetime import datetime -from core.providers.tts.base import TTSProviderBase -from config.logger import setup_logging -import time -import uuid + from urllib import parse +from datetime import datetime +from config.logger import setup_logging +from core.providers.tts.base import TTSProviderBase +from core.utils.tts import convert_percentage_to_range + TAG = __name__ logger = setup_logging() @@ -84,6 +86,11 @@ class AccessToken: class TTSProvider(TTSProviderBase): + TTS_PARAM_CONFIG = [ + ("ttsVolume", "volume", 0, 100, 50, int), + ("ttsRate", "speech_rate", -500, 500, 0, int), + ("ttsPitch", "pitch_rate", -500, 500, 0, int), + ] def __init__(self, config, delete_audio_file): super().__init__(config, delete_audio_file) @@ -110,6 +117,9 @@ class TTSProvider(TTSProviderBase): pitch_rate = config.get("pitch_rate", "0") self.pitch_rate = int(pitch_rate) if pitch_rate else 0 + # 应用百分比调整(如果存在),否则使用公有化配置 + self._apply_percentage_params(config) + self.host = config.get("host", "nls-gateway-cn-shanghai.aliyuncs.com") self.api_url = f"https://{self.host}/stream/v1/tts" self.header = {"Content-Type": "application/json"} diff --git a/main/xiaozhi-server/core/providers/tts/aliyun_stream.py b/main/xiaozhi-server/core/providers/tts/aliyun_stream.py index 6026eafa..8fd20084 100644 --- a/main/xiaozhi-server/core/providers/tts/aliyun_stream.py +++ b/main/xiaozhi-server/core/providers/tts/aliyun_stream.py @@ -1,4 +1,4 @@ -import random +import os import uuid import json import hmac @@ -8,16 +8,16 @@ import time import queue import asyncio import traceback -from asyncio import Task import websockets -import os -from datetime import datetime + +from asyncio import Task from urllib import parse +from datetime import datetime +from config.logger import setup_logging +from core.utils.tts import MarkdownCleaner from core.providers.tts.base import TTSProviderBase from core.providers.tts.dto.dto import SentenceType, ContentType, InterfaceType -from core.utils.tts import MarkdownCleaner -from core.utils import opus_encoder_utils, textUtils -from config.logger import setup_logging + TAG = __name__ logger = setup_logging() @@ -86,6 +86,12 @@ class AccessToken: class TTSProvider(TTSProviderBase): + TTS_PARAM_CONFIG = [ + ("ttsVolume", "volume", 0, 100, 50, int), + ("ttsRate", "speech_rate", -500, 500, 0, int), + ("ttsPitch", "pitch_rate", -500, 500, 0, int), + ] + def __init__(self, config, delete_audio_file): super().__init__(config, delete_audio_file) @@ -115,6 +121,9 @@ class TTSProvider(TTSProviderBase): pitch_rate = config.get("pitch_rate", "0") self.pitch_rate = int(pitch_rate) if pitch_rate else 0 + # 应用百分比调整(如果存在),否则使用公有化配置 + self._apply_percentage_params(config) + # WebSocket配置 self.host = config.get("host", "nls-gateway-cn-beijing.aliyuncs.com") # 如果配置的是内网地址(包含-internal.aliyuncs.com),则使用ws协议,默认是wss协议 diff --git a/main/xiaozhi-server/core/providers/tts/base.py b/main/xiaozhi-server/core/providers/tts/base.py index d2126dba..4fef87dd 100644 --- a/main/xiaozhi-server/core/providers/tts/base.py +++ b/main/xiaozhi-server/core/providers/tts/base.py @@ -13,7 +13,7 @@ from typing import Callable, Any from abc import ABC, abstractmethod from config.logger import setup_logging from core.utils import opus_encoder_utils -from core.utils.tts import MarkdownCleaner +from core.utils.tts import MarkdownCleaner, convert_percentage_to_range from core.utils.output_counter import add_device_output from core.handle.reportHandle import enqueue_tts_report from core.handle.sendAudioHandle import sendAudioMessage @@ -463,3 +463,10 @@ class TTSProviderBase(ABC): self.processed_chars += len(full_text) return True return False + + def _apply_percentage_params(self, config): + """根据子类定义的 TTS_PARAM_CONFIG 批量应用百分比参数""" + for config_key, attr_name, min_val, max_val, base_val, transform in self.TTS_PARAM_CONFIG: + if config_key in config: + val = convert_percentage_to_range(config[config_key], min_val, max_val, base_val) + setattr(self, attr_name, transform(val) if transform else val) diff --git a/main/xiaozhi-server/core/providers/tts/doubao.py b/main/xiaozhi-server/core/providers/tts/doubao.py index 2eed9bcf..42434c4f 100644 --- a/main/xiaozhi-server/core/providers/tts/doubao.py +++ b/main/xiaozhi-server/core/providers/tts/doubao.py @@ -2,15 +2,24 @@ import uuid import json import base64 import requests + +from config.logger import setup_logging from core.utils.util import check_model_key from core.providers.tts.base import TTSProviderBase -from config.logger import setup_logging +from core.utils.tts import convert_percentage_to_range + TAG = __name__ logger = setup_logging() class TTSProvider(TTSProviderBase): + TTS_PARAM_CONFIG = [ + ("ttsVolume", "volume_ratio", 0.1, 3, 1.0, lambda v: round(float(v), 1)), + ("ttsRate", "speed_ratio", 0.2, 3, 1.0, lambda v: round(float(v), 1)), + ("ttsPitch", "pitch_ratio", 0.1, 3, 1.0, lambda v: round(float(v), 1)), + ] + def __init__(self, config, delete_audio_file): super().__init__(config, delete_audio_file) if config.get("appid"): @@ -34,6 +43,9 @@ class TTSProvider(TTSProviderBase): self.volume_ratio = float(volume_ratio) if volume_ratio else 1.0 self.pitch_ratio = float(pitch_ratio) if pitch_ratio else 1.0 + # 应用百分比调整(如果存在),否则使用公有化配置 + self._apply_percentage_params(config) + self.api_url = config.get("api_url") self.authorization = config.get("authorization") self.header = {"Authorization": f"{self.authorization}{self.access_token}"} diff --git a/main/xiaozhi-server/core/providers/tts/huoshan_double_stream.py b/main/xiaozhi-server/core/providers/tts/huoshan_double_stream.py index b1ba1bed..a24d09b0 100644 --- a/main/xiaozhi-server/core/providers/tts/huoshan_double_stream.py +++ b/main/xiaozhi-server/core/providers/tts/huoshan_double_stream.py @@ -7,11 +7,10 @@ import traceback import websockets from typing import Callable, Any -from core.utils.tts import MarkdownCleaner from config.logger import setup_logging -from core.utils import opus_encoder_utils from core.utils.util import check_model_key from core.providers.tts.base import TTSProviderBase +from core.utils.tts import MarkdownCleaner, convert_percentage_to_range from core.providers.tts.dto.dto import SentenceType, ContentType, InterfaceType @@ -178,6 +177,22 @@ class TTSProvider(TTSProviderBase): self.additions = {**default_additions, **config.get("additions", {})} self.mix_speaker = {**default_mix_speaker, **config.get("mix_speaker", {})} + # 应用百分比调整(如果存在),否则使用公有化配置 + if "ttsVolume" in config: + self.audio_params["loudness_rate"] = int(convert_percentage_to_range( + config["ttsVolume"], min_val=-50, max_val=100, base_val=0 + )) + + if "ttsRate" in config: + self.audio_params["speech_rate"] = int(convert_percentage_to_range( + config["ttsRate"], min_val=-50, max_val=100, base_val=0 + )) + + if "ttsPitch" in config: + self.additions["post_process"]["pitch"] = int(convert_percentage_to_range( + config["ttsPitch"], min_val=-12, max_val=12, base_val=0 + )) + self.ws_url = config.get("ws_url") self.authorization = config.get("authorization") self.header = {"Authorization": f"{self.authorization}{self.access_token}"} diff --git a/main/xiaozhi-server/core/providers/tts/minimax_httpstream.py b/main/xiaozhi-server/core/providers/tts/minimax_httpstream.py index 9f145933..b5b0cd37 100644 --- a/main/xiaozhi-server/core/providers/tts/minimax_httpstream.py +++ b/main/xiaozhi-server/core/providers/tts/minimax_httpstream.py @@ -6,12 +6,14 @@ import asyncio import aiohttp import requests import traceback + +from core.utils import textUtils from config.logger import setup_logging -from core.utils.tts import MarkdownCleaner from core.utils.util import parse_string_to_list from core.providers.tts.base import TTSProviderBase -from core.utils import opus_encoder_utils, textUtils from core.providers.tts.dto.dto import SentenceType, ContentType +from core.utils.tts import MarkdownCleaner, convert_percentage_to_range + TAG = __name__ logger = setup_logging() @@ -56,6 +58,22 @@ class TTSProvider(TTSProviderBase): if self.voice: self.voice_setting["voice_id"] = self.voice + # 应用百分比调整(如果存在),否则使用公有化配置 + if "ttsVolume" in config: + self.voice_setting["vol"] = round(convert_percentage_to_range( + config["ttsVolume"], min_val=0.1, max_val=10, base_val=1.0 + ), 1) + + if "ttsRate" in config: + self.voice_setting["speed"] = round(convert_percentage_to_range( + config["ttsRate"], min_val=0.5, max_val=2, base_val=1.0 + ), 1) + + if "ttsPitch" in config: + self.voice_setting["pitch"] = int(convert_percentage_to_range( + config["ttsPitch"], min_val=-12, max_val=12, base_val=0 + )) + self.host = "api.minimaxi.com" # 备用地址:api-bj.minimaxi.com self.api_url = f"https://{self.host}/v1/t2a_v2?GroupId={self.group_id}" self.header = { diff --git a/main/xiaozhi-server/core/providers/tts/paddle_speech.py b/main/xiaozhi-server/core/providers/tts/paddle_speech.py index 48b0bb0c..1427499e 100644 --- a/main/xiaozhi-server/core/providers/tts/paddle_speech.py +++ b/main/xiaozhi-server/core/providers/tts/paddle_speech.py @@ -16,6 +16,11 @@ logger = setup_logging() class TTSProvider(TTSProviderBase): + TTS_PARAM_CONFIG = [ + ("ttsVolume", "volume", 0, 3, 1.0, lambda v: round(float(v), 1)), + ("ttsRate", "speed", 0, 3, 1.0, lambda v: round(float(v), 1)), + ] + def __init__(self, config, delete_audio_file): super().__init__(config, delete_audio_file) self.url = config.get("url", "ws://192.168.1.10:8092/paddlespeech/tts/streaming") @@ -33,6 +38,10 @@ class TTSProvider(TTSProviderBase): self.volume = float(volume) if volume else 1.0 self.delete_audio_file = config.get("delete_audio", True) + + # 应用百分比调整(如果存在),否则使用公有化配置 + self._apply_percentage_params(config) + if not self.delete_audio_file: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") save_path = config.get("save_path") diff --git a/main/xiaozhi-server/core/providers/tts/siliconflow.py b/main/xiaozhi-server/core/providers/tts/siliconflow.py index ebc3dbb9..48b709d6 100644 --- a/main/xiaozhi-server/core/providers/tts/siliconflow.py +++ b/main/xiaozhi-server/core/providers/tts/siliconflow.py @@ -3,6 +3,11 @@ from core.providers.tts.base import TTSProviderBase class TTSProvider(TTSProviderBase): + TTS_PARAM_CONFIG = [ + ("ttsVolume", "gain", -10, 10, 0, int), + ("ttsRate", "speed", 0.25, 4, 1, lambda v: round(float(v), 1)), + ] + def __init__(self, config, delete_audio_file): super().__init__(config, delete_audio_file) self.model = config.get("model") @@ -16,6 +21,9 @@ class TTSProvider(TTSProviderBase): self.speed = float(config.get("speed", 1.0)) self.gain = config.get("gain") + # 应用百分比调整(如果存在),否则使用公有化配置 + self._apply_percentage_params(config) + self.host = "api.siliconflow.cn" self.api_url = f"https://{self.host}/v1/audio/speech" diff --git a/main/xiaozhi-server/core/providers/tts/ttson.py b/main/xiaozhi-server/core/providers/tts/ttson.py index 91deeed9..0a9f8261 100644 --- a/main/xiaozhi-server/core/providers/tts/ttson.py +++ b/main/xiaozhi-server/core/providers/tts/ttson.py @@ -12,6 +12,12 @@ logger = setup_logging() class TTSProvider(TTSProviderBase): + TTS_PARAM_CONFIG = [ + ("ttsVolume", "volume_change_dB", -10, 10, 0, int), + ("ttsRate", "speed_factor", 0.5, 2, 0, lambda v: round(float(v), 1)), + ("ttsPitch", "pitch_factor", -8, 8, 0, lambda v: round(float(v), 1)), + ] + def __init__(self, config, delete_audio_file): super().__init__(config, delete_audio_file) self.url = config.get( @@ -34,6 +40,9 @@ class TTSProvider(TTSProviderBase): self.emotion = int(config.get("emotion", 1)) self.header = {"Content-Type": "application/json"} + # 应用百分比调整(如果存在),否则使用公有化配置 + self._apply_percentage_params(config) + def generate_filename(self, extension=".mp3"): return os.path.join( self.output_file, diff --git a/main/xiaozhi-server/core/providers/tts/xunfei_stream.py b/main/xiaozhi-server/core/providers/tts/xunfei_stream.py index e7d4d4e9..6ecefc86 100644 --- a/main/xiaozhi-server/core/providers/tts/xunfei_stream.py +++ b/main/xiaozhi-server/core/providers/tts/xunfei_stream.py @@ -9,9 +9,9 @@ import hashlib import asyncio import traceback import websockets + from asyncio import Task from config.logger import setup_logging -from core.utils import opus_encoder_utils from core.utils.tts import MarkdownCleaner from urllib.parse import urlencode, urlparse from core.providers.tts.base import TTSProviderBase @@ -59,6 +59,12 @@ class XunfeiWSAuth: class TTSProvider(TTSProviderBase): + TTS_PARAM_CONFIG = [ + ("ttsVolume", "volume", 0, 100, 50, int), + ("ttsRate", "speed", 0, 100, 50, int), + ("ttsPitch", "pitch", 0, 100, 50, int), + ] + def __init__(self, config, delete_audio_file): super().__init__(config, delete_audio_file) @@ -88,6 +94,9 @@ class TTSProvider(TTSProviderBase): pitch = config.get("pitch", "50") self.pitch = int(pitch) if pitch else 50 + # 应用百分比调整(如果存在),否则使用公有化配置 + self._apply_percentage_params(config) + # 音频编码配置 self.format = config.get("format", "raw") diff --git a/main/xiaozhi-server/core/utils/tts.py b/main/xiaozhi-server/core/utils/tts.py index 6727f6de..a0ac3c6b 100644 --- a/main/xiaozhi-server/core/utils/tts.py +++ b/main/xiaozhi-server/core/utils/tts.py @@ -141,4 +141,31 @@ class MarkdownCleaner: # 去除emoji表情 text = check_emoji(text) - return text.strip() \ No newline at end of file + return text.strip() + +def convert_percentage_to_range(percentage, min_val, max_val, base_val=None): + """ + 将百分比(-100~100)转换为指定范围的值 + + Args: + percentage: 百分比值 (-100 到 100) + min_val: 目标范围最小值 + max_val: 目标范围最大值 + base_val: 基准值(可选,默认为范围中点) + + Returns: + 转换后的值 + """ + if base_val is None: + base_val = (min_val + max_val) / 2 + + # 百分比 -100 对应 min_val, 0 对应 base_val, 100 对应 max_val + if percentage < 0: + # 负百分比:从 base_val 向 min_val 线性插值 + result = base_val + (base_val - min_val) * (percentage / 100) + else: + # 正百分比:从 base_val 向 max_val 线性插值 + result = base_val + (max_val - base_val) * (percentage / 100) + + # 确保结果在有效范围内 + return max(min_val, min(max_val, result)) From b49c4520f928ce406f8238ba7e29d2f70b31acd9 Mon Sep 17 00:00:00 2001 From: Sakura-RanChen <1908198662@qq.com> Date: Fri, 27 Feb 2026 16:37:31 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E9=83=A8=E5=88=86TTS=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/db/changelog/202602271137.sql | 42 +++++++++++++++++++ .../db/changelog/db.changelog-master.yaml | 7 ++++ main/xiaozhi-server/config.yaml | 8 +++- .../core/providers/tts/aliyun.py | 3 +- .../core/providers/tts/aliyun_stream.py | 1 - .../core/providers/tts/cozecn.py | 21 +++++++++- .../core/providers/tts/custom.py | 3 +- .../core/providers/tts/fishspeech.py | 3 +- .../core/providers/tts/openai.py | 10 ++++- .../core/providers/tts/siliconflow.py | 5 +-- .../core/providers/tts/tencent.py | 19 +++++++++ .../core/providers/tts/ttson.py | 3 +- main/xiaozhi-server/core/utils/tts.py | 5 +-- 13 files changed, 110 insertions(+), 20 deletions(-) create mode 100644 main/manager-api/src/main/resources/db/changelog/202602271137.sql diff --git a/main/manager-api/src/main/resources/db/changelog/202602271137.sql b/main/manager-api/src/main/resources/db/changelog/202602271137.sql new file mode 100644 index 00000000..010acd8f --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202602271137.sql @@ -0,0 +1,42 @@ +-- 更新腾讯TTS供应器配置,增加speed、volume和format参数 +UPDATE `ai_model_provider` +SET fields = '[{"key":"appid","label":"应用ID","type":"string"},{"key":"secret_id","label":"Secret ID","type":"string"},{"key":"secret_key","label":"Secret Key","type":"string"},{"key":"format","label":"音频格式","type":"string"},{"key":"speed","label":"语速","type":"number"},{"key":"volume","label":"音量","type":"number"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"voice","label":"音色ID","type":"string"},{"key":"region","label":"区域","type":"string"}]' +WHERE id = 'SYSTEM_TTS_TencentTTS'; + +-- 更新腾讯TTS模型配置,增加speed和volume参数,补充参数说明 +UPDATE `ai_model_config` SET + `config_json` = JSON_SET(`config_json`, '$.speed', 0, '$.volume', 0), + `remark` = '腾讯TTS配置说明: +1. 需要在腾讯云平台开通智能语音交互服务 +2. 支持多种音色,当前配置使用101001 +3. 需要网络连接 +4. 输出文件保存在tmp/目录 +申请步骤: +1. 访问 https://console.cloud.tencent.com/cam/capi 获取密钥 +2. 访问 https://console.cloud.tencent.com/tts/resourcebundle 领取免费资源 +3. 创建新应用 +4. 获取appid、secret_id和secret_key +5. 填入配置文件中 +音频参数: +- format: 音频格式,支持pcm、wav、mp3 +- speed: 语速,范围-2~6,默认0 +- volume: 音量,范围-10~10,默认0' +WHERE `id` = 'TTS_TencentTTS'; + +-- 更新CozeCnTTS供应器配置,增加speed和loudness_rate参数 +UPDATE `ai_model_provider` +SET fields = '[{"key":"voice","label":"音色","type":"string"},{"key":"access_token","label":"访问令牌","type":"string"},{"key":"speed","label":"语速","type":"number"},{"key":"loudness_rate","label":"音量增益","type":"number"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"response_format","label":"响应格式","type":"string"}]' +WHERE id = 'SYSTEM_TTS_cozecn'; + +-- 更新CozeCnTTS模型配置,增加speed和loudness_rate参数,补充参数说明 +UPDATE `ai_model_config` SET + `config_json` = JSON_SET(`config_json`, '$.speed', 1, '$.loudness_rate', 0), + `remark` = 'Coze中文语音合成配置说明: +1. 访问 https://www.coze.cn/ 注册并登录 +2. 创建应用并获取access_token +3. 选择合适的音色ID +音频参数: +- response_format: 音频格式,支持pcm、wav、mp3 +- speed: 语速,范围0.5~2,默认1 +- loudness_rate: 音量增益,范围-50~100,默认0' +WHERE `id` = 'TTS_CozeCnTTS'; 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 e2ce2412..606cefa7 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 @@ -536,3 +536,10 @@ databaseChangeLog: - sqlFile: encoding: utf8 path: classpath:db/changelog/202602061650.sql + - changeSet: + id: 202602271137 + author: RanChen + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202602271137.sql diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 81c5c0b1..c4662afa 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -813,6 +813,9 @@ TTS: access_token: 你的coze web key response_format: wav # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 + # 以下可不用设置,使用默认设置 + # speed: 1 # 语速:-0.5到2 + # loudness_rate: 0 # 音量:-50到100 VolcesAiGatewayTTS: type: openai # 火山引擎 - 边缘大模型网关 @@ -984,7 +987,10 @@ TTS: region: ap-guangzhou voice: 101001 # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 - + # 以下可不用设置,使用默认设置 + # format: wav # 音频格式:pcm、wav、mp3 + # volume: 0 # 音量:-10到10 + # speech_rate: 0 # 语速:-2到6 TTS302AI: # 302AI语音合成服务,需要先在302平台创建账户充值,并获取密钥信息 # 添加 302.ai TTS 配置 diff --git a/main/xiaozhi-server/core/providers/tts/aliyun.py b/main/xiaozhi-server/core/providers/tts/aliyun.py index 05ea2031..2dc39a9d 100644 --- a/main/xiaozhi-server/core/providers/tts/aliyun.py +++ b/main/xiaozhi-server/core/providers/tts/aliyun.py @@ -100,7 +100,6 @@ class TTSProvider(TTSProviderBase): self.access_key_secret = config.get("access_key_secret") self.appkey = config.get("appkey") - self.format = config.get("format", "wav") self.audio_file_type = config.get("format", "wav") if config.get("private_voice"): @@ -179,7 +178,7 @@ class TTSProvider(TTSProviderBase): "appkey": self.appkey, "token": self.token, "text": text, - "format": self.format, + "format": self.audio_file_type, "sample_rate": self.conn.sample_rate, "voice": self.voice, "volume": self.volume, diff --git a/main/xiaozhi-server/core/providers/tts/aliyun_stream.py b/main/xiaozhi-server/core/providers/tts/aliyun_stream.py index 8fd20084..0e17a229 100644 --- a/main/xiaozhi-server/core/providers/tts/aliyun_stream.py +++ b/main/xiaozhi-server/core/providers/tts/aliyun_stream.py @@ -103,7 +103,6 @@ class TTSProvider(TTSProviderBase): self.access_key_secret = config.get("access_key_secret") self.appkey = config.get("appkey") self.format = config.get("format", "pcm") - self.audio_file_type = config.get("format", "pcm") # 音色配置 - CosyVoice大模型音色 if config.get("private_voice"): diff --git a/main/xiaozhi-server/core/providers/tts/cozecn.py b/main/xiaozhi-server/core/providers/tts/cozecn.py index 6c5d0547..1d727a9d 100644 --- a/main/xiaozhi-server/core/providers/tts/cozecn.py +++ b/main/xiaozhi-server/core/providers/tts/cozecn.py @@ -3,6 +3,11 @@ from core.providers.tts.base import TTSProviderBase class TTSProvider(TTSProviderBase): + TTS_PARAM_CONFIG = [ + ("speed", "speed", 0.5, 2, 1, lambda v: round(float(v), 1)), + ("loudness_rate", "loudness_rate", -50, 100, 0, int), + ] + def __init__(self, config, delete_audio_file): super().__init__(config, delete_audio_file) self.model = config.get("model") @@ -11,17 +16,29 @@ class TTSProvider(TTSProviderBase): self.voice = config.get("private_voice") else: self.voice = config.get("voice") - self.response_format = config.get("response_format", "wav") self.audio_file_type = config.get("response_format", "wav") self.host = "api.coze.cn" self.api_url = f"https://{self.host}/v1/audio/speech" + # 音频参数配置 + speed = config.get("speed", "0") + self.speed = int(speed) if speed else 0 + + loudness_rate = config.get("loudness_rate", "0") + self.loudness_rate = int(loudness_rate) if loudness_rate else 0 + + # 应用百分比调整(如果存在),否则使用公有化配置 + self._apply_percentage_params(config) + async def text_to_speak(self, text, output_file): request_json = { "model": self.model, "input": text, "voice_id": self.voice, - "response_format": self.response_format, + "response_format": self.audio_file_type, + "sample_rate": self.conn.sample_rate, + "speed": self.speed, + "loudness_rate": self.loudness_rate, } headers = { "Authorization": f"Bearer {self.access_token}", diff --git a/main/xiaozhi-server/core/providers/tts/custom.py b/main/xiaozhi-server/core/providers/tts/custom.py index b417825e..5e6a43ac 100644 --- a/main/xiaozhi-server/core/providers/tts/custom.py +++ b/main/xiaozhi-server/core/providers/tts/custom.py @@ -15,7 +15,6 @@ class TTSProvider(TTSProviderBase): self.url = config.get("url") self.method = config.get("method", "GET") self.headers = config.get("headers", {}) - self.format = config.get("format", "wav") self.audio_file_type = config.get("format", "wav") self.output_file = config.get("output_dir", "tmp/") self.params = config.get("params") @@ -29,7 +28,7 @@ class TTSProvider(TTSProviderBase): raise TypeError("Custom TTS配置参数出错, 请参考配置说明") def generate_filename(self): - return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}.{self.format}") + return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}.{self.audio_file_type}") async def text_to_speak(self, text, output_file): request_params = {} diff --git a/main/xiaozhi-server/core/providers/tts/fishspeech.py b/main/xiaozhi-server/core/providers/tts/fishspeech.py index 83b3bc3f..163ba2c1 100644 --- a/main/xiaozhi-server/core/providers/tts/fishspeech.py +++ b/main/xiaozhi-server/core/providers/tts/fishspeech.py @@ -91,7 +91,6 @@ class TTSProvider(TTSProviderBase): self.reference_text = parse_string_to_list( config.get('ref_text')if config.get('ref_text') else config.get("reference_text") ) - self.format = config.get("response_format", "wav") self.audio_file_type = config.get("response_format", "wav") self.api_key = config.get("api_key", "YOUR_API_KEY") model_key_msg = check_model_key("FishSpeech TTS", self.api_key) @@ -148,7 +147,7 @@ class TTSProvider(TTSProviderBase): ], "reference_id": self.reference_id, "normalize": self.normalize, - "format": self.format, + "format": self.audio_file_type, "max_new_tokens": self.max_new_tokens, "chunk_length": self.chunk_length, "top_p": self.top_p, diff --git a/main/xiaozhi-server/core/providers/tts/openai.py b/main/xiaozhi-server/core/providers/tts/openai.py index 20bf9a96..40a0134e 100644 --- a/main/xiaozhi-server/core/providers/tts/openai.py +++ b/main/xiaozhi-server/core/providers/tts/openai.py @@ -8,6 +8,10 @@ logger = setup_logging() class TTSProvider(TTSProviderBase): + TTS_PARAM_CONFIG = [ + ("ttsRate", "speed", 0.25, 4, 1, lambda v: round(float(v), 2)), + ] + def __init__(self, config, delete_audio_file): super().__init__(config, delete_audio_file) self.api_key = config.get("api_key") @@ -17,13 +21,15 @@ class TTSProvider(TTSProviderBase): self.voice = config.get("private_voice") else: self.voice = config.get("voice", "alloy") - self.response_format = config.get("format", "wav") self.audio_file_type = config.get("format", "wav") # 处理空字符串的情况 speed = config.get("speed", "1.0") self.speed = float(speed) if speed else 1.0 + # 应用百分比调整(如果存在),否则使用公有化配置 + self._apply_percentage_params(config) + self.output_file = config.get("output_dir", "tmp/") model_key_msg = check_model_key("TTS", self.api_key) if model_key_msg: @@ -38,7 +44,7 @@ class TTSProvider(TTSProviderBase): "model": self.model, "input": text, "voice": self.voice, - "response_format": "wav", + "response_format": self.audio_file_type, "speed": self.speed, } response = requests.post(self.api_url, json=data, headers=headers) diff --git a/main/xiaozhi-server/core/providers/tts/siliconflow.py b/main/xiaozhi-server/core/providers/tts/siliconflow.py index 48b709d6..b5c618ec 100644 --- a/main/xiaozhi-server/core/providers/tts/siliconflow.py +++ b/main/xiaozhi-server/core/providers/tts/siliconflow.py @@ -5,7 +5,7 @@ from core.providers.tts.base import TTSProviderBase class TTSProvider(TTSProviderBase): TTS_PARAM_CONFIG = [ ("ttsVolume", "gain", -10, 10, 0, int), - ("ttsRate", "speed", 0.25, 4, 1, lambda v: round(float(v), 1)), + ("ttsRate", "speed", 0.25, 4, 1, lambda v: round(float(v), 2)), ] def __init__(self, config, delete_audio_file): @@ -16,7 +16,6 @@ class TTSProvider(TTSProviderBase): self.voice = config.get("private_voice") else: self.voice = config.get("voice") - self.response_format = config.get("response_format", "mp3") self.audio_file_type = config.get("response_format", "mp3") self.speed = float(config.get("speed", 1.0)) self.gain = config.get("gain") @@ -32,7 +31,7 @@ class TTSProvider(TTSProviderBase): "model": self.model, "input": text, "voice": self.voice, - "response_format": self.response_format, + "response_format": self.audio_file_type, } headers = { "Authorization": f"Bearer {self.access_token}", diff --git a/main/xiaozhi-server/core/providers/tts/tencent.py b/main/xiaozhi-server/core/providers/tts/tencent.py index a2493632..50964d33 100644 --- a/main/xiaozhi-server/core/providers/tts/tencent.py +++ b/main/xiaozhi-server/core/providers/tts/tencent.py @@ -10,6 +10,11 @@ from core.providers.tts.base import TTSProviderBase class TTSProvider(TTSProviderBase): + TTS_PARAM_CONFIG = [ + ("ttsVolume", "volume", -10, 10, 0, lambda v: round(float(v), 1)), + ("ttsRate", "speed", -2, 6, 0, lambda v: round(float(v), 2)), + ] + def __init__(self, config, delete_audio_file): super().__init__(config, delete_audio_file) self.appid = config.get("appid") @@ -24,6 +29,16 @@ class TTSProvider(TTSProviderBase): self.output_file = config.get("output_dir") self.audio_file_type = config.get("format", "wav") + # 音频参数配置 + speed = config.get("speed", "0") + self.speed = int(speed) if speed else 0 + + volume = config.get("volume", "0") + self.volume = int(volume) if volume else 0 + + # 应用百分比调整(如果存在),否则使用公有化配置 + self._apply_percentage_params(config) + def _get_auth_headers(self, request_body): """生成鉴权请求头""" # 获取当前UTC时间戳 @@ -127,6 +142,10 @@ class TTSProvider(TTSProviderBase): "Text": text, # 合成语音的源文本 "SessionId": str(uuid.uuid4()), # 会话ID,随机生成 "VoiceType": int(self.voice), # 音色 + "Codec": self.audio_file_type, # 音频编码格式 + "Volume": self.volume, # 音量 + "Speed": self.speed, # 语速 + "SampleRate": self.conn.sample_rate, # 采样率部分支持24000 } try: diff --git a/main/xiaozhi-server/core/providers/tts/ttson.py b/main/xiaozhi-server/core/providers/tts/ttson.py index 0a9f8261..2f54a5c5 100644 --- a/main/xiaozhi-server/core/providers/tts/ttson.py +++ b/main/xiaozhi-server/core/providers/tts/ttson.py @@ -35,7 +35,6 @@ class TTSProvider(TTSProviderBase): self.stream = str(config.get("stream", False)).lower() in ("true", "1", "yes") self.output_file = config.get("output_dir") self.pitch_factor = int(config.get("pitch_factor", 0)) - self.format = config.get("format", "mp3") self.audio_file_type = config.get("format", "mp3") self.emotion = int(config.get("emotion", 1)) self.header = {"Content-Type": "application/json"} @@ -57,7 +56,7 @@ class TTSProvider(TTSProviderBase): "to_lang": self.to_lang, "text": text, "emotion": self.emotion, - "format": self.format, + "format": self.audio_file_type, "volume_change_dB": self.volume_change_dB, "voice_id": self.voice, "pitch_factor": self.pitch_factor, diff --git a/main/xiaozhi-server/core/utils/tts.py b/main/xiaozhi-server/core/utils/tts.py index a0ac3c6b..df3e90b4 100644 --- a/main/xiaozhi-server/core/utils/tts.py +++ b/main/xiaozhi-server/core/utils/tts.py @@ -156,10 +156,9 @@ def convert_percentage_to_range(percentage, min_val, max_val, base_val=None): Returns: 转换后的值 """ - if base_val is None: - base_val = (min_val + max_val) / 2 + percentage, min_val, max_val = float(percentage), float(min_val), float(max_val) + base_val = float(base_val) if base_val is not None else (min_val + max_val) / 2 - # 百分比 -100 对应 min_val, 0 对应 base_val, 100 对应 max_val if percentage < 0: # 负百分比:从 base_val 向 min_val 线性插值 result = base_val + (base_val - min_val) * (percentage / 100)