update:server连接api (#747)

* update:server连接manager-api

* update:读取智能体模型配置

* update:添加默认模型的按钮

* update:优化配置读取方式

* update:server兼容manager接口改造

* update:优化私有配置加载

* update:加载私有模型配置
This commit is contained in:
hrz
2025-04-12 17:36:04 +08:00
committed by GitHub
parent c39ad97b8e
commit 5d69ba0796
57 changed files with 1618 additions and 1066 deletions
@@ -71,6 +71,14 @@ public class SwaggerConfig {
.build();
}
@Bean
public GroupedOpenApi configApi() {
return GroupedOpenApi.builder()
.group("config")
.pathsToMatch("/config/**")
.build();
}
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI().info(new Info()
@@ -51,4 +51,5 @@ public interface ErrorCode {
int PARAM_NUMBER_INVALID = 10037;
int PARAM_BOOLEAN_INVALID = 10038;
int PARAM_ARRAY_INVALID = 10039;
int PARAM_JSON_INVALID = 10040;
}
@@ -38,7 +38,14 @@ public class RedisKeys {
* 模型名称的Key
*/
public static String getModelNameById(String id) {
return "sys:model:name:" + id;
return "model:name:" + id;
}
/**
* 模型配置的Key
*/
public static String getModelConfigById(String id) {
return "model:data:" + id;
}
/**
@@ -54,4 +61,18 @@ public class RedisKeys {
public static String getAgentDeviceCountById(String id) {
return "agent:device:count:" + id;
}
/**
* 获取系统配置缓存key
*/
public static String getServerConfigKey() {
return "server:config";
}
/**
* 获取音色详情缓存key
*/
public static String getTimbreDetailsKey(String id) {
return "timbre:details:" + id;
}
}
@@ -191,4 +191,5 @@ public class AgentController {
.list(new QueryWrapper<AgentTemplateEntity>().orderByAsc("sort"));
return new Result<List<AgentTemplateEntity>>().ok(list);
}
}
@@ -17,4 +17,12 @@ public interface AgentTemplateService extends IService<AgentTemplateEntity> {
* @return 默认模板实体
*/
AgentTemplateEntity getDefaultTemplate();
/**
* 更新默认模板中的模型ID
*
* @param modelType 模型类型
* @param modelId 模型ID
*/
void updateDefaultTemplateModelId(String modelType, String modelId);
}
@@ -29,4 +29,39 @@ public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, Agen
.last("LIMIT 1");
return this.getOne(wrapper);
}
/**
* 更新默认模板中的模型ID
*
* @param modelType 模型类型
* @param modelId 模型ID
*/
@Override
public void updateDefaultTemplateModelId(String modelType, String modelId) {
modelType = modelType.toUpperCase();
AgentTemplateEntity defaultTemplate = getDefaultTemplate();
if (defaultTemplate != null) {
switch (modelType) {
case "ASR":
defaultTemplate.setAsrModelId(modelId);
break;
case "VAD":
defaultTemplate.setVadModelId(modelId);
break;
case "LLM":
defaultTemplate.setLlmModelId(modelId);
break;
case "TTS":
defaultTemplate.setTtsModelId(modelId);
break;
case "Memory":
defaultTemplate.setMemModelId(modelId);
break;
case "Intent":
defaultTemplate.setIntentModelId(modelId);
break;
}
this.updateById(defaultTemplate);
}
}
}
@@ -0,0 +1,61 @@
package xiaozhi.modules.config.controller;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import xiaozhi.common.constant.Constant;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.utils.Result;
import xiaozhi.common.validator.ValidatorUtils;
import xiaozhi.modules.config.dto.AgentModelsDTO;
import xiaozhi.modules.config.service.ConfigService;
import xiaozhi.modules.sys.dto.ConfigSecretDTO;
import xiaozhi.modules.sys.service.SysParamsService;
/**
* xiaozhi-server 配置获取
*
* @since 1.0.0
*/
@RestController
@RequestMapping("config")
@Tag(name = "参数管理")
@AllArgsConstructor
public class ConfigController {
private final ConfigService configService;
private final SysParamsService sysParamsService;
@PostMapping("server-base")
@Operation(summary = "获取配置")
public Result<Object> getConfig(@RequestBody ConfigSecretDTO dto) {
// 效验数据
ValidatorUtils.validateEntity(dto);
checkSecret(dto.getSecret());
Object config = configService.getConfig(true);
return new Result<Object>().ok(config);
}
@PostMapping("agent-models")
@Operation(summary = "获取智能体模型")
public Result<Object> getAgentModels(@RequestBody AgentModelsDTO dto) {
// 效验数据
ValidatorUtils.validateEntity(dto);
checkSecret(dto.getSecret());
Object models = configService.getAgentModels(dto.getMacAddress(), dto.getSelectedModule());
return new Result<Object>().ok(models);
}
private void checkSecret(String secret) {
String secretParam = sysParamsService.getValue(Constant.SERVER_SECRET, true);
// 验证密钥
if (StringUtils.isBlank(secret) || !secret.equals(secretParam)) {
throw new RenException("密钥错误");
}
}
}
@@ -0,0 +1,28 @@
package xiaozhi.modules.config.dto;
import java.util.Map;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
@Schema(description = "获取智能体模型配置DTO")
public class AgentModelsDTO {
@NotBlank(message = "密钥不能为空")
@Schema(description = "密钥")
private String secret;
@NotBlank(message = "设备MAC地址不能为空")
@Schema(description = "设备MAC地址")
private String macAddress;
@NotBlank(message = "客户端ID不能为空")
@Schema(description = "客户端ID")
private String clientId;
@NotNull(message = "客户端已实例化的模型不能为空")
@Schema(description = "客户端已实例化的模型")
private Map<String, String> selectedModule;
}
@@ -1,10 +1,11 @@
package xiaozhi.modules.sys.config;
package xiaozhi.modules.config.init;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import jakarta.annotation.PostConstruct;
import xiaozhi.modules.config.service.ConfigService;
import xiaozhi.modules.sys.service.SysParamsService;
@Configuration
@@ -14,8 +15,12 @@ public class SystemInitConfig {
@Autowired
private SysParamsService sysParamsService;
@Autowired
private ConfigService configService;
@PostConstruct
public void init() {
sysParamsService.initServerSecret();
configService.getConfig(false);
}
}
@@ -0,0 +1,22 @@
package xiaozhi.modules.config.service;
import java.util.Map;
public interface ConfigService {
/**
* 获取服务器配置
*
* @param isCache 是否缓存
* @return 配置信息
*/
Object getConfig(Boolean isCache);
/**
* 获取智能体模型配置
*
* @param macAddress MAC地址
* @param selectedModule 客户端已实例化的模型
* @return 模型配置信息
*/
Map<String, Object> getAgentModels(String macAddress, Map<String, String> selectedModule);
}
@@ -0,0 +1,253 @@
package xiaozhi.modules.config.service.impl;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import lombok.AllArgsConstructor;
import xiaozhi.common.exception.RenException;
import xiaozhi.common.redis.RedisKeys;
import xiaozhi.common.redis.RedisUtils;
import xiaozhi.common.utils.JsonUtils;
import xiaozhi.modules.agent.entity.AgentEntity;
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.AgentTemplateService;
import xiaozhi.modules.config.service.ConfigService;
import xiaozhi.modules.device.entity.DeviceEntity;
import xiaozhi.modules.device.service.DeviceService;
import xiaozhi.modules.model.entity.ModelConfigEntity;
import xiaozhi.modules.model.service.ModelConfigService;
import xiaozhi.modules.sys.dto.SysParamsDTO;
import xiaozhi.modules.sys.service.SysParamsService;
import xiaozhi.modules.timbre.service.TimbreService;
import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
@Service
@AllArgsConstructor
public class ConfigServiceImpl implements ConfigService {
private final SysParamsService sysParamsService;
private final DeviceService deviceService;
private final ModelConfigService modelConfigService;
private final AgentService agentService;
private final AgentTemplateService agentTemplateService;
private final RedisUtils redisUtils;
private final TimbreService timbreService;
@Override
public Object getConfig(Boolean isCache) {
if (isCache) {
// 先从Redis获取配置
Object cachedConfig = redisUtils.get(RedisKeys.getServerConfigKey());
if (cachedConfig != null) {
return cachedConfig;
}
}
// 构建配置信息
Map<String, Object> result = new HashMap<>();
buildConfig(result);
// 查询默认智能体
AgentTemplateEntity agent = agentTemplateService.getDefaultTemplate();
if (agent == null) {
throw new RenException("默认智能体未找到");
}
// 构建模块配置
buildModuleConfig(
agent.getSystemPrompt(),
null,
agent.getVadModelId(),
agent.getAsrModelId(),
agent.getLlmModelId(),
agent.getTtsModelId(),
agent.getMemModelId(),
agent.getIntentModelId(),
result,
isCache);
// 将配置存入Redis
redisUtils.set(RedisKeys.getServerConfigKey(), result);
return result;
}
@Override
public Map<String, Object> getAgentModels(String macAddress, Map<String, String> selectedModule) {
// 根据MAC地址查找设备
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
if (device == null) {
throw new RenException("设备未找到");
}
// 获取智能体信息
AgentEntity agent = agentService.getAgentById(device.getAgentId());
if (agent == null) {
throw new RenException("智能体未找到");
}
// 获取音色信息
String voice = null;
TimbreDetailsVO timbre = timbreService.get(agent.getTtsVoiceId());
if (timbre != null) {
voice = timbre.getTtsVoice();
}
// 构建返回数据
Map<String, Object> result = new HashMap<>();
// 如果客户端已实例化模型,则不返回
String alreadySelectedVadModelId = (String) selectedModule.get("VAD");
if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) {
agent.setVadModelId(null);
}
String alreadySelectedAsrModelId = (String) selectedModule.get("ASR");
if (alreadySelectedAsrModelId != null && alreadySelectedAsrModelId.equals(agent.getAsrModelId())) {
agent.setAsrModelId(null);
}
String alreadySelectedLlmModelId = (String) selectedModule.get("LLM");
if (alreadySelectedLlmModelId != null && alreadySelectedLlmModelId.equals(agent.getLlmModelId())) {
agent.setLlmModelId(null);
}
String alreadySelectedMemModelId = (String) selectedModule.get("Memory");
if (alreadySelectedMemModelId != null && alreadySelectedMemModelId.equals(agent.getMemModelId())) {
agent.setMemModelId(null);
}
String alreadySelectedIntentModelId = (String) selectedModule.get("Intent");
if (alreadySelectedIntentModelId != null && alreadySelectedIntentModelId.equals(agent.getIntentModelId())) {
agent.setIntentModelId(null);
}
// 构建模块配置
buildModuleConfig(
agent.getSystemPrompt(),
voice,
agent.getVadModelId(),
agent.getAsrModelId(),
agent.getLlmModelId(),
agent.getTtsModelId(),
agent.getMemModelId(),
agent.getIntentModelId(),
result,
true);
return result;
}
/**
* 构建配置信息
*
* @param paramsList 系统参数列表
* @return 配置信息
*/
@SuppressWarnings("unchecked")
private Object buildConfig(Map<String, Object> config) {
// 查询所有系统参数
List<SysParamsDTO> paramsList = sysParamsService.list(new HashMap<>());
for (SysParamsDTO param : paramsList) {
String[] keys = param.getParamCode().split("\\.");
Map<String, Object> current = config;
// 遍历除最后一个key之外的所有key
for (int i = 0; i < keys.length - 1; i++) {
String key = keys[i];
if (!current.containsKey(key)) {
current.put(key, new HashMap<String, Object>());
}
current = (Map<String, Object>) current.get(key);
}
// 处理最后一个key
String lastKey = keys[keys.length - 1];
String value = param.getParamValue();
// 根据valueType转换值
switch (param.getValueType().toLowerCase()) {
case "number":
try {
current.put(lastKey, Double.parseDouble(value));
} catch (NumberFormatException e) {
current.put(lastKey, value);
}
break;
case "boolean":
current.put(lastKey, Boolean.parseBoolean(value));
break;
case "array":
// 将分号分隔的字符串转换为数字数组
List<String> list = new ArrayList<>();
for (String num : value.split(";")) {
if (StringUtils.isNotBlank(num)) {
list.add(num.trim());
}
}
current.put(lastKey, list);
break;
case "json":
try {
current.put(lastKey, JsonUtils.parseObject(value, Object.class));
} catch (Exception e) {
current.put(lastKey, value);
}
break;
default:
current.put(lastKey, value);
}
}
return config;
}
/**
* 构建模块配置
*
* @param prompt 提示词
* @param voice 音色
* @param vadModelId VAD模型ID
* @param asrModelId ASR模型ID
* @param llmModelId LLM模型ID
* @param ttsModelId TTS模型ID
* @param memModelId 记忆模型ID
* @param intentModelId 意图模型ID
* @param result 结果Map
*/
private void buildModuleConfig(
String prompt,
String voice,
String vadModelId,
String asrModelId,
String llmModelId,
String ttsModelId,
String memModelId,
String intentModelId,
Map<String, Object> result,
boolean isCache) {
Map<String, String> selectedModule = new HashMap<>();
String[] modelTypes = { "VAD", "ASR", "LLM", "TTS", "Memory", "Intent" };
String[] modelIds = { vadModelId, asrModelId, llmModelId, ttsModelId, memModelId, intentModelId };
for (int i = 0; i < modelIds.length; i++) {
if (modelIds[i] == null) {
continue;
}
ModelConfigEntity model = modelConfigService.getModelById(modelIds[i], isCache);
Map<String, Object> typeConfig = new HashMap<>();
if (model.getConfigJson() != null) {
typeConfig.put(model.getId(), model.getConfigJson());
// 如果是TTS类型,添加private_voice属性
if ("TTS".equals(modelTypes[i]) && voice != null) {
((Map<String, Object>) model.getConfigJson()).put("private_voice", voice);
}
}
result.put(modelTypes[i], typeConfig);
selectedModule.put(modelTypes[i], model.getId());
}
result.put("selected_module", selectedModule);
result.put("prompt", prompt);
}
}
@@ -66,4 +66,12 @@ public interface DeviceService {
* @return 用户列表分页数据
*/
PageData<UserShowDeviceListVO> page(DevicePageUserDTO dto);
/**
* 根据MAC地址获取设备信息
*
* @param macAddress MAC地址
* @return 设备信息
*/
DeviceEntity getDeviceByMacAddress(String macAddress);
}
@@ -240,6 +240,16 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
return new PageData<>(list, page.getTotal());
}
@Override
public DeviceEntity getDeviceByMacAddress(String macAddress) {
if (StringUtils.isBlank(macAddress)) {
return null;
}
QueryWrapper<DeviceEntity> wrapper = new QueryWrapper<>();
wrapper.eq("mac_address", macAddress);
return baseDao.selectOne(wrapper);
}
private DeviceReportRespDTO.ServerTime buildServerTime() {
DeviceReportRespDTO.ServerTime serverTime = new DeviceReportRespDTO.ServerTime();
TimeZone tz = TimeZone.getDefault();
@@ -19,6 +19,8 @@ import lombok.AllArgsConstructor;
import xiaozhi.common.page.PageData;
import xiaozhi.common.utils.ConvertUtils;
import xiaozhi.common.utils.Result;
import xiaozhi.modules.agent.service.AgentTemplateService;
import xiaozhi.modules.config.service.ConfigService;
import xiaozhi.modules.model.dto.ModelBasicInfoDTO;
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
import xiaozhi.modules.model.dto.ModelConfigDTO;
@@ -38,6 +40,8 @@ public class ModelController {
private final ModelProviderService modelProviderService;
private final TimbreService timbreService;
private final ModelConfigService modelConfigService;
private final ConfigService configService;
private final AgentTemplateService agentTemplateService;
@GetMapping("/names")
@Operation(summary = "获取所有模型名称")
@@ -75,6 +79,7 @@ public class ModelController {
@PathVariable String provideCode,
@RequestBody ModelConfigBodyDTO modelConfigBodyDTO) {
ModelConfigDTO modelConfigDTO = modelConfigService.add(modelType, provideCode, modelConfigBodyDTO);
configService.getConfig(false);
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
}
@@ -86,6 +91,7 @@ public class ModelController {
@PathVariable String id,
@RequestBody ModelConfigBodyDTO modelConfigBodyDTO) {
ModelConfigDTO modelConfigDTO = modelConfigService.edit(modelType, provideCode, id, modelConfigBodyDTO);
configService.getConfig(false);
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
}
@@ -119,6 +125,26 @@ public class ModelController {
return new Result<Void>();
}
@PutMapping("/default/{id}")
@Operation(summary = "设置默认模型")
@RequiresPermissions("sys:role:superAdmin")
public Result<Void> setDefaultModel(@PathVariable String id) {
ModelConfigEntity entity = modelConfigService.selectById(id);
if (entity == null) {
return new Result<Void>().error("模型配置不存在");
}
// 将其他模型设置为非默认
modelConfigService.setDefaultModel(entity.getModelType(), 0);
entity.setIsDefault(1);
modelConfigService.updateById(entity);
// 更新模板表中对应的模型ID
agentTemplateService.updateDefaultTemplateModelId(entity.getModelType(), entity.getId());
configService.getConfig(false);
return new Result<Void>();
}
@GetMapping("/{modelId}/voices")
@Operation(summary = "获取模型音色")
@RequiresPermissions("sys:role:normal")
@@ -28,4 +28,21 @@ public interface ModelConfigService extends BaseService<ModelConfigEntity> {
* @return 模型名称
*/
String getModelNameById(String id);
/**
* 根据ID获取模型配置
*
* @param id 模型ID
* @param isCache 是否缓存
* @return 模型配置实体
*/
ModelConfigEntity getModelById(String id, boolean isCache);
/**
* 设置默认模型
*
* @param modelType 模型类型
* @param isDefault 是否默认
*/
void setDefaultModel(String modelType, int isDefault);
}
@@ -94,6 +94,8 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
modelConfigEntity.setId(id);
modelConfigEntity.setModelType(modelType);
modelConfigDao.updateById(modelConfigEntity);
// 清除缓存
redisUtils.delete(RedisKeys.getModelConfigById(modelConfigEntity.getId()));
return ConvertUtils.sourceToTarget(modelConfigEntity, ModelConfigDTO.class);
}
@@ -125,4 +127,30 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
return null;
}
@Override
public ModelConfigEntity getModelById(String id, boolean isCache) {
if (StringUtils.isBlank(id)) {
return null;
}
if (isCache) {
ModelConfigEntity cachedConfig = (ModelConfigEntity) redisUtils.get(RedisKeys.getModelConfigById(id));
if (cachedConfig != null) {
return ConvertUtils.sourceToTarget(cachedConfig, ModelConfigEntity.class);
}
}
ModelConfigEntity entity = modelConfigDao.selectById(id);
if (entity != null) {
redisUtils.set(RedisKeys.getModelConfigById(id), entity);
}
return entity;
}
@Override
public void setDefaultModel(String modelType, int isDefault) {
ModelConfigEntity entity = new ModelConfigEntity();
entity.setIsDefault(isDefault);
modelConfigDao.update(entity, new QueryWrapper<ModelConfigEntity>()
.eq("model_type", modelType));
}
}
@@ -77,7 +77,8 @@ public class ShiroConfig {
filterMap.put("/user/captcha", "anon");
filterMap.put("/user/login", "anon");
filterMap.put("/user/register", "anon");
filterMap.put("/device/register", "anon");
filterMap.put("/config/server-base", "anon");
filterMap.put("/config/agent-models", "anon");
filterMap.put("/**", "oauth2");
shiroFilter.setFilterChainDefinitionMap(filterMap);
@@ -0,0 +1,13 @@
package xiaozhi.modules.sys.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Schema(description = "配置密钥DTO")
public class ConfigSecretDTO {
@Schema(description = "密钥")
@NotBlank(message = "密钥不能为空")
private String secret;
}
@@ -125,6 +125,13 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
throw new RenException(ErrorCode.PARAM_BOOLEAN_INVALID);
}
break;
case "json":
try {
JsonUtils.parseObject(paramValue, Object.class);
} catch (Exception e) {
throw new RenException(ErrorCode.PARAM_JSON_INVALID);
}
break;
default:
throw new RenException(ErrorCode.PARAM_TYPE_INVALID);
}
@@ -31,7 +31,7 @@ public interface TimbreService extends BaseService<TimbreEntity> {
* @param timbreId 音色表id
* @return 音色信息
*/
TimbreDetailsVO get(Long timbreId);
TimbreDetailsVO get(String timbreId);
/**
* 保存音色信息
@@ -59,9 +59,33 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
}
@Override
public TimbreDetailsVO get(Long timbreId) {
public TimbreDetailsVO get(String timbreId) {
if (StringUtils.isBlank(timbreId)) {
return null;
}
// 先从Redis获取缓存
String key = RedisKeys.getTimbreDetailsKey(timbreId);
TimbreDetailsVO cachedDetails = (TimbreDetailsVO) redisUtils.get(key);
if (cachedDetails != null) {
return cachedDetails;
}
// 如果缓存中没有,则从数据库获取
TimbreEntity entity = baseDao.selectById(timbreId);
return ConvertUtils.sourceToTarget(entity, TimbreDetailsVO.class);
if (entity == null) {
return null;
}
// 转换为VO对象
TimbreDetailsVO details = ConvertUtils.sourceToTarget(entity, TimbreDetailsVO.class);
// 存入Redis缓存
if (details != null) {
redisUtils.set(key, details);
}
return details;
}
@Override
@@ -79,6 +103,8 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
TimbreEntity timbreEntity = ConvertUtils.sourceToTarget(dto, TimbreEntity.class);
timbreEntity.setId(timbreId);
baseDao.updateById(timbreEntity);
// 删除缓存
redisUtils.delete(RedisKeys.getTimbreDetailsKey(timbreId));
}
@Override
@@ -4,50 +4,52 @@
DELETE FROM `ai_model_config`;
-- VAD模型配置
INSERT INTO `ai_model_config` VALUES ('VAD_SileroVAD', 'VAD', 'SileroVAD', '语音活动检测', 1, 1, '{\"type\": \"silero\", \"model_dir\": \"models/snakers4_silero-vad\", \"threshold\": 0.5, \"min_silence_duration_ms\": 700}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('VAD_SileroVAD', 'VAD', 'SileroVAD', '语音活动检测', 1, 1, '{\"type\": \"silero\", \"model_dir\": \"models/snakers4_silero-vad\", \"threshold\": 0.5, \"min_silence_duration_ms\": 700}', NULL, NULL, 1, NULL, NULL, NULL, NULL);
-- ASR模型配置
INSERT INTO `ai_model_config` VALUES ('ASR_FunASR', 'ASR', 'FunASR', 'FunASR语音识别', 1, 1, '{\"type\": \"fun_local\", \"model_dir\": \"models/SenseVoiceSmall\", \"output_dir\": \"tmp/\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('ASR_SherpaASR', 'ASR', 'SherpaASR', 'Sherpa语音识别', 1, 0, '{\"type\": \"sherpa_onnx_local\", \"model_dir\": \"models/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17\", \"output_dir\": \"tmp/\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('ASR_DoubaoASR', 'ASR', 'DoubaoASR', '豆包语音识别', 1, 0, '{\"type\": \"doubao\", \"appid\": \"\", \"access_token\": \"\", \"cluster\": \"volcengine_input_common\", \"output_dir\": \"tmp/\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('ASR_FunASR', 'ASR', 'FunASR', 'FunASR语音识别', 1, 1, '{\"type\": \"fun_local\", \"model_dir\": \"models/SenseVoiceSmall\", \"output_dir\": \"tmp/\"}', NULL, NULL, 1, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('ASR_SherpaASR', 'ASR', 'SherpaASR', 'Sherpa语音识别', 0, 1, '{\"type\": \"sherpa_onnx_local\", \"model_dir\": \"models/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17\", \"output_dir\": \"tmp/\"}', NULL, NULL, 2, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('ASR_DoubaoASR', 'ASR', 'DoubaoASR', '豆包语音识别', 0, 1, '{\"type\": \"doubao\", \"appid\": \"\", \"access_token\": \"\", \"cluster\": \"volcengine_input_common\", \"output_dir\": \"tmp/\"}', NULL, NULL, 3, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('ASR_TencentASR', 'ASR', 'TencentASR', '腾讯语音识别', 0, 1, '{\"type\": \"tencent\", \"appid\": \"\", \"secret_id\": \"\", \"secret_key\": \"你的secret_key\", \"output_dir\": \"tmp/\"}', NULL, NULL, 4, NULL, NULL, NULL, NULL);
-- LLM模型配置
INSERT INTO `ai_model_config` VALUES ('LLM_AliLLM', 'LLM', 'AliLLM', '通义千问', 1, 0, '{\"type\": \"openai\", \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\", \"model_name\": \"qwen-turbo\", \"api_key\": \"\", \"temperature\": 0.7, \"max_tokens\": 500, \"top_p\": 1, \"top_k\": 50, \"frequency_penalty\": 0}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_AliAppLLM', 'LLM', 'AliAppLLM', '通义百炼', 1, 0, '{\"type\": \"AliBL\", \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\", \"app_id\": \"\", \"api_key\": \"\", \"is_no_prompt\": true, \"ali_memory_id\": false}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_DoubaoLLM', 'LLM', 'DoubaoLLM', '豆包大模型', 1, 0, '{\"type\": \"openai\", \"base_url\": \"https://ark.cn-beijing.volces.com/api/v3\", \"model_name\": \"doubao-pro-32k-functioncall-241028\", \"api_key\": \"\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_DeepSeekLLM', 'LLM', 'DeepSeekLLM', 'DeepSeek', 1, 0, '{\"type\": \"openai\", \"model_name\": \"deepseek-chat\", \"url\": \"https://api.deepseek.com\", \"api_key\": \"\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_ChatGLMLLM', 'LLM', 'ChatGLMLLM', '智谱AI', 1, 1, '{\"type\": \"openai\", \"model_name\": \"glm-4-flash\", \"url\": \"https://open.bigmodel.cn/api/paas/v4/\", \"api_key\": \"\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_OllamaLLM', 'LLM', 'OllamaLLM', 'Ollama本地模型', 1, 0, '{\"type\": \"ollama\", \"model_name\": \"qwen2.5\", \"base_url\": \"http://localhost:11434\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_DifyLLM', 'LLM', 'DifyLLM', 'Dify', 1, 0, '{\"type\": \"dify\", \"base_url\": \"https://api.dify.ai/v1\", \"api_key\": \"\", \"mode\": \"chat-messages\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_GeminiLLM', 'LLM', 'GeminiLLM', '谷歌Gemini', 1, 0, '{\"type\": \"gemini\", \"api_key\": \"\", \"model_name\": \"gemini-2.0-flash\", \"http_proxy\": \"\", \"https_proxy\": \"\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_CozeLLM', 'LLM', 'CozeLLM', 'Coze', 1, 0, '{\"type\": \"coze\", \"bot_id\": \"\", \"user_id\": \"\", \"personal_access_token\": \"\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_LMStudioLLM', 'LLM', 'LMStudioLLM', 'LM Studio', 1, 0, '{\"type\": \"openai\", \"model_name\": \"deepseek-r1-distill-llama-8b@q4_k_m\", \"url\": \"http://localhost:1234/v1\", \"api_key\": \"lm-studio\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_FastgptLLM', 'LLM', 'FastgptLLM', 'FastGPT', 1, 0, '{\"type\": \"fastgpt\", \"base_url\": \"https://host/api/v1\", \"api_key\": \"fastgpt-xxx\", \"variables\": {\"k\": \"v\", \"k2\": \"v2\"}}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_XinferenceLLM', 'LLM', 'XinferenceLLM', 'Xinference大模型', 1, 0, '{\"type\": \"xinference\", \"model_name\": \"qwen2.5:72b-AWQ\", \"base_url\": \"http://localhost:9997\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_XinferenceSmallLLM', 'LLM', 'XinferenceSmallLLM', 'Xinference小模型', 1, 0, '{\"type\": \"xinference\", \"model_name\": \"qwen2.5:3b-AWQ\", \"base_url\": \"http://localhost:9997\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_ChatGLMLLM', 'LLM', 'ChatGLMLLM', '智谱AI', 1, 1, '{\"type\": \"openai\", \"model_name\": \"glm-4-flash\", \"base_url\": \"https://open.bigmodel.cn/api/paas/v4/\", \"api_key\": \"你的api_key\"}', NULL, NULL, 1, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_OllamaLLM', 'LLM', 'OllamaLLM', 'Ollama本地模型', 0, 1, '{\"type\": \"ollama\", \"model_name\": \"qwen2.5\", \"base_url\": \"http://localhost:11434\"}', NULL, NULL, 2, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_AliLLM', 'LLM', 'AliLLM', '通义千问', 0, 1, '{\"type\": \"openai\", \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\", \"model_name\": \"qwen-turbo\", \"api_key\": \"你的api_key\", \"temperature\": 0.7, \"max_tokens\": 500, \"top_p\": 1, \"top_k\": 50, \"frequency_penalty\": 0}', NULL, NULL, 3, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_AliAppLLM', 'LLM', 'AliAppLLM', '通义百炼', 0, 1, '{\"type\": \"AliBL\", \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\", \"app_id\": \"你的app_id\", \"api_key\": \"你的api_key\", \"is_no_prompt\": true, \"ali_memory_id\": false}', NULL, NULL, 4, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_DoubaoLLM', 'LLM', 'DoubaoLLM', '豆包大模型', 0, 1, '{\"type\": \"openai\", \"base_url\": \"https://ark.cn-beijing.volces.com/api/v3\", \"model_name\": \"doubao-pro-32k-functioncall-241028\", \"api_key\": \"你的api_key\"}', NULL, NULL, 5, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_DeepSeekLLM', 'LLM', 'DeepSeekLLM', 'DeepSeek', 0, 1, '{\"type\": \"openai\", \"model_name\": \"deepseek-chat\", \"base_url\": \"https://api.deepseek.com\", \"api_key\": \"你的api_key\"}', NULL, NULL, 6, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_DifyLLM', 'LLM', 'DifyLLM', 'Dify', 0, 1, '{\"type\": \"dify\", \"base_url\": \"https://api.dify.ai/v1\", \"api_key\": \"你的api_key\", \"mode\": \"chat-messages\"}', NULL, NULL, 7, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_GeminiLLM', 'LLM', 'GeminiLLM', '谷歌Gemini', 0, 1, '{\"type\": \"gemini\", \"api_key\": \"你的api_key\", \"model_name\": \"gemini-2.0-flash\", \"http_proxy\": \"\", \"https_proxy\": \"\"}', NULL, NULL, 8, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_CozeLLM', 'LLM', 'CozeLLM', 'Coze', 0, 1, '{\"type\": \"coze\", \"bot_id\": \"你的bot_id\", \"user_id\": \"你的user_id\", \"personal_access_token\": \"你的personal_access_token\"}', NULL, NULL, 9, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_LMStudioLLM', 'LLM', 'LMStudioLLM', 'LM Studio', 0, 1, '{\"type\": \"openai\", \"model_name\": \"deepseek-r1-distill-llama-8b@q4_k_m\", \"base_url\": \"http://localhost:1234/v1\", \"api_key\": \"lm-studio\"}', NULL, NULL, 10, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_FastgptLLM', 'LLM', 'FastgptLLM', 'FastGPT', 0, 1, '{\"type\": \"fastgpt\", \"base_url\": \"https://host/api/v1\", \"api_key\": \"fastgpt-xxx\", \"variables\": {\"k\": \"v\", \"k2\": \"v2\"}}', NULL, NULL, 11, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_XinferenceLLM', 'LLM', 'XinferenceLLM', 'Xinference大模型', 0, 1, '{\"type\": \"xinference\", \"model_name\": \"qwen2.5:72b-AWQ\", \"base_url\": \"http://localhost:9997\"}', NULL, NULL, 12, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('LLM_XinferenceSmallLLM', 'LLM', 'XinferenceSmallLLM', 'Xinference小模型', 0, 1, '{\"type\": \"xinference\", \"model_name\": \"qwen2.5:3b-AWQ\", \"base_url\": \"http://localhost:9997\"}', NULL, NULL, 13, NULL, NULL, NULL, NULL);
-- TTS模型配置
INSERT INTO `ai_model_config` VALUES ('TTS_EdgeTTS', 'TTS', 'EdgeTTS', 'Edge语音合成', 1, 1, '{\"type\": \"edge\", \"voice\": \"zh-CN-XiaoxiaoNeural\", \"output_dir\": \"tmp/\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_DoubaoTTS', 'TTS', 'DoubaoTTS', '豆包语音合成', 1, 0, '{\"type\": \"doubao\", \"api_url\": \"https://openspeech.bytedance.com/api/v1/tts\", \"voice\": \"BV001_streaming\", \"output_dir\": \"tmp/\", \"authorization\": \"Bearer;\", \"appid\": \"\", \"access_token\": \"\", \"cluster\": \"volcano_tts\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_CosyVoiceSiliconflow', 'TTS', 'CosyVoiceSiliconflow', '硅基流动语音合成', 1, 0, '{\"type\": \"siliconflow\", \"model\": \"FunAudioLLM/CosyVoice2-0.5B\", \"voice\": \"FunAudioLLM/CosyVoice2-0.5B:alex\", \"output_dir\": \"tmp/\", \"access_token\": \"\", \"response_format\": \"wav\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_CozeCnTTS', 'TTS', 'CozeCnTTS', 'Coze中文语音合成', 1, 0, '{\"type\": \"cozecn\", \"voice\": \"7426720361733046281\", \"output_dir\": \"tmp/\", \"access_token\": \"\", \"response_format\": \"wav\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_FishSpeech', 'TTS', 'FishSpeech', 'FishSpeech语音合成', 1, 0, '{\"type\": \"fishspeech\", \"output_dir\": \"tmp/\", \"response_format\": \"wav\", \"reference_id\": null, \"reference_audio\": [\"/tmp/test.wav\"], \"reference_text\": [\"你弄来这些吟词宴曲来看,还是这些混话来欺负我。\"], \"normalize\": true, \"max_new_tokens\": 1024, \"chunk_length\": 200, \"top_p\": 0.7, \"repetition_penalty\": 1.2, \"temperature\": 0.7, \"streaming\": false, \"use_memory_cache\": \"on\", \"seed\": null, \"channels\": 1, \"rate\": 44100, \"api_key\": \"\", \"api_url\": \"http://127.0.0.1:8080/v1/tts\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_GPT_SOVITS_V2', 'TTS', 'GPT_SOVITS_V2', 'GPT-SoVITS V2', 1, 0, '{\"type\": \"gpt_sovits_v2\", \"url\": \"http://127.0.0.1:9880/tts\", \"output_dir\": \"tmp/\", \"text_lang\": \"auto\", \"ref_audio_path\": \"caixukun.wav\", \"prompt_text\": \"\", \"prompt_lang\": \"zh\", \"top_k\": 5, \"top_p\": 1, \"temperature\": 1, \"text_split_method\": \"cut0\", \"batch_size\": 1, \"batch_threshold\": 0.75, \"split_bucket\": true, \"return_fragment\": false, \"speed_factor\": 1.0, \"streaming_mode\": false, \"seed\": -1, \"parallel_infer\": true, \"repetition_penalty\": 1.35, \"aux_ref_audio_paths\": []}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_GPT_SOVITS_V3', 'TTS', 'GPT_SOVITS_V3', 'GPT-SoVITS V3', 1, 0, '{\"type\": \"gpt_sovits_v3\", \"url\": \"http://127.0.0.1:9880\", \"output_dir\": \"tmp/\", \"text_language\": \"auto\", \"refer_wav_path\": \"caixukun.wav\", \"prompt_language\": \"zh\", \"prompt_text\": \"\", \"top_k\": 15, \"top_p\": 1.0, \"temperature\": 1.0, \"cut_punc\": \"\", \"speed\": 1.0, \"inp_refs\": [], \"sample_steps\": 32, \"if_sr\": false}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_MinimaxTTS', 'TTS', 'MinimaxTTS', 'MiniMax语音合成', 1, 0, '{\"type\": \"minimax\", \"output_dir\": \"tmp/\", \"group_id\": \"\", \"api_key\": \"\", \"model\": \"speech-01-turbo\", \"voice_id\": \"female-shaonv\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_AliyunTTS', 'TTS', 'AliyunTTS', '阿里云语音合成', 1, 0, '{\"type\": \"aliyun\", \"output_dir\": \"tmp/\", \"appkey\": \"\", \"token\": \"\", \"voice\": \"xiaoyun\", \"access_key_id\": \"\", \"access_key_secret\": \"\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_TTS302AI', 'TTS', 'TTS302AI', '302AI语音合成', 1, 0, '{\"type\": \"doubao\", \"api_url\": \"https://api.302ai.cn/doubao/tts_hd\", \"authorization\": \"Bearer \", \"voice\": \"zh_female_wanwanxiaohe_moon_bigtts\", \"output_dir\": \"tmp/\", \"access_token\": \"\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_GizwitsTTS', 'TTS', 'GizwitsTTS', '机智云语音合成', 1, 0, '{\"type\": \"doubao\", \"api_url\": \"https://bytedance.gizwitsapi.com/api/v1/tts\", \"authorization\": \"Bearer \", \"voice\": \"zh_female_wanwanxiaohe_moon_bigtts\", \"output_dir\": \"tmp/\", \"access_token\": \"\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_ACGNTTS', 'TTS', 'ACGNTTS', 'ACGN语音合成', 1, 0, '{\"type\": \"ttson\", \"token\": \"\", \"voice_id\": 1695, \"speed_factor\": 1, \"pitch_factor\": 0, \"volume_change_dB\": 0, \"to_lang\": \"ZH\", \"url\": \"https://u95167-bd74-2aef8085.westx.seetacloud.com:8443/flashsummary/tts?token=\", \"format\": \"mp3\", \"output_dir\": \"tmp/\", \"emotion\": 1}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_OpenAITTS', 'TTS', 'OpenAITTS', 'OpenAI语音合成', 1, 0, '{\"type\": \"openai\", \"api_key\": \"\", \"api_url\": \"https://api.openai.com/v1/audio/speech\", \"model\": \"tts-1\", \"voice\": \"onyx\", \"speed\": 1, \"output_dir\": \"tmp/\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_CustomTTS', 'TTS', 'CustomTTS', '自定义语音合成', 1, 0, '{\"type\": \"custom\", \"url\": \"http://127.0.0.1:9880/tts\", \"params\": {}, \"headers\": {}, \"format\": \"wav\", \"output_dir\": \"tmp/\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_EdgeTTS', 'TTS', 'EdgeTTS', 'Edge语音合成', 1, 1, '{\"type\": \"edge\", \"voice\": \"zh-CN-XiaoxiaoNeural\", \"output_dir\": \"tmp/\"}', NULL, NULL, 1, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_DoubaoTTS', 'TTS', 'DoubaoTTS', '豆包语音合成', 0, 1, '{\"type\": \"doubao\", \"api_url\": \"https://openspeech.bytedance.com/api/v1/tts\", \"voice\": \"BV001_streaming\", \"output_dir\": \"tmp/\", \"authorization\": \"Bearer;\", \"appid\": \"\", \"access_token\": \"\", \"cluster\": \"volcano_tts\"}', NULL, NULL, 2, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_CosyVoiceSiliconflow', 'TTS', 'CosyVoiceSiliconflow', '硅基流动语音合成', 0, 1, '{\"type\": \"siliconflow\", \"model\": \"FunAudioLLM/CosyVoice2-0.5B\", \"voice\": \"FunAudioLLM/CosyVoice2-0.5B:alex\", \"output_dir\": \"tmp/\", \"access_token\": \"\", \"response_format\": \"wav\"}', NULL, NULL, 3, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_CozeCnTTS', 'TTS', 'CozeCnTTS', 'Coze中文语音合成', 0, 1, '{\"type\": \"cozecn\", \"voice\": \"7426720361733046281\", \"output_dir\": \"tmp/\", \"access_token\": \"\", \"response_format\": \"wav\"}', NULL, NULL, 4, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_FishSpeech', 'TTS', 'FishSpeech', 'FishSpeech语音合成', 0, 1, '{\"type\": \"fishspeech\", \"output_dir\": \"tmp/\", \"response_format\": \"wav\", \"reference_id\": null, \"reference_audio\": [\"/tmp/test.wav\"], \"reference_text\": [\"你弄来这些吟词宴曲来看,还是这些混话来欺负我。\"], \"normalize\": true, \"max_new_tokens\": 1024, \"chunk_length\": 200, \"top_p\": 0.7, \"repetition_penalty\": 1.2, \"temperature\": 0.7, \"streaming\": false, \"use_memory_cache\": \"on\", \"seed\": null, \"channels\": 1, \"rate\": 44100, \"api_key\": \"\", \"api_url\": \"http://127.0.0.1:8080/v1/tts\"}', NULL, NULL, 5, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_GPT_SOVITS_V2', 'TTS', 'GPT_SOVITS_V2', 'GPT-SoVITS V2', 0, 1, '{\"type\": \"gpt_sovits_v2\", \"url\": \"http://127.0.0.1:9880/tts\", \"output_dir\": \"tmp/\", \"text_lang\": \"auto\", \"ref_audio_path\": \"caixukun.wav\", \"prompt_text\": \"\", \"prompt_lang\": \"zh\", \"top_k\": 5, \"top_p\": 1, \"temperature\": 1, \"text_split_method\": \"cut0\", \"batch_size\": 1, \"batch_threshold\": 0.75, \"split_bucket\": true, \"return_fragment\": false, \"speed_factor\": 1.0, \"streaming_mode\": false, \"seed\": -1, \"parallel_infer\": true, \"repetition_penalty\": 1.35, \"aux_ref_audio_paths\": []}', NULL, NULL, 6, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_GPT_SOVITS_V3', 'TTS', 'GPT_SOVITS_V3', 'GPT-SoVITS V3', 0, 1, '{\"type\": \"gpt_sovits_v3\", \"url\": \"http://127.0.0.1:9880\", \"output_dir\": \"tmp/\", \"text_language\": \"auto\", \"refer_wav_path\": \"caixukun.wav\", \"prompt_language\": \"zh\", \"prompt_text\": \"\", \"top_k\": 15, \"top_p\": 1.0, \"temperature\": 1.0, \"cut_punc\": \"\", \"speed\": 1.0, \"inp_refs\": [], \"sample_steps\": 32, \"if_sr\": false}', NULL, NULL, 7, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_MinimaxTTS', 'TTS', 'MinimaxTTS', 'MiniMax语音合成', 0, 1, '{\"type\": \"minimax\", \"output_dir\": \"tmp/\", \"group_id\": \"\", \"api_key\": \"你的api_key\", \"model\": \"speech-01-turbo\", \"voice_id\": \"female-shaonv\"}', NULL, NULL, 8, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_AliyunTTS', 'TTS', 'AliyunTTS', '阿里云语音合成', 0, 1, '{\"type\": \"aliyun\", \"output_dir\": \"tmp/\", \"appkey\": \"\", \"token\": \"\", \"voice\": \"xiaoyun\", \"access_key_id\": \"\", \"access_key_secret\": \"\"}', NULL, NULL, 9, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_TTS302AI', 'TTS', 'TTS302AI', '302AI语音合成', 0, 1, '{\"type\": \"doubao\", \"api_url\": \"https://api.302ai.cn/doubao/tts_hd\", \"authorization\": \"Bearer \", \"voice\": \"zh_female_wanwanxiaohe_moon_bigtts\", \"output_dir\": \"tmp/\", \"access_token\": \"\"}', NULL, NULL, 10, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_GizwitsTTS', 'TTS', 'GizwitsTTS', '机智云语音合成', 0, 1, '{\"type\": \"doubao\", \"api_url\": \"https://bytedance.gizwitsapi.com/api/v1/tts\", \"authorization\": \"Bearer \", \"voice\": \"zh_female_wanwanxiaohe_moon_bigtts\", \"output_dir\": \"tmp/\", \"access_token\": \"\"}', NULL, NULL, 11, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_ACGNTTS', 'TTS', 'ACGNTTS', 'ACGN语音合成', 0, 1, '{\"type\": \"ttson\", \"token\": \"\", \"voice_id\": 1695, \"speed_factor\": 1, \"pitch_factor\": 0, \"volume_change_dB\": 0, \"to_lang\": \"ZH\", \"url\": \"https://u95167-bd74-2aef8085.westx.seetacloud.com:8443/flashsummary/tts?token=\", \"format\": \"mp3\", \"output_dir\": \"tmp/\", \"emotion\": 1}', NULL, NULL, 12, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_OpenAITTS', 'TTS', 'OpenAITTS', 'OpenAI语音合成', 0, 1, '{\"type\": \"openai\", \"api_key\": \"你的api_key\", \"api_url\": \"https://api.openai.com/v1/audio/speech\", \"model\": \"tts-1\", \"voice\": \"onyx\", \"speed\": 1, \"output_dir\": \"tmp/\"}', NULL, NULL, 13, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_CustomTTS', 'TTS', 'CustomTTS', '自定义语音合成', 0, 1, '{\"type\": \"custom\", \"url\": \"http://127.0.0.1:9880/tts\", \"params\": {}, \"headers\": {}, \"format\": \"wav\", \"output_dir\": \"tmp/\"}', NULL, NULL, 14, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('TTS_TencentTTS', 'TTS', 'TencentTTS', '腾讯语音合成', 0, 1, '{\"type\": \"tencent\", \"appid\": \"\", \"secret_id\": \"\", \"secret_key\": \"\", \"region\": \"ap-guangzhou\", \"voice\": \"101001\", \"output_dir\": \"tmp/\"}', NULL, NULL, 3, NULL, NULL, NULL, NULL);
-- Memory模型配置
INSERT INTO `ai_model_config` VALUES ('Memory_mem0ai', 'Memory', 'mem0ai', 'Mem0AI记忆', 1, 0, '{\"type\": \"mem0ai\", \"api_key\": \"\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('Memory_nomem', 'Memory', 'nomem', '记忆', 1, 1, '{\"type\": \"nomem\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('Memory_mem_local_short', 'Memory', 'mem_local_short', '本地短期记忆', 1, 0, '{\"type\": \"mem_local_short\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('Memory_nomem', 'Memory', 'nomem', '记忆', 1, 1, '{\"type\": \"nomem\"}', NULL, NULL, 1, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('Memory_mem_local_short', 'Memory', 'mem_local_short', '本地短期记忆', 0, 1, '{\"type\": \"mem_local_short\"}', NULL, NULL, 2, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('Memory_mem0ai', 'Memory', 'mem0ai', 'Mem0AI记忆', 0, 1, '{\"type\": \"mem0ai\", \"api_key\": \"你的api_key\"}', NULL, NULL, 3, NULL, NULL, NULL, NULL);
-- Intent模型配置
INSERT INTO `ai_model_config` VALUES ('Intent_nointent', 'Intent', 'nointent', '无意图识别', 1, 0, '{\"type\": \"nointent\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('Intent_intent_llm', 'Intent', 'intent_llm', 'LLM意图识别', 1, 0, '{\"type\": \"intent_llm\", \"llm\": \"ChatGLMLLM\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('Intent_function_call', 'Intent', 'function_call', '函数调用意图识别', 1, 1, '{\"type\": \"function_call\", \"functions\": [\"change_role\", \"get_weather\", \"get_news\", \"play_music\"]}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('Intent_nointent', 'Intent', 'nointent', '无意图识别', 1, 0, '{\"type\": \"nointent\"}', NULL, NULL, 1, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('Intent_intent_llm', 'Intent', 'intent_llm', 'LLM意图识别', 0, 1, '{\"type\": \"intent_llm\", \"llm\": \"ChatGLMLLM\"}', NULL, NULL, 2, NULL, NULL, NULL, NULL);
INSERT INTO `ai_model_config` VALUES ('Intent_function_call', 'Intent', 'function_call', '函数调用意图识别', 0, 1, '{\"type\": \"function_call\", \"functions\": [\"change_role\", \"get_weather\", \"get_news\", \"play_music\"]}', NULL, NULL, 3, NULL, NULL, NULL, NULL);
@@ -23,7 +23,7 @@ create table sys_params
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (100, 'server.ip', '0.0.0.0', 'string', 1, '服务器监听IP地址');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (101, 'server.port', '8000', 'number', 1, '服务器监听端口');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (102, 'server.secret', 'null', 'string', 1, '服务器密钥');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (201, 'log.log_format', '<green>{time:YYMMDD HH:mm:ss}</green>[{version}_{selected_module}][<light-blue>{extra[tag]}</light-blue>]-<level>{level}</level>-<light-green>{message}</light-green>', 'string', 1, '控制台日志格式');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (201, 'log.log_format', '<green>{time:YYMMDD HH:mm:ss}</green>[<light-blue>{version}-{selected_module}</light-blue>][<light-blue>{extra[tag]}</light-blue>]-<level>{level}</level>-<light-green>{message}</light-green>', 'string', 1, '控制台日志格式');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (202, 'log.log_format_file', '{time:YYYY-MM-DD HH:mm:ss} - {version}_{selected_module} - {name} - {level} - {extra[tag]} - {message}', 'string', 1, '文件日志格式');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (203, 'log.log_level', 'INFO', 'string', 1, '日志级别');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (204, 'log.log_dir', 'tmp', 'string', 1, '日志目录');
@@ -47,15 +47,15 @@ INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, r
"channels": 1,
"frame_duration": 60
}
}', 'string', 1, '');
}', 'json', 1, '');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (310, 'wakeup_words', '你好小智;你好小志;小爱同学;你好小鑫;你好小新;小美同学;小龙小龙;喵喵同学;小滨小滨;小冰小冰', 'array', 1, '唤醒词列表,用于识别唤醒词');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (400, 'plugin.weather.api_key', 'a861d0d5e7bf4ee1a83d9a9e4f96d4da', 'string', 1, '天气插件API密钥');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (401, 'plugin.weather.default_location', '广州', 'string', 1, '天气插件默认城市');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (410, 'plugin.news.default_rss_url', 'https://www.chinanews.com.cn/rss/society.xml', 'string', 1, '新闻插件默认RSS地址');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (411, 'plugin.news.category_urls', '{"society":"https://www.chinanews.com.cn/rss/society.xml","world":"https://www.chinanews.com.cn/rss/world.xml","finance":"https://www.chinanews.com.cn/rss/finance.xml"}', 'string', 1, '新闻插件分类RSS地址');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (421, 'plugin.home_assistant.devices', '客厅,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1;卧室,台灯,switch.iot_cn_831898993_socn1_on_p_2_1', 'array', 1, 'Home Assistant设备列表');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (422, 'plugin.home_assistant.base_url', 'http://homeassistant.local:8123', 'string', 1, 'Home Assistant服务器地址');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (423, 'plugin.home_assistant.api_key', '你的home assistant api访问令牌', 'string', 1, 'Home Assistant API密钥');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (430, 'plugin.music.music_dir', './music', 'string', 1, '音乐文件存放路径');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (431, 'plugin.music.music_ext', 'mp3;wav;p3', 'array', 1, '音乐文件类型');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (432, 'plugin.music.refresh_time', '300', 'number', 1, '音乐列表刷新间隔(秒)');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (400, 'plugins.get_weather.api_key', 'a861d0d5e7bf4ee1a83d9a9e4f96d4da', 'string', 1, '天气插件API密钥');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (401, 'plugins.get_weather.default_location', '广州', 'string', 1, '天气插件默认城市');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (410, 'plugins.get_news.default_rss_url', 'https://www.chinanews.com.cn/rss/society.xml', 'string', 1, '新闻插件默认RSS地址');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (411, 'plugins.get_news.category_urls', '{"society":"https://www.chinanews.com.cn/rss/society.xml","world":"https://www.chinanews.com.cn/rss/world.xml","finance":"https://www.chinanews.com.cn/rss/finance.xml"}', 'json', 1, '新闻插件分类RSS地址');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (421, 'plugins.home_assistant.devices', '客厅,玩具灯,switch.cuco_cn_460494544_cp1_on_p_2_1;卧室,台灯,switch.iot_cn_831898993_socn1_on_p_2_1', 'array', 1, 'Home Assistant设备列表');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (422, 'plugins.home_assistant.base_url', 'http://homeassistant.local:8123', 'string', 1, 'Home Assistant服务器地址');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (423, 'plugins.home_assistant.api_key', '你的home assistant api访问令牌', 'string', 1, 'Home Assistant API密钥');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (430, 'plugins.play_music.music_dir', './music', 'string', 1, '音乐文件存放路径');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (431, 'plugins.play_music.music_ext', 'mp3;wav;p3', 'array', 1, '音乐文件类型');
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (432, 'plugins.play_music.refresh_time', '300', 'number', 1, '音乐列表刷新间隔(秒)');
@@ -23,13 +23,6 @@ databaseChangeLog:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/2025_tts_voive.sql
- changeSet:
id: 202504082210
author: John
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202504082210.sql
- changeSet:
id: 202504082211
author: John
@@ -37,17 +30,24 @@ databaseChangeLog:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202504082211.sql
- changeSet:
id: 202504092313
author: John
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202504092313.sql
- changeSet:
id: 202504092335
author: John
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202504092335.sql
path: classpath:db/changelog/202504092335.sql
- changeSet:
id: 202504112044
author: John
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202504112044.sql
- changeSet:
id: 202504112057
author: John
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202504112057.sql
@@ -40,3 +40,4 @@
10037=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684\u6570\u5B57
10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
@@ -39,4 +39,5 @@
10036=Unsupported parameter type
10037=Parameter value must be a valid number
10038=Parameter value must be true or false
10039=Parameter value must be a valid JSON array format
10039=Parameter value must be a valid JSON array format
10040=Parameter value must be a valid JSON format
@@ -39,4 +39,5 @@
10036=\u4E0D\u652F\u6301\u7684\u53C2\u6570\u7C7B\u578B
10037=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684\u6570\u5B57
10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
@@ -39,4 +39,5 @@
10036=\u4E0D\u652F\u63F4\u7684\u53C3\u6578\u985E\u578B
10037=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684\u6578\u5B57
10038=\u53C3\u6578\u503C\u5FC5\u9808\u662Ftrue\u6216false
10039=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u6578\u7D44\u683C\u5F0F
10039=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u6578\u7D44\u683C\u5F0F
10040=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
+51 -34
View File
@@ -142,40 +142,57 @@ export default {
})
}).send()
},
// 启用/禁用模型状态
updateModelStatus(id, status, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/models/enable/${id}/${status}`)
.method('PUT')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
// 启用/禁用模型状态
updateModelStatus(id, status, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/models/enable/${id}/${status}`)
.method('PUT')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('更新模型状态失败:', err)
this.$message.error(err.msg || '更新模型状态失败')
RequestService.reAjaxFun(() => {
this.updateModelStatus(id, status, callback)
})
.fail((err) => {
console.error('更新模型状态失败:', err)
this.$message.error(err.msg || '更新模型状态失败')
RequestService.reAjaxFun(() => {
this.updateModelStatus(id, status, callback)
})
}).send()
},
// 更新模型配置
updateModel(params, callback) {
const { modelType, provideCode, id, formData } = params;
RequestService.sendRequest()
.url(`${getServiceUrl()}/models/${modelType}/${provideCode}/${id}`)
.method('PUT')
.data(formData)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
}).send()
},
// 更新模型配置
updateModel(params, callback) {
const { modelType, provideCode, id, formData } = params;
RequestService.sendRequest()
.url(`${getServiceUrl()}/models/${modelType}/${provideCode}/${id}`)
.method('PUT')
.data(formData)
.success((res) => {
RequestService.clearRequestTime();
callback(res);
})
.fail((err) => {
console.error('更新模型失败:', err);
this.$message.error(err.msg || '更新模型失败');
RequestService.reAjaxFun(() => {
this.updateModel(params, callback);
});
}).send();
},
// 设置默认模型
setDefaultModel(id, callback) {
RequestService.sendRequest()
.url(`${getServiceUrl()}/models/default/${id}`)
.method('PUT')
.success((res) => {
RequestService.clearRequestTime()
callback(res)
})
.fail((err) => {
console.error('设置默认模型失败:', err)
this.$message.error(err.msg || '设置默认模型失败')
RequestService.reAjaxFun(() => {
this.setDefaultModel(id, callback)
})
.fail((err) => {
console.error('更新模型失败:', err);
this.$message.error(err.msg || '更新模型失败');
RequestService.reAjaxFun(() => {
this.updateModel(params, callback);
});
}).send();
},
}).send()
}
}
+22 -26
View File
@@ -4,16 +4,6 @@
<div class="operation-bar">
<h2 class="page-title">{{ modelTypeText }}</h2>
<!-- <div class="right-operations">-->
<!-- <el-button plain size="small" @click="handleImport" style="background: #7b9de5; color: white;">-->
<!-- <img loading="lazy" alt="" src="@/assets/model/inner_conf.png">-->
<!-- 导入配置-->
<!-- </el-button>-->
<!-- <el-button plain size="small" @click="handleExport" style="background: #71c9d1; color: white;">-->
<!-- <img loading="lazy" alt="" src="@/assets/model/output_conf.png">-->
<!-- 导出配置-->
<!-- </el-button>-->
<!-- </div>-->
<div class="action-group">
<div class="search-group">
<el-input
@@ -76,13 +66,15 @@
@change="handleStatusChange(scope.row)" />
</template>
</el-table-column>
<el-table-column label="是否默认" align="center">
<template slot-scope="scope">
<el-switch v-model="scope.row.isDefault" class="custom-switch" :active-value="1" :inactive-value="0"
@change="handleDefaultChange(scope.row)" />
</template>
</el-table-column>
<el-table-column v-if="activeTab === 'tts'" label="音色管理" align="center">
<template slot-scope="scope">
<el-button
type="text"
size="mini"
@click="openTtsDialog(scope.row)"
class="voice-management-btn">
<el-button type="text" size="mini" @click="openTtsDialog(scope.row)" class="voice-management-btn">
音色管理
</el-button>
</template>
@@ -130,7 +122,7 @@
<ModelEditDialog :modelType="activeTab" :visible.sync="editDialogVisible" :modelData="editModelData"
@save="handleModelSave" />
<TtsModel :visible.sync="ttsDialogVisible" :ttsModelId="selectedTtsModelId"/>
<TtsModel :visible.sync="ttsDialogVisible" :ttsModelId="selectedTtsModelId" />
<AddModelDialog :modelType="activeTab" :visible.sync="addDialogVisible" @confirm="handleAddConfirm" />
</div>
@@ -304,14 +296,6 @@ export default {
this.currentPage = page;
this.$refs.modelTable.clearSelection();
},
handleImport() {
// TODO: 导入配置
console.log('导入配置');
},
handleExport() {
// TODO: 导出配置
console.log('导出配置');
},
handleModelSave({ provideCode, formData }) {
const modelType = this.activeTab;
const id = formData.id;
@@ -412,11 +396,11 @@ export default {
}
});
},
// 处理启用/禁用状态变更
// 处理启用/禁用状态变更
handleStatusChange(model) {
const newStatus = model.isEnabled ? 1 : 0
const originalStatus = model.isEnabled
model.isEnabled = !model.isEnabled
Api.model.updateModelStatus(
@@ -434,12 +418,24 @@ export default {
}
}
)
},
handleDefaultChange(model) {
Api.model.setDefaultModel(model.id, ({ data }) => {
if (data.code === 0) {
this.$message.success('设置默认模型成功')
this.loadData()
}
})
}
},
};
</script>
<style scoped>
.el-switch {
height: 23px;
}
::v-deep .el-table tr {
background: transparent;
}
@@ -564,6 +564,7 @@ export default {
}
}
}
:deep(.el-table .el-button--text) {
color: #7079aa !important;
}
+4
View File
@@ -7,6 +7,7 @@ from core.utils.util import check_ffmpeg_installed
TAG = __name__
async def wait_for_exit():
"""Windows 和 Linux 兼容的退出监听"""
loop = asyncio.get_running_loop()
@@ -19,10 +20,12 @@ async def wait_for_exit():
# Linux/macOS: 用 signal 监听 Ctrl + C
def stop():
stop_event.set()
loop.add_signal_handler(signal.SIGINT, stop)
loop.add_signal_handler(signal.SIGTERM, stop) # 支持 kill 进程
await stop_event.wait()
async def main():
check_config_file()
check_ffmpeg_installed()
@@ -44,6 +47,7 @@ async def main():
pass
print("服务器已关闭,程序退出。")
if __name__ == "__main__":
try:
asyncio.run(main())
+2 -1
View File
@@ -52,7 +52,7 @@ enable_stop_tts_notify: false
# 说完话是否开启提示音,音效地址
stop_tts_notify_voice: "config/assets/tts_notify.mp3"
CMD_exit:
exit_commands:
- "退出"
- "关闭"
@@ -222,6 +222,7 @@ ASR:
output_dir: tmp/
VAD:
SileroVAD:
type: silero
threshold: 0.5
model_dir: models/snakers4_silero-vad
min_silence_duration_ms: 700 # 如果说话停顿比较长,可以把这个值设置大一些
+178
View File
@@ -0,0 +1,178 @@
import os
import argparse
import requests
import yaml
import time
# 添加全局配置缓存
_config_cache = None
def get_project_dir():
"""获取项目根目录"""
return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + "/"
def read_config(config_path):
with open(config_path, "r", encoding="utf-8") as file:
config = yaml.safe_load(file)
return config
def load_config():
"""加载配置文件"""
global _config_cache
if _config_cache is not None:
return _config_cache
parser = argparse.ArgumentParser(description="Server configuration")
config_file = get_config_file()
parser.add_argument("--config_path", type=str, default=config_file)
args = parser.parse_args()
config = read_config(args.config_path)
if config.get("manager-api", {}).get("url"):
config = get_config_from_api(config)
# 初始化目录
ensure_directories(config)
_config_cache = config
return config
def get_config_file():
"""获取配置文件路径,优先使用私有配置文件(若存在)。
Returns:
str: 配置文件路径(相对路径或默认路径)
"""
default_config_file = "config.yaml"
config_file = default_config_file
if os.path.exists(get_project_dir() + "data/." + default_config_file):
config_file = "data/." + default_config_file
return config_file
def _make_api_request(api_url, secret, endpoint, json_data=None):
"""执行API请求的通用函数
Args:
api_url: API的基础URL
secret: API密钥
endpoint: API端点
json_data: 请求的JSON数据
Returns:
dict: API返回的数据
Raises:
Exception: 当请求失败时抛出异常
"""
if not api_url or not secret:
raise Exception("manager-api的url或secret配置错误")
if "" in secret:
raise Exception("请先配置manager-api的secret")
max_retries = 10
retry_delay = 2 # 秒
for attempt in range(max_retries):
try:
response = requests.post(f"{api_url}{endpoint}", json=json_data)
if response.status_code == 200:
result = response.json()
if result.get("code") != 0:
raise Exception(f"API返回错误: {result.get('msg', '未知错误')}")
return result.get("data")
error_msg = f"manager-api请求失败,状态码: {response.status_code}"
try:
error_data = response.json()
if "msg" in error_data:
error_msg = f"{error_msg}, 错误信息: {error_data['msg']}"
except:
error_msg = f"{error_msg}, 响应内容: {response.text}"
if attempt < max_retries - 1:
print(f"请求manager-api失败,正在重试 ({attempt + 1}/{max_retries})...")
time.sleep(retry_delay)
else:
raise Exception(error_msg)
except requests.exceptions.RequestException as e:
if attempt < max_retries - 1:
print(f"请求manager-api异常,正在重试 ({attempt + 1}/{max_retries})...")
time.sleep(retry_delay)
else:
raise Exception(f"manager-api请求异常: {str(e)}")
def get_config_from_api(config):
"""从Java API获取配置"""
api_url = config["manager-api"].get("url", "")
secret = config["manager-api"].get("secret", "")
config_data = _make_api_request(
api_url, secret, "/config/server-base", {"secret": secret}
)
config_data["read_config_from_api"] = True
config_data["manager-api"] = {
"url": api_url,
"secret": secret,
}
return config_data
def get_private_config_from_api(config, device_id, client_id):
"""从Java API获取私有配置"""
api_url = config["manager-api"].get("url", "")
secret = config["manager-api"].get("secret", "")
return _make_api_request(
api_url,
secret,
"/config/agent-models",
{
"secret": secret,
"macAddress": device_id,
"clientId": client_id,
"selectedModule": config["selected_module"],
},
)
def ensure_directories(config):
"""确保所有配置路径存在"""
dirs_to_create = set()
project_dir = get_project_dir() # 获取项目根目录
# 日志文件目录
log_dir = config.get("log", {}).get("log_dir", "tmp")
dirs_to_create.add(os.path.join(project_dir, log_dir))
# ASR/TTS模块输出目录
for module in ["ASR", "TTS"]:
for provider in config.get(module, {}).values():
output_dir = provider.get("output_dir", "")
if output_dir:
dirs_to_create.add(output_dir)
# 根据selected_module创建模型目录
selected_modules = config.get("selected_module", {})
for module_type in ["ASR", "LLM", "TTS"]:
selected_provider = selected_modules.get(module_type)
if not selected_provider:
continue
provider_config = config.get(module_type, {}).get(selected_provider, {})
output_dir = provider_config.get("output_dir")
if output_dir:
full_model_dir = os.path.join(project_dir, output_dir)
dirs_to_create.add(full_model_dir)
# 统一创建目录(保留原data目录创建)
for dir_path in dirs_to_create:
try:
os.makedirs(dir_path, exist_ok=True)
except PermissionError:
print(f"警告:无法创建目录 {dir_path},请检查写入权限")
+21 -6
View File
@@ -1,11 +1,30 @@
import os
import sys
from loguru import logger
from config.settings import load_config
from config.config_loader import load_config
SERVER_VERSION = "0.2.1"
def get_module_abbreviation(module_name, module_dict):
"""获取模块名称的缩写,如果为空则返回00"""
return (
module_dict.get(module_name, "")[:2] if module_dict.get(module_name) else "00"
)
def build_module_string(selected_module):
"""构建模块字符串"""
return (
get_module_abbreviation("VAD", selected_module)
+ get_module_abbreviation("ASR", selected_module)
+ get_module_abbreviation("LLM", selected_module)
+ get_module_abbreviation("TTS", selected_module)
+ get_module_abbreviation("Memory", selected_module)
+ get_module_abbreviation("Intent", selected_module)
)
def setup_logging():
"""从配置文件中读取日志配置,并设置日志输出格式和级别"""
config = load_config()
@@ -18,11 +37,7 @@ def setup_logging():
"log_format_file",
"{time:YYYY-MM-DD HH:mm:ss} - {version_{selected_module}} - {name} - {level} - {extra[tag]} - {message}",
)
selected_module = config.get("selected_module")
selected_module_str = "".join(
[value[0] + value[1] for key, value in selected_module.items()]
)
selected_module_str = build_module_string(config.get("selected_module", {}))
log_format = log_format.replace("{version}", SERVER_VERSION)
log_format = log_format.replace("{selected_module}", selected_module_str)
@@ -1,241 +0,0 @@
import os
import time
import yaml
from config.logger import setup_logging
from typing import Dict, Any, Optional
from copy import deepcopy
from core.utils.util import get_project_dir
from core.utils import llm, tts
from core.utils.lock_manager import FileLockManager
TAG = __name__
class PrivateConfig:
def __init__(self, device_id: str, default_config: Dict[str, Any], auth_code_gen=None):
self.device_id = device_id
self.default_config = default_config
self.config_path = get_project_dir() + 'data/.private_config.yaml'
self.logger = setup_logging()
self.private_config = {}
self.auth_code_gen = auth_code_gen
self.lock_manager = FileLockManager()
async def load_or_create(self):
try:
await self.lock_manager.acquire_lock(self.config_path)
try:
if os.path.exists(self.config_path):
with open(self.config_path, 'r', encoding='utf-8') as f:
all_configs = yaml.safe_load(f) or {}
else:
all_configs = {}
if self.device_id not in all_configs:
# Get selected module names
selected_modules = self.default_config['selected_module']
selected_tts = selected_modules['TTS']
selected_llm = selected_modules['LLM']
selected_asr = selected_modules['ASR']
selected_vad = selected_modules['VAD']
# 生成认证码
auth_code = None
if self.auth_code_gen:
auth_code = self.auth_code_gen.generate_code()
# Initialize device config with only necessary configurations
device_config = {
'selected_module': deepcopy(selected_modules),
'prompt': self.default_config['prompt'],
'LLM': {
selected_llm: deepcopy(self.default_config['LLM'][selected_llm])
},
'TTS': {
selected_tts: deepcopy(self.default_config['TTS'][selected_tts])
},
'ASR': {
selected_asr: deepcopy(self.default_config['ASR'][selected_asr])
},
'VAD': {
selected_vad: deepcopy(self.default_config['VAD'][selected_vad])
},
'auth_code': auth_code # 添加认证码字段
}
all_configs[self.device_id] = device_config
# Save updated configs
with open(self.config_path, 'w', encoding='utf-8') as f:
yaml.dump(all_configs, f, allow_unicode=True)
self.private_config = all_configs[self.device_id]
finally:
self.lock_manager.release_lock(self.config_path)
except Exception as e:
self.logger.bind(tag=TAG).error(f"Error handling private config: {e}")
self.private_config = {}
async def update_config(self, selected_modules: Dict[str, str], prompt: str, nickname: str) -> bool:
"""更新设备配置
Args:
selected_modules: 选择的模块配置,格式如 {'LLM': 'AliLLM', 'TTS': 'EdgeTTS',...}
prompt: 提示词配置
Returns:
bool: 更新是否成功
"""
try:
await self.lock_manager.acquire_lock(self.config_path)
try:
# Read main config to get full module configurations
main_config = self.default_config
# Create new device config
device_config = {
'selected_module': selected_modules,
'prompt': prompt,
'nickname': nickname,
}
if self.private_config.get('last_chat_time'):
device_config['last_chat_time'] = self.private_config['last_chat_time']
if self.private_config.get('owner'):
device_config['owner'] = self.private_config['owner']
# Copy full module configurations from main config
for module_type, selected_name in selected_modules.items():
if selected_name and selected_name in main_config.get(module_type, {}):
device_config[module_type] = {
selected_name: main_config[module_type][selected_name]
}
# Read all configs
if os.path.exists(self.config_path):
with open(self.config_path, 'r', encoding='utf-8') as f:
all_configs = yaml.safe_load(f) or {}
else:
all_configs = {}
# Update device config
all_configs[self.device_id] = device_config
self.private_config = device_config
# Save back to file
with open(self.config_path, 'w', encoding='utf-8') as f:
yaml.dump(all_configs, f, allow_unicode=True)
return True
finally:
self.lock_manager.release_lock(self.config_path)
except Exception as e:
self.logger.bind(tag=TAG).error(f"Error updating config: {e}")
return False
async def delete_config(self) -> bool:
"""删除设备配置
Returns:
bool: 删除是否成功
"""
try:
await self.lock_manager.acquire_lock(self.config_path)
try:
# 读取所有配置
if os.path.exists(self.config_path):
with open(self.config_path, 'r', encoding='utf-8') as f:
all_configs = yaml.safe_load(f) or {}
else:
return False
# 删除设备配置
if self.device_id in all_configs:
del all_configs[self.device_id]
# 保存更新后的配置
with open(self.config_path, 'w', encoding='utf-8') as f:
yaml.dump(all_configs, f, allow_unicode=True)
self.private_config = {}
return True
return False
finally:
self.lock_manager.release_lock(self.config_path)
except Exception as e:
self.logger.bind(tag=TAG).error(f"Error deleting config: {e}")
return False
def create_private_instances(self):
# 判断存在私有配置,并且self.device_id在私有配置中
if not self.private_config:
self.logger.bind(tag=TAG).error("Private config not found for device_id: {}", self.device_id)
return None, None
"""创建私有处理模块实例"""
config = self.private_config
selected_modules = config['selected_module']
return (
llm.create_instance(
selected_modules["LLM"]
if not 'type' in config["LLM"][selected_modules["LLM"]]
else
config["LLM"][selected_modules["LLM"]]['type'],
config["LLM"][selected_modules["LLM"]],
),
tts.create_instance(
selected_modules["TTS"]
if not 'type' in config["TTS"][selected_modules["TTS"]]
else
config["TTS"][selected_modules["TTS"]]["type"],
config["TTS"][selected_modules["TTS"]],
self.default_config.get("delete_audio", True) # Using default_config for global settings
)
)
async def update_last_chat_time(self, timestamp=None):
"""更新设备最近一次的聊天时间
Args:
timestamp: 指定的时间戳,不传则使用当前时间
"""
if not self.private_config:
self.logger.bind(tag=TAG).error("Private config not found")
return False
try:
await self.lock_manager.acquire_lock(self.config_path)
try:
if timestamp is None:
timestamp = int(time.time())
self.private_config['last_chat_time'] = timestamp
# 读取所有配置
with open(self.config_path, 'r', encoding='utf-8') as f:
all_configs = yaml.safe_load(f) or {}
# 更新当前设备配置
all_configs[self.device_id] = self.private_config
# 保存回文件
with open(self.config_path, 'w', encoding='utf-8') as f:
yaml.dump(all_configs, f, allow_unicode=True)
return True
finally:
self.lock_manager.release_lock(self.config_path)
except Exception as e:
self.logger.bind(tag=TAG).error(f"Error updating last chat time: {e}")
return False
def get_auth_code(self) -> str:
"""获取设备的认证码
Returns:
str: 认证码,如果没有返回空字符串
"""
return self.private_config.get('auth_code', '')
def get_owner(self) -> Optional[str]:
"""获取设备当前所有者"""
return self.private_config.get('owner')
+11 -82
View File
@@ -1,82 +1,11 @@
import os
import argparse
from ruamel.yaml import YAML
from collections.abc import Mapping
from core.utils.util import read_config, get_project_dir
from config.config_loader import read_config, get_project_dir, load_config
default_config_file = "config.yaml"
def ensure_directories(config):
"""确保所有配置路径存在"""
dirs_to_create = set()
project_dir = get_project_dir() # 获取项目根目录
# 日志文件目录
log_dir = config.get('log', {}).get('log_dir', 'tmp')
dirs_to_create.add(os.path.join(project_dir, log_dir))
# ASR/TTS模块输出目录
for module in ['ASR', 'TTS']:
for provider in config.get(module, {}).values():
output_dir = provider.get('output_dir', '')
if output_dir:
dirs_to_create.add(output_dir)
# 根据selected_module创建模型目录
selected_modules = config.get('selected_module', {})
for module_type in ['ASR', 'LLM', 'TTS']:
selected_provider = selected_modules.get(module_type)
if not selected_provider:
continue
provider_config = config.get(module_type, {}).get(selected_provider, {})
output_dir = provider_config.get('output_dir')
if output_dir:
full_model_dir = os.path.join(project_dir, output_dir)
dirs_to_create.add(full_model_dir)
# 统一创建目录(保留原data目录创建)
for dir_path in dirs_to_create:
try:
os.makedirs(dir_path, exist_ok=True)
except PermissionError:
print(f"警告:无法创建目录 {dir_path},请检查写入权限")
def get_config_file():
global default_config_file
"""获取配置文件路径,优先使用私有配置文件(若存在)。
Returns:
str: 配置文件路径(相对路径或默认路径)
"""
config_file = default_config_file
if os.path.exists(get_project_dir() + "data/." + default_config_file):
config_file = "data/." + default_config_file
return config_file
def load_config():
"""加载配置文件"""
parser = argparse.ArgumentParser(description="Server configuration")
config_file = get_config_file()
parser.add_argument("--config_path", type=str, default=config_file)
args = parser.parse_args()
config = read_config(args.config_path)
# 初始化目录
ensure_directories(config)
return config
def update_config(config):
yaml = YAML()
yaml.preserve_quotes = True
"""将配置保存到YAML文件"""
with open(get_config_file(), 'w') as f:
yaml.dump(config, f)
def find_missing_keys(new_config, old_config, parent_key=''):
def find_missing_keys(new_config, old_config, parent_key=""):
"""
递归查找缺失的配置项
返回格式:[缺失配置路径]
@@ -98,28 +27,28 @@ def find_missing_keys(new_config, old_config, parent_key=''):
# 递归检查嵌套字典
if isinstance(value, Mapping):
sub_missing = find_missing_keys(
value,
old_config[key],
parent_key=full_path
value, old_config[key], parent_key=full_path
)
missing_keys.extend(sub_missing)
return missing_keys
def check_config_file():
old_config_file = get_config_file()
global default_config_file
if not 'data' in old_config_file:
old_config_file = get_project_dir() + "data/." + default_config_file
if not os.path.exists(old_config_file):
return
old_config = read_config(get_project_dir() + old_config_file)
old_config = load_config()
new_config = read_config(get_project_dir() + default_config_file)
# 查找缺失的配置项
missing_keys = find_missing_keys(new_config, old_config)
read_config_from_api = old_config.get("read_config_from_api", False)
if read_config_from_api:
return
if missing_keys:
missing_keys_str = "\n".join(f"- {key}" for key in missing_keys)
error_msg = "您的配置文件太旧了,缺少了:\n"
error_msg += "\n".join(f"- {key}" for key in missing_keys)
error_msg += missing_keys_str
error_msg += "\n建议您:\n"
error_msg += "1、备份data/.config.yaml文件\n"
error_msg += "2、将根目录的config.yaml文件复制到data下,重命名为.config.yaml\n"
+12
View File
@@ -0,0 +1,12 @@
# 如果你只想轻量化安装xiaozhi-server,只使用本地的配置文件,不需要理会这个文件,不需要改动本文件任何东西
# 如果你想从manager-api获取配置,请往下看:
# 请将本文件复制到xiaozhi-server/data目录下,没有data目录,请创建一个,并将复制过去的文件命名为.config.yaml
# 注意如果data目录有.config.yaml文件,请先删除它
# 先启动manager-api和manager-web,注册一个账号,第一个注册的账号为管理员
# 使用管理员,进入【参数管理】页面,找到【server.secret】,复制它到参数值,注意每次从零部署,server.secret都会变化
# 打开本data目录下的.config.yaml文件,修改manager-api.secret为刚才复制出来的server.secret
manager-api:
# 你的manager-api的地址,最好使用局域网ip
url: http://127.0.0.1:8002/xiaozhi
# 你的manager-api的token,就是刚才复制出来的server.secret
secret: 你的server.secret值
+96 -78
View File
@@ -1,4 +1,5 @@
import os
import copy
import json
import uuid
import time
@@ -17,16 +18,16 @@ from core.utils.util import (
get_string_no_punctuation_or_emoji,
extract_json_from_string,
get_ip_info,
initialize_modules,
)
from concurrent.futures import ThreadPoolExecutor, TimeoutError
from core.handle.sendAudioHandle import sendAudioMessage
from core.handle.receiveAudioHandle import handleAudioMessage
from core.handle.functionHandler import FunctionHandler
from plugins_func.register import Action, ActionResponse
from config.private_config import PrivateConfig
from core.auth import AuthMiddleware, AuthenticationError
from core.utils.auth_code_gen import AuthCodeGenerator
from core.mcp.manager import MCPManager
from config.config_loader import get_private_config_from_api
TAG = __name__
@@ -41,7 +42,7 @@ class ConnectionHandler:
def __init__(
self, config: Dict[str, Any], _vad, _asr, _llm, _tts, _memory, _intent
):
self.config = config
self.config = copy.deepcopy(config)
self.logger = setup_logging()
self.auth = AuthMiddleware(config)
@@ -95,15 +96,12 @@ class ConnectionHandler:
self.iot_descriptors = {}
self.func_handler = None
self.cmd_exit = self.config["CMD_exit"]
self.cmd_exit = self.config["exit_commands"]
self.max_cmd_length = 0
for cmd in self.cmd_exit:
if len(cmd) > self.max_cmd_length:
self.max_cmd_length = len(cmd)
self.private_config = None
self.auth_code_gen = AuthCodeGenerator.get_instance()
self.is_device_verified = False # 添加设备验证状态标志
self.close_after_chat = False # 是否在聊天结束后关闭连接
self.use_function_call_mode = False
if self.config["selected_module"]["Intent"] == "function_call":
@@ -121,7 +119,6 @@ class ConnectionHandler:
# 进行认证
await self.auth.authenticate(self.headers)
device_id = self.headers.get("device-id", None)
# 认证通过,继续处理
self.websocket = ws
@@ -130,39 +127,6 @@ class ConnectionHandler:
self.welcome_msg = self.config["xiaozhi"]
self.welcome_msg["session_id"] = self.session_id
await self.websocket.send(json.dumps(self.welcome_msg))
# Load private configuration if device_id is provided
bUsePrivateConfig = self.config.get("use_private_config", False)
if bUsePrivateConfig and device_id:
try:
self.private_config = PrivateConfig(
device_id, self.config, self.auth_code_gen
)
await self.private_config.load_or_create()
# 判断是否已经绑定
owner = self.private_config.get_owner()
self.is_device_verified = owner is not None
if self.is_device_verified:
await self.private_config.update_last_chat_time()
llm, tts = self.private_config.create_private_instances()
if all([llm, tts]):
self.llm = llm
self.tts = tts
self.logger.bind(tag=TAG).info(
f"Loaded private config and instances for device {device_id}"
)
else:
self.logger.bind(tag=TAG).error(
f"Failed to create instances for device {device_id}"
)
self.private_config = None
except Exception as e:
self.logger.bind(tag=TAG).error(
f"Error initializing private config: {e}"
)
self.private_config = None
raise
# 异步初始化
self.executor.submit(self._initialize_components)
@@ -211,10 +175,11 @@ class ConnectionHandler:
await handleAudioMessage(self, message)
def _initialize_components(self):
"""初始化组件"""
self._initialize_models()
"""加载提示词"""
self.prompt = self.config["prompt"]
if self.private_config:
self.prompt = self.private_config.private_config.get("prompt", self.prompt)
self.dialogue.put(Message(role="system", content=self.prompt))
"""加载记忆"""
@@ -222,13 +187,100 @@ class ConnectionHandler:
"""加载意图识别"""
self._initialize_intent()
"""加载位置信息"""
self.client_ip_info = get_ip_info(self.client_ip)
self.client_ip_info = get_ip_info(self.client_ip, self.logger)
if self.client_ip_info is not None and "city" in self.client_ip_info:
self.logger.bind(tag=TAG).info(f"Client ip info: {self.client_ip_info}")
self.prompt = self.prompt + f"\nuser location:{self.client_ip_info}"
self.dialogue.update_system_message(self.prompt)
def _initialize_models(self):
read_config_from_api = self.config.get("read_config_from_api", False)
"""如果是从配置文件获取,则进行二次实例化"""
if not read_config_from_api:
return
"""从接口获取差异化的配置进行二次实例化,非全量重新实例化"""
try:
private_config = get_private_config_from_api(
self.config,
self.headers.get("device-id", None),
self.headers.get("client-id", None),
)
private_config["delete_audio"] = self.config["delete_audio"]
self.logger.bind(tag=TAG).info(f"获取差异化配置成功: {private_config}")
except Exception as e:
self.logger.bind(tag=TAG).error(f"获取差异化配置失败: {e}")
private_config = {}
init_vad, init_asr, init_llm, init_tts, init_memory, init_intent = (
False,
False,
False,
False,
False,
False,
)
if private_config.get("VAD", None) is not None:
init_vad = True
self.config["vad"] = private_config["VAD"]
self.config["selected_module"]["VAD"] = private_config["selected_module"][
"VAD"
]
if private_config.get("ASR", None) is not None:
init_asr = True
self.config["asr"] = private_config["ASR"]
self.config["selected_module"]["ASR"] = private_config["selected_module"][
"ASR"
]
if private_config.get("LLM", None) is not None:
init_llm = True
self.config["llm"] = private_config["LLM"]
self.config["selected_module"]["LLM"] = private_config["selected_module"][
"LLM"
]
if private_config.get("TTS", None) is not None:
init_tts = True
self.config["tts"] = private_config["TTS"]
self.config["selected_module"]["TTS"] = private_config["selected_module"][
"TTS"
]
if private_config.get("Memory", None) is not None:
init_memory = True
self.config["memory"] = private_config["Memory"]
self.config["selected_module"]["Memory"] = private_config[
"selected_module"
]["Memory"]
if private_config.get("Intent", None) is not None:
init_intent = True
self.config["intent"] = private_config["Intent"]
self.config["selected_module"]["Intent"] = private_config[
"selected_module"
]["Intent"]
if private_config.get("prompt", None) is not None:
self.config["prompt"] = private_config["prompt"]
try:
modules = initialize_modules(
self.logger,
private_config,
init_vad,
init_asr,
init_llm,
init_tts,
init_memory,
init_intent,
)
except Exception as e:
self.logger.bind(tag=TAG).error(f"初始化组件失败: {e}")
modules = {}
if modules.get("tts", None) is not None:
self.tts = modules["tts"]
if modules.get("llm", None) is not None:
self.llm = modules["llm"]
if modules.get("intent", None) is not None:
self.intent = modules["intent"]
if modules.get("memory", None) is not None:
self.memory = modules["memory"]
def _initialize_memory(self):
"""初始化记忆模块"""
device_id = self.headers.get("device-id", None)
@@ -281,34 +333,7 @@ class ConnectionHandler:
if m.role == "system":
m.content = prompt
async def _check_and_broadcast_auth_code(self):
"""检查设备绑定状态并广播认证码"""
if not self.private_config.get_owner():
auth_code = self.private_config.get_auth_code()
if auth_code:
# 发送验证码语音提示
text = f"请在后台输入验证码:{' '.join(auth_code)}"
self.recode_first_last_text(text)
future = self.executor.submit(self.speak_and_play, text)
self.tts_queue.put(future)
return False
return True
def isNeedAuth(self):
bUsePrivateConfig = self.config.get("use_private_config", False)
if not bUsePrivateConfig:
# 如果不使用私有配置,就不需要验证
return False
return not self.is_device_verified
def chat(self, query):
if self.isNeedAuth():
self.llm_finish_task = True
future = asyncio.run_coroutine_threadsafe(
self._check_and_broadcast_auth_code(), self.loop
)
future.result()
return True
self.dialogue.put(Message(role="user", content=query))
@@ -391,13 +416,6 @@ class ConnectionHandler:
def chat_with_function_calling(self, query, tool_call=False):
self.logger.bind(tag=TAG).debug(f"Chat with function calling start: {query}")
"""Chat with function calling for intent detection using streaming"""
if self.isNeedAuth():
self.llm_finish_task = True
future = asyncio.run_coroutine_threadsafe(
self._check_and_broadcast_auth_code(), self.loop
)
future.result()
return True
if not tool_call:
self.dialogue.put(Message(role="user", content=query))
@@ -8,6 +8,7 @@ import os
import random
import time
TAG = __name__
logger = setup_logging()
WAKEUP_CONFIG = {
@@ -69,6 +70,14 @@ def getWakeupWordFile(file_name):
async def wakeupWordsResponse(conn):
wait_max_time = 5
while conn.llm is None or not conn.llm.response_no_stream:
await asyncio.sleep(1)
wait_max_time -= 1
if wait_max_time <= 0:
logger.bind(tag=TAG).error("连接对象没有llm")
return
"""唤醒词响应"""
wakeup_word = random.choice(WAKEUP_CONFIG["words"])
result = conn.llm.response_no_stream(conn.config["prompt"], wakeup_word)
+40 -19
View File
@@ -1,26 +1,30 @@
"""MCP服务管理器"""
import os, json
from typing import Dict, Any, List
from .MCPClient import MCPClient
from config.logger import setup_logging
from core.utils.util import get_project_dir
from plugins_func.register import register_function, ActionResponse, Action, ToolType
from plugins_func.register import register_function, ToolType
from config.config_loader import get_project_dir
TAG = __name__
class MCPManager:
"""管理多个MCP服务的集中管理器"""
def __init__(self,conn) -> None:
def __init__(self, conn) -> None:
"""
初始化MCP管理器
"""
self.conn = conn
self.logger = setup_logging()
self.config_path = get_project_dir() + 'data/.mcp_server_settings.json'
self.config_path = get_project_dir() + "data/.mcp_server_settings.json"
if os.path.exists(self.config_path) == False:
self.config_path = ""
self.logger.bind(tag=TAG).warning(f"请检查mcp服务配置文件:data/.mcp_server_settings.json")
self.logger.bind(tag=TAG).warning(
f"请检查mcp服务配置文件:data/.mcp_server_settings.json"
)
self.client: Dict[str, MCPClient] = {}
self.tools = []
@@ -31,13 +35,15 @@ class MCPManager:
"""
if len(self.config_path) == 0:
return {}
try:
with open(self.config_path, 'r', encoding='utf-8') as f:
with open(self.config_path, "r", encoding="utf-8") as f:
config = json.load(f)
return config.get('mcpServers', {})
return config.get("mcpServers", {})
except Exception as e:
self.logger.bind(tag=TAG).error(f"Error loading MCP config from {self.config_path}: {e}")
self.logger.bind(tag=TAG).error(
f"Error loading MCP config from {self.config_path}: {e}"
)
return {}
async def initialize_servers(self) -> None:
@@ -45,7 +51,9 @@ class MCPManager:
config = self.load_config()
for name, srv_config in config.items():
if not srv_config.get("command"):
self.logger.bind(tag=TAG).warning(f"Skipping server {name}: command not specified")
self.logger.bind(tag=TAG).warning(
f"Skipping server {name}: command not specified"
)
continue
try:
@@ -56,12 +64,18 @@ class MCPManager:
client_tools = client.get_available_tools()
self.tools.extend(client_tools)
for tool in client_tools:
func_name = "mcp_"+tool["function"]["name"]
register_function(func_name, tool, ToolType.MCP_CLIENT)(self.execute_tool)
self.conn.func_handler.function_registry.register_function(func_name)
func_name = "mcp_" + tool["function"]["name"]
register_function(func_name, tool, ToolType.MCP_CLIENT)(
self.execute_tool
)
self.conn.func_handler.function_registry.register_function(
func_name
)
except Exception as e:
self.logger.bind(tag=TAG).error(f"Failed to initialize MCP server {name}: {e}")
self.logger.bind(tag=TAG).error(
f"Failed to initialize MCP server {name}: {e}"
)
self.conn.func_handler.upload_functions_desc()
def get_all_tools(self) -> List[Dict[str, Any]]:
@@ -79,7 +93,10 @@ class MCPManager:
bool: 是否是MCP工具
"""
for tool in self.tools:
if tool.get("function") != None and tool["function"].get("name") == tool_name:
if (
tool.get("function") != None
and tool["function"].get("name") == tool_name
):
return True
return False
@@ -87,17 +104,19 @@ class MCPManager:
"""执行工具调用
Args:
tool_name: 工具名称
arguments: 工具参数
arguments: 工具参数
Returns:
Any: 工具执行结果
Raises:
ValueError: 工具未找到时抛出
"""
self.logger.bind(tag=TAG).info(f"Executing tool {tool_name} with arguments: {arguments}")
self.logger.bind(tag=TAG).info(
f"Executing tool {tool_name} with arguments: {arguments}"
)
for client in self.client.values():
if client.has_tool(tool_name):
return await client.call_tool(tool_name, arguments)
raise ValueError(f"Tool {tool_name} not found in any MCP server")
async def cleanup_all(self) -> None:
@@ -106,5 +125,7 @@ class MCPManager:
await client.cleanup()
self.logger.bind(tag=TAG).info(f"Cleaned up MCP client: {name}")
except Exception as e:
self.logger.bind(tag=TAG).error(f"Error cleaning up MCP client {name}: {e}")
self.logger.bind(tag=TAG).error(
f"Error cleaning up MCP client {name}: {e}"
)
self.client.clear()
@@ -4,15 +4,17 @@ from core.providers.llm.base import LLMProviderBase
from config.logger import setup_logging
import requests
import json
TAG = __name__
logger = setup_logging()
class LLMProvider(LLMProviderBase):
def __init__(self, config):
"""初始化Gemini LLM Provider"""
self.model_name = config.get("model_name", "gemini-1.5-pro")
self.api_key = config.get("api_key")
self.http_proxy=config.get("http_proxy")
self.http_proxy = config.get("http_proxy")
self.https_proxy = config.get("https_proxy")
have_key = check_model_key("LLM", self.api_key)
@@ -22,7 +24,7 @@ class LLMProvider(LLMProviderBase):
try:
# 初始化Gemini客户端
# 配置代理(如果提供了代理配置)
self.proxies=None
self.proxies = None
if self.http_proxy is not "" or self.https_proxy is not "":
self.proxies = {
@@ -62,19 +64,16 @@ class LLMProvider(LLMProviderBase):
role = "model" if msg["role"] == "assistant" else "user"
content = msg["content"].strip()
if content:
chat_history.append({
"role": role,
"parts": [{"text":content}]
})
chat_history.append({"role": role, "parts": [{"text": content}]})
# 获取当前消息
current_msg = dialogue[-1]["content"]
# 构建请求体
request_body = {
"contents": chat_history + [{"role": "user", "parts": [{"text":current_msg}]}],
"generationConfig": self.generation_config
"contents": chat_history
+ [{"role": "user", "parts": [{"text": current_msg}]}],
"generationConfig": self.generation_config,
}
# 构建请求URL
@@ -87,11 +86,17 @@ class LLMProvider(LLMProviderBase):
# 发送POST请求,经测试手动 request 无法使用 stream 模式
if self.proxies:
response = requests.post(url, headers=headers, json=request_body, stream=False, proxies=self.proxies)
response = requests.post(
url,
headers=headers,
json=request_body,
stream=False,
proxies=self.proxies,
)
try:
data = response.json() # 直接解析JSON
if 'candidates' in data and data['candidates']:
yield data['candidates'][0]['content']['parts'][0]['text']
if "candidates" in data and data["candidates"]:
yield data["candidates"][0]["content"]["parts"][0]["text"]
else:
yield "未找到候选回复。"
except json.JSONDecodeError as e:
@@ -104,13 +109,11 @@ class LLMProvider(LLMProviderBase):
# 发送消息并获取流式响应
response = chat.send_message(
current_msg,
stream=True,
generation_config=self.generation_config
current_msg, stream=True, generation_config=self.generation_config
)
# 处理流式响应
for chunk in response:
if hasattr(chunk, 'text') and chunk.text:
if hasattr(chunk, "text") and chunk.text:
yield chunk.text
except Exception as e:
@@ -125,9 +128,6 @@ class LLMProvider(LLMProviderBase):
else:
yield f"【Gemini服务响应异常: {error_msg}"
except requests.exceptions.RequestException as e:
yield f"请求失败:{e}"
except json.JSONDecodeError as e:
@@ -11,7 +11,7 @@ class LLMProvider(LLMProviderBase):
def __init__(self, config):
self.model_name = config.get("model_name")
self.api_key = config.get("api_key")
if 'base_url' in config:
if "base_url" in config:
self.base_url = config.get("base_url")
else:
self.base_url = config.get("url")
@@ -33,18 +33,22 @@ class LLMProvider(LLMProviderBase):
for chunk in responses:
try:
# 检查是否存在有效的choice且content不为空
delta = chunk.choices[0].delta if getattr(chunk, 'choices', None) else None
content = delta.content if hasattr(delta, 'content') else ''
delta = (
chunk.choices[0].delta
if getattr(chunk, "choices", None)
else None
)
content = delta.content if hasattr(delta, "content") else ""
except IndexError:
content = ''
content = ""
if content:
# 处理标签跨多个chunk的情况
if '<think>' in content:
if "<think>" in content:
is_active = False
content = content.split('<think>')[0]
if '</think>' in content:
content = content.split("<think>")[0]
if "</think>" in content:
is_active = True
content = content.split('</think>')[-1]
content = content.split("</think>")[-1]
if is_active:
yield content
@@ -54,10 +58,7 @@ class LLMProvider(LLMProviderBase):
def response_with_functions(self, session_id, dialogue, functions=None):
try:
stream = self.client.chat.completions.create(
model=self.model_name,
messages=dialogue,
stream=True,
tools=functions
model=self.model_name, messages=dialogue, stream=True, tools=functions
)
for chunk in stream:
@@ -6,13 +6,14 @@ from core.utils.util import check_model_key
TAG = __name__
class MemoryProvider(MemoryProviderBase):
def __init__(self, config):
super().__init__(config)
self.api_key = config.get("api_key", "")
self.api_version = config.get("api_version", "v1.1")
have_key = check_model_key("Mem0ai", self.api_key)
if not have_key :
if not have_key:
self.use_mem0 = False
return
else:
@@ -30,54 +31,55 @@ class MemoryProvider(MemoryProviderBase):
return None
if len(msgs) < 2:
return None
try:
# Format the content as a message list for mem0
messages = [
{"role": message.role, "content": message.content}
for message in msgs if message.role != "system"
for message in msgs
if message.role != "system"
]
result = self.client.add(messages, user_id=self.role_id, output_format=self.api_version)
result = self.client.add(
messages, user_id=self.role_id, output_format=self.api_version
)
logger.bind(tag=TAG).debug(f"Save memory result: {result}")
except Exception as e:
logger.bind(tag=TAG).error(f"保存记忆失败: {str(e)}")
return None
async def query_memory(self, query: str)-> str:
async def query_memory(self, query: str) -> str:
if not self.use_mem0:
return ""
try:
results = self.client.search(
query,
user_id=self.role_id,
output_format=self.api_version
query, user_id=self.role_id, output_format=self.api_version
)
if not results or 'results' not in results:
if not results or "results" not in results:
return ""
# Format each memory entry with its update time up to minutes
memories = []
for entry in results['results']:
timestamp = entry.get('updated_at', '')
for entry in results["results"]:
timestamp = entry.get("updated_at", "")
if timestamp:
try:
# Parse and reformat the timestamp
dt = timestamp.split('.')[0] # Remove milliseconds
formatted_time = dt.replace('T', ' ')
dt = timestamp.split(".")[0] # Remove milliseconds
formatted_time = dt.replace("T", " ")
except:
formatted_time = timestamp
memory = entry.get('memory', '')
memory = entry.get("memory", "")
if timestamp and memory:
# Store tuple of (timestamp, formatted_string) for sorting
memories.append((timestamp, f"[{formatted_time}] {memory}"))
# Sort by timestamp in descending order (newest first)
memories.sort(key=lambda x: x[0], reverse=True)
# Extract only the formatted strings
memories_str = "\n".join(f"- {memory[1]}" for memory in memories)
logger.bind(tag=TAG).debug(f"Query results: {memories_str}")
return memories_str
except Exception as e:
logger.bind(tag=TAG).error(f"查询记忆失败: {str(e)}")
return ""
return ""
@@ -3,7 +3,8 @@ import time
import json
import os
import yaml
from core.utils.util import get_project_dir
from config.config_loader import get_project_dir
short_term_memory_prompt = """
# 时空记忆编织者
@@ -71,11 +72,12 @@ short_term_memory_prompt = """
```
"""
def extract_json_data(json_code):
start = json_code.find("```json")
# 从start开始找到下一个```结束
end = json_code.find("```", start+1)
#print("start:", start, "end:", end)
end = json_code.find("```", start + 1)
# print("start:", start, "end:", end)
if start == -1 or end == -1:
try:
jsonData = json.loads(json_code)
@@ -83,74 +85,76 @@ def extract_json_data(json_code):
except Exception as e:
print("Error:", e)
return ""
jsonData = json_code[start+7:end]
jsonData = json_code[start + 7 : end]
return jsonData
TAG = __name__
class MemoryProvider(MemoryProviderBase):
def __init__(self, config):
super().__init__(config)
self.short_momery = ""
self.memory_path = get_project_dir() + 'data/.memory.yaml'
self.memory_path = get_project_dir() + "data/.memory.yaml"
self.load_memory()
def init_memory(self, role_id, llm):
super().init_memory(role_id, llm)
self.load_memory()
def load_memory(self):
all_memory = {}
if os.path.exists(self.memory_path):
with open(self.memory_path, 'r', encoding='utf-8') as f:
with open(self.memory_path, "r", encoding="utf-8") as f:
all_memory = yaml.safe_load(f) or {}
if self.role_id in all_memory:
self.short_momery = all_memory[self.role_id]
def save_memory_to_file(self):
all_memory = {}
if os.path.exists(self.memory_path):
with open(self.memory_path, 'r', encoding='utf-8') as f:
all_memory = yaml.safe_load(f) or {}
with open(self.memory_path, "r", encoding="utf-8") as f:
all_memory = yaml.safe_load(f) or {}
all_memory[self.role_id] = self.short_momery
with open(self.memory_path, 'w', encoding='utf-8') as f:
with open(self.memory_path, "w", encoding="utf-8") as f:
yaml.dump(all_memory, f, allow_unicode=True)
async def save_memory(self, msgs):
if self.llm is None:
logger.bind(tag=TAG).error("LLM is not set for memory provider")
return None
if len(msgs) < 2:
return None
msgStr = ""
for msg in msgs:
if msg.role == "user":
msgStr += f"User: {msg.content}\n"
elif msg.role== "assistant":
elif msg.role == "assistant":
msgStr += f"Assistant: {msg.content}\n"
if len(self.short_momery) > 0:
msgStr+="历史记忆:\n"
msgStr+=self.short_momery
#当前时间
msgStr += "历史记忆:\n"
msgStr += self.short_momery
# 当前时间
time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
msgStr += f"当前时间:{time_str}"
result = self.llm.response_no_stream(short_term_memory_prompt, msgStr)
json_str = extract_json_data(result)
try:
json_data = json.loads(json_str) # 检查json格式是否正确
json_data = json.loads(json_str) # 检查json格式是否正确
self.short_momery = json_str
except Exception as e:
print("Error:", e)
self.save_memory_to_file()
logger.bind(tag=TAG).info(f"Save memory successful - Role: {self.role_id}")
return self.short_momery
async def query_memory(self, query: str)-> str:
return self.short_momery
async def query_memory(self, query: str) -> str:
return self.short_momery
@@ -6,6 +6,10 @@ import requests
from datetime import datetime
from core.utils.util import check_model_key
from core.providers.tts.base import TTSProviderBase
from config.logger import setup_logging
TAG = __name__
logger = setup_logging()
class TTSProvider(TTSProviderBase):
@@ -21,18 +25,19 @@ class TTSProvider(TTSProviderBase):
check_model_key("TTS", self.access_token)
def generate_filename(self, extension=".wav"):
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
return os.path.join(
self.output_file,
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
)
async def text_to_speak(self, text, output_file):
request_json = {
"app": {
"appid": f"{self.appid}",
"token": "access_token",
"cluster": self.cluster
},
"user": {
"uid": "1"
"cluster": self.cluster,
},
"user": {"uid": "1"},
"audio": {
"voice_type": self.voice,
"encoding": "wav",
@@ -46,17 +51,21 @@ class TTSProvider(TTSProviderBase):
"text_type": "plain",
"operation": "query",
"with_frontend": 1,
"frontend_type": "unitTson"
}
"frontend_type": "unitTson",
},
}
try:
resp = requests.post(self.api_url, json.dumps(request_json), headers=self.header)
resp = requests.post(
self.api_url, json.dumps(request_json), headers=self.header
)
if "data" in resp.json():
data = resp.json()["data"]
file_to_save = open(output_file, "wb")
file_to_save.write(base64.b64decode(data))
else:
raise Exception(f"{__name__} status_code: {resp.status_code} response: {resp.content}")
raise Exception(
f"{__name__} status_code: {resp.status_code} response: {resp.content}"
)
except Exception as e:
raise Exception(f"{__name__} error: {e}")
@@ -24,7 +24,7 @@ class ServeReferenceAudio(BaseModel):
def decode_audio(cls, values):
audio = values.get("audio")
if (
isinstance(audio, str) and len(audio) > 255
isinstance(audio, str) and len(audio) > 255
): # Check if audio is a string (Base64)
try:
values["audio"] = base64.b64decode(audio)
@@ -107,7 +107,10 @@ class TTSProvider(TTSProviderBase):
self.api_url = config.get("api_url", "http://127.0.0.1:8080/v1/tts")
def generate_filename(self, extension=".wav"):
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
return os.path.join(
self.output_file,
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
)
async def text_to_speak(self, text, output_file):
# Prepare reference data
@@ -117,9 +120,7 @@ class TTSProvider(TTSProviderBase):
data = {
"text": text,
"references": [
ServeReferenceAudio(
audio=audio if audio else b"", text=text
)
ServeReferenceAudio(audio=audio if audio else b"", text=text)
for text, audio in zip(ref_texts, byte_audios)
],
"reference_id": self.reference_id,
@@ -139,7 +140,9 @@ class TTSProvider(TTSProviderBase):
response = requests.post(
self.api_url,
data=ormsgpack.packb(pydantic_data, option=ormsgpack.OPT_SERIALIZE_PYDANTIC),
data=ormsgpack.packb(
pydantic_data, option=ormsgpack.OPT_SERIALIZE_PYDANTIC
),
headers={
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/msgpack",
@@ -152,8 +155,6 @@ class TTSProvider(TTSProviderBase):
with open(output_file, "wb") as audio_file:
audio_file.write(audio_content)
else:
print(f"Request failed with status code {response.status_code}")
print(response.json())
@@ -4,6 +4,11 @@ import requests
from datetime import datetime
from core.utils.util import check_model_key
from core.providers.tts.base import TTSProviderBase
from config.logger import setup_logging
TAG = __name__
logger = setup_logging()
class TTSProvider(TTSProviderBase):
def __init__(self, config, delete_audio_file):
@@ -18,23 +23,28 @@ class TTSProvider(TTSProviderBase):
check_model_key("TTS", self.api_key)
def generate_filename(self, extension=".wav"):
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
return os.path.join(
self.output_file,
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
)
async def text_to_speak(self, text, output_file):
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
"Content-Type": "application/json",
}
data = {
"model": self.model,
"input": text,
"voice": self.voice,
"response_format": "wav",
"speed": self.speed
"speed": self.speed,
}
response = requests.post(self.api_url, json=data, headers=headers)
if response.status_code == 200:
with open(output_file, "wb") as audio_file:
audio_file.write(response.content)
else:
raise Exception(f"OpenAI TTS请求失败: {response.status_code} - {response.text}")
raise Exception(
f"OpenAI TTS请求失败: {response.status_code} - {response.text}"
)
@@ -0,0 +1,9 @@
from abc import ABC, abstractmethod
from typing import Optional
class VADProviderBase(ABC):
@abstractmethod
def is_vad(self, conn, data) -> bool:
"""检测音频数据中的语音活动"""
pass
@@ -0,0 +1,63 @@
import time
import numpy as np
import torch
import opuslib_next
from config.logger import setup_logging
from core.providers.vad.base import VADProviderBase
TAG = __name__
logger = setup_logging()
class VADProvider(VADProviderBase):
def __init__(self, config):
logger.bind(tag=TAG).info("SileroVAD", config)
self.model, self.utils = torch.hub.load(
repo_or_dir=config["model_dir"],
source="local",
model="silero_vad",
force_reload=False,
)
(get_speech_timestamps, _, _, _, _) = self.utils
self.decoder = opuslib_next.Decoder(16000, 1)
self.vad_threshold = config.get("threshold")
self.silence_threshold_ms = config.get("min_silence_duration_ms")
def is_vad(self, conn, opus_packet):
try:
pcm_frame = self.decoder.decode(opus_packet, 960)
conn.client_audio_buffer.extend(pcm_frame) # 将新数据加入缓冲区
# 处理缓冲区中的完整帧(每次处理512采样点)
client_have_voice = False
while len(conn.client_audio_buffer) >= 512 * 2:
# 提取前512个采样点(1024字节)
chunk = conn.client_audio_buffer[: 512 * 2]
conn.client_audio_buffer = conn.client_audio_buffer[512 * 2 :]
# 转换为模型需要的张量格式
audio_int16 = np.frombuffer(chunk, dtype=np.int16)
audio_float32 = audio_int16.astype(np.float32) / 32768.0
audio_tensor = torch.from_numpy(audio_float32)
# 检测语音活动
speech_prob = self.model(audio_tensor, 16000).item()
client_have_voice = speech_prob >= self.vad_threshold
# 如果之前有声音,但本次没有声音,且与上次有声音的时间查已经超过了静默阈值,则认为已经说完一句话
if conn.client_have_voice and not client_have_voice:
stop_duration = (
time.time() * 1000 - conn.client_have_voice_last_time
)
if stop_duration >= self.silence_threshold_ms:
conn.client_voice_stop = True
if client_have_voice:
conn.client_have_voice = True
conn.client_have_voice_last_time = time.time() * 1000
return client_have_voice
except opuslib_next.OpusError as e:
logger.bind(tag=TAG).info(f"解码错误: {e}")
except Exception as e:
logger.bind(tag=TAG).error(f"Error processing audio packet: {e}")
@@ -1,97 +0,0 @@
import random
import threading
import time
from typing import Set
class AuthCodeGenerator:
_instance = None
_instance_lock = threading.Lock()
def __new__(cls):
if not cls._instance:
with cls._instance_lock:
if not cls._instance:
cls._instance = super(AuthCodeGenerator, cls).__new__(cls)
# 初始化随机种子
random.seed(time.time())
return cls._instance
def __init__(self):
# 确保 __init__ 只被调用一次
if not hasattr(self, '_initialized'):
self._used_codes: Set[str] = set()
self._code_timestamps = {}
self._lock = threading.Lock()
self._code_timeout = 3 * 24 * 60 * 60
self._initialized = True
@classmethod
def get_instance(cls):
"""获取AuthCodeGenerator的单例实例"""
return cls()
def generate_code(self) -> str:
"""
生成6位数字认证码,确保不重复
返回: 6位数字字符串
"""
with self._lock:
self._clean_expired_codes() # 清理过期code
while True:
# 使用时间戳和已用码数量作为种子,确保每次生成不同的随机数
seed = int(time.time() * 1000) + len(self._used_codes)
random.seed(seed)
# 生成6位随机数字
code = ''.join(str(random.randint(0, 9)) for _ in range(6))
# 检查是否已存在
if code not in self._used_codes:
self._used_codes.add(code)
self._code_timestamps[code] = time.time()
return code
def remove_code(self, code: str) -> bool:
"""
删除已使用的认证码
参数:
code: 要删除的认证码
返回:
bool: 删除成功返回True,码不存在返回False
"""
print('remove_code', code)
with self._lock:
if code in self._used_codes:
self._used_codes.remove(code)
if code in self._code_timestamps:
del self._code_timestamps[code]
return True
return False
def is_code_used(self, code: str) -> bool:
"""
检查认证码是否已被使用
参数:
code: 要检查的认证码
返回:
bool: 如果码存在返回True,否则返回False
"""
with self._lock:
return code in self._used_codes
def clear_codes(self):
"""清空所有已使用的认证码"""
with self._lock:
self._used_codes.clear()
self._code_timestamps.clear()
def _clean_expired_codes(self):
"""清理过期的认证码"""
current_time = time.time()
expired_codes = [
code for code, timestamp in self._code_timestamps.items()
if (current_time - timestamp) > self._code_timeout
]
for code in expired_codes:
self._used_codes.remove(code)
del self._code_timestamps[code]
@@ -1,39 +0,0 @@
import asyncio
from typing import Dict
from config.logger import setup_logging
TAG = __name__
logger = setup_logging()
class FileLockManager:
_instance = None
_locks: Dict[str, asyncio.Lock] = {}
def __new__(cls):
if cls._instance is None:
cls._instance = super(FileLockManager, cls).__new__(cls)
return cls._instance
@classmethod
def get_lock(cls, file_path: str) -> asyncio.Lock:
"""获取指定文件的锁"""
if file_path not in cls._locks:
cls._locks[file_path] = asyncio.Lock()
return cls._locks[file_path]
@classmethod
async def acquire_lock(cls, file_path: str):
"""获取锁"""
lock = cls.get_lock(file_path)
await lock.acquire()
logger.bind(tag=TAG).debug(f"Acquired lock for {file_path}")
@classmethod
def release_lock(cls, file_path: str):
"""释放锁"""
if file_path in cls._locks:
try:
cls._locks[file_path].release()
logger.bind(tag=TAG).debug(f"Released lock for {file_path}")
except RuntimeError as e:
logger.bind(tag=TAG).warning(f"Failed to release lock for {file_path}: {e}")
+6 -5
View File
@@ -2,16 +2,17 @@ import os
import sys
import importlib
from config.logger import setup_logging
from core.utils.util import read_config, get_project_dir
logger = setup_logging()
def create_instance(class_name, *args, **kwargs):
if os.path.exists(os.path.join('core', 'providers', 'memory', class_name, f'{class_name}.py')):
lib_name = f'core.providers.memory.{class_name}.{class_name}'
if os.path.exists(
os.path.join("core", "providers", "memory", class_name, f"{class_name}.py")
):
lib_name = f"core.providers.memory.{class_name}.{class_name}"
if lib_name not in sys.modules:
sys.modules[lib_name] = importlib.import_module(f'{lib_name}')
sys.modules[lib_name] = importlib.import_module(f"{lib_name}")
return sys.modules[lib_name].MemoryProvider(*args, **kwargs)
raise ValueError(f"不支持的记忆服务类型: {class_name}")
+109 -22
View File
@@ -1,19 +1,12 @@
import os
import json
import yaml
import socket
import subprocess
import logging
import re
import requests
from typing import Dict, Any
from core.utils import tts, llm, intent, memory, vad, asr
def get_project_dir():
"""获取项目根目录"""
return (
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+ "/"
)
TAG = __name__
def get_local_ip():
@@ -72,7 +65,7 @@ def is_private_ip(ip_addr):
return False # IP address format error or insufficient segments
def get_ip_info(ip_addr):
def get_ip_info(ip_addr, logger):
try:
if is_private_ip(ip_addr):
ip_addr = ""
@@ -81,16 +74,10 @@ def get_ip_info(ip_addr):
ip_info = {"city": resp.get("city")}
return ip_info
except Exception as e:
logging.error(f"Error getting client ip info: {e}")
logger.bind(tag=TAG).error(f"Error getting client ip info: {e}")
return {}
def read_config(config_path):
with open(config_path, "r", encoding="utf-8") as file:
config = yaml.safe_load(file)
return config
def write_json_file(file_path, data):
"""将数据写入 JSON 文件"""
with open(file_path, "w", encoding="utf-8") as file:
@@ -169,10 +156,8 @@ def remove_punctuation_and_length(text):
def check_model_key(modelType, modelKey):
if "" in modelKey:
logging.error(
"你还没配置"
+ modelType
+ "的密钥,请在配置文件中配置密钥,否则无法正常工作"
raise ValueError(
"你还没配置" + modelType + "的密钥,请检查一下所使用的LLM是否配置了密钥"
)
return False
return True
@@ -212,3 +197,105 @@ def extract_json_from_string(input_string):
if match:
return match.group(1) # 返回提取的 JSON 字符串
return None
def initialize_modules(
logger,
config: Dict[str, Any],
init_vad=False,
init_asr=False,
init_llm=False,
init_tts=False,
init_memory=False,
init_intent=False,
) -> Dict[str, Any]:
"""
初始化所有模块组件
Args:
config: 配置字典
Returns:
Dict[str, Any]: 包含所有初始化后的模块的字典
"""
modules = {}
# 初始化TTS模块
if init_tts:
tts_type = (
config["selected_module"]["TTS"]
if "type" not in config["TTS"][config["selected_module"]["TTS"]]
else config["TTS"][config["selected_module"]["TTS"]]["type"]
)
modules["tts"] = tts.create_instance(
tts_type,
config["TTS"][config["selected_module"]["TTS"]],
config["delete_audio"],
)
logger.bind(tag=TAG).info(f"初始化组件: tts成功")
# 初始化LLM模块
if init_llm:
llm_type = (
config["selected_module"]["LLM"]
if "type" not in config["LLM"][config["selected_module"]["LLM"]]
else config["LLM"][config["selected_module"]["LLM"]]["type"]
)
modules["llm"] = llm.create_instance(
llm_type,
config["LLM"][config["selected_module"]["LLM"]],
)
logger.bind(tag=TAG).info(f"初始化组件: llm成功")
# 初始化Intent模块
if init_intent:
intent_type = (
config["selected_module"]["Intent"]
if "type" not in config["Intent"][config["selected_module"]["Intent"]]
else config["Intent"][config["selected_module"]["Intent"]]["type"]
)
modules["intent"] = intent.create_instance(
intent_type,
config["Intent"][config["selected_module"]["Intent"]],
)
logger.bind(tag=TAG).info(f"初始化组件: intent成功")
# 初始化Memory模块
if init_memory:
memory_type = (
config["selected_module"]["Memory"]
if "type" not in config["Memory"][config["selected_module"]["Memory"]]
else config["Memory"][config["selected_module"]["Memory"]]["type"]
)
modules["memory"] = memory.create_instance(
memory_type,
config["Memory"][config["selected_module"]["Memory"]],
)
logger.bind(tag=TAG).info(f"初始化组件: memory成功")
# 初始化VAD模块
if init_vad:
vad_type = (
config["selected_module"]["VAD"]
if "type" not in config["VAD"][config["selected_module"]["VAD"]]
else config["VAD"][config["selected_module"]["VAD"]]["type"]
)
modules["vad"] = vad.create_instance(
vad_type,
config["VAD"][config["selected_module"]["VAD"]],
)
logger.bind(tag=TAG).info(f"初始化组件: vad成功")
# 初始化ASR模块
if init_asr:
asr_type = (
config["selected_module"]["ASR"]
if "type" not in config["ASR"][config["selected_module"]["ASR"]]
else config["ASR"][config["selected_module"]["ASR"]]["type"]
)
modules["asr"] = asr.create_instance(
asr_type,
config["ASR"][config["selected_module"]["ASR"]],
config["delete_audio"],
)
logger.bind(tag=TAG).info(f"初始化组件: asr成功")
return modules
+12 -70
View File
@@ -1,77 +1,19 @@
from abc import ABC, abstractmethod
import importlib
import os
import sys
from core.providers.vad.base import VADProviderBase
from config.logger import setup_logging
import opuslib_next
import time
import numpy as np
import torch
TAG = __name__
logger = setup_logging()
class VAD(ABC):
@abstractmethod
def is_vad(self, conn, data):
"""检测音频数据中的语音活动"""
pass
def create_instance(class_name: str, *args, **kwargs) -> VADProviderBase:
"""工厂方法创建VAD实例"""
if os.path.exists(os.path.join("core", "providers", "vad", f"{class_name}.py")):
lib_name = f"core.providers.vad.{class_name}"
if lib_name not in sys.modules:
sys.modules[lib_name] = importlib.import_module(f"{lib_name}")
return sys.modules[lib_name].VADProvider(*args, **kwargs)
class SileroVAD(VAD):
def __init__(self, config):
logger.bind(tag=TAG).info("SileroVAD", config)
self.model, self.utils = torch.hub.load(repo_or_dir=config["model_dir"],
source='local',
model='silero_vad',
force_reload=False)
(get_speech_timestamps, _, _, _, _) = self.utils
self.decoder = opuslib_next.Decoder(16000, 1)
self.vad_threshold = config.get("threshold")
self.silence_threshold_ms = config.get("min_silence_duration_ms")
def is_vad(self, conn, opus_packet):
try:
pcm_frame = self.decoder.decode(opus_packet, 960)
conn.client_audio_buffer.extend(pcm_frame) # 将新数据加入缓冲区
# 处理缓冲区中的完整帧(每次处理512采样点)
client_have_voice = False
while len(conn.client_audio_buffer) >= 512 * 2:
# 提取前512个采样点(1024字节)
chunk = conn.client_audio_buffer[:512 * 2]
conn.client_audio_buffer = conn.client_audio_buffer[512 * 2:]
# 转换为模型需要的张量格式
audio_int16 = np.frombuffer(chunk, dtype=np.int16)
audio_float32 = audio_int16.astype(np.float32) / 32768.0
audio_tensor = torch.from_numpy(audio_float32)
# 检测语音活动
speech_prob = self.model(audio_tensor, 16000).item()
client_have_voice = speech_prob >= self.vad_threshold
# 如果之前有声音,但本次没有声音,且与上次有声音的时间查已经超过了静默阈值,则认为已经说完一句话
if conn.client_have_voice and not client_have_voice:
stop_duration = time.time() * 1000 - conn.client_have_voice_last_time
if stop_duration >= self.silence_threshold_ms:
conn.client_voice_stop = True
if client_have_voice:
conn.client_have_voice = True
conn.client_have_voice_last_time = time.time() * 1000
return client_have_voice
except opuslib_next.OpusError as e:
logger.bind(tag=TAG).info(f"解码错误: {e}")
except Exception as e:
logger.bind(tag=TAG).error(f"Error processing audio packet: {e}")
def create_instance(class_name, *args, **kwargs) -> VAD:
# 获取类对象
cls_map = {
"SileroVAD": SileroVAD,
# 可扩展其他SileroVAD实现
}
if cls := cls_map.get(class_name):
return cls(*args, **kwargs)
raise ValueError(f"不支持的SileroVAD类型: {class_name}")
raise ValueError(f"不支持的VAD类型: {class_name},请检查该配置的type是否设置正确")
+11 -70
View File
@@ -2,8 +2,7 @@ import asyncio
import websockets
from config.logger import setup_logging
from core.connection import ConnectionHandler
from core.utils.util import get_local_ip
from core.utils import asr, vad, llm, tts, memory, intent
from core.utils.util import get_local_ip, initialize_modules
TAG = __name__
@@ -12,74 +11,16 @@ class WebSocketServer:
def __init__(self, config: dict):
self.config = config
self.logger = setup_logging()
self._vad, self._asr, self._llm, self._tts, self._memory, self.intent = (
self._create_processing_instances()
)
self.active_connections = set() # 添加全局连接记录
def _create_processing_instances(self):
memory_cls_name = self.config["selected_module"].get(
"Memory", "nomem"
) # 默认使用nomem
has_memory_cfg = (
self.config.get("Memory") and memory_cls_name in self.config["Memory"]
)
memory_cfg = self.config["Memory"][memory_cls_name] if has_memory_cfg else {}
"""创建处理模块实例"""
return (
vad.create_instance(
self.config["selected_module"]["VAD"],
self.config["VAD"][self.config["selected_module"]["VAD"]],
),
asr.create_instance(
(
self.config["selected_module"]["ASR"]
if not "type"
in self.config["ASR"][self.config["selected_module"]["ASR"]]
else self.config["ASR"][self.config["selected_module"]["ASR"]][
"type"
]
),
self.config["ASR"][self.config["selected_module"]["ASR"]],
self.config["delete_audio"],
),
llm.create_instance(
(
self.config["selected_module"]["LLM"]
if not "type"
in self.config["LLM"][self.config["selected_module"]["LLM"]]
else self.config["LLM"][self.config["selected_module"]["LLM"]][
"type"
]
),
self.config["LLM"][self.config["selected_module"]["LLM"]],
),
tts.create_instance(
(
self.config["selected_module"]["TTS"]
if not "type"
in self.config["TTS"][self.config["selected_module"]["TTS"]]
else self.config["TTS"][self.config["selected_module"]["TTS"]][
"type"
]
),
self.config["TTS"][self.config["selected_module"]["TTS"]],
self.config["delete_audio"],
),
memory.create_instance(memory_cls_name, memory_cfg),
intent.create_instance(
(
self.config["selected_module"]["Intent"]
if not "type"
in self.config["Intent"][self.config["selected_module"]["Intent"]]
else self.config["Intent"][
self.config["selected_module"]["Intent"]
]["type"]
),
self.config["Intent"][self.config["selected_module"]["Intent"]],
),
modules = initialize_modules(
self.logger, self.config, True, True, True, True, True, True
)
self._vad = modules["vad"]
self._asr = modules["asr"]
self._tts = modules["tts"]
self._llm = modules["llm"]
self._intent = modules["intent"]
self._memory = modules["memory"]
self.active_connections = set()
async def start(self):
server_config = self.config["server"]
@@ -111,7 +52,7 @@ class WebSocketServer:
self._llm,
self._tts,
self._memory,
self.intent,
self._intent,
)
self.active_connections.add(handler)
try:
+181 -108
View File
@@ -5,9 +5,8 @@ from tabulate import tabulate
from typing import Dict, List
from core.utils.llm import create_instance as create_llm_instance
from core.utils.tts import create_instance as create_tts_instance
from core.utils.util import read_config
import statistics
from config.settings import get_config_file
from config.settings import load_config
import inspect
import os
import logging
@@ -18,17 +17,16 @@ logging.basicConfig(level=logging.WARNING)
class AsyncPerformanceTester:
def __init__(self):
self.config = read_config(get_config_file())
self.config = load_config()
self.test_sentences = self.config.get("module_test", {}).get(
"test_sentences",
["你好,请介绍一下你自己", "What's the weather like today?",
"请用100字概括量子计算的基本原理和应用前景"]
[
"你好,请介绍一下你自己",
"What's the weather like today?",
"请用100字概括量子计算的基本原理和应用前景",
],
)
self.results = {
"llm": {},
"tts": {},
"combinations": []
}
self.results = {"llm": {}, "tts": {}, "combinations": []}
async def _check_ollama_service(self, base_url: str, model_name: str) -> bool:
"""异步检查Ollama服务状态"""
@@ -46,7 +44,9 @@ class AsyncPerformanceTester:
data = await response.json()
models = data.get("models", [])
if not any(model["name"] == model_name for model in models):
print(f"🚫 Ollama模型 {model_name} 未找到,请先使用 ollama pull {model_name} 下载")
print(
f"🚫 Ollama模型 {model_name} 未找到,请先使用 ollama pull {model_name} 下载"
)
return False
else:
print(f"🚫 无法获取Ollama模型列表")
@@ -62,17 +62,16 @@ class AsyncPerformanceTester:
logging.getLogger("core.providers.tts.base").setLevel(logging.WARNING)
token_fields = ["access_token", "api_key", "token"]
if any(field in config and any(x in config[field] for x in ["你的", "placeholder"]) for field in
token_fields):
if any(
field in config
and any(x in config[field] for x in ["你的", "placeholder"])
for field in token_fields
):
print(f"⏭️ TTS {tts_name} 未配置access_token/api_key,已跳过")
return {"name": tts_name, "type": "tts", "errors": 1}
module_type = config.get('type', tts_name)
tts = create_tts_instance(
module_type,
config,
delete_audio_file=True
)
module_type = config.get("type", tts_name)
tts = create_tts_instance(module_type, config, delete_audio_file=True)
print(f"🎵 测试 TTS: {tts_name}")
@@ -103,7 +102,7 @@ class AsyncPerformanceTester:
"name": tts_name,
"type": "tts",
"avg_time": total_time / test_count,
"errors": 0
"errors": 0,
}
except Exception as e:
@@ -115,8 +114,8 @@ class AsyncPerformanceTester:
try:
# 对于Ollama,跳过api_key检查并进行特殊处理
if llm_name == "Ollama":
base_url = config.get('base_url', 'http://localhost:11434')
model_name = config.get('model_name')
base_url = config.get("base_url", "http://localhost:11434")
model_name = config.get("model_name")
if not model_name:
print(f"🚫 Ollama未配置model_name")
return {"name": llm_name, "type": "llm", "errors": 1}
@@ -124,21 +123,27 @@ class AsyncPerformanceTester:
if not await self._check_ollama_service(base_url, model_name):
return {"name": llm_name, "type": "llm", "errors": 1}
else:
if "api_key" in config and any(x in config["api_key"] for x in ["你的", "placeholder", "sk-xxx"]):
if "api_key" in config and any(
x in config["api_key"] for x in ["你的", "placeholder", "sk-xxx"]
):
print(f"🚫 跳过未配置的LLM: {llm_name}")
return {"name": llm_name, "type": "llm", "errors": 1}
# 获取实际类型(兼容旧配置)
module_type = config.get('type', llm_name)
module_type = config.get("type", llm_name)
llm = create_llm_instance(module_type, config)
# 统一使用UTF-8编码
test_sentences = [s.encode('utf-8').decode('utf-8') for s in self.test_sentences]
test_sentences = [
s.encode("utf-8").decode("utf-8") for s in self.test_sentences
]
# 创建所有句子的测试任务
sentence_tasks = []
for sentence in test_sentences:
sentence_tasks.append(self._test_single_sentence(llm_name, llm, sentence))
sentence_tasks.append(
self._test_single_sentence(llm_name, llm, sentence)
)
# 并发执行所有句子测试
sentence_results = await asyncio.gather(*sentence_tasks)
@@ -166,9 +171,15 @@ class AsyncPerformanceTester:
"type": "llm",
"avg_response": sum(response_times) / len(response_times),
"avg_first_token": sum(first_token_times) / len(first_token_times),
"std_first_token": statistics.stdev(first_token_times) if len(first_token_times) > 1 else 0,
"std_response": statistics.stdev(response_times) if len(response_times) > 1 else 0,
"errors": 0
"std_first_token": (
statistics.stdev(first_token_times)
if len(first_token_times) > 1
else 0
),
"std_response": (
statistics.stdev(response_times) if len(response_times) > 1 else 0
),
"errors": 0,
}
except Exception as e:
print(f"LLM {llm_name} 测试失败: {str(e)}")
@@ -184,8 +195,10 @@ class AsyncPerformanceTester:
async def process_response():
nonlocal first_token_received, first_token_time
for chunk in llm.response("perf_test", [{"role": "user", "content": sentence}]):
if not first_token_received and chunk.strip() != '':
for chunk in llm.response(
"perf_test", [{"role": "user", "content": sentence}]
):
if not first_token_received and chunk.strip() != "":
first_token_time = time.time() - sentence_start
first_token_received = True
print(f"{llm_name} 首个Token: {first_token_time:.3f}s")
@@ -199,13 +212,15 @@ class AsyncPerformanceTester:
print(f"{llm_name} 完成响应: {response_time:.3f}s")
if first_token_time is None:
first_token_time = response_time # 如果没有检测到first token,使用总响应时间
first_token_time = (
response_time # 如果没有检测到first token,使用总响应时间
)
return {
"name": llm_name,
"type": "llm",
"first_token_time": first_token_time,
"response_time": response_time
"response_time": response_time,
}
except Exception as e:
print(f"⚠️ {llm_name} 句子测试失败: {str(e)}")
@@ -214,24 +229,37 @@ class AsyncPerformanceTester:
def _generate_combinations(self):
"""生成最佳组合建议"""
valid_llms = [
k for k, v in self.results["llm"].items()
k
for k, v in self.results["llm"].items()
if v["errors"] == 0 and v["avg_first_token"] >= 0.05
]
valid_tts = [k for k, v in self.results["tts"].items() if v["errors"] == 0]
# 找出基准值
min_first_token = min([self.results["llm"][llm]["avg_first_token"] for llm in valid_llms]) if valid_llms else 1
min_tts_time = min([self.results["tts"][tts]["avg_time"] for tts in valid_tts]) if valid_tts else 1
min_first_token = (
min([self.results["llm"][llm]["avg_first_token"] for llm in valid_llms])
if valid_llms
else 1
)
min_tts_time = (
min([self.results["tts"][tts]["avg_time"] for tts in valid_tts])
if valid_tts
else 1
)
for llm in valid_llms:
for tts in valid_tts:
# 计算相对性能分数(越小越好)
llm_score = self.results["llm"][llm]["avg_first_token"] / min_first_token
llm_score = (
self.results["llm"][llm]["avg_first_token"] / min_first_token
)
tts_score = self.results["tts"][tts]["avg_time"] / min_tts_time
# 计算稳定性分数(标准差/平均值,越小越稳定)
llm_stability = self.results["llm"][llm]["std_first_token"] / self.results["llm"][llm][
"avg_first_token"]
llm_stability = (
self.results["llm"][llm]["std_first_token"]
/ self.results["llm"][llm]["avg_first_token"]
)
# 综合得分(考虑性能和稳定性)
# 性能权重0.7,稳定性权重0.3
@@ -240,16 +268,20 @@ class AsyncPerformanceTester:
# 总分 = LLM得分(70%) + TTS得分(30%)
total_score = llm_final_score * 0.7 + tts_score * 0.3
self.results["combinations"].append({
"llm": llm,
"tts": tts,
"score": total_score,
"details": {
"llm_first_token": self.results["llm"][llm]["avg_first_token"],
"llm_stability": llm_stability,
"tts_time": self.results["tts"][tts]["avg_time"]
self.results["combinations"].append(
{
"llm": llm,
"tts": tts,
"score": total_score,
"details": {
"llm_first_token": self.results["llm"][llm][
"avg_first_token"
],
"llm_stability": llm_stability,
"tts_time": self.results["tts"][tts]["avg_time"],
},
}
})
)
# 分数越小越好
self.results["combinations"].sort(key=lambda x: x["score"])
@@ -260,42 +292,45 @@ class AsyncPerformanceTester:
for name, data in self.results["llm"].items():
if data["errors"] == 0:
stability = data["std_first_token"] / data["avg_first_token"]
llm_table.append([
name, # 不需要固定宽度,让tabulate自己处理对齐
f"{data['avg_first_token']:.3f}",
f"{data['avg_response']:.3f}",
f"{stability:.3f}"
])
llm_table.append(
[
name, # 不需要固定宽度,让tabulate自己处理对齐
f"{data['avg_first_token']:.3f}",
f"{data['avg_response']:.3f}",
f"{stability:.3f}",
]
)
if llm_table:
print("\nLLM 性能排行:")
print(tabulate(
llm_table,
headers=["模型名称", "首字耗时", "总耗时", "稳定性"],
tablefmt="github",
colalign=("left", "right", "right", "right"),
disable_numparse=True
))
print(
tabulate(
llm_table,
headers=["模型名称", "首字耗时", "总耗时", "稳定性"],
tablefmt="github",
colalign=("left", "right", "right", "right"),
disable_numparse=True,
)
)
else:
print("\n⚠️ 没有可用的LLM模块进行测试。")
tts_table = []
for name, data in self.results["tts"].items():
if data["errors"] == 0:
tts_table.append([
name, # 不需要固定宽度
f"{data['avg_time']:.3f}"
])
tts_table.append([name, f"{data['avg_time']:.3f}"]) # 不需要固定宽度
if tts_table:
print("\nTTS 性能排行:")
print(tabulate(
tts_table,
headers=["模型名称", "合成耗时"],
tablefmt="github",
colalign=("left", "right"),
disable_numparse=True
))
print(
tabulate(
tts_table,
headers=["模型名称", "合成耗时"],
tablefmt="github",
colalign=("left", "right"),
disable_numparse=True,
)
)
else:
print("\n⚠️ 没有可用的TTS模块进行测试。")
@@ -303,21 +338,31 @@ class AsyncPerformanceTester:
print("\n推荐配置组合 (得分越小越好):")
combo_table = []
for combo in self.results["combinations"][:5]:
combo_table.append([
f"{combo['llm']} + {combo['tts']}", # 不需要固定宽度
f"{combo['score']:.3f}",
f"{combo['details']['llm_first_token']:.3f}",
f"{combo['details']['llm_stability']:.3f}",
f"{combo['details']['tts_time']:.3f}"
])
combo_table.append(
[
f"{combo['llm']} + {combo['tts']}", # 不需要固定宽度
f"{combo['score']:.3f}",
f"{combo['details']['llm_first_token']:.3f}",
f"{combo['details']['llm_stability']:.3f}",
f"{combo['details']['tts_time']:.3f}",
]
)
print(tabulate(
combo_table,
headers=["组合方案", "综合得分", "LLM首字耗时", "稳定性", "TTS合成耗时"],
tablefmt="github",
colalign=("left", "right", "right", "right", "right"),
disable_numparse=True
))
print(
tabulate(
combo_table,
headers=[
"组合方案",
"综合得分",
"LLM首字耗时",
"稳定性",
"TTS合成耗时",
],
tablefmt="github",
colalign=("left", "right", "right", "right", "right"),
disable_numparse=True,
)
)
else:
print("\n⚠️ 没有可用的模块组合建议。")
@@ -341,18 +386,21 @@ class AsyncPerformanceTester:
for llm_name, config in self.config.get("LLM", {}).items():
# 检查配置有效性
if llm_name == "CozeLLM":
if any(x in config.get("bot_id", "") for x in ["你的"]) \
or any(x in config.get("user_id", "") for x in ["你的"]):
if any(x in config.get("bot_id", "") for x in ["你的"]) or any(
x in config.get("user_id", "") for x in ["你的"]
):
print(f"⏭️ LLM {llm_name} 未配置bot_id/user_id,已跳过")
continue
elif "api_key" in config and any(x in config["api_key"] for x in ["你的", "placeholder", "sk-xxx"]):
elif "api_key" in config and any(
x in config["api_key"] for x in ["你的", "placeholder", "sk-xxx"]
):
print(f"⏭️ LLM {llm_name} 未配置api_key,已跳过")
continue
# 对于Ollama,先检查服务状态
if llm_name == "Ollama":
base_url = config.get('base_url', 'http://localhost:11434')
model_name = config.get('model_name')
base_url = config.get("base_url", "http://localhost:11434")
model_name = config.get("model_name")
if not model_name:
print(f"🚫 Ollama未配置model_name")
continue
@@ -361,27 +409,33 @@ class AsyncPerformanceTester:
continue
print(f"📋 添加LLM测试任务: {llm_name}")
module_type = config.get('type', llm_name)
module_type = config.get("type", llm_name)
llm = create_llm_instance(module_type, config)
# 为每个句子创建独立任务
for sentence in self.test_sentences:
sentence = sentence.encode('utf-8').decode('utf-8')
sentence = sentence.encode("utf-8").decode("utf-8")
all_tasks.append(self._test_single_sentence(llm_name, llm, sentence))
# TTS测试任务
for tts_name, config in self.config.get("TTS", {}).items():
token_fields = ["access_token", "api_key", "token"]
if any(field in config and any(x in config[field] for x in ["你的", "placeholder"]) for field in
token_fields):
if any(
field in config
and any(x in config[field] for x in ["你的", "placeholder"])
for field in token_fields
):
print(f"⏭️ TTS {tts_name} 未配置access_token/api_key,已跳过")
continue
print(f"🎵 添加TTS测试任务: {tts_name}")
all_tasks.append(self._test_tts(tts_name, config))
print(
f"\n✅ 找到 {len([t for t in all_tasks if 'test_single_sentence' in str(t)]) / len(self.test_sentences):.0f} 个可用LLM模块")
print(f"✅ 找到 {len([t for t in all_tasks if '_test_tts' in str(t)])} 个可用TTS模块")
f"\n✅ 找到 {len([t for t in all_tasks if 'test_single_sentence' in str(t)]) / len(self.test_sentences):.0f} 个可用LLM模块"
)
print(
f"✅ 找到 {len([t for t in all_tasks if '_test_tts' in str(t)])} 个可用TTS模块"
)
print("\n⏳ 开始并发测试所有模块...\n")
# 并发执行所有测试任务
@@ -389,7 +443,11 @@ class AsyncPerformanceTester:
# 处理LLM结果
llm_results = {}
for result in [r for r in all_results if r and isinstance(r, dict) and r.get("type") == "llm"]:
for result in [
r
for r in all_results
if r and isinstance(r, dict) and r.get("type") == "llm"
]:
llm_name = result["name"]
if llm_name not in llm_results:
llm_results[llm_name] = {
@@ -397,9 +455,11 @@ class AsyncPerformanceTester:
"type": "llm",
"first_token_times": [],
"response_times": [],
"errors": 0
"errors": 0,
}
llm_results[llm_name]["first_token_times"].append(result["first_token_time"])
llm_results[llm_name]["first_token_times"].append(
result["first_token_time"]
)
llm_results[llm_name]["response_times"].append(result["response_time"])
# 计算LLM平均值和标准差
@@ -408,16 +468,29 @@ class AsyncPerformanceTester:
self.results["llm"][llm_name] = {
"name": llm_name,
"type": "llm",
"avg_response": sum(data["response_times"]) / len(data["response_times"]),
"avg_first_token": sum(data["first_token_times"]) / len(data["first_token_times"]),
"std_first_token": statistics.stdev(data["first_token_times"]) if len(
data["first_token_times"]) > 1 else 0,
"std_response": statistics.stdev(data["response_times"]) if len(data["response_times"]) > 1 else 0,
"errors": 0
"avg_response": sum(data["response_times"])
/ len(data["response_times"]),
"avg_first_token": sum(data["first_token_times"])
/ len(data["first_token_times"]),
"std_first_token": (
statistics.stdev(data["first_token_times"])
if len(data["first_token_times"]) > 1
else 0
),
"std_response": (
statistics.stdev(data["response_times"])
if len(data["response_times"]) > 1
else 0
),
"errors": 0,
}
# 处理TTS结果
for result in [r for r in all_results if r and isinstance(r, dict) and r.get("type") == "tts"]:
for result in [
r
for r in all_results
if r and isinstance(r, dict) and r.get("type") == "tts"
]:
if result["errors"] == 0:
self.results["tts"][result["name"]] = result
@@ -433,4 +506,4 @@ async def main():
if __name__ == "__main__":
asyncio.run(main())
asyncio.run(main())