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))