mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 07:03:53 +08:00
update:server连接api (#747)
* update:server连接manager-api * update:读取智能体模型配置 * update:添加默认模型的按钮 * update:优化配置读取方式 * update:server兼容manager接口改造 * update:优化私有配置加载 * update:加载私有模型配置
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
+35
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+61
@@ -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;
|
||||
}
|
||||
+6
-1
@@ -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);
|
||||
}
|
||||
+253
@@ -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);
|
||||
}
|
||||
+10
@@ -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);
|
||||
}
|
||||
|
||||
+28
@@ -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;
|
||||
}
|
||||
+7
@@ -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);
|
||||
|
||||
/**
|
||||
* 保存音色信息
|
||||
|
||||
+28
-2
@@ -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
|
||||
|
||||
+39
-37
@@ -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);
|
||||
+12
-12
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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 # 如果说话停顿比较长,可以把这个值设置大一些
|
||||
|
||||
@@ -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},请检查写入权限")
|
||||
@@ -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')
|
||||
@@ -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"
|
||||
|
||||
@@ -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值
|
||||
@@ -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)
|
||||
|
||||
@@ -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}")
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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是否设置正确")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user