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/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java index ebb29fbd..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; @@ -41,6 +42,18 @@ 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 = "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 d92d49b8..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; @@ -45,6 +46,18 @@ public class AgentEntity { @Schema(description = "音色标识") private String ttsVoiceId; + @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 a7704e5b..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; @@ -64,6 +65,26 @@ public class AgentTemplateEntity implements Serializable { */ private String ttsVoiceId; + /** + * 音色语言 + */ + 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 3624a8f9..22029ec5 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 @@ -309,6 +309,18 @@ public class AgentServiceImpl extends BaseServiceImpl imp if (dto.getTtsVoiceId() != null) { existingEntity.setTtsVoiceId(dto.getTtsVoiceId()); } + 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 49885614..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 @@ -87,6 +87,10 @@ public class ConfigServiceImpl implements ConfigService { null, null, null, + null, + null, + null, + null, agent.getVadModelId(), agent.getAsrModelId(), null, @@ -135,15 +139,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 +221,10 @@ public class ConfigServiceImpl implements ConfigService { voice, referenceAudio, referenceText, + language, + agent.getTtsVolume(), + agent.getTtsRate(), + agent.getTtsPitch(), agent.getVadModelId(), agent.getAsrModelId(), agent.getLlmModelId(), @@ -385,6 +402,10 @@ public class ConfigServiceImpl implements ConfigService { String voice, String referenceAudio, String referenceText, + String language, + Integer ttsVolume, + Integer ttsRate, + Integer ttsPitch, String vadModelId, String asrModelId, String llmModelId, @@ -423,6 +444,14 @@ 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); + 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/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 new file mode 100644 index 00000000..b60f489d --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202602061650.sql @@ -0,0 +1,48 @@ +-- 统一规范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 IN ('粤语', '中文-广东口音') THEN '粤语' + WHEN languages = '中文(粤语)及中英文混合' THEN '粤语、英语' + WHEN languages = '粤语及粤英混合' THEN '粤语、英语' + ELSE languages +END; + +-- 添加音色语言、音量、语速、音调字段到 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; + +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/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 9e4811d5..2555a05a 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 @@ -530,6 +530,19 @@ databaseChangeLog: encoding: utf8 path: classpath:db/changelog/202602051125.sql - changeSet: + id: 202602061650 + author: DaGou12138 + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202602061650.sql + - changeSet: + id: 202602271137 + author: RanChen + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202602271137.sql id: 202602281000 author: rainv123 changes: 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..9c417909 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,10 @@ + + + + @@ -45,6 +49,10 @@ a.vllm_model_id AS vllmModelId, 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/HeaderBar.vue b/main/manager-web/src/components/HeaderBar.vue index 025beae5..24f55ab9 100644 --- a/main/manager-web/src/components/HeaderBar.vue +++ b/main/manager-web/src/components/HeaderBar.vue @@ -879,8 +879,8 @@ export default { } .equipment-management { - width: 79px; - font-size: 9px; + min-width: 80px; + font-size: 10px; } } 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 ad3a81db..5a14c4ae 100644 --- a/main/manager-web/src/i18n/de.js +++ b/main/manager-web/src/i18n/de.js @@ -764,7 +764,16 @@ export default { 'roleConfig.memoryHis': 'Speicher', 'roleConfig.memory': 'Speicher', '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 7326cf30..aa77f85f 100644 --- a/main/manager-web/src/i18n/en.js +++ b/main/manager-web/src/i18n/en.js @@ -764,7 +764,16 @@ export default { 'roleConfig.memoryHis': 'Memory', 'roleConfig.memory': 'Memory Model', '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 a7ed9f6d..39cffeb3 100644 --- a/main/manager-web/src/i18n/vi.js +++ b/main/manager-web/src/i18n/vi.js @@ -764,7 +764,16 @@ 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.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 dee8820c..d38c6bce 100644 --- a/main/manager-web/src/i18n/zh_CN.js +++ b/main/manager-web/src/i18n/zh_CN.js @@ -764,7 +764,16 @@ export default { 'roleConfig.memoryHis': '记忆', 'roleConfig.memory': '记忆模式', '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 7c517421..9d15052d 100644 --- a/main/manager-web/src/i18n/zh_TW.js +++ b/main/manager-web/src/i18n/zh_TW.js @@ -764,7 +764,16 @@ export default { 'roleConfig.memoryHis': '記憶', 'roleConfig.memory': '記憶模式', '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 470b7c66..92971737 100644 --- a/main/manager-web/src/views/roleConfig.vue +++ b/main/manager-web/src/views/roleConfig.vue @@ -256,47 +256,78 @@ - - - -
+ + +
+ - {{ item.label }} - -
- - -
+ > + {{ item.label }} + +
+
+
+ + {{ $t('roleConfig.advancedSettings') }} + + +
+ @@ -318,6 +349,11 @@ :providers="currentContextProviders" @confirm="handleUpdateContext" /> + @@ -327,20 +363,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: "", @@ -381,6 +427,9 @@ export default { isPaused: false, currentAudio: null, currentPlayingVoiceId: null, + // 语言筛选相关状态 + languageOptions: [], // 语言选项列表 + selectedLanguage: '', // 当前选中的语言 // 功能状态 featureStatus: { vad: false, // 语言检测活动功能状态 @@ -412,6 +461,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, @@ -428,6 +478,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({ @@ -543,6 +604,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 || []; @@ -628,32 +697,91 @@ 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 : ''; + } + + // 同步到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) { if (!name || name.length === 0) return ""; @@ -719,6 +847,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; }, @@ -1354,6 +1492,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; @@ -1529,6 +1676,31 @@ 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; .input-new-tag { width: 90px; &::v-deep(.el-input__inner) { 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..c4662afa 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,10 @@ TTS: output_dir: tmp/ access_token: 你的coze web key response_format: wav + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 + # 以下可不用设置,使用默认设置 + # speed: 1 # 语速:-0.5到2 + # loudness_rate: 0 # 音量:-50到100 VolcesAiGatewayTTS: type: openai # 火山引擎 - 边缘大模型网关 @@ -822,6 +830,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 +852,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 +878,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 +898,7 @@ TTS: inp_refs: [] sample_steps: 32 if_sr: false + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 MinimaxTTSHTTPStream: # Minimax流式语音合成服务 type: minimax_httpstream @@ -918,6 +930,7 @@ TTS: # voice_id: female-shaonv # weight: 1 # language_boost: auto + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 AliyunTTS: # 阿里云智能语音交互服务,需要先在阿里云平台开通服务,然后获取验证信息 # 平台地址:https://nls-portal.console.aliyun.com/ @@ -937,6 +950,7 @@ TTS: # volume: 50 # speech_rate: 0 # pitch_rate: 0 + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 AliyunStreamTTS: # 阿里云CosyVoice大模型流式文本语音合成 # 采用FlowingSpeechSynthesizer接口,支持更低延迟和更自然的语音质量 @@ -950,7 +964,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 +974,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,7 +986,11 @@ TTS: secret_key: 你的腾讯云SecretKey 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 配置 @@ -985,6 +1004,7 @@ TTS: voice: "zh_female_wanwanxiaohe_moon_bigtts" output_dir: tmp/ access_token: "你的302API密钥" + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 GizwitsTTS: type: doubao # 火山引擎作为基座,可以完全使用企业级火山引擎语音合成服务 @@ -996,6 +1016,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 +1034,7 @@ TTS: format: mp3 output_dir: tmp/ emotion: 1 + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 OpenAITTS: # openai官方文本转语音服务,可支持全球大多数语种 type: openai @@ -1028,6 +1050,7 @@ TTS: # 语速范围0.25-4.0 speed: 1 output_dir: tmp/ + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 CustomTTS: # 自定义的TTS接口服务,请求参数可自定义,可接入众多TTS服务 # 以本地部署的KokoroTTS为例 @@ -1050,6 +1073,7 @@ TTS: # Authorization: Bearer xxxx format: mp3 # 接口返回的音频格式 output_dir: tmp/ + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 LinkeraiTTS: type: linkerai api_url: https://tts.linkerai.cn/tts @@ -1061,6 +1085,7 @@ TTS: access_token: "U4YdYXVfpwWnk2t5Gp822zWPCuORyeJL" voice: "OUeAo1mhq6IBExi" output_dir: tmp/ + # language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文 PaddleSpeechTTS: #百度飞浆 PaddleSpeech 支持本地离线部署 支持模型训练 #框架地址 https://www.paddlepaddle.org.cn/ @@ -1074,6 +1099,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 +1109,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 +1124,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 +1146,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 1d5d89c8..20b55a91 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/providers/tts/alibl_stream.py b/main/xiaozhi-server/core/providers/tts/alibl_stream.py index 8974124c..8eb0f708 100644 --- a/main/xiaozhi-server/core/providers/tts/alibl_stream.py +++ b/main/xiaozhi-server/core/providers/tts/alibl_stream.py @@ -6,10 +6,10 @@ import queue import asyncio import traceback import websockets -from typing import Callable, Any + from asyncio import Task +from typing import Callable, Any 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 @@ -19,6 +19,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) @@ -52,6 +58,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..2dc39a9d 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) @@ -93,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"): @@ -110,6 +116,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"} @@ -169,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 0a09c65e..cf06e7ff 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,17 +8,17 @@ import time import queue import asyncio import traceback -from typing import Callable, Any -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 typing import Callable, Any +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() @@ -87,6 +87,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) @@ -98,7 +104,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"): @@ -116,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/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/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/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/huoshan_double_stream.py b/main/xiaozhi-server/core/providers/tts/huoshan_double_stream.py index eefee383..152aa33f 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/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/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..b5c618ec 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), 2)), + ] + def __init__(self, config, delete_audio_file): super().__init__(config, delete_audio_file) self.model = config.get("model") @@ -11,11 +16,13 @@ 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") + # 应用百分比调整(如果存在),否则使用公有化配置 + self._apply_percentage_params(config) + self.host = "api.siliconflow.cn" self.api_url = f"https://{self.host}/v1/audio/speech" @@ -24,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 91deeed9..2f54a5c5 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( @@ -29,11 +35,13 @@ 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"} + # 应用百分比调整(如果存在),否则使用公有化配置 + self._apply_percentage_params(config) + def generate_filename(self, extension=".mp3"): return os.path.join( self.output_file, @@ -48,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/providers/tts/xunfei_stream.py b/main/xiaozhi-server/core/providers/tts/xunfei_stream.py index e08c5617..be53284e 100644 --- a/main/xiaozhi-server/core/providers/tts/xunfei_stream.py +++ b/main/xiaozhi-server/core/providers/tts/xunfei_stream.py @@ -9,10 +9,10 @@ import hashlib import asyncio import traceback import websockets -from typing import Callable, Any + from asyncio import Task +from typing import Callable, Any 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 @@ -60,6 +60,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) @@ -89,6 +95,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/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, ) diff --git a/main/xiaozhi-server/core/utils/tts.py b/main/xiaozhi-server/core/utils/tts.py index 6727f6de..df3e90b4 100644 --- a/main/xiaozhi-server/core/utils/tts.py +++ b/main/xiaozhi-server/core/utils/tts.py @@ -141,4 +141,30 @@ 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: + 转换后的值 + """ + 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 + + 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))