update: 增加智能体独立音频设置

This commit is contained in:
Sakura-RanChen
2026-02-27 11:17:08 +08:00
parent 4ea3bea85c
commit 7b020ba55c
26 changed files with 638 additions and 29 deletions
@@ -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;
@@ -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;
@@ -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;
/**
* 记忆模型标识
*/
@@ -285,6 +285,15 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> 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());
}
@@ -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<String, Object>) model.getConfigJson()).put("ref_text", referenceText);
if (language != null)
((Map<String, Object>) model.getConfigJson()).put("language", language);
if (ttsVolume != null)
((Map<String, Object>) model.getConfigJson()).put("ttsVolume", ttsVolume);
if (ttsRate != null)
((Map<String, Object>) model.getConfigJson()).put("ttsRate", ttsRate);
if (ttsPitch != null)
((Map<String, Object>) model.getConfigJson()).put("ttsPitch", ttsPitch);
// 火山引擎声音克隆需要替换resource_id
Map<String, Object> map = (Map<String, Object>) model.getConfigJson();
@@ -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;
@@ -17,6 +17,9 @@
<result column="ttsModelId" property="ttsModelId"/>
<result column="ttsVoiceId" property="ttsVoiceId"/>
<result column="ttsLanguage" property="ttsLanguage"/>
<result column="ttsVolume" property="ttsVolume"/>
<result column="ttsRate" property="ttsRate"/>
<result column="ttsPitch" property="ttsPitch"/>
<result column="memModelId" property="memModelId"/>
<result column="intentModelId" property="intentModelId"/>
@@ -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(
@@ -0,0 +1,240 @@
<template>
<el-drawer
:visible.sync="drawerVisible"
:before-close="handleClose"
direction="rtl"
size="400px"
:modal="true"
:show-close="false"
custom-class="tts-advanced-drawer"
>
<div class="drawer-header" slot="title">
<span class="drawer-title">{{ $t('roleConfig.advancedSettings') }}</span>
<button class="drawer-close-btn" @click="handleClose">×</button>
</div>
<div class="drawer-content">
<el-form label-position="top">
<!-- 音量 -->
<el-form-item :label="$t('roleConfig.ttsVolume')">
<div class="slider-container">
<el-slider
v-model="localSettings.volume"
:min="-100"
:max="100"
:step="1"
:format-tooltip="formatTooltip"
class="tts-slider"
/>
<span class="slider-hint">{{ $t('roleConfig.volumeHint') }}</span>
</div>
</el-form-item>
<!-- 语速 -->
<el-form-item :label="$t('roleConfig.ttsRate')">
<div class="slider-container">
<el-slider
v-model="localSettings.speed"
:min="-100"
:max="100"
:step="1"
:format-tooltip="formatTooltip"
class="tts-slider"
/>
<span class="slider-hint">{{ $t('roleConfig.speedHint') }}</span>
</div>
</el-form-item>
<!-- 音调 -->
<el-form-item :label="$t('roleConfig.ttsPitch')">
<div class="slider-container">
<el-slider
v-model="localSettings.pitch"
:min="-100"
:max="100"
:step="1"
:format-tooltip="formatTooltip"
class="tts-slider"
/>
<span class="slider-hint">{{ $t('roleConfig.pitchHint') }}</span>
</div>
</el-form-item>
</el-form>
</div>
<div class="drawer-footer">
<el-button @click="handleCancel">{{ $t('button.cancel') }}</el-button>
<el-button type="primary" @click="handleSave">{{ $t('button.save') }}</el-button>
</div>
</el-drawer>
</template>
<script>
export default {
name: 'TtsAdvancedSettings',
props: {
visible: {
type: Boolean,
default: false
},
settings: {
type: Object,
default: () => ({
volume: 0,
speed: 0,
pitch: 0
})
}
},
data() {
return {
localSettings: {
volume: 0,
speed: 0,
pitch: 0
}
};
},
computed: {
drawerVisible: {
get() {
return this.visible;
},
set(val) {
this.$emit('update:visible', val);
}
}
},
watch: {
visible(newVal) {
if (newVal) {
// 当抽屉打开时,复制当前设置到本地
this.localSettings = { ...this.settings };
}
}
},
methods: {
handleClose() {
this.$emit('update:visible', false);
},
handleCancel() {
// 取消时不保存,直接关闭
this.handleClose();
},
handleSave() {
// 保存设置并关闭
this.$emit('save', { ...this.localSettings });
this.handleClose();
},
formatTooltip(val) {
return `${val}%`;
}
}
};
</script>
<style scoped>
.drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
border-bottom: 1px solid #e8f0ff;
}
.drawer-title {
font-size: 18px;
font-weight: 600;
color: #3d4566;
}
.drawer-close-btn {
width: 32px;
height: 32px;
border-radius: 50%;
border: 2px solid #cfcfcf;
background: none;
font-size: 28px;
font-weight: lighter;
color: #cfcfcf;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
outline: none;
transition: all 0.3s;
}
.drawer-close-btn:hover {
color: #409eff;
border-color: #409eff;
}
.drawer-content {
padding: 24px;
flex: 1;
overflow-y: auto;
}
.slider-container {
width: 100%;
}
.slider-hint {
display: block;
font-size: 12px;
color: #909399;
margin-top: 8px;
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;
}
.drawer-footer {
padding: 16px 24px;
border-top: 1px solid #e8f0ff;
display: flex;
justify-content: center;
gap: 12px;
}
.drawer-footer .el-button {
min-width: 80px;
}
::v-deep .el-form-item__label {
font-size: 14px !important;
color: #3d4566 !important;
font-weight: 500;
padding-bottom: 8px;
}
::v-deep .el-form-item {
margin-bottom: 24px;
}
</style>
<style>
.tts-advanced-drawer .el-drawer__header {
margin-bottom: 0;
padding: 0;
}
.tts-advanced-drawer .el-drawer__body {
display: flex;
flex-direction: column;
padding: 0;
}
</style>
+8
View File
@@ -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',
+8
View File
@@ -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',
+8
View File
@@ -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',
+8
View File
@@ -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': '请输入交互语种,如:中文',
+8
View File
@@ -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': '請輸入交互語種,如:繁體中文',
+97 -4
View File
@@ -234,12 +234,12 @@
</el-form-item>
<div class="model-row">
<!-- 语言筛选器 -->
<el-form-item :label="$t('roleConfig.language')" class="model-item">
<el-form-item :label="$t('roleConfig.language')" class="model-item language-select-item">
<div class="model-select-wrapper">
<el-select
v-model="selectedLanguage"
:placeholder="$t('roleConfig.selectLanguage')"
class="form-select"
class="form-select language-select"
@change="filterVoicesByLanguage"
>
<el-option
@@ -251,7 +251,7 @@
</el-select>
</div>
</el-form-item>
<!-- 音色选择器 -->
<el-form-item :label="$t('roleConfig.voiceType')" class="model-item">
<div class="model-select-wrapper">
@@ -294,6 +294,13 @@
</div>
</el-option>
</el-select>
<el-button
class="edit-function-btn"
style="margin-left: 10px;"
@click="openTtsAdvancedSettings"
>
{{ $t('roleConfig.advancedSettings') }}
</el-button>
</div>
</el-form-item>
</div>
@@ -318,6 +325,11 @@
:providers="currentContextProviders"
@confirm="handleUpdateContext"
/>
<tts-advanced-settings
:visible.sync="showTtsAdvancedDialog"
:settings="ttsSettings"
@save="handleTtsSettingsSave"
/>
</div>
</template>
@@ -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;
}
</style>
@@ -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", // 可选
@@ -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"}
@@ -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协议
@@ -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)
@@ -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}"}
@@ -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}"}
@@ -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 = {
@@ -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")
@@ -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"
@@ -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,
@@ -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")
+28 -1
View File
@@ -141,4 +141,31 @@ class MarkdownCleaner:
# 去除emoji表情
text = check_emoji(text)
return text.strip()
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))