Merge pull request #1320 from xinnan-tech/manager-icp-num

可自定义智控台底部的系统名称、ICP备案号、公安备案号
This commit is contained in:
欣南科技
2025-05-20 11:15:38 +08:00
committed by GitHub
14 changed files with 350 additions and 137 deletions
+1 -1
View File
@@ -40,5 +40,5 @@
2、开启允许非管理员用户可注册,将参数`server.allow_user_register`设置成`true` 2、开启允许非管理员用户可注册,将参数`server.allow_user_register`设置成`true`
3、开启手机注册功能,将参数`system.enable_mobile_register`设置成`true` 3、开启手机注册功能,将参数`server.enable_mobile_register`设置成`true`
![步骤](images/alisms/sms-32.png) ![步骤](images/alisms/sms-32.png)
@@ -118,29 +118,17 @@ public interface Constant {
enum SysBaseParam { enum SysBaseParam {
/** /**
* 系统全称 * ICP备案号
*/ */
SYS_NAME("SYS_NAME"), BEIAN_ICP_NUM("server.beian_icp_num"),
/** /**
* 系统简称 * GA备案号
*/ */
SYS_SHORT_NAME("SYS_SHORT_NAME"), BEIAN_GA_NUM("server.beian_ga_num"),
/** /**
* 系统描述 * 系统名称
*/ */
SYS_DES("SYS_DES"), SERVER_NAME("server.name");
/**
* 登录失败几次锁定
*/
LOGIN_LOCK_COUNT("LOGIN_LOCK_COUNT"),
/**
* 账号失败锁定分钟数
*/
LOGIN_LOCK_TIME("LOGIN_LOCK_TIME"),
/**
* TOKEN强验证
*/
SYS_TOKEN_SECURITY("SYS_TOKEN_SECURITY");
private String value; private String value;
@@ -176,11 +164,11 @@ public interface Constant {
/** /**
* 单号码最大短信发送条数 * 单号码最大短信发送条数
*/ */
SYSTEM_SMS_MAX_SEND_COUNT("system.sms.max_send_count"), SERVER_SMS_MAX_SEND_COUNT("server.sms_max_send_count"),
/** /**
* 是否开启手机注册 * 是否开启手机注册
*/ */
SYSTEM_ENABLE_MOBILE_REGISTER("system.enable_mobile_register"); SERVER_ENABLE_MOBILE_REGISTER("server.enable_mobile_register");
private String value; private String value;
@@ -245,4 +233,24 @@ public interface Constant {
* 无效固件URL * 无效固件URL
*/ */
String INVALID_FIRMWARE_URL = "http://xiaozhi.server.com:8002/xiaozhi/otaMag/download/NOT_ACTIVATED_FIRMWARE_THIS_IS_A_INVALID_URL"; String INVALID_FIRMWARE_URL = "http://xiaozhi.server.com:8002/xiaozhi/otaMag/download/NOT_ACTIVATED_FIRMWARE_THIS_IS_A_INVALID_URL";
/**
* 字典类型
*/
enum DictType {
/**
* 手机区号
*/
MOBILE_AREA("MOBILE_AREA");
private String value;
DictType(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
} }
@@ -1,6 +1,7 @@
package xiaozhi.modules.security.controller; package xiaozhi.modules.security.controller;
import java.io.IOException; import java.io.IOException;
import java.util.Calendar;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -70,7 +71,7 @@ public class LoginController {
throw new RenException("图形验证码错误"); throw new RenException("图形验证码错误");
} }
Boolean isMobileRegister = sysParamsService Boolean isMobileRegister = sysParamsService
.getValueObject(Constant.SysMSMParam.SYSTEM_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class); .getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
if (!isMobileRegister) { if (!isMobileRegister) {
throw new RenException("没有开启手机注册,没法使用短信验证码功能"); throw new RenException("没有开启手机注册,没法使用短信验证码功能");
} }
@@ -108,7 +109,7 @@ public class LoginController {
} }
// 是否开启手机注册 // 是否开启手机注册
Boolean isMobileRegister = sysParamsService Boolean isMobileRegister = sysParamsService
.getValueObject(Constant.SysMSMParam.SYSTEM_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class); .getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
boolean validate; boolean validate;
if (isMobileRegister) { if (isMobileRegister) {
// 验证用户是否是手机号码 // 验证用户是否是手机号码
@@ -165,7 +166,7 @@ public class LoginController {
public Result<?> retrievePassword(@RequestBody RetrievePasswordDTO dto) { public Result<?> retrievePassword(@RequestBody RetrievePasswordDTO dto) {
// 是否开启手机注册 // 是否开启手机注册
Boolean isMobileRegister = sysParamsService Boolean isMobileRegister = sysParamsService
.getValueObject(Constant.SysMSMParam.SYSTEM_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class); .getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
if (!isMobileRegister) { if (!isMobileRegister) {
throw new RenException("没有开启手机注册,没法使用找回密码功能"); throw new RenException("没有开启手机注册,没法使用找回密码功能");
} }
@@ -198,11 +199,15 @@ public class LoginController {
public Result<Map<String, Object>> pubConfig() { public Result<Map<String, Object>> pubConfig() {
Map<String, Object> config = new HashMap<>(); Map<String, Object> config = new HashMap<>();
config.put("enableMobileRegister", sysParamsService config.put("enableMobileRegister", sysParamsService
.getValueObject(Constant.SysMSMParam.SYSTEM_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class)); .getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class));
config.put("version", Constant.VERSION); config.put("version", Constant.VERSION);
config.put("year", "©" + Calendar.getInstance().get(Calendar.YEAR));
config.put("allowUserRegister", sysUserService.getAllowUserRegister()); config.put("allowUserRegister", sysUserService.getAllowUserRegister());
List<SysDictDataItem> list = sysDictDataService.getDictDataByType("MOBILE_AREA"); List<SysDictDataItem> list = sysDictDataService.getDictDataByType(Constant.DictType.MOBILE_AREA.getValue());
config.put("mobileAreaList", list); config.put("mobileAreaList", list);
config.put("beianIcpNum", sysParamsService.getValue(Constant.SysBaseParam.BEIAN_ICP_NUM.getValue(), true));
config.put("beianGaNum", sysParamsService.getValue(Constant.SysBaseParam.BEIAN_GA_NUM.getValue(), true));
config.put("name", sysParamsService.getValue(Constant.SysBaseParam.SERVER_NAME.getValue(), true));
return new Result<Map<String, Object>>().ok(config); return new Result<Map<String, Object>>().ok(config);
} }
@@ -101,7 +101,7 @@ public class CaptchaServiceImpl implements CaptchaService {
// 获取最大发送次数限制 // 获取最大发送次数限制
Integer maxSendCount = sysParamsService.getValueObject( Integer maxSendCount = sysParamsService.getValueObject(
Constant.SysMSMParam.SYSTEM_SMS_MAX_SEND_COUNT.getValue(), Constant.SysMSMParam.SERVER_SMS_MAX_SEND_COUNT.getValue(),
Integer.class); Integer.class);
if (maxSendCount == null) { if (maxSendCount == null) {
maxSendCount = 5; // 默认值 maxSendCount = 5; // 默认值
@@ -59,7 +59,7 @@ public class SysParamsController {
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", in = ParameterIn.QUERY, required = true, ref = "int"), @Parameter(name = Constant.LIMIT, description = "每页显示记录数", in = ParameterIn.QUERY, required = true, ref = "int"),
@Parameter(name = Constant.ORDER_FIELD, description = "排序字段", in = ParameterIn.QUERY, ref = "String"), @Parameter(name = Constant.ORDER_FIELD, description = "排序字段", in = ParameterIn.QUERY, ref = "String"),
@Parameter(name = Constant.ORDER, description = "排序方式,可选值(asc、desc)", in = ParameterIn.QUERY, ref = "String"), @Parameter(name = Constant.ORDER, description = "排序方式,可选值(asc、desc)", in = ParameterIn.QUERY, ref = "String"),
@Parameter(name = "paramCode", description = "参数编码", in = ParameterIn.QUERY, ref = "String") @Parameter(name = "paramCode", description = "参数编码或参数备注", in = ParameterIn.QUERY, ref = "String")
}) })
@RequiresPermissions("sys:role:superAdmin") @RequiresPermissions("sys:role:superAdmin")
public Result<PageData<SysParamsDTO>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) { public Result<PageData<SysParamsDTO>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
@@ -52,7 +52,9 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
QueryWrapper<SysParamsEntity> wrapper = new QueryWrapper<>(); QueryWrapper<SysParamsEntity> wrapper = new QueryWrapper<>();
wrapper.eq("param_type", 1); wrapper.eq("param_type", 1);
wrapper.like(StringUtils.isNotBlank(paramCode), "param_code", paramCode); wrapper.nested(StringUtils.isNotBlank(paramCode),i->i.like("param_code", paramCode)
.or()
.like("remark", paramCode));
return wrapper; return wrapper;
} }
@@ -1,15 +1,21 @@
-- 添加手机短信注册功能的需要的参数 -- 添加手机短信注册功能的需要的参数
delete from sys_params where id in (601, 602, 610, 611, 612, 613); delete from sys_params where id in (108, 109, 110, 111, 112, 113, 114, 115);
delete from sys_params where id in (610, 611, 612, 613);
INSERT INTO sys_params INSERT INTO sys_params
(id, param_code, param_value, value_type, param_type, remark, creator, create_date, updater, update_date) (id, param_code, param_value, value_type, param_type, remark, creator, create_date, updater, update_date)
VALUES VALUES
(601, 'system.enable_mobile_register', 'false', 'boolean', 1, '是否开启手机注册', NULL, NULL, NULL, NULL), (108, 'server.name', 'xiaozhi-esp32-server', 'string', 1, '系统名称', NULL, NULL, NULL, NULL),
(602, 'system.sms.max_send_count', '10', 'number', 1, '单号码单日最大短信发送条数', NULL, NULL, NULL, NULL), (109, 'server.beian_icp_num', 'null', 'string', 1, 'icp备案号,填写null则不设置', NULL, NULL, NULL, NULL),
(110, 'server.beian_ga_num', 'null', 'string', 1, '公安备案号,填写null则不设置', NULL, NULL, NULL, NULL),
(111, 'server.enable_mobile_register', 'false', 'boolean', 1, '是否开启手机注册', NULL, NULL, NULL, NULL),
(112, 'server.sms_max_send_count', '10', 'number', 1, '单号码单日最大短信发送条数', NULL, NULL, NULL, NULL),
(610, 'aliyun.sms.access_key_id', '', 'string', 1, '阿里云平台access_key', NULL, NULL, NULL, NULL), (610, 'aliyun.sms.access_key_id', '', 'string', 1, '阿里云平台access_key', NULL, NULL, NULL, NULL),
(611, 'aliyun.sms.access_key_secret', '', 'string', 1, '阿里云平台access_key_secret', NULL, NULL, NULL, NULL), (611, 'aliyun.sms.access_key_secret', '', 'string', 1, '阿里云平台access_key_secret', NULL, NULL, NULL, NULL),
(612, 'aliyun.sms.sign_name', '', 'string', 1, '阿里云短信签名', NULL, NULL, NULL, NULL), (612, 'aliyun.sms.sign_name', '', 'string', 1, '阿里云短信签名', NULL, NULL, NULL, NULL),
(613, 'aliyun.sms.sms_code_template_code', '', 'string', 1, '阿里云短信模板', NULL, NULL, NULL, NULL); (613, 'aliyun.sms.sms_code_template_code', '', 'string', 1, '阿里云短信模板', NULL, NULL, NULL, NULL);
update sys_params set remark = '是否允许管理员以外的人注册' where param_code = 'server.allow_user_register';
-- 增加手机区域字典 -- 增加手机区域字典
-- 插入固件类型字典类型 -- 插入固件类型字典类型
delete from `sys_dict_type` where `id` = 102; delete from `sys_dict_type` where `id` = 102;
@@ -114,13 +114,6 @@ databaseChangeLog:
- sqlFile: - sqlFile:
encoding: utf8 encoding: utf8
path: classpath:db/changelog/202505091409.sql path: classpath:db/changelog/202505091409.sql
- changeSet:
id: 202505141132
author: zjy
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202505141132.sql
- changeSet: - changeSet:
id: 202505091555 id: 202505091555
author: whosmyqueen author: whosmyqueen
@@ -149,3 +142,10 @@ databaseChangeLog:
- sqlFile: - sqlFile:
encoding: utf8 encoding: utf8
path: classpath:db/changelog/202505142037.sql path: classpath:db/changelog/202505142037.sql
- changeSet:
id: 202505182234
author: amen
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202505182234.sql
@@ -1,23 +1,74 @@
<template> <template>
<div class="copyright"> <div class="copyright">
©2025 xiaozhi-esp32-server v{{ version }} <div class="footer-content">
<span>{{ year }} {{ name }} {{ version }}</span>
<template v-if="beianGaNum !== 'null'">
<span v-if="beianIcpNum !== 'null' || name">|</span>
<a :href="'http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=' + beianGaNum" target="_blank"
rel="noopener" class="beian-link">
<img
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAMAAABhEH5lAAACXlBMVEUAAADax4rGxqP556zmsVD/6KXu5JvwvFPglEFnY3nnpEjosU1+XFTmuVfu13Hrv1jpsFa1k3mne2HlvnPdqmPqz3fu3YrJoWv04J/mz5z246aSi3i2p2/0zGj82WDrtFjar1O7klO4dkfimUJcWnbho0rgr1vntFfmvFflvGOZdFKXZkOUemnmv2XkuWWVe2ziqlXnt1Tr0HDpwl6ObmCkbEKqhVyOc2jdoVHmuljfrmDq0nPapWDUnErqy3XjtWXesXLrzHjz34rqzYfmw3fy4YrqyYDevW/dzoTIhkndsXLRsIneuHjlyIzQroTjuXjlyYfktWvw2prhvoHTmGfx45j3773TuYT/25LdijyKf321oXU+Nm3cumXfuWLtwlzpwVz40FvpvlvqvFfouFXjrlHipEjcmEfknEXPbzfbZyzZQR/YMBrWKBrhJhLdHxDTGg5HV4xBT4YYKYaWk4SDgYIWIYBybH0qLn1KS3tlZHpNS3WGfHIcHXEwLHAnF28rKW07NGuzn2pNQ2ruzWnDrGnuy2jryGjAnGgoH2j1x2RCNGPcrmIsEGGQdl/pu17YtV7FoV3muVxoRVzWqFvSmVp2SVbyw1XbkVTuvFPfo1PgmVPpt1LcnFLWkVLlp1F7W1HpsU92QE5oMUtFAUrpqUjnjUFOAEHUgz+DUj14FzvbczieMTdhATfodjXWcDXcbjXVcTS/Xi/hay7WXS3cWS3vYyvaVSrUVymnJSjXSyeKDSbNSiWfHCPMTyLFQiHkSR/lPxvVLRrPLBnUJRfUHhbaJxXVGxTZJxPNFhBdOhm/AAAAWXRSTlMABQIU/hIJ/v79/Pj29PPx4cC6s7CNfmxZWRv+/v7+/v7+/v78/Pr6+fPz8vHq6ujl5eTj4ODe2NTQzMW6ubKsnJeQkJCJiIOBgX9ubGpoW1lMREM0JR8dDgvYx1gAAAE7SURBVBjTYgADJmZJQUFJZiYGOGD2EzLV1jIT8paCibCJ86zctGPD1D4ecUaICKuPyYLMYznZOfNqzP0jwEJBOt1KB4/mHjqZ3dGsHwxW5M4Zk5l/PPdIQcGMShUPVqBlgQLlMSvyDq+P3HVidjWnkQQTA5MXV1SFYtb+jZGrl02si2J3Y2JgFMmILl68efueLVvXLSqLXirCCBgDow1HT+GS/AP7srblLS+K5bIHCjmodpXUr929d+eaVb3SsuouQCHhufPjShsbJk/rrK2KW8hiBxRyVkvnkI9tnaDQJDNFOU1DDOjRMEfe9Mjpiewz5RIzZmmKMoPcKqGbkJqSkBSfnDqpXS+ACeRpJ76W/uSUpDlpLPFtfK5soLAKFbU05Odm4eblN7YWk4KEGWOIp62FgYCVsG84SAAAL7BaooX965sAAAAASUVORK5CYII="
class="beian-icon" alt="备案图标">
<span class="beian-text">{{ beianGaNum }}</span>
</a>
</template>
<template v-if="beianIcpNum !== 'null'">
<span v-if="name">|</span>
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" class="beian-link">
<span class="beian-text">{{ beianIcpNum }}</span>
</a>
</template>
</div> </div>
</div>
</template> </template>
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
export default { export default {
name: 'VersionFooter', name: 'VersionFooter',
computed: { computed: {
...mapState({ ...mapState({
version: state => state.pubConfig.version version: state => state.pubConfig.version,
}) name: state => state.pubConfig.name,
}, beianIcpNum: state => state.pubConfig.beianIcpNum,
mounted() { beianGaNum: state => state.pubConfig.beianGaNum,
this.$store.dispatch('fetchPubConfig'); year: state => state.pubConfig.year
} })
},
mounted() {
this.$store.dispatch('fetchPubConfig')
}
} }
</script> </script>
<style scoped></style> <style scoped>
.copyright {
padding: 10px 0;
}
.footer-content {
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.beian-link {
text-decoration: none;
display: inline-flex;
align-items: center;
}
.beian-icon {
width: 16px;
height: 16px;
vertical-align: middle;
}
.beian-text {
color: #000;
font-size: 12px;
vertical-align: middle;
}
</style>
+2
View File
@@ -13,6 +13,8 @@ export default new Vuex.Store({
isSuperAdmin: false, // 添加superAdmin状态 isSuperAdmin: false, // 添加superAdmin状态
pubConfig: { // 添加公共配置存储 pubConfig: { // 添加公共配置存储
version: '', version: '',
beianIcpNum: 'null',
beianGaNum: 'null',
allowUserRegister: false allowUserRegister: false
} }
}, },
@@ -5,7 +5,7 @@
<div class="operation-bar"> <div class="operation-bar">
<h2 class="page-title">参数管理</h2> <h2 class="page-title">参数管理</h2>
<div class="right-operations"> <div class="right-operations">
<el-input placeholder="请输入参数编码查询" v-model="searchCode" class="search-input" <el-input placeholder="请输入参数编码或备注查询" v-model="searchCode" class="search-input"
@keyup.enter.native="handleSearch" clearable /> @keyup.enter.native="handleSearch" clearable />
<el-button class="btn-search" @click="handleSearch">搜索</el-button> <el-button class="btn-search" @click="handleSearch">搜索</el-button>
</div> </div>
+1 -1
View File
@@ -411,7 +411,7 @@ LLM:
base_url: https://host/api/v1 base_url: https://host/api/v1
# 你可以在这里找到你的api_key # 你可以在这里找到你的api_key
# https://cloud.tryfastgpt.ai/account/apikey # https://cloud.tryfastgpt.ai/account/apikey
api_key: fastgpt-xxx api_key: 你的fastgpt密钥
variables: variables:
k: "v" k: "v"
k2: "v2" k2: "v2"
@@ -82,6 +82,8 @@ def ensure_directories(config):
# ASR/TTS模块输出目录 # ASR/TTS模块输出目录
for module in ["ASR", "TTS"]: for module in ["ASR", "TTS"]:
if config.get(module) is None:
continue
for provider in config.get(module, {}).values(): for provider in config.get(module, {}).values():
output_dir = provider.get("output_dir", "") output_dir = provider.get("output_dir", "")
if output_dir: if output_dir:
@@ -93,6 +95,10 @@ def ensure_directories(config):
selected_provider = selected_modules.get(module_type) selected_provider = selected_modules.get(module_type)
if not selected_provider: if not selected_provider:
continue continue
if config.get(module) is None:
continue
if config.get(selected_provider) is None:
continue
provider_config = config.get(module_type, {}).get(selected_provider, {}) provider_config = config.get(module_type, {}).get(selected_provider, {})
output_dir = provider_config.get("output_dir") output_dir = provider_config.get("output_dir")
if output_dir: if output_dir:
+217 -84
View File
@@ -1,15 +1,17 @@
import time
import aiohttp
import asyncio import asyncio
import logging
import os
import statistics
import time
from typing import Dict
import aiohttp
from tabulate import tabulate from tabulate import tabulate
from typing import Dict, List
from config.settings import load_config
from core.utils.asr import create_instance as create_stt_instance
from core.utils.llm import create_instance as create_llm_instance 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.tts import create_instance as create_tts_instance
import statistics
from config.settings import load_config
import inspect
import os
import logging
# 设置全局日志级别为WARNING,抑制INFO级别日志 # 设置全局日志级别为WARNING,抑制INFO级别日志
logging.basicConfig(level=logging.WARNING) logging.basicConfig(level=logging.WARNING)
@@ -26,7 +28,17 @@ class AsyncPerformanceTester:
"请用100字概括量子计算的基本原理和应用前景", "请用100字概括量子计算的基本原理和应用前景",
], ],
) )
self.results = {"llm": {}, "tts": {}, "combinations": []}
self.test_wav_list = []
self.wav_root = r"config/assets"
for file_name in os.listdir(self.wav_root):
file_path = os.path.join(self.wav_root, file_name)
# 检查文件大小是否大于300KB
if os.path.getsize(file_path) > 300 * 1024: # 300KB = 300 * 1024 bytes
with open(file_path, "rb") as f:
self.test_wav_list.append(f.read())
self.results = {"llm": {}, "tts": {}, "stt": {}, "combinations": []}
async def _check_ollama_service(self, base_url: str, model_name: str) -> bool: async def _check_ollama_service(self, base_url: str, model_name: str) -> bool:
"""异步检查Ollama服务状态""" """异步检查Ollama服务状态"""
@@ -109,6 +121,57 @@ class AsyncPerformanceTester:
print(f"⚠️ {tts_name} 测试失败: {str(e)}") print(f"⚠️ {tts_name} 测试失败: {str(e)}")
return {"name": tts_name, "type": "tts", "errors": 1} return {"name": tts_name, "type": "tts", "errors": 1}
async def _test_stt(self, stt_name: str, config: Dict) -> Dict:
"""异步测试单个STT性能"""
try:
logging.getLogger("core.providers.asr.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
):
print(f"⏭️ STT {stt_name} 未配置access_token/api_key,已跳过")
return {"name": stt_name, "type": "stt", "errors": 1}
module_type = config.get("type", stt_name)
stt = create_stt_instance(module_type, config, delete_audio_file=True)
stt.audio_format = "pcm"
print(f"🎵 测试 STT: {stt_name}")
text, _ = await stt.speech_to_text([self.test_wav_list[0]], "1")
if text is None:
print(f"{stt_name} 连接失败")
return {"name": stt_name, "type": "stt", "errors": 1}
total_time = 0
test_count = len(self.test_wav_list)
for i, sentence in enumerate(self.test_wav_list, 1):
start = time.time()
text, _ = await stt.speech_to_text([sentence], "1")
duration = time.time() - start
total_time += duration
if text:
print(f"{stt_name} [{i}/{test_count}]")
else:
print(f"{stt_name} [{i}/{test_count}]")
return {"name": stt_name, "type": "stt", "errors": 1}
return {
"name": stt_name,
"type": "stt",
"avg_time": total_time / test_count,
"errors": 0,
}
except Exception as e:
print(f"⚠️ {stt_name} 测试失败: {str(e)}")
return {"name": stt_name, "type": "stt", "errors": 1}
async def _test_llm(self, llm_name: str, config: Dict) -> Dict: async def _test_llm(self, llm_name: str, config: Dict) -> Dict:
"""异步测试单个LLM性能""" """异步测试单个LLM性能"""
try: try:
@@ -234,6 +297,7 @@ class AsyncPerformanceTester:
if v["errors"] == 0 and v["avg_first_token"] >= 0.05 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] valid_tts = [k for k, v in self.results["tts"].items() if v["errors"] == 0]
valid_stt = [k for k, v in self.results["stt"].items() if v["errors"] == 0]
# 找出基准值 # 找出基准值
min_first_token = ( min_first_token = (
@@ -246,42 +310,53 @@ class AsyncPerformanceTester:
if valid_tts if valid_tts
else 1 else 1
) )
min_stt_time = (
min([self.results["stt"][stt]["avg_time"] for stt in valid_stt])
if valid_stt
else 1
)
for llm in valid_llms: for llm in valid_llms:
for tts in valid_tts: for tts in valid_tts:
# 计算相对性能分数(越小越好) for stt in valid_stt:
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 )
tts_score = self.results["tts"][tts]["avg_time"] / min_tts_time
stt_score = self.results["stt"][stt]["avg_time"] / min_stt_time
# 计算稳定性分数(标准差/平均值,越小越稳定) # 计算稳定性分数(标准差/平均值,越小越稳定)
llm_stability = ( llm_stability = (
self.results["llm"][llm]["std_first_token"] self.results["llm"][llm]["std_first_token"]
/ self.results["llm"][llm]["avg_first_token"] / self.results["llm"][llm]["avg_first_token"]
) )
# 综合得分(考虑性能和稳定性) # 综合得分(考虑性能和稳定性)
# 性能权重0.7,稳定性权重0.3 # LLM得分: 性能权重(70%) + 稳定性权重(30%)
llm_final_score = llm_score * 0.7 + llm_stability * 0.3 llm_final_score = llm_score * 0.7 + llm_stability * 0.3
# 总分 = LLM得分(70%) + TTS得分(30%) # 总分 = LLM得分(70%) + TTS得分(30%) + STT得分(30%)
total_score = llm_final_score * 0.7 + tts_score * 0.3 total_score = (
llm_final_score * 0.7 + tts_score * 0.3 + stt_score * 0.3
)
self.results["combinations"].append( self.results["combinations"].append(
{ {
"llm": llm, "llm": llm,
"tts": tts, "tts": tts,
"score": total_score, "stt": stt,
"details": { "score": total_score,
"llm_first_token": self.results["llm"][llm][ "details": {
"avg_first_token" "llm_first_token": self.results["llm"][llm][
], "avg_first_token"
"llm_stability": llm_stability, ],
"tts_time": self.results["tts"][tts]["avg_time"], "llm_stability": llm_stability,
}, "tts_time": self.results["tts"][tts]["avg_time"],
} "stt_time": self.results["stt"][stt]["avg_time"],
) },
}
)
# 分数越小越好 # 分数越小越好
self.results["combinations"].sort(key=lambda x: x["score"]) self.results["combinations"].sort(key=lambda x: x["score"])
@@ -302,7 +377,7 @@ class AsyncPerformanceTester:
) )
if llm_table: if llm_table:
print("\nLLM 性能排行:") print("\nLLM 性能排行:\n")
print( print(
tabulate( tabulate(
llm_table, llm_table,
@@ -321,7 +396,7 @@ class AsyncPerformanceTester:
tts_table.append([name, f"{data['avg_time']:.3f}"]) # 不需要固定宽度 tts_table.append([name, f"{data['avg_time']:.3f}"]) # 不需要固定宽度
if tts_table: if tts_table:
print("\nTTS 性能排行:") print("\nTTS 性能排行:\n")
print( print(
tabulate( tabulate(
tts_table, tts_table,
@@ -334,17 +409,37 @@ class AsyncPerformanceTester:
else: else:
print("\n⚠️ 没有可用的TTS模块进行测试。") print("\n⚠️ 没有可用的TTS模块进行测试。")
stt_table = []
for name, data in self.results["stt"].items():
if data["errors"] == 0:
stt_table.append([name, f"{data['avg_time']:.3f}"]) # 不需要固定宽度
if stt_table:
print("\nSTT 性能排行:\n")
print(
tabulate(
stt_table,
headers=["模型名称", "合成耗时"],
tablefmt="github",
colalign=("left", "right"),
disable_numparse=True,
)
)
else:
print("\n⚠️ 没有可用的STT模块进行测试。")
if self.results["combinations"]: if self.results["combinations"]:
print("\n推荐配置组合 (得分越小越好):") print("\n推荐配置组合 (得分越小越好):\n")
combo_table = [] combo_table = []
for combo in self.results["combinations"][:5]: for combo in self.results["combinations"][:]:
combo_table.append( combo_table.append(
[ [
f"{combo['llm']} + {combo['tts']}", # 不需要固定宽度 f"{combo['llm']} + {combo['tts']} + {combo['stt']}", # 不需要固定宽度
f"{combo['score']:.3f}", f"{combo['score']:.3f}",
f"{combo['details']['llm_first_token']:.3f}", f"{combo['details']['llm_first_token']:.3f}",
f"{combo['details']['llm_stability']:.3f}", f"{combo['details']['llm_stability']:.3f}",
f"{combo['details']['tts_time']:.3f}", f"{combo['details']['tts_time']:.3f}",
f"{combo['details']['stt_time']:.3f}",
] ]
) )
@@ -357,9 +452,10 @@ class AsyncPerformanceTester:
"LLM首字耗时", "LLM首字耗时",
"稳定性", "稳定性",
"TTS合成耗时", "TTS合成耗时",
"STT合成耗时",
], ],
tablefmt="github", tablefmt="github",
colalign=("left", "right", "right", "right", "right"), colalign=("left", "right", "right", "right", "right", "right"),
disable_numparse=True, disable_numparse=True,
) )
) )
@@ -372,8 +468,12 @@ class AsyncPerformanceTester:
if result["errors"] == 0: if result["errors"] == 0:
if result["type"] == "llm": if result["type"] == "llm":
self.results["llm"][result["name"]] = result self.results["llm"][result["name"]] = result
else: elif result["type"] == "tts":
self.results["tts"][result["name"]] = result self.results["tts"][result["name"]] = result
elif result["type"] == "stt":
self.results["stt"][result["name"]] = result
else:
pass
async def run(self): async def run(self):
"""执行全量异步测试""" """执行全量异步测试"""
@@ -383,52 +483,73 @@ class AsyncPerformanceTester:
all_tasks = [] all_tasks = []
# LLM测试任务 # LLM测试任务
for llm_name, config in self.config.get("LLM", {}).items(): if self.config.get("LLM") is not None:
# 检查配置有效性 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( if llm_name == "CozeLLM":
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"]
): ):
print(f"⏭️ LLM {llm_name} 未配置bot_id/user_id,已跳过") print(f"⏭️ LLM {llm_name} 未配置api_key,已跳过")
continue
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")
if not model_name:
print(f"🚫 Ollama未配置model_name")
continue continue
if not await self._check_ollama_service(base_url, model_name): # 对于Ollama,先检查服务状态
continue if llm_name == "Ollama":
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
print(f"📋 添加LLM测试任务: {llm_name}") if not await self._check_ollama_service(base_url, model_name):
module_type = config.get("type", llm_name) continue
llm = create_llm_instance(module_type, config)
# 为每个句子创建独立任务 print(f"📋 添加LLM测试任务: {llm_name}")
for sentence in self.test_sentences: module_type = config.get("type", llm_name)
sentence = sentence.encode("utf-8").decode("utf-8") llm = create_llm_instance(module_type, config)
all_tasks.append(self._test_single_sentence(llm_name, llm, sentence))
# 为每个句子创建独立任务
for sentence in self.test_sentences:
sentence = sentence.encode("utf-8").decode("utf-8")
all_tasks.append(
self._test_single_sentence(llm_name, llm, sentence)
)
# TTS测试任务 # TTS测试任务
for tts_name, config in self.config.get("TTS", {}).items(): if self.config.get("TTS") is not None:
token_fields = ["access_token", "api_key", "token"] for tts_name, config in self.config.get("TTS", {}).items():
if any( token_fields = ["access_token", "api_key", "token"]
field in config if any(
and any(x in config[field] for x in ["你的", "placeholder"]) field in config
for field in token_fields 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} 未配置access_token/api_key,已跳过")
print(f"🎵 添加TTS测试任务: {tts_name}") continue
all_tasks.append(self._test_tts(tts_name, config)) print(f"🎵 添加TTS测试任务: {tts_name}")
all_tasks.append(self._test_tts(tts_name, config))
# STT测试任务
if len(self.test_wav_list) >= 1:
if self.config.get("ASR") is not None:
for stt_name, config in self.config.get("ASR", {}).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
):
print(f"⏭️ ASR {stt_name} 未配置access_token/api_key,已跳过")
continue
print(f"🎵 添加ASR测试任务: {stt_name}")
all_tasks.append(self._test_stt(stt_name, config))
else:
print(f"\n⚠️ {self.wav_root} 路径下没有音频文件,已跳过STT测试任务")
print( print(
f"\n✅ 找到 {len([t for t in all_tasks if 'test_single_sentence' in str(t)]) / len(self.test_sentences):.0f} 个可用LLM模块" f"\n✅ 找到 {len([t for t in all_tasks if 'test_single_sentence' in str(t)]) / len(self.test_sentences):.0f} 个可用LLM模块"
@@ -436,6 +557,9 @@ class AsyncPerformanceTester:
print( print(
f"✅ 找到 {len([t for t in all_tasks if '_test_tts' in str(t)])} 个可用TTS模块" f"✅ 找到 {len([t for t in all_tasks if '_test_tts' in str(t)])} 个可用TTS模块"
) )
print(
f"✅ 找到 {len([t for t in all_tasks if '_test_stt' in str(t)])} 个可用STT模块"
)
print("\n⏳ 开始并发测试所有模块...\n") print("\n⏳ 开始并发测试所有模块...\n")
# 并发执行所有测试任务 # 并发执行所有测试任务
@@ -494,6 +618,15 @@ class AsyncPerformanceTester:
if result["errors"] == 0: if result["errors"] == 0:
self.results["tts"][result["name"]] = result self.results["tts"][result["name"]] = result
# 处理STT结果
for result in [
r
for r in all_results
if r and isinstance(r, dict) and r.get("type") == "stt"
]:
if result["errors"] == 0:
self.results["stt"][result["name"]] = result
# 生成组合建议并打印结果 # 生成组合建议并打印结果
print("\n📊 生成测试报告...") print("\n📊 生成测试报告...")
self._generate_combinations() self._generate_combinations()