Merge pull request #2972 from xinnan-tech/update-tts-voice-data

Update tts voice data
This commit is contained in:
wengzh
2026-03-02 16:50:33 +08:00
committed by GitHub
42 changed files with 981 additions and 101 deletions
+3
View File
@@ -3,6 +3,9 @@ __pycache__/
.idea/
*.py[cod]
*$py.class
.vscode
.claude
AGENTS.md
# C extensions
*.so
@@ -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;
@@ -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;
@@ -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;
/**
* 记忆模型标识
*/
@@ -309,6 +309,18 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> 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());
}
@@ -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<String, Object>) model.getConfigJson()).put("ref_audio", referenceAudio);
if (referenceText != null)
((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();
@@ -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;
}
@@ -129,6 +129,7 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
.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<TimbreDao, TimbreEntity>
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);
@@ -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;
@@ -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';
@@ -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:
@@ -16,6 +16,10 @@
<result column="llmModelId" property="llmModelId"/>
<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"/>
@@ -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(
@@ -879,8 +879,8 @@ export default {
}
.equipment-management {
width: 79px;
font-size: 9px;
min-width: 80px;
font-size: 10px;
}
}
@@ -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>
+9
View File
@@ -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',
+9
View File
@@ -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',
+9
View File
@@ -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',
+9
View File
@@ -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': '请输入交互语种,如:中文',
+9
View File
@@ -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': '請輸入交互語種,如:繁體中文',
+224 -52
View File
@@ -256,47 +256,78 @@
</div>
</div>
</el-form-item>
<el-form-item :label="$t('roleConfig.voiceType')">
<el-select
v-model="form.ttsVoiceId"
filterable
:placeholder="$t('roleConfig.pleaseSelect')"
class="form-select"
>
<el-option
v-for="(item, index) in voiceOptions"
:key="`voice-${index}`"
:label="item.label"
:value="item.value"
>
<div
style="
display: flex;
justify-content: space-between;
align-items: center;
"
<div class="model-row">
<!-- 语言筛选器 -->
<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 language-select"
@change="filterVoicesByLanguage"
>
<span>{{ item.label }}</span>
<template v-if="hasAudioPreview(item)">
<el-button
type="text"
:icon="
playingVoice &&
currentPlayingVoiceId === item.value &&
!isPaused
? 'el-icon-video-pause'
: 'el-icon-video-play'
<el-option
v-for="(lang, index) in languageOptions"
:key="`lang-${index}`"
:label="lang.label"
:value="lang.value"
/>
</el-select>
</div>
</el-form-item>
<!-- 音色选择器 -->
<el-form-item :label="$t('roleConfig.voiceType')" class="model-item">
<div class="model-select-wrapper">
<el-select
v-model="form.ttsVoiceId"
filterable
:placeholder="$t('roleConfig.pleaseSelect')"
class="form-select"
>
<el-option
v-for="(item, index) in voiceOptions"
:key="`voice-${index}`"
:label="item.label"
:value="item.value"
>
<div
style="
display: flex;
justify-content: space-between;
align-items: center;
"
size="small"
@click.stop="toggleAudioPlayback(item.value)"
:loading="false"
class="play-button"
/>
</template>
</div>
</el-option>
</el-select>
</el-form-item>
>
<span>{{ item.label }}</span>
<template v-if="hasAudioPreview(item)">
<el-button
type="text"
:icon="
playingVoice &&
currentPlayingVoiceId === item.value &&
!isPaused
? 'el-icon-video-pause'
: 'el-icon-video-play'
"
size="small"
@click.stop="toggleAudioPlayback(item.value)"
:loading="false"
class="play-button"
/>
</template>
</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>
</div>
</div>
</div>
@@ -318,6 +349,11 @@
:providers="currentContextProviders"
@confirm="handleUpdateContext"
/>
<tts-advanced-settings
:visible.sync="showTtsAdvancedDialog"
:settings="ttsSettings"
@save="handleTtsSettingsSave"
/>
</div>
</template>
@@ -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) {
@@ -2,6 +2,10 @@
{{base_prompt}}
</identity>
<language>
【语言规范】你必须使用{{language}}进行回复和交流。无论用户使用何种语言提问,你都应该用{{language}}来回答。
</language>
<emotion>
【核心目标】你不是冰冷的机器!请敏锐感知用户情绪,作为懂人心的伙伴,用有温度的回应照亮对话。
- **情感融入:**
+31 -2
View File
@@ -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_idaccess_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: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
-1
View File
@@ -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()
@@ -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)
@@ -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", // 可选
@@ -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,
@@ -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协议
@@ -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)
@@ -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}",
@@ -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 = {}
@@ -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}"}
@@ -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,
@@ -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 = {
@@ -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)
@@ -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), 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}",
@@ -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:
@@ -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,
@@ -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")
@@ -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,
)
+27 -1
View File
@@ -141,4 +141,30 @@ 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:
转换后的值
"""
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))