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/ .idea/
*.py[cod] *.py[cod]
*$py.class *$py.class
.vscode
.claude
AGENTS.md
# C extensions # C extensions
*.so *.so
@@ -1,6 +1,7 @@
package xiaozhi.modules.agent.dto; package xiaozhi.modules.agent.dto;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -41,6 +42,18 @@ public class AgentUpdateDTO implements Serializable {
@Schema(description = "音色标识", example = "voice_02", nullable = true) @Schema(description = "音色标识", example = "voice_02", nullable = true)
private String ttsVoiceId; 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) @Schema(description = "记忆模型标识", example = "mem_model_02", nullable = true)
private String memModelId; private String memModelId;
@@ -1,5 +1,6 @@
package xiaozhi.modules.agent.entity; package xiaozhi.modules.agent.entity;
import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
@@ -45,6 +46,18 @@ public class AgentEntity {
@Schema(description = "音色标识") @Schema(description = "音色标识")
private String ttsVoiceId; 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 = "记忆模型标识") @Schema(description = "记忆模型标识")
private String memModelId; private String memModelId;
@@ -1,6 +1,7 @@
package xiaozhi.modules.agent.entity; package xiaozhi.modules.agent.entity;
import java.io.Serializable; import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
@@ -64,6 +65,26 @@ public class AgentTemplateEntity implements Serializable {
*/ */
private String ttsVoiceId; 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) { if (dto.getTtsVoiceId() != null) {
existingEntity.setTtsVoiceId(dto.getTtsVoiceId()); 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) { if (dto.getMemModelId() != null) {
existingEntity.setMemModelId(dto.getMemModelId()); existingEntity.setMemModelId(dto.getMemModelId());
} }
@@ -87,6 +87,10 @@ public class ConfigServiceImpl implements ConfigService {
null, null,
null, null,
null, null,
null,
null,
null,
null,
agent.getVadModelId(), agent.getVadModelId(),
agent.getAsrModelId(), agent.getAsrModelId(),
null, null,
@@ -135,15 +139,24 @@ public class ConfigServiceImpl implements ConfigService {
String voice = null; String voice = null;
String referenceAudio = null; String referenceAudio = null;
String referenceText = null; String referenceText = null;
String language = null;
TimbreDetailsVO timbre = timbreService.get(agent.getTtsVoiceId()); TimbreDetailsVO timbre = timbreService.get(agent.getTtsVoiceId());
if (timbre != null) { if (timbre != null) {
voice = timbre.getTtsVoice(); voice = timbre.getTtsVoice();
referenceAudio = timbre.getReferenceAudio(); referenceAudio = timbre.getReferenceAudio();
referenceText = timbre.getReferenceText(); referenceText = timbre.getReferenceText();
// 优先使用用户选择的语言,如果没有则使用音色支持的第一个语言
if (StringUtils.isNotBlank(agent.getTtsLanguage())) {
language = agent.getTtsLanguage();
} else if (StringUtils.isNotBlank(timbre.getLanguages())) {
language = timbre.getLanguages().split("")[0].trim();
}
} else { } else {
VoiceCloneEntity voice_print = cloneVoiceService.selectById(agent.getTtsVoiceId()); VoiceCloneEntity voice_print = cloneVoiceService.selectById(agent.getTtsVoiceId());
if (voice_print != null) { if (voice_print != null) {
voice = voice_print.getVoiceId(); voice = voice_print.getVoiceId();
// 优先使用用户选择的语言,如果没有则使用默认值
language = StringUtils.isNotBlank(agent.getTtsLanguage()) ? agent.getTtsLanguage() : "普通话";
} }
} }
// 构建返回数据 // 构建返回数据
@@ -208,6 +221,10 @@ public class ConfigServiceImpl implements ConfigService {
voice, voice,
referenceAudio, referenceAudio,
referenceText, referenceText,
language,
agent.getTtsVolume(),
agent.getTtsRate(),
agent.getTtsPitch(),
agent.getVadModelId(), agent.getVadModelId(),
agent.getAsrModelId(), agent.getAsrModelId(),
agent.getLlmModelId(), agent.getLlmModelId(),
@@ -385,6 +402,10 @@ public class ConfigServiceImpl implements ConfigService {
String voice, String voice,
String referenceAudio, String referenceAudio,
String referenceText, String referenceText,
String language,
Integer ttsVolume,
Integer ttsRate,
Integer ttsPitch,
String vadModelId, String vadModelId,
String asrModelId, String asrModelId,
String llmModelId, String llmModelId,
@@ -423,6 +444,14 @@ public class ConfigServiceImpl implements ConfigService {
((Map<String, Object>) model.getConfigJson()).put("ref_audio", referenceAudio); ((Map<String, Object>) model.getConfigJson()).put("ref_audio", referenceAudio);
if (referenceText != null) if (referenceText != null)
((Map<String, Object>) model.getConfigJson()).put("ref_text", referenceText); ((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 // 火山引擎声音克隆需要替换resource_id
Map<String, Object> map = (Map<String, Object>) model.getConfigJson(); Map<String, Object> map = (Map<String, Object>) model.getConfigJson();
@@ -23,6 +23,9 @@ public class VoiceDTO implements Serializable {
@Schema(description = "音频播放地址") @Schema(description = "音频播放地址")
private String voiceDemo; private String voiceDemo;
@Schema(description = "语言类型")
private String languages;
@Schema(description = "是否为克隆音色") @Schema(description = "是否为克隆音色")
private Boolean isClone; private Boolean isClone;
@@ -31,6 +34,7 @@ public class VoiceDTO implements Serializable {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.voiceDemo = null; this.voiceDemo = null;
this.languages = null;
this.isClone = false; // 默认不是克隆音色 this.isClone = false; // 默认不是克隆音色
} }
@@ -39,6 +43,7 @@ public class VoiceDTO implements Serializable {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.voiceDemo = voiceDemo; this.voiceDemo = voiceDemo;
this.languages = null;
this.isClone = false; this.isClone = false;
} }
@@ -129,6 +129,7 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
.map(entity -> { .map(entity -> {
VoiceDTO dto = new VoiceDTO(entity.getId(), entity.getName()); VoiceDTO dto = new VoiceDTO(entity.getId(), entity.getName());
dto.setVoiceDemo(entity.getVoiceDemo()); dto.setVoiceDemo(entity.getVoiceDemo());
dto.setLanguages(entity.getLanguages()); // 设置语言类型
dto.setIsClone(false); // 设置为普通音色 dto.setIsClone(false); // 设置为普通音色
return dto; return dto;
}) })
@@ -146,6 +147,7 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
voiceDTO.setName(MessageUtils.getMessage(ErrorCode.VOICE_CLONE_PREFIX) + entity.getName()); voiceDTO.setName(MessageUtils.getMessage(ErrorCode.VOICE_CLONE_PREFIX) + entity.getName());
// 保留从数据库查询到的voiceDemo字段 // 保留从数据库查询到的voiceDemo字段
voiceDTO.setVoiceDemo(entity.getVoiceDemo()); voiceDTO.setVoiceDemo(entity.getVoiceDemo());
voiceDTO.setLanguages(entity.getLanguages());
voiceDTO.setIsClone(true); // 设置为克隆音色 voiceDTO.setIsClone(true); // 设置为克隆音色
redisUtils.set(RedisKeys.getTimbreNameById(voiceDTO.getId()), voiceDTO.getName(), redisUtils.set(RedisKeys.getTimbreNameById(voiceDTO.getId()), voiceDTO.getName(),
RedisUtils.NOT_EXPIRE); 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 encoding: utf8
path: classpath:db/changelog/202602051125.sql path: classpath:db/changelog/202602051125.sql
- changeSet: - 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 id: 202602281000
author: rainv123 author: rainv123
changes: changes:
@@ -16,6 +16,10 @@
<result column="llmModelId" property="llmModelId"/> <result column="llmModelId" property="llmModelId"/>
<result column="ttsModelId" property="ttsModelId"/> <result column="ttsModelId" property="ttsModelId"/>
<result column="ttsVoiceId" property="ttsVoiceId"/> <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="memModelId" property="memModelId"/>
<result column="intentModelId" property="intentModelId"/> <result column="intentModelId" property="intentModelId"/>
@@ -45,6 +49,10 @@
a.vllm_model_id AS vllmModelId, a.vllm_model_id AS vllmModelId,
a.tts_model_id AS ttsModelId, a.tts_model_id AS ttsModelId,
a.tts_voice_id AS ttsVoiceId, 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.mem_model_id AS memModelId,
a.intent_model_id AS intentModelId, a.intent_model_id AS intentModelId,
COALESCE( COALESCE(
@@ -879,8 +879,8 @@ export default {
} }
.equipment-management { .equipment-management {
width: 79px; min-width: 80px;
font-size: 9px; 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.memoryHis': 'Speicher',
'roleConfig.memory': 'Speicher', 'roleConfig.memory': 'Speicher',
'roleConfig.intent': 'Intent', 'roleConfig.intent': 'Intent',
'roleConfig.language': 'Sprache auswählen',
'roleConfig.voiceType': 'Stimmtyp', '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.pleaseEnterContent': 'Bitte Inhalt eingeben',
'roleConfig.pleaseEnterLangCode': 'Bitte Sprachcode eingeben, z.B.: en_US', 'roleConfig.pleaseEnterLangCode': 'Bitte Sprachcode eingeben, z.B.: en_US',
'roleConfig.pleaseEnterLangName': 'Bitte Interaktionssprache eingeben, z.B.: Englisch', 'roleConfig.pleaseEnterLangName': 'Bitte Interaktionssprache eingeben, z.B.: Englisch',
+9
View File
@@ -764,7 +764,16 @@ export default {
'roleConfig.memoryHis': 'Memory', 'roleConfig.memoryHis': 'Memory',
'roleConfig.memory': 'Memory Model', 'roleConfig.memory': 'Memory Model',
'roleConfig.intent': 'Intent Recognition', 'roleConfig.intent': 'Intent Recognition',
'roleConfig.language': 'Select Language',
'roleConfig.voiceType': 'Voice Type', '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.pleaseEnterContent': 'Please enter content',
'roleConfig.pleaseEnterLangCode': 'Please enter language code, e.g.: en_US', 'roleConfig.pleaseEnterLangCode': 'Please enter language code, e.g.: en_US',
'roleConfig.pleaseEnterLangName': 'Please enter interaction language, e.g.: English', 'roleConfig.pleaseEnterLangName': 'Please enter interaction language, e.g.: English',
+9
View File
@@ -764,7 +764,16 @@ export default {
'roleConfig.memoryHis': 'Bộ nhớ', 'roleConfig.memoryHis': 'Bộ nhớ',
'roleConfig.memory': 'Mô hình bộ nhớ', 'roleConfig.memory': 'Mô hình bộ nhớ',
'roleConfig.intent': 'Nhận dạng ý định', 'roleConfig.intent': 'Nhận dạng ý định',
'roleConfig.language': 'Chọn ngôn ngữ',
'roleConfig.voiceType': 'Loại giọng nói', '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.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.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', '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.memoryHis': '记忆',
'roleConfig.memory': '记忆模式', 'roleConfig.memory': '记忆模式',
'roleConfig.tts': '语音合成(TTS)', 'roleConfig.tts': '语音合成(TTS)',
'roleConfig.language': '选择语言',
'roleConfig.voiceType': '声音音色(Voice)', '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.pleaseEnterContent': '请输入内容',
'roleConfig.pleaseEnterLangCode': '请输入语言编码,如:zh_CN', 'roleConfig.pleaseEnterLangCode': '请输入语言编码,如:zh_CN',
'roleConfig.pleaseEnterLangName': '请输入交互语种,如:中文', 'roleConfig.pleaseEnterLangName': '请输入交互语种,如:中文',
+9
View File
@@ -764,7 +764,16 @@ export default {
'roleConfig.memoryHis': '記憶', 'roleConfig.memoryHis': '記憶',
'roleConfig.memory': '記憶模式', 'roleConfig.memory': '記憶模式',
'roleConfig.intent': '意圖識別(Intent)', 'roleConfig.intent': '意圖識別(Intent)',
'roleConfig.language': '選擇語言',
'roleConfig.voiceType': '聲音音色(Voice)', '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.pleaseEnterContent': '請輸入內容',
'roleConfig.pleaseEnterLangCode': '請輸入語言編碼,如:zh_TW', 'roleConfig.pleaseEnterLangCode': '請輸入語言編碼,如:zh_TW',
'roleConfig.pleaseEnterLangName': '請輸入交互語種,如:繁體中文', 'roleConfig.pleaseEnterLangName': '請輸入交互語種,如:繁體中文',
+224 -52
View File
@@ -256,47 +256,78 @@
</div> </div>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item :label="$t('roleConfig.voiceType')"> <div class="model-row">
<el-select <!-- 语言筛选器 -->
v-model="form.ttsVoiceId" <el-form-item :label="$t('roleConfig.language')" class="model-item language-select-item">
filterable <div class="model-select-wrapper">
:placeholder="$t('roleConfig.pleaseSelect')" <el-select
class="form-select" v-model="selectedLanguage"
> :placeholder="$t('roleConfig.selectLanguage')"
<el-option class="form-select language-select"
v-for="(item, index) in voiceOptions" @change="filterVoicesByLanguage"
:key="`voice-${index}`"
:label="item.label"
:value="item.value"
>
<div
style="
display: flex;
justify-content: space-between;
align-items: center;
"
> >
<span>{{ item.label }}</span> <el-option
<template v-if="hasAudioPreview(item)"> v-for="(lang, index) in languageOptions"
<el-button :key="`lang-${index}`"
type="text" :label="lang.label"
:icon=" :value="lang.value"
playingVoice && />
currentPlayingVoiceId === item.value && </el-select>
!isPaused </div>
? 'el-icon-video-pause' </el-form-item>
: 'el-icon-video-play'
<!-- 音色选择器 -->
<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)" <span>{{ item.label }}</span>
:loading="false" <template v-if="hasAudioPreview(item)">
class="play-button" <el-button
/> type="text"
</template> :icon="
</div> playingVoice &&
</el-option> currentPlayingVoiceId === item.value &&
</el-select> !isPaused
</el-form-item> ? '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> </div>
</div> </div>
@@ -318,6 +349,11 @@
:providers="currentContextProviders" :providers="currentContextProviders"
@confirm="handleUpdateContext" @confirm="handleUpdateContext"
/> />
<tts-advanced-settings
:visible.sync="showTtsAdvancedDialog"
:settings="ttsSettings"
@save="handleTtsSettingsSave"
/>
</div> </div>
</template> </template>
@@ -327,20 +363,30 @@ import { getServiceUrl } from "@/apis/api";
import RequestService from "@/apis/httpRequest"; import RequestService from "@/apis/httpRequest";
import FunctionDialog from "@/components/FunctionDialog.vue"; import FunctionDialog from "@/components/FunctionDialog.vue";
import ContextProviderDialog from "@/components/ContextProviderDialog.vue"; import ContextProviderDialog from "@/components/ContextProviderDialog.vue";
import TtsAdvancedSettings from "@/components/TtsAdvancedSettings.vue";
import HeaderBar from "@/components/HeaderBar.vue"; import HeaderBar from "@/components/HeaderBar.vue";
import i18n from "@/i18n"; import i18n from "@/i18n";
import featureManager from "@/utils/featureManager"; import featureManager from "@/utils/featureManager";
export default { export default {
name: "RoleConfigPage", name: "RoleConfigPage",
components: { HeaderBar, FunctionDialog, ContextProviderDialog }, components: { HeaderBar, FunctionDialog, ContextProviderDialog, TtsAdvancedSettings },
data() { data() {
return { return {
showContextProviderDialog: false, showContextProviderDialog: false,
showTtsAdvancedDialog: false,
ttsSettings: {
volume: 0,
speed: 0,
pitch: 0
},
form: { form: {
agentCode: "", agentCode: "",
agentName: "", agentName: "",
ttsVoiceId: "", ttsVoiceId: "",
ttsVolume: null,
ttsRate: null,
ttsPitch: null,
chatHistoryConf: 0, chatHistoryConf: 0,
systemPrompt: "", systemPrompt: "",
summaryMemory: "", summaryMemory: "",
@@ -381,6 +427,9 @@ export default {
isPaused: false, isPaused: false,
currentAudio: null, currentAudio: null,
currentPlayingVoiceId: null, currentPlayingVoiceId: null,
// 语言筛选相关状态
languageOptions: [], // 语言选项列表
selectedLanguage: '', // 当前选中的语言
// 功能状态 // 功能状态
featureStatus: { featureStatus: {
vad: false, // 语言检测活动功能状态 vad: false, // 语言检测活动功能状态
@@ -412,6 +461,7 @@ export default {
vllmModelId: this.form.model.vllmModelId, vllmModelId: this.form.model.vllmModelId,
ttsModelId: this.form.model.ttsModelId, ttsModelId: this.form.model.ttsModelId,
ttsVoiceId: this.form.ttsVoiceId, ttsVoiceId: this.form.ttsVoiceId,
ttsLanguage: this.selectedLanguage,
chatHistoryConf: this.form.chatHistoryConf, chatHistoryConf: this.form.chatHistoryConf,
memModelId: this.form.model.memModelId, memModelId: this.form.model.memModelId,
intentModelId: this.form.model.intentModelId, intentModelId: this.form.model.intentModelId,
@@ -428,6 +478,17 @@ export default {
}), }),
contextProviders: this.currentContextProviders, 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 }) => { Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => {
if (data.code === 0) { if (data.code === 0) {
this.$message.success({ this.$message.success({
@@ -543,6 +604,14 @@ export default {
intentModelId: data.data.intentModelId, 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 }, ...] // 后端只给了最小映射:[{ id, agentId, pluginId }, ...]
const savedMappings = data.data.functions || []; const savedMappings = data.data.functions || [];
@@ -628,32 +697,91 @@ export default {
if (!modelId) { if (!modelId) {
this.voiceOptions = []; this.voiceOptions = [];
this.voiceDetails = {}; this.voiceDetails = {};
this.languageOptions = [];
this.selectedLanguage = '';
return; return;
} }
Api.model.getModelVoices(modelId, "", ({ data }) => { Api.model.getModelVoices(modelId, "", ({ data }) => {
if (data.code === 0 && data.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) => { this.voiceDetails = data.data.reduce((acc, voice) => {
acc[voice.id] = voice; acc[voice.id] = voice;
return acc; 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 { } else {
this.voiceOptions = []; this.voiceOptions = [];
this.voiceDetails = {}; 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) { getFunctionDisplayChar(name) {
if (!name || name.length === 0) return ""; if (!name || name.length === 0) return "";
@@ -719,6 +847,16 @@ export default {
openContextProviderDialog() { openContextProviderDialog() {
this.showContextProviderDialog = true; 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) { handleUpdateContext(providers) {
this.currentContextProviders = providers; this.currentContextProviders = providers;
}, },
@@ -1354,6 +1492,15 @@ export default {
margin-bottom: 0; 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 { .model-row .el-form-item__label {
font-size: 12px !important; font-size: 12px !important;
color: #3d4566 !important; color: #3d4566 !important;
@@ -1529,6 +1676,31 @@ export default {
text-decoration: underline; 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 { .input-new-tag {
width: 90px; width: 90px;
&::v-deep(.el-input__inner) { &::v-deep(.el-input__inner) {
@@ -2,6 +2,10 @@
{{base_prompt}} {{base_prompt}}
</identity> </identity>
<language>
【语言规范】你必须使用{{language}}进行回复和交流。无论用户使用何种语言提问,你都应该用{{language}}来回答。
</language>
<emotion> <emotion>
【核心目标】你不是冰冷的机器!请敏锐感知用户情绪,作为懂人心的伙伴,用有温度的回应照亮对话。 【核心目标】你不是冰冷的机器!请敏锐感知用户情绪,作为懂人心的伙伴,用有温度的回应照亮对话。
- **情感融入:** - **情感融入:**
+30 -1
View File
@@ -733,6 +733,7 @@ TTS:
type: edge type: edge
voice: zh-CN-XiaoxiaoNeural voice: zh-CN-XiaoxiaoNeural
output_dir: tmp/ output_dir: tmp/
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
DoubaoTTS: DoubaoTTS:
# 定义TTS API类型 # 定义TTS API类型
type: doubao type: doubao
@@ -751,6 +752,7 @@ TTS:
speed_ratio: 1.0 speed_ratio: 1.0
volume_ratio: 1.0 volume_ratio: 1.0
pitch_ratio: 1.0 pitch_ratio: 1.0
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
#火山tts,支持双向流式tts #火山tts,支持双向流式tts
HuoshanDoubleStreamTTS: HuoshanDoubleStreamTTS:
type: huoshan_double_stream type: huoshan_double_stream
@@ -791,6 +793,7 @@ TTS:
# mix_factor: 0.3 # mix_factor: 0.3
# - source_speaker: zh_male_ahu_conversation_wvae_bigtts # - source_speaker: zh_male_ahu_conversation_wvae_bigtts
# mix_factor: 0.4 # mix_factor: 0.4
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
CosyVoiceSiliconflow: CosyVoiceSiliconflow:
type: siliconflow type: siliconflow
# 硅基流动TTS # 硅基流动TTS
@@ -800,6 +803,7 @@ TTS:
output_dir: tmp/ output_dir: tmp/
access_token: 你的硅基流动API密钥 access_token: 你的硅基流动API密钥
response_format: wav response_format: wav
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
CozeCnTTS: CozeCnTTS:
type: cozecn type: cozecn
# COZECN TTS # COZECN TTS
@@ -808,6 +812,10 @@ TTS:
output_dir: tmp/ output_dir: tmp/
access_token: 你的coze web key access_token: 你的coze web key
response_format: wav response_format: wav
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
# 以下可不用设置,使用默认设置
# speed: 1 # 语速:-0.5到2
# loudness_rate: 0 # 音量:-50到100
VolcesAiGatewayTTS: VolcesAiGatewayTTS:
type: openai type: openai
# 火山引擎 - 边缘大模型网关 # 火山引擎 - 边缘大模型网关
@@ -822,6 +830,7 @@ TTS:
voice: zh_male_shaonianzixin_moon_bigtts voice: zh_male_shaonianzixin_moon_bigtts
speed: 1 speed: 1
output_dir: tmp/ output_dir: tmp/
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
FishSpeech: FishSpeech:
# 参照教程:https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/fish-speech-integration.md # 参照教程:https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/fish-speech-integration.md
type: fishspeech type: fishspeech
@@ -843,6 +852,7 @@ TTS:
rate: 44100 rate: 44100
api_key: "你的api_key" api_key: "你的api_key"
api_url: "http://127.0.0.1:8080/v1/tts" api_url: "http://127.0.0.1:8080/v1/tts"
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
GPT_SOVITS_V2: GPT_SOVITS_V2:
# 定义TTS API类型 # 定义TTS API类型
#启动tts方法: #启动tts方法:
@@ -868,6 +878,7 @@ TTS:
parallel_infer: true parallel_infer: true
repetition_penalty: 1.35 repetition_penalty: 1.35
aux_ref_audio_paths: [] aux_ref_audio_paths: []
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
GPT_SOVITS_V3: GPT_SOVITS_V3:
# 定义TTS API类型 GPT-SoVITS-v3lora-20250228 # 定义TTS API类型 GPT-SoVITS-v3lora-20250228
#启动tts方法: #启动tts方法:
@@ -887,6 +898,7 @@ TTS:
inp_refs: [] inp_refs: []
sample_steps: 32 sample_steps: 32
if_sr: false if_sr: false
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
MinimaxTTSHTTPStream: MinimaxTTSHTTPStream:
# Minimax流式语音合成服务 # Minimax流式语音合成服务
type: minimax_httpstream type: minimax_httpstream
@@ -918,6 +930,7 @@ TTS:
# voice_id: female-shaonv # voice_id: female-shaonv
# weight: 1 # weight: 1
# language_boost: auto # language_boost: auto
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
AliyunTTS: AliyunTTS:
# 阿里云智能语音交互服务,需要先在阿里云平台开通服务,然后获取验证信息 # 阿里云智能语音交互服务,需要先在阿里云平台开通服务,然后获取验证信息
# 平台地址:https://nls-portal.console.aliyun.com/ # 平台地址:https://nls-portal.console.aliyun.com/
@@ -937,6 +950,7 @@ TTS:
# volume: 50 # volume: 50
# speech_rate: 0 # speech_rate: 0
# pitch_rate: 0 # pitch_rate: 0
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
AliyunStreamTTS: AliyunStreamTTS:
# 阿里云CosyVoice大模型流式文本语音合成 # 阿里云CosyVoice大模型流式文本语音合成
# 采用FlowingSpeechSynthesizer接口,支持更低延迟和更自然的语音质量 # 采用FlowingSpeechSynthesizer接口,支持更低延迟和更自然的语音质量
@@ -960,6 +974,7 @@ TTS:
# volume: 50 # 音量:0-100 # volume: 50 # 音量:0-100
# speech_rate: 0 # 语速:-500到500 # speech_rate: 0 # 语速:-500到500
# pitch_rate: 0 # 语调:-500到500 # pitch_rate: 0 # 语调:-500到500
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
TencentTTS: TencentTTS:
# 腾讯云智能语音交互服务,需要先在腾讯云平台开通服务 # 腾讯云智能语音交互服务,需要先在腾讯云平台开通服务
# appid、secret_id、secret_key申请地址:https://console.cloud.tencent.com/cam/capi # appid、secret_id、secret_key申请地址:https://console.cloud.tencent.com/cam/capi
@@ -971,7 +986,11 @@ TTS:
secret_key: 你的腾讯云SecretKey secret_key: 你的腾讯云SecretKey
region: ap-guangzhou region: ap-guangzhou
voice: 101001 voice: 101001
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
# 以下可不用设置,使用默认设置
# format: wav # 音频格式:pcm、wav、mp3
# volume: 0 # 音量:-10到10
# speech_rate: 0 # 语速:-2到6
TTS302AI: TTS302AI:
# 302AI语音合成服务,需要先在302平台创建账户充值,并获取密钥信息 # 302AI语音合成服务,需要先在302平台创建账户充值,并获取密钥信息
# 添加 302.ai TTS 配置 # 添加 302.ai TTS 配置
@@ -985,6 +1004,7 @@ TTS:
voice: "zh_female_wanwanxiaohe_moon_bigtts" voice: "zh_female_wanwanxiaohe_moon_bigtts"
output_dir: tmp/ output_dir: tmp/
access_token: "你的302API密钥" access_token: "你的302API密钥"
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
GizwitsTTS: GizwitsTTS:
type: doubao type: doubao
# 火山引擎作为基座,可以完全使用企业级火山引擎语音合成服务 # 火山引擎作为基座,可以完全使用企业级火山引擎语音合成服务
@@ -996,6 +1016,7 @@ TTS:
voice: "zh_female_wanwanxiaohe_moon_bigtts" voice: "zh_female_wanwanxiaohe_moon_bigtts"
output_dir: tmp/ output_dir: tmp/
access_token: "你的机智云API key" access_token: "你的机智云API key"
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
ACGNTTS: ACGNTTS:
#在线网址:https://acgn.ttson.cn/ #在线网址:https://acgn.ttson.cn/
#token购买:www.ttson.cn #token购买:www.ttson.cn
@@ -1013,6 +1034,7 @@ TTS:
format: mp3 format: mp3
output_dir: tmp/ output_dir: tmp/
emotion: 1 emotion: 1
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
OpenAITTS: OpenAITTS:
# openai官方文本转语音服务,可支持全球大多数语种 # openai官方文本转语音服务,可支持全球大多数语种
type: openai type: openai
@@ -1028,6 +1050,7 @@ TTS:
# 语速范围0.25-4.0 # 语速范围0.25-4.0
speed: 1 speed: 1
output_dir: tmp/ output_dir: tmp/
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
CustomTTS: CustomTTS:
# 自定义的TTS接口服务,请求参数可自定义,可接入众多TTS服务 # 自定义的TTS接口服务,请求参数可自定义,可接入众多TTS服务
# 以本地部署的KokoroTTS为例 # 以本地部署的KokoroTTS为例
@@ -1050,6 +1073,7 @@ TTS:
# Authorization: Bearer xxxx # Authorization: Bearer xxxx
format: mp3 # 接口返回的音频格式 format: mp3 # 接口返回的音频格式
output_dir: tmp/ output_dir: tmp/
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
LinkeraiTTS: LinkeraiTTS:
type: linkerai type: linkerai
api_url: https://tts.linkerai.cn/tts api_url: https://tts.linkerai.cn/tts
@@ -1061,6 +1085,7 @@ TTS:
access_token: "U4YdYXVfpwWnk2t5Gp822zWPCuORyeJL" access_token: "U4YdYXVfpwWnk2t5Gp822zWPCuORyeJL"
voice: "OUeAo1mhq6IBExi" voice: "OUeAo1mhq6IBExi"
output_dir: tmp/ output_dir: tmp/
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
PaddleSpeechTTS: PaddleSpeechTTS:
#百度飞浆 PaddleSpeech 支持本地离线部署 支持模型训练 #百度飞浆 PaddleSpeech 支持本地离线部署 支持模型训练
#框架地址 https://www.paddlepaddle.org.cn/ #框架地址 https://www.paddlepaddle.org.cn/
@@ -1074,6 +1099,7 @@ TTS:
speed: 1.0 # 语速,1.0 表示正常语速,>1 表示加快,<1 表示减慢 speed: 1.0 # 语速,1.0 表示正常语速,>1 表示加快,<1 表示减慢
volume: 1.0 # 音量,1.0 表示正常音量,>1 表示增大,<1 表示减小 volume: 1.0 # 音量,1.0 表示正常音量,>1 表示增大,<1 表示减小
save_path: # 保存路径 save_path: # 保存路径
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
IndexStreamTTS: IndexStreamTTS:
# 基于Index-TTS-vLLM项目的TTS接口服务 # 基于Index-TTS-vLLM项目的TTS接口服务
# 参照教程:https://github.com/Ksuriuri/index-tts-vllm/blob/master/README.md # 参照教程:https://github.com/Ksuriuri/index-tts-vllm/blob/master/README.md
@@ -1083,6 +1109,7 @@ TTS:
# 默认音色,如需其他音色可到项目assets文件夹下注册 # 默认音色,如需其他音色可到项目assets文件夹下注册
voice: "jay_klee" voice: "jay_klee"
output_dir: tmp/ output_dir: tmp/
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
AliBLTTS: AliBLTTS:
# 阿里百炼CosyVoice大模型流式文本语音合成 # 阿里百炼CosyVoice大模型流式文本语音合成
# 可在这里找到你的 api_key https://bailian.console.aliyun.com/?apiKey=1#/api-key # 可在这里找到你的 api_key https://bailian.console.aliyun.com/?apiKey=1#/api-key
@@ -1097,6 +1124,7 @@ TTS:
# volume: 50 # 音量:0-100 # volume: 50 # 音量:0-100
# rate: 1 # 语速:0.5~2 # rate: 1 # 语速:0.5~2
# pitch: 1 # 语调:0.5~2 # pitch: 1 # 语调:0.5~2
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
XunFeiTTS: XunFeiTTS:
# 讯飞TTS服务 官方网站:https://www.xfyun.cn/ # 讯飞TTS服务 官方网站:https://www.xfyun.cn/
# 登录讯飞语音技术平台 https://console.xfyun.cn/app/myapp 创建相关应用 # 登录讯飞语音技术平台 https://console.xfyun.cn/app/myapp 创建相关应用
@@ -1118,3 +1146,4 @@ TTS:
# volume: 50 # 音量:0-100 # volume: 50 # 音量:0-100
# speed: 50 # 语速:0-100 # speed: 50 # 语速:0-100
# pitch: 50 # 语调:0-100 # pitch: 50 # 语调:0-100
# language: "中文" # 指定输出语种,如:中文、英语、日语、韩语等,请根据所选音色支持的语言进行设置,不填则默认为中文
-1
View File
@@ -134,7 +134,6 @@ class ConnectionHandler:
self.asr_audio = [] self.asr_audio = []
self.asr_audio_queue = queue.Queue() self.asr_audio_queue = queue.Queue()
self.current_speaker = None # 存储当前说话人 self.current_speaker = None # 存储当前说话人
self.current_language_tag = None # 存储当前ASR识别的语言标签
# llm相关变量 # llm相关变量
self.dialogue = Dialogue() self.dialogue = Dialogue()
@@ -67,11 +67,6 @@ async def startToChat(conn: "ConnectionHandler", text):
conn.current_speaker = speaker_name conn.current_speaker = speaker_name
else: else:
conn.current_speaker = None conn.current_speaker = None
# 保存语种信息到连接对象
if language_tag:
conn.current_language_tag = language_tag
else:
conn.current_language_tag = "zh"
if conn.need_bind: if conn.need_bind:
await check_bind_device(conn) await check_bind_device(conn)
@@ -6,10 +6,10 @@ import queue
import asyncio import asyncio
import traceback import traceback
import websockets import websockets
from typing import Callable, Any
from asyncio import Task from asyncio import Task
from typing import Callable, Any
from config.logger import setup_logging 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
from core.providers.tts.base import TTSProviderBase from core.providers.tts.base import TTSProviderBase
from core.providers.tts.dto.dto import SentenceType, ContentType, InterfaceType from core.providers.tts.dto.dto import SentenceType, ContentType, InterfaceType
@@ -19,6 +19,12 @@ logger = setup_logging()
class TTSProvider(TTSProviderBase): 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): def __init__(self, config, delete_audio_file):
super().__init__(config, delete_audio_file) super().__init__(config, delete_audio_file)
@@ -52,6 +58,9 @@ class TTSProvider(TTSProviderBase):
pitch = config.get("pitch", "1.0") pitch = config.get("pitch", "1.0")
self.pitch = float(pitch) if pitch else 1.0 self.pitch = float(pitch) if pitch else 1.0
# 应用百分比调整(如果存在),否则使用公有化配置
self._apply_percentage_params(config)
self.header = { self.header = {
"Authorization": f"Bearer {self.api_key}", "Authorization": f"Bearer {self.api_key}",
# "user-agent": "your_platform_info", // 可选 # "user-agent": "your_platform_info", // 可选
@@ -1,15 +1,17 @@
import uuid import uuid
import json import json
import hmac import hmac
import time
import hashlib import hashlib
import base64 import base64
import requests 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 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__ TAG = __name__
logger = setup_logging() logger = setup_logging()
@@ -84,6 +86,11 @@ class AccessToken:
class TTSProvider(TTSProviderBase): 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): def __init__(self, config, delete_audio_file):
super().__init__(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.access_key_secret = config.get("access_key_secret")
self.appkey = config.get("appkey") self.appkey = config.get("appkey")
self.format = config.get("format", "wav")
self.audio_file_type = config.get("format", "wav") self.audio_file_type = config.get("format", "wav")
if config.get("private_voice"): if config.get("private_voice"):
@@ -110,6 +116,9 @@ class TTSProvider(TTSProviderBase):
pitch_rate = config.get("pitch_rate", "0") pitch_rate = config.get("pitch_rate", "0")
self.pitch_rate = int(pitch_rate) if pitch_rate else 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.host = config.get("host", "nls-gateway-cn-shanghai.aliyuncs.com")
self.api_url = f"https://{self.host}/stream/v1/tts" self.api_url = f"https://{self.host}/stream/v1/tts"
self.header = {"Content-Type": "application/json"} self.header = {"Content-Type": "application/json"}
@@ -169,7 +178,7 @@ class TTSProvider(TTSProviderBase):
"appkey": self.appkey, "appkey": self.appkey,
"token": self.token, "token": self.token,
"text": text, "text": text,
"format": self.format, "format": self.audio_file_type,
"sample_rate": self.conn.sample_rate, "sample_rate": self.conn.sample_rate,
"voice": self.voice, "voice": self.voice,
"volume": self.volume, "volume": self.volume,
@@ -1,4 +1,4 @@
import random import os
import uuid import uuid
import json import json
import hmac import hmac
@@ -8,17 +8,17 @@ import time
import queue import queue
import asyncio import asyncio
import traceback import traceback
from typing import Callable, Any
from asyncio import Task
import websockets import websockets
import os
from datetime import datetime from asyncio import Task
from urllib import parse 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.base import TTSProviderBase
from core.providers.tts.dto.dto import SentenceType, ContentType, InterfaceType 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__ TAG = __name__
logger = setup_logging() logger = setup_logging()
@@ -87,6 +87,12 @@ class AccessToken:
class TTSProvider(TTSProviderBase): 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): def __init__(self, config, delete_audio_file):
super().__init__(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.access_key_secret = config.get("access_key_secret")
self.appkey = config.get("appkey") self.appkey = config.get("appkey")
self.format = config.get("format", "pcm") self.format = config.get("format", "pcm")
self.audio_file_type = config.get("format", "pcm")
# 音色配置 - CosyVoice大模型音色 # 音色配置 - CosyVoice大模型音色
if config.get("private_voice"): if config.get("private_voice"):
@@ -116,6 +121,9 @@ class TTSProvider(TTSProviderBase):
pitch_rate = config.get("pitch_rate", "0") pitch_rate = config.get("pitch_rate", "0")
self.pitch_rate = int(pitch_rate) if pitch_rate else 0 self.pitch_rate = int(pitch_rate) if pitch_rate else 0
# 应用百分比调整(如果存在),否则使用公有化配置
self._apply_percentage_params(config)
# WebSocket配置 # WebSocket配置
self.host = config.get("host", "nls-gateway-cn-beijing.aliyuncs.com") self.host = config.get("host", "nls-gateway-cn-beijing.aliyuncs.com")
# 如果配置的是内网地址(包含-internal.aliyuncs.com),则使用ws协议,默认是wss协议 # 如果配置的是内网地址(包含-internal.aliyuncs.com),则使用ws协议,默认是wss协议
@@ -13,7 +13,7 @@ from typing import Callable, Any
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from config.logger import setup_logging from config.logger import setup_logging
from core.utils import opus_encoder_utils 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.utils.output_counter import add_device_output
from core.handle.reportHandle import enqueue_tts_report from core.handle.reportHandle import enqueue_tts_report
from core.handle.sendAudioHandle import sendAudioMessage from core.handle.sendAudioHandle import sendAudioMessage
@@ -463,3 +463,10 @@ class TTSProviderBase(ABC):
self.processed_chars += len(full_text) self.processed_chars += len(full_text)
return True return True
return False 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): 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): def __init__(self, config, delete_audio_file):
super().__init__(config, delete_audio_file) super().__init__(config, delete_audio_file)
self.model = config.get("model") self.model = config.get("model")
@@ -11,17 +16,29 @@ class TTSProvider(TTSProviderBase):
self.voice = config.get("private_voice") self.voice = config.get("private_voice")
else: else:
self.voice = config.get("voice") self.voice = config.get("voice")
self.response_format = config.get("response_format", "wav")
self.audio_file_type = config.get("response_format", "wav") self.audio_file_type = config.get("response_format", "wav")
self.host = "api.coze.cn" self.host = "api.coze.cn"
self.api_url = f"https://{self.host}/v1/audio/speech" 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): async def text_to_speak(self, text, output_file):
request_json = { request_json = {
"model": self.model, "model": self.model,
"input": text, "input": text,
"voice_id": self.voice, "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 = { headers = {
"Authorization": f"Bearer {self.access_token}", "Authorization": f"Bearer {self.access_token}",
@@ -15,7 +15,6 @@ class TTSProvider(TTSProviderBase):
self.url = config.get("url") self.url = config.get("url")
self.method = config.get("method", "GET") self.method = config.get("method", "GET")
self.headers = config.get("headers", {}) self.headers = config.get("headers", {})
self.format = config.get("format", "wav")
self.audio_file_type = config.get("format", "wav") self.audio_file_type = config.get("format", "wav")
self.output_file = config.get("output_dir", "tmp/") self.output_file = config.get("output_dir", "tmp/")
self.params = config.get("params") self.params = config.get("params")
@@ -29,7 +28,7 @@ class TTSProvider(TTSProviderBase):
raise TypeError("Custom TTS配置参数出错, 请参考配置说明") raise TypeError("Custom TTS配置参数出错, 请参考配置说明")
def generate_filename(self): 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): async def text_to_speak(self, text, output_file):
request_params = {} request_params = {}
@@ -2,15 +2,24 @@ import uuid
import json import json
import base64 import base64
import requests import requests
from config.logger import setup_logging
from core.utils.util import check_model_key from core.utils.util import check_model_key
from core.providers.tts.base import TTSProviderBase from core.providers.tts.base import TTSProviderBase
from config.logger import setup_logging from core.utils.tts import convert_percentage_to_range
TAG = __name__ TAG = __name__
logger = setup_logging() logger = setup_logging()
class TTSProvider(TTSProviderBase): 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): def __init__(self, config, delete_audio_file):
super().__init__(config, delete_audio_file) super().__init__(config, delete_audio_file)
if config.get("appid"): if config.get("appid"):
@@ -34,6 +43,9 @@ class TTSProvider(TTSProviderBase):
self.volume_ratio = float(volume_ratio) if volume_ratio else 1.0 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.pitch_ratio = float(pitch_ratio) if pitch_ratio else 1.0
# 应用百分比调整(如果存在),否则使用公有化配置
self._apply_percentage_params(config)
self.api_url = config.get("api_url") self.api_url = config.get("api_url")
self.authorization = config.get("authorization") self.authorization = config.get("authorization")
self.header = {"Authorization": f"{self.authorization}{self.access_token}"} self.header = {"Authorization": f"{self.authorization}{self.access_token}"}
@@ -91,7 +91,6 @@ class TTSProvider(TTSProviderBase):
self.reference_text = parse_string_to_list( self.reference_text = parse_string_to_list(
config.get('ref_text')if config.get('ref_text') else config.get("reference_text") 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.audio_file_type = config.get("response_format", "wav")
self.api_key = config.get("api_key", "YOUR_API_KEY") self.api_key = config.get("api_key", "YOUR_API_KEY")
model_key_msg = check_model_key("FishSpeech TTS", self.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, "reference_id": self.reference_id,
"normalize": self.normalize, "normalize": self.normalize,
"format": self.format, "format": self.audio_file_type,
"max_new_tokens": self.max_new_tokens, "max_new_tokens": self.max_new_tokens,
"chunk_length": self.chunk_length, "chunk_length": self.chunk_length,
"top_p": self.top_p, "top_p": self.top_p,
@@ -7,11 +7,10 @@ import traceback
import websockets import websockets
from typing import Callable, Any from typing import Callable, Any
from core.utils.tts import MarkdownCleaner
from config.logger import setup_logging from config.logger import setup_logging
from core.utils import opus_encoder_utils
from core.utils.util import check_model_key from core.utils.util import check_model_key
from core.providers.tts.base import TTSProviderBase 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 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.additions = {**default_additions, **config.get("additions", {})}
self.mix_speaker = {**default_mix_speaker, **config.get("mix_speaker", {})} 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.ws_url = config.get("ws_url")
self.authorization = config.get("authorization") self.authorization = config.get("authorization")
self.header = {"Authorization": f"{self.authorization}{self.access_token}"} self.header = {"Authorization": f"{self.authorization}{self.access_token}"}
@@ -6,12 +6,14 @@ import asyncio
import aiohttp import aiohttp
import requests import requests
import traceback import traceback
from core.utils import textUtils
from config.logger import setup_logging from config.logger import setup_logging
from core.utils.tts import MarkdownCleaner
from core.utils.util import parse_string_to_list from core.utils.util import parse_string_to_list
from core.providers.tts.base import TTSProviderBase 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.providers.tts.dto.dto import SentenceType, ContentType
from core.utils.tts import MarkdownCleaner, convert_percentage_to_range
TAG = __name__ TAG = __name__
logger = setup_logging() logger = setup_logging()
@@ -56,6 +58,22 @@ class TTSProvider(TTSProviderBase):
if self.voice: if self.voice:
self.voice_setting["voice_id"] = 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.host = "api.minimaxi.com" # 备用地址:api-bj.minimaxi.com
self.api_url = f"https://{self.host}/v1/t2a_v2?GroupId={self.group_id}" self.api_url = f"https://{self.host}/v1/t2a_v2?GroupId={self.group_id}"
self.header = { self.header = {
@@ -8,6 +8,10 @@ logger = setup_logging()
class TTSProvider(TTSProviderBase): 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): def __init__(self, config, delete_audio_file):
super().__init__(config, delete_audio_file) super().__init__(config, delete_audio_file)
self.api_key = config.get("api_key") self.api_key = config.get("api_key")
@@ -17,13 +21,15 @@ class TTSProvider(TTSProviderBase):
self.voice = config.get("private_voice") self.voice = config.get("private_voice")
else: else:
self.voice = config.get("voice", "alloy") self.voice = config.get("voice", "alloy")
self.response_format = config.get("format", "wav")
self.audio_file_type = config.get("format", "wav") self.audio_file_type = config.get("format", "wav")
# 处理空字符串的情况 # 处理空字符串的情况
speed = config.get("speed", "1.0") speed = config.get("speed", "1.0")
self.speed = float(speed) if speed else 1.0 self.speed = float(speed) if speed else 1.0
# 应用百分比调整(如果存在),否则使用公有化配置
self._apply_percentage_params(config)
self.output_file = config.get("output_dir", "tmp/") self.output_file = config.get("output_dir", "tmp/")
model_key_msg = check_model_key("TTS", self.api_key) model_key_msg = check_model_key("TTS", self.api_key)
if model_key_msg: if model_key_msg:
@@ -38,7 +44,7 @@ class TTSProvider(TTSProviderBase):
"model": self.model, "model": self.model,
"input": text, "input": text,
"voice": self.voice, "voice": self.voice,
"response_format": "wav", "response_format": self.audio_file_type,
"speed": self.speed, "speed": self.speed,
} }
response = requests.post(self.api_url, json=data, headers=headers) response = requests.post(self.api_url, json=data, headers=headers)
@@ -16,6 +16,11 @@ logger = setup_logging()
class TTSProvider(TTSProviderBase): 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): def __init__(self, config, delete_audio_file):
super().__init__(config, delete_audio_file) super().__init__(config, delete_audio_file)
self.url = config.get("url", "ws://192.168.1.10:8092/paddlespeech/tts/streaming") 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.volume = float(volume) if volume else 1.0
self.delete_audio_file = config.get("delete_audio", True) self.delete_audio_file = config.get("delete_audio", True)
# 应用百分比调整(如果存在),否则使用公有化配置
self._apply_percentage_params(config)
if not self.delete_audio_file: if not self.delete_audio_file:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
save_path = config.get("save_path") save_path = config.get("save_path")
@@ -3,6 +3,11 @@ from core.providers.tts.base import TTSProviderBase
class TTSProvider(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): def __init__(self, config, delete_audio_file):
super().__init__(config, delete_audio_file) super().__init__(config, delete_audio_file)
self.model = config.get("model") self.model = config.get("model")
@@ -11,11 +16,13 @@ class TTSProvider(TTSProviderBase):
self.voice = config.get("private_voice") self.voice = config.get("private_voice")
else: else:
self.voice = config.get("voice") self.voice = config.get("voice")
self.response_format = config.get("response_format", "mp3")
self.audio_file_type = config.get("response_format", "mp3") self.audio_file_type = config.get("response_format", "mp3")
self.speed = float(config.get("speed", 1.0)) self.speed = float(config.get("speed", 1.0))
self.gain = config.get("gain") self.gain = config.get("gain")
# 应用百分比调整(如果存在),否则使用公有化配置
self._apply_percentage_params(config)
self.host = "api.siliconflow.cn" self.host = "api.siliconflow.cn"
self.api_url = f"https://{self.host}/v1/audio/speech" self.api_url = f"https://{self.host}/v1/audio/speech"
@@ -24,7 +31,7 @@ class TTSProvider(TTSProviderBase):
"model": self.model, "model": self.model,
"input": text, "input": text,
"voice": self.voice, "voice": self.voice,
"response_format": self.response_format, "response_format": self.audio_file_type,
} }
headers = { headers = {
"Authorization": f"Bearer {self.access_token}", "Authorization": f"Bearer {self.access_token}",
@@ -10,6 +10,11 @@ from core.providers.tts.base import TTSProviderBase
class TTSProvider(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): def __init__(self, config, delete_audio_file):
super().__init__(config, delete_audio_file) super().__init__(config, delete_audio_file)
self.appid = config.get("appid") self.appid = config.get("appid")
@@ -24,6 +29,16 @@ class TTSProvider(TTSProviderBase):
self.output_file = config.get("output_dir") self.output_file = config.get("output_dir")
self.audio_file_type = config.get("format", "wav") 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): def _get_auth_headers(self, request_body):
"""生成鉴权请求头""" """生成鉴权请求头"""
# 获取当前UTC时间戳 # 获取当前UTC时间戳
@@ -127,6 +142,10 @@ class TTSProvider(TTSProviderBase):
"Text": text, # 合成语音的源文本 "Text": text, # 合成语音的源文本
"SessionId": str(uuid.uuid4()), # 会话ID,随机生成 "SessionId": str(uuid.uuid4()), # 会话ID,随机生成
"VoiceType": int(self.voice), # 音色 "VoiceType": int(self.voice), # 音色
"Codec": self.audio_file_type, # 音频编码格式
"Volume": self.volume, # 音量
"Speed": self.speed, # 语速
"SampleRate": self.conn.sample_rate, # 采样率部分支持24000
} }
try: try:
@@ -12,6 +12,12 @@ logger = setup_logging()
class TTSProvider(TTSProviderBase): 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): def __init__(self, config, delete_audio_file):
super().__init__(config, delete_audio_file) super().__init__(config, delete_audio_file)
self.url = config.get( self.url = config.get(
@@ -29,11 +35,13 @@ class TTSProvider(TTSProviderBase):
self.stream = str(config.get("stream", False)).lower() in ("true", "1", "yes") self.stream = str(config.get("stream", False)).lower() in ("true", "1", "yes")
self.output_file = config.get("output_dir") self.output_file = config.get("output_dir")
self.pitch_factor = int(config.get("pitch_factor", 0)) self.pitch_factor = int(config.get("pitch_factor", 0))
self.format = config.get("format", "mp3")
self.audio_file_type = config.get("format", "mp3") self.audio_file_type = config.get("format", "mp3")
self.emotion = int(config.get("emotion", 1)) self.emotion = int(config.get("emotion", 1))
self.header = {"Content-Type": "application/json"} self.header = {"Content-Type": "application/json"}
# 应用百分比调整(如果存在),否则使用公有化配置
self._apply_percentage_params(config)
def generate_filename(self, extension=".mp3"): def generate_filename(self, extension=".mp3"):
return os.path.join( return os.path.join(
self.output_file, self.output_file,
@@ -48,7 +56,7 @@ class TTSProvider(TTSProviderBase):
"to_lang": self.to_lang, "to_lang": self.to_lang,
"text": text, "text": text,
"emotion": self.emotion, "emotion": self.emotion,
"format": self.format, "format": self.audio_file_type,
"volume_change_dB": self.volume_change_dB, "volume_change_dB": self.volume_change_dB,
"voice_id": self.voice, "voice_id": self.voice,
"pitch_factor": self.pitch_factor, "pitch_factor": self.pitch_factor,
@@ -9,10 +9,10 @@ import hashlib
import asyncio import asyncio
import traceback import traceback
import websockets import websockets
from typing import Callable, Any
from asyncio import Task from asyncio import Task
from typing import Callable, Any
from config.logger import setup_logging 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
from urllib.parse import urlencode, urlparse from urllib.parse import urlencode, urlparse
from core.providers.tts.base import TTSProviderBase from core.providers.tts.base import TTSProviderBase
@@ -60,6 +60,12 @@ class XunfeiWSAuth:
class TTSProvider(TTSProviderBase): 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): def __init__(self, config, delete_audio_file):
super().__init__(config, delete_audio_file) super().__init__(config, delete_audio_file)
@@ -89,6 +95,9 @@ class TTSProvider(TTSProviderBase):
pitch = config.get("pitch", "50") pitch = config.get("pitch", "50")
self.pitch = int(pitch) if pitch else 50 self.pitch = int(pitch) if pitch else 50
# 应用百分比调整(如果存在),否则使用公有化配置
self._apply_percentage_params(config)
# 音频编码配置 # 音频编码配置
self.format = config.get("format", "raw") self.format = config.get("format", "raw")
@@ -251,6 +251,15 @@ class PromptManager:
or "" 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) template = Template(self.base_prompt_template)
enhanced_prompt = template.render( enhanced_prompt = template.render(
@@ -265,6 +274,7 @@ class PromptManager:
device_id=device_id, device_id=device_id,
client_ip=client_ip, client_ip=client_ip,
dynamic_context=self.context_data, dynamic_context=self.context_data,
language=language,
*args, *args,
**kwargs, **kwargs,
) )
+26
View File
@@ -142,3 +142,29 @@ class MarkdownCleaner:
text = check_emoji(text) 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))