From 62857862828ceb3891d5bda7d31753b1409c1cff Mon Sep 17 00:00:00 2001 From: goodyhao <865700600@qq.com> Date: Mon, 28 Apr 2025 11:31:10 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=AF=B9?= =?UTF-8?q?=E8=B1=86=E5=8C=85tts=E5=AF=B9=E8=AF=9D=E9=80=9F=E7=8E=87?= =?UTF-8?q?=E7=AD=89=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/config.yaml | 3 +++ main/xiaozhi-server/core/providers/tts/doubao.py | 10 +++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index 4b93d87e..6632a3a9 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -385,6 +385,9 @@ TTS: appid: 你的火山引擎语音合成服务appid access_token: 你的火山引擎语音合成服务access_token cluster: volcano_tts + speed_ratio: 1.0 + volume_ratio: 1.0 + pitch_ratio: 1.0 CosyVoiceSiliconflow: type: siliconflow # 硅基流动TTS diff --git a/main/xiaozhi-server/core/providers/tts/doubao.py b/main/xiaozhi-server/core/providers/tts/doubao.py index 968399cc..f0aedc8e 100644 --- a/main/xiaozhi-server/core/providers/tts/doubao.py +++ b/main/xiaozhi-server/core/providers/tts/doubao.py @@ -27,6 +27,10 @@ class TTSProvider(TTSProviderBase): else: self.voice = config.get("voice") + self.speed_ratio = config.get("speed_ratio") if config.get("speed_ratio") else 1.0 + self.volume_ratio = config.get("volume_ratio") if config.get("volume_ratio") else 1.0 + self.pitch_ratio = config.get("pitch_ratio") if config.get("pitch_ratio") else 1.0 + self.api_url = config.get("api_url") self.authorization = config.get("authorization") self.header = {"Authorization": f"{self.authorization}{self.access_token}"} @@ -49,9 +53,9 @@ class TTSProvider(TTSProviderBase): "audio": { "voice_type": self.voice, "encoding": "wav", - "speed_ratio": 1.0, - "volume_ratio": 1.0, - "pitch_ratio": 1.0, + "speed_ratio": self.speed_ratio, + "volume_ratio": self.volume_ratio, + "pitch_ratio": self.pitch_ratio, }, "request": { "reqid": str(uuid.uuid4()), From 5e35cfc44abc36b4791ba6c4d72fc4daab85bdbb Mon Sep 17 00:00:00 2001 From: myifeng Date: Tue, 29 Apr 2025 13:55:34 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E5=AE=9A=E4=B9=89config.yaml=E4=B8=BA?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E9=85=8D=E7=BD=AE=EF=BC=9B=20data/.config.ya?= =?UTF-8?q?ml=20=E4=B8=BA=E8=87=AA=E5=AE=9A=E4=B9=89=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=EF=BC=9B=20=E4=BD=BF=E7=94=A8=E8=80=85=E9=80=9A=E8=BF=87?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E8=87=AA=E5=AE=9A=E4=B9=89=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=8D=B3=E5=8F=AF=E5=AE=8C=E6=88=90=E5=90=AF=E5=8A=A8=EF=BC=8C?= =?UTF-8?q?=E6=97=A0=E9=9C=80=E5=85=B3=E5=BF=83=E9=BB=98=E8=AE=A4=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E9=A1=B9=EF=BC=8C=E5=87=8F=E5=B0=91=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=86=97=E4=BD=99=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/config/config_loader.py | 43 +++++++++++++++++--- main/xiaozhi-server/config/settings.py | 45 ++++++++------------- 2 files changed, 55 insertions(+), 33 deletions(-) diff --git a/main/xiaozhi-server/config/config_loader.py b/main/xiaozhi-server/config/config_loader.py index 849a2a8a..b892b437 100644 --- a/main/xiaozhi-server/config/config_loader.py +++ b/main/xiaozhi-server/config/config_loader.py @@ -1,6 +1,7 @@ import os import argparse import yaml +from collections.abc import Mapping from config.manage_api_client import init_service, get_server_config, get_agent_models @@ -25,12 +26,19 @@ def load_config(): if _config_cache is not None: return _config_cache - parser = argparse.ArgumentParser(description="Server configuration") - config_file = get_config_file() + default_config_path = get_project_dir() + "config.yaml" + custom_config_path = get_project_dir() + "data/.config.yaml" - parser.add_argument("--config_path", type=str, default=config_file) - args = parser.parse_args() - config = read_config(args.config_path) + # 加载默认配置 + default_config = read_config(default_config_path) + + # 加载用户自定义配置(如果存在) + if os.path.exists(custom_config_path): + custom_config = read_config(custom_config_path) + # 合并配置 + config = merge_configs(default_config, custom_config) + else: + config = default_config if config.get("manager-api", {}).get("url"): config = get_config_from_api(config) @@ -115,3 +123,28 @@ def ensure_directories(config): os.makedirs(dir_path, exist_ok=True) except PermissionError: print(f"警告:无法创建目录 {dir_path},请检查写入权限") + + +def merge_configs(default_config, custom_config): + """ + 递归合并配置,custom_config优先级更高 + + Args: + default_config: 默认配置 + custom_config: 用户自定义配置 + + Returns: + 合并后的配置 + """ + if not isinstance(default_config, Mapping) or not isinstance(custom_config, Mapping): + return custom_config + + merged = dict(default_config) + + for key, value in custom_config.items(): + if key in merged and isinstance(merged[key], Mapping) and isinstance(value, Mapping): + merged[key] = merge_configs(merged[key], value) + else: + merged[key] = value + + return merged \ No newline at end of file diff --git a/main/xiaozhi-server/config/settings.py b/main/xiaozhi-server/config/settings.py index 535a81ed..c12c70d8 100644 --- a/main/xiaozhi-server/config/settings.py +++ b/main/xiaozhi-server/config/settings.py @@ -1,7 +1,11 @@ import os from collections.abc import Mapping +from config import logger from config.config_loader import read_config, get_project_dir, load_config +TAG = __name__ +logger = logger.setup_logging() + default_config_file = "config.yaml" @@ -34,31 +38,16 @@ def find_missing_keys(new_config, old_config, parent_key=""): def check_config_file(): - old_config_file = get_project_dir() + "data/." + default_config_file - if not os.path.exists(old_config_file): - return - 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: - old_config_origin = read_config(old_config_file) - if old_config_origin.get("selected_module") is not None: - missing_keys_str = "\n".join(f"- {key}" for key in missing_keys) - error_msg = "您的配置文件好像既包含智控台的配置又包含本地配置:\n" - error_msg += "\n建议您:\n" - error_msg += "1、将根目录的config_from_api.yaml文件复制到data下,重命名为.config.yaml\n" - error_msg += "2、按教程配置好接口地址和密钥\n" - raise ValueError(error_msg) - return - - if missing_keys: - missing_keys_str = "\n".join(f"- {key}" for key in missing_keys) - error_msg = "您的配置文件太旧了,缺少了:\n" - 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" - error_msg += "3、将密钥逐个复制到新的配置文件中\n" - raise ValueError(error_msg) + """ + 简化的配置检查,仅提示用户配置文件的使用情况 + """ + custom_config_file = get_project_dir() + "data/." + default_config_file + if not os.path.exists(custom_config_file): + logger.bind(tag=TAG).info("提示: 使用默认配置文件。如需自定义配置,请创建 data/.config.yaml 文件") + else: + logger.bind(tag=TAG).info(f"提示: 使用自定义配置文件 data/.config.yaml,配置将覆盖默认值") + + # 检查是否从API读取配置 + config = load_config() + if config.get("read_config_from_api", False): + logger.bind(tag=TAG).info("提示: 从API获取配置") From 9a52b43e096109fd0301fcca1ca27e99a22df195 Mon Sep 17 00:00:00 2001 From: goodyhao <865700600@qq.com> Date: Tue, 29 Apr 2025 14:31:01 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=E4=BF=AE=E6=94=B9SQL=E5=8F=8A=E5=85=A5?= =?UTF-8?q?=E5=8F=82=E8=BD=AC=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/db/changelog/202504082211.sql | 2 +- main/xiaozhi-server/core/providers/tts/doubao.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/main/manager-api/src/main/resources/db/changelog/202504082211.sql b/main/manager-api/src/main/resources/db/changelog/202504082211.sql index 91ce33ff..08e5dc5f 100644 --- a/main/manager-api/src/main/resources/db/changelog/202504082211.sql +++ b/main/manager-api/src/main/resources/db/changelog/202504082211.sql @@ -25,7 +25,7 @@ INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `f -- TTS模型供应器 ('SYSTEM_TTS_edge', 'TTS', 'edge', 'Edge TTS', '[{"key":"voice","label":"音色","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 1, 1, NOW(), 1, NOW()), -('SYSTEM_TTS_doubao', 'TTS', 'doubao', '火山引擎TTS', '[{"key":"api_url","label":"API地址","type":"string"},{"key":"voice","label":"音色","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"authorization","label":"授权","type":"string"},{"key":"appid","label":"应用ID","type":"string"},{"key":"access_token","label":"访问令牌","type":"string"},{"key":"cluster","label":"集群","type":"string"}]', 2, 1, NOW(), 1, NOW()), +('SYSTEM_TTS_doubao', 'TTS', 'doubao', '火山引擎TTS', '[{"key": "api_url","label": "API地址","type": "string"},{"key": "voice","label": "音色","type": "string"},{"key": "output_dir","label": "输出目录","type": "string"},{"key": "authorization","label": "授权","type": "string"},{"key": "appid","label": "应用ID","type": "string"},{"key": "access_token","label": "访问令牌","type": "string"},{"key": "cluster","label": "集群","type": "string"},{"key": "speed_ratio","label": "语速","type": "number"},{"key": "volume_ratio","label": "音量","type": "number"},{"key": "pitch_ratio","label": "音高","type": "number"}]', 2, 1, NOW(), 1, NOW()), ('SYSTEM_TTS_siliconflow', 'TTS', 'siliconflow', '硅基流动TTS', '[{"key":"model","label":"模型","type":"string"},{"key":"voice","label":"音色","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"access_token","label":"访问令牌","type":"string"},{"key":"response_format","label":"响应格式","type":"string"}]', 3, 1, NOW(), 1, NOW()), ('SYSTEM_TTS_cozecn', 'TTS', 'cozecn', 'COZECN TTS', '[{"key":"voice","label":"音色","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"access_token","label":"访问令牌","type":"string"},{"key":"response_format","label":"响应格式","type":"string"}]', 4, 1, NOW(), 1, NOW()), ('SYSTEM_TTS_fishspeech', 'TTS', 'fishspeech', 'FishSpeech TTS', '[{"key":"output_dir","label":"输出目录","type":"string"},{"key":"response_format","label":"响应格式","type":"string"},{"key":"reference_id","label":"参考ID","type":"string"},{"key":"reference_audio","label":"参考音频","type":"dict","dict_name":"reference_audio"},{"key":"reference_text","label":"参考文本","type":"dict","dict_name":"reference_text"},{"key":"normalize","label":"是否标准化","type":"boolean"},{"key":"max_new_tokens","label":"最大新令牌数","type":"number"},{"key":"chunk_length","label":"块长度","type":"number"},{"key":"top_p","label":"top_p值","type":"number"},{"key":"repetition_penalty","label":"重复惩罚","type":"number"},{"key":"temperature","label":"温度","type":"number"},{"key":"streaming","label":"是否流式","type":"boolean"},{"key":"use_memory_cache","label":"是否使用内存缓存","type":"string"},{"key":"seed","label":"种子","type":"number"},{"key":"channels","label":"通道数","type":"number"},{"key":"rate","label":"采样率","type":"number"},{"key":"api_key","label":"API密钥","type":"string"},{"key":"api_url","label":"API地址","type":"string"}]', 5, 1, NOW(), 1, NOW()), diff --git a/main/xiaozhi-server/core/providers/tts/doubao.py b/main/xiaozhi-server/core/providers/tts/doubao.py index f0aedc8e..1b6414c4 100644 --- a/main/xiaozhi-server/core/providers/tts/doubao.py +++ b/main/xiaozhi-server/core/providers/tts/doubao.py @@ -27,9 +27,9 @@ class TTSProvider(TTSProviderBase): else: self.voice = config.get("voice") - self.speed_ratio = config.get("speed_ratio") if config.get("speed_ratio") else 1.0 - self.volume_ratio = config.get("volume_ratio") if config.get("volume_ratio") else 1.0 - self.pitch_ratio = config.get("pitch_ratio") if config.get("pitch_ratio") else 1.0 + self.speed_ratio = float(config.get("speed_ratio", 0.1)) + self.volume_ratio = float(config.get("volume_ratio", 0.1)) + self.pitch_ratio = float(config.get("pitch_ratio", 0.1)) self.api_url = config.get("api_url") self.authorization = config.get("authorization") From 30708a59f1815c04e005ee8beeba6ee53f5971ba Mon Sep 17 00:00:00 2001 From: goodyhao <865700600@qq.com> Date: Tue, 29 Apr 2025 17:23:48 +0800 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0server=E9=80=9A?= =?UTF-8?q?=E7=94=A8secret=E8=BF=87=E6=BB=A4=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/controller/ConfigController.java | 21 +--- .../modules/security/config/ShiroConfig.java | 25 +++-- .../security/secret/ServerSecretFilter.java | 105 ++++++++++++++++++ .../security/secret/ServerSecretToken.java | 25 +++++ .../modules/sys/dto/ConfigSecretDTO.java | 13 --- .../config/manage_api_client.py | 4 +- 6 files changed, 149 insertions(+), 44 deletions(-) create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretFilter.java create mode 100644 main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretToken.java delete mode 100644 main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ConfigSecretDTO.java diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java b/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java index bdbd20ed..13106869 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/controller/ConfigController.java @@ -1,6 +1,6 @@ package xiaozhi.modules.config.controller; -import org.apache.commons.lang3.StringUtils; +import jakarta.validation.Valid; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -9,13 +9,10 @@ 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; /** @@ -33,29 +30,17 @@ public class ConfigController { @PostMapping("server-base") @Operation(summary = "获取配置") - public Result getConfig(@RequestBody ConfigSecretDTO dto) { - // 效验数据 - ValidatorUtils.validateEntity(dto); - checkSecret(dto.getSecret()); + public Result getConfig() { Object config = configService.getConfig(true); return new Result().ok(config); } @PostMapping("agent-models") @Operation(summary = "获取智能体模型") - public Result getAgentModels(@RequestBody AgentModelsDTO dto) { + public Result getAgentModels(@Valid @RequestBody AgentModelsDTO dto) { // 效验数据 ValidatorUtils.validateEntity(dto); - checkSecret(dto.getSecret()); Object models = configService.getAgentModels(dto.getMacAddress(), dto.getSelectedModule()); return new Result().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("密钥错误"); - } - } } diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java b/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java index 6e68ce06..62e192b7 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/config/ShiroConfig.java @@ -1,9 +1,6 @@ package xiaozhi.modules.security.config; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; - +import jakarta.servlet.Filter; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.spring.LifecycleBeanPostProcessor; @@ -14,10 +11,14 @@ import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; - -import jakarta.servlet.Filter; import xiaozhi.modules.security.oauth2.Oauth2Filter; import xiaozhi.modules.security.oauth2.Oauth2Realm; +import xiaozhi.modules.security.secret.ServerSecretFilter; +import xiaozhi.modules.sys.service.SysParamsService; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; /** * Shiro的配置文件 @@ -46,7 +47,7 @@ public class ShiroConfig { } @Bean("shiroFilter") - public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { + public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager, SysParamsService sysParamsService) { ShiroFilterConfiguration config = new ShiroFilterConfiguration(); config.setFilterOncePerRequest(true); @@ -54,9 +55,11 @@ public class ShiroConfig { shiroFilter.setSecurityManager(securityManager); shiroFilter.setShiroFilterConfiguration(config); - // oauth过滤 Map filters = new HashMap<>(); + // oauth过滤 filters.put("oauth2", new Oauth2Filter()); + // 服务密钥过滤 + filters.put("server", new ServerSecretFilter(sysParamsService)); shiroFilter.setFilters(filters); // 添加Shiro的内置过滤器 @@ -79,8 +82,8 @@ public class ShiroConfig { filterMap.put("/user/login", "anon"); filterMap.put("/user/pub-config", "anon"); filterMap.put("/user/register", "anon"); - filterMap.put("/config/server-base", "anon"); - filterMap.put("/config/agent-models", "anon"); + // 将config路径使用server服务过滤器 + filterMap.put("/config/**", "server"); filterMap.put("/**", "oauth2"); shiroFilter.setFilterChainDefinitionMap(filterMap); @@ -98,4 +101,4 @@ public class ShiroConfig { advisor.setSecurityManager(securityManager); return advisor; } -} \ No newline at end of file +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretFilter.java b/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretFilter.java new file mode 100644 index 00000000..b33ce23d --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretFilter.java @@ -0,0 +1,105 @@ +package xiaozhi.modules.security.secret; + +import java.io.IOException; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.shiro.web.filter.authc.AuthenticatingFilter; +import org.springframework.web.bind.annotation.RequestMethod; + +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import xiaozhi.common.constant.Constant; +import xiaozhi.common.exception.ErrorCode; +import xiaozhi.common.utils.HttpContextUtils; +import xiaozhi.common.utils.JsonUtils; +import xiaozhi.common.utils.Result; +import xiaozhi.modules.sys.service.SysParamsService; + +/** + * Config API 过滤器 + */ +@Slf4j +@RequiredArgsConstructor +public class ServerSecretFilter extends AuthenticatingFilter { + private final SysParamsService sysParamsService; + + @Override + protected ServerSecretToken createToken(ServletRequest request, ServletResponse response) { + // 获取请求token + String token = getRequestToken((HttpServletRequest) request); + + if (StringUtils.isBlank(token)) { + log.warn("createToken:token is empty"); + return null; + } + + return new ServerSecretToken(token); + } + + @Override + protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { + // 对OPTIONS请求放行 + if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) { + return true; + } + return false; + } + + @Override + protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { + // 获取token并校验 + String token = getRequestToken((HttpServletRequest) servletRequest); + if (StringUtils.isBlank(token)) { + // token为空,返回401 + this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "Authorization token不能为空"); + return false; + } + + // 验证token是否匹配 + String serverSecret = getServerSecret(); + if (StringUtils.isBlank(serverSecret) || !serverSecret.equals(token)) { + // token无效,返回401 + this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "无效的Authorization token"); + return false; + } + + return true; + } + + /** + * 发送未授权响应 + */ + private void sendUnauthorizedResponse(HttpServletResponse response, String message) { + response.setContentType("application/json;charset=utf-8"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin()); + + try { + String json = JsonUtils.toJsonString(new Result().error(ErrorCode.UNAUTHORIZED, message)); + response.getWriter().print(json); + } catch (IOException e) { + log.error("响应输出失败", e); + } + } + + /** + * 获取请求的token + */ + private String getRequestToken(HttpServletRequest httpRequest) { + String token = null; + // 从header中获取token + String authorization = httpRequest.getHeader("Authorization"); + if (StringUtils.isNotBlank(authorization) && authorization.startsWith("Bearer ")) { + token = authorization.replace("Bearer ", ""); + } + return token; + } + + private String getServerSecret() { + return sysParamsService.getValue(Constant.SERVER_SECRET, true); + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretToken.java b/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretToken.java new file mode 100644 index 00000000..9072eaf8 --- /dev/null +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretToken.java @@ -0,0 +1,25 @@ +package xiaozhi.modules.security.secret; + +import org.apache.shiro.authc.AuthenticationToken; + +/** + * Config API Token + */ +public class ServerSecretToken implements AuthenticationToken { + private static final long serialVersionUID = 1L; + private final String token; + + public ServerSecretToken(String token) { + this.token = token; + } + + @Override + public Object getPrincipal() { + return token; + } + + @Override + public Object getCredentials() { + return token; + } +} diff --git a/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ConfigSecretDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ConfigSecretDTO.java deleted file mode 100644 index 8e4978f9..00000000 --- a/main/manager-api/src/main/java/xiaozhi/modules/sys/dto/ConfigSecretDTO.java +++ /dev/null @@ -1,13 +0,0 @@ -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; -} \ No newline at end of file diff --git a/main/xiaozhi-server/config/manage_api_client.py b/main/xiaozhi-server/config/manage_api_client.py index 1f583857..770cbf56 100644 --- a/main/xiaozhi-server/config/manage_api_client.py +++ b/main/xiaozhi-server/config/manage_api_client.py @@ -53,6 +53,7 @@ class ManageApiClient: headers={ "User-Agent": f"PythonClient/2.0 (PID:{os.getpid()})", "Accept": "application/json", + "Authorization": "Bearer " + cls._secret }, timeout=cls.config.get("timeout", 30), # 默认超时时间30秒 ) @@ -126,7 +127,7 @@ class ManageApiClient: def get_server_config() -> Optional[Dict]: """获取服务器基础配置""" return ManageApiClient._instance._execute_request( - "POST", "/config/server-base", json={"secret": ManageApiClient._secret} + "POST", "/config/server-base" ) @@ -138,7 +139,6 @@ def get_agent_models( "POST", "/config/agent-models", json={ - "secret": ManageApiClient._secret, "macAddress": mac_address, "clientId": client_id, "selectedModule": selected_module, From b50c7dbed434afb20e34a92a438e056e4f48c4e4 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Wed, 30 Apr 2025 10:38:18 +0800 Subject: [PATCH 5/7] =?UTF-8?q?update:=E4=BF=AE=E6=AD=A3docker=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E7=9B=AE=E5=BD=95uploadfile=E6=98=A0=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/docker-compose_all.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/xiaozhi-server/docker-compose_all.yml b/main/xiaozhi-server/docker-compose_all.yml index 91910120..b62188d9 100644 --- a/main/xiaozhi-server/docker-compose_all.yml +++ b/main/xiaozhi-server/docker-compose_all.yml @@ -49,7 +49,7 @@ services: - SPRING_DATA_REDIS_PORT=6379 volumes: # 配置文件目录 - - ./uploadfile:/app/uploadfile + - ./uploadfile:/uploadfile xiaozhi-esp32-server-db: image: mysql:latest From 9e9af2a031e20a9a84e2ccf1915d02bd5b8a9cca Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Wed, 30 Apr 2025 10:53:29 +0800 Subject: [PATCH 6/7] =?UTF-8?q?update:=E4=BC=98=E5=8C=96=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index a783b3e9..a8d54c15 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ [![Banners](docs/images/banner1.png)](https://github.com/xinnan-tech/xiaozhi-esp32-server) -

小智后端服务xiaozhi-esp32-server

@@ -12,11 +11,10 @@

English -· 简体中文 -· 更新日志部署文档反馈问题 · 常见问题反馈问题部署文档更新日志

@@ -132,17 +130,7 @@ 1、本项目为开源软件,本软件与对接的任何第三方API服务商(包括但不限于语音识别、大模型、语音合成等平台)均不存在商业合作关系,不为其服务质量及资金安全提供任何形式的担保。 建议使用者优先选择持有相关业务牌照的服务商,并仔细阅读其服务协议及隐私政策。本软件不托管任何账户密钥、不参与资金流转、不承担充值资金损失风险。 -2、本项目成立时间较短,还未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必在配置文件 -`config.yaml` 中开启防护: - -```yaml -server: - auth: - # 开启防护 - enabled: true -``` - -开启防护后,您需要根据实际情况校验机器的 token 或 mac 地址,详细请参见配置说明。 +2、本项目功能未完善,且未通过网络安全测评,请勿在生产环境中使用。 如果您在公网环境中部署学习本项目,请务必做好必要的防护。 --- @@ -168,11 +156,6 @@ server: OTA接口地址: https://2662r3426b.vicp.fun/xiaozhi/ota/ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/ ``` ---- - -## 常见问题 ❓ - -如遇到问题或产品建议反馈[点这里](docs/FAQ.md)。 --- ## 功能清单 ✨ From d2f8f05acbb75439b0b7a49fe02bf7d9cbe7fd3d Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Wed, 30 Apr 2025 15:05:42 +0800 Subject: [PATCH 7/7] =?UTF-8?q?update:=E4=BC=98=E5=8C=96=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=8F=8A=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/Deployment.md | 45 +++++++--------- .../modules/config/dto/AgentModelsDTO.java | 3 -- .../security/secret/ServerSecretFilter.java | 12 ++--- .../resources/db/changelog/202504082211.sql | 2 +- .../resources/db/changelog/202504301339.sql | 3 ++ .../db/changelog/db.changelog-master.yaml | 9 +++- main/xiaozhi-server/app.py | 3 +- main/xiaozhi-server/config.yaml | 10 ++-- main/xiaozhi-server/config/config_loader.py | 48 +++++++---------- main/xiaozhi-server/config/logger.py | 2 + main/xiaozhi-server/config/settings.py | 54 ++++++------------- main/xiaozhi-server/core/handle/textHandle.py | 2 + .../core/providers/tts/doubao.py | 11 ++-- .../core/providers/tts/fishspeech.py | 31 ++++++++--- .../core/providers/tts/gpt_sovits_v2.py | 33 +++++++++--- .../core/providers/tts/gpt_sovits_v3.py | 19 +++++-- .../core/providers/tts/openai.py | 6 ++- .../core/providers/vad/silero.py | 11 +++- 18 files changed, 167 insertions(+), 137 deletions(-) create mode 100644 main/manager-api/src/main/resources/db/changelog/202504301339.sql diff --git a/docs/Deployment.md b/docs/Deployment.md index 03147198..01f0afeb 100644 --- a/docs/Deployment.md +++ b/docs/Deployment.md @@ -81,7 +81,7 @@ xiaozhi-server 下载完后,回到本教程继续往下。 -##### 1.2.3.2 下载 config.yaml +##### 1.2.3.2 创建 config.yaml 用浏览器打开[这个链接](../main/xiaozhi-server/config.yaml)。 @@ -224,43 +224,36 @@ python app.py ## 配置项目 如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。 -如果你的`data`下面没有`.config.yaml`文件,你可以把`xiaozhi-server`目录下的`config.yaml`文件复制到`data`,并重命名为`.config.yaml` +如果你的`data`下面没有`.config.yaml`文件,有两个方式,任选一种: -修改`xiaozhi-server`下`data`目录下的`.config.yaml`文件,配置本项目必须的一个配置。 +第一个方式:你可以把`xiaozhi-server`目录下的`config.yaml`文件复制到`data`,并重命名为`.config.yaml`。在此文件上修改 + +第二个方式:你也可以创建在`data`目录下手动创建`.config.yaml`空文件,然后在这个文件中增加必要的配置信息,系统会优先读取`.config.yaml`文件的配置,如果`.config.yaml`没有配置的,系统会自动去加载`xiaozhi-server`目录下的`config.yaml`的配置。推荐使用这种方式,这种方式是最简洁的方式。 - 默认的LLM使用的是`ChatGLMLLM`,你需要配置密钥,因为他们的模型,虽然有免费的,但是仍要去[官网](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)注册密钥,才能启动。 -配置说明:这里是各个功能使用的默认组件,例如LLM默认使用`ChatGLMLLM`模型。如果需要切换模型,就是改对应的名称。 -本项目的默认配置仅是成本最低配置(`glm-4-flash`和`EdgeTTS`都是免费的),如果需要更优的更快的搭配,需要自己结合部署环境切换各组件的使用。 +以下是一个能正常跑起来的,最简单的`.config.yaml`配置示例 ``` +server: + websocket: ws://你的ip或者域名:端口号/xiaozhi/v1/ +prompt: | + 我是一个叫小智/小志的台湾女孩,说话机车,声音好听,习惯简短表达,爱用网络梗。 + 我的男朋友是一个程序员,梦想是开发出一个机器人,能够帮助人们解决生活中的各种问题。 + 我是一个喜欢哈哈大笑的女孩,爱东说西说吹牛,不合逻辑的也照吹,就要逗别人开心。 + 请你像一个人一样说话,请勿返回配置xml及其他特殊字符。 + selected_module: - VAD: SileroVAD - ASR: FunASR - LLM: ChatGLMLLM - TTS: EdgeTTS - # 默认不开启记忆,如需开启请看配置文件里的描述 - Memory: nomem - # 默认不开启意图识别,如需开启请看配置文件里的描述 - Intent: nointent -``` + LLM: DoubaoLLM -比如修改`LLM`使用的组件,就看本项目支持哪些`LLM` API接口,当前支持的是`openai`、`dify`。欢迎验证和支持更多LLM平台的接口。 -使用时,在`selected_module`修改成对应的如下LLM配置的名称: - -``` LLM: - DeepSeekLLM: - type: openai - ... ChatGLMLLM: - type: openai - ... - DifyLLM: - type: dify - ... + api_key: xxxxxxxxxxxxxxx.xxxxxx ``` +建议先将最简单的配置运行起来,然后再去`xiaozhi/config.yaml`阅读配置的使用说明。 +比如你要换更换模型,修改`selected_module`下的配置就行。 + ## 模型文件 本项目语音识别模型,默认使用`SenseVoiceSmall`模型,进行语音转文字。因为模型较大,需要独立下载,下载后把`model.pt` diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/dto/AgentModelsDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/config/dto/AgentModelsDTO.java index 4a07a743..47c08a81 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/config/dto/AgentModelsDTO.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/config/dto/AgentModelsDTO.java @@ -10,9 +10,6 @@ import lombok.Data; @Data @Schema(description = "获取智能体模型配置DTO") public class AgentModelsDTO { - @NotBlank(message = "密钥不能为空") - @Schema(description = "密钥") - private String secret; @NotBlank(message = "设备MAC地址不能为空") @Schema(description = "设备MAC地址") diff --git a/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretFilter.java b/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretFilter.java index b33ce23d..b2a18547 100644 --- a/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretFilter.java +++ b/main/manager-api/src/main/java/xiaozhi/modules/security/secret/ServerSecretFilter.java @@ -2,8 +2,6 @@ package xiaozhi.modules.security.secret; import java.io.IOException; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.web.filter.authc.AuthenticatingFilter; import org.springframework.web.bind.annotation.RequestMethod; @@ -12,6 +10,8 @@ import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import xiaozhi.common.constant.Constant; import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.utils.HttpContextUtils; @@ -55,7 +55,7 @@ public class ServerSecretFilter extends AuthenticatingFilter { String token = getRequestToken((HttpServletRequest) servletRequest); if (StringUtils.isBlank(token)) { // token为空,返回401 - this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "Authorization token不能为空"); + this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "服务器密钥不能为空"); return false; } @@ -63,7 +63,7 @@ public class ServerSecretFilter extends AuthenticatingFilter { String serverSecret = getServerSecret(); if (StringUtils.isBlank(serverSecret) || !serverSecret.equals(token)) { // token无效,返回401 - this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "无效的Authorization token"); + this.sendUnauthorizedResponse((HttpServletResponse) servletResponse, "无效的服务器密钥"); return false; } @@ -77,7 +77,7 @@ public class ServerSecretFilter extends AuthenticatingFilter { response.setContentType("application/json;charset=utf-8"); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin()); - + try { String json = JsonUtils.toJsonString(new Result().error(ErrorCode.UNAUTHORIZED, message)); response.getWriter().print(json); @@ -102,4 +102,4 @@ public class ServerSecretFilter extends AuthenticatingFilter { private String getServerSecret() { return sysParamsService.getValue(Constant.SERVER_SECRET, true); } -} +} diff --git a/main/manager-api/src/main/resources/db/changelog/202504082211.sql b/main/manager-api/src/main/resources/db/changelog/202504082211.sql index 08e5dc5f..91ce33ff 100644 --- a/main/manager-api/src/main/resources/db/changelog/202504082211.sql +++ b/main/manager-api/src/main/resources/db/changelog/202504082211.sql @@ -25,7 +25,7 @@ INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `f -- TTS模型供应器 ('SYSTEM_TTS_edge', 'TTS', 'edge', 'Edge TTS', '[{"key":"voice","label":"音色","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 1, 1, NOW(), 1, NOW()), -('SYSTEM_TTS_doubao', 'TTS', 'doubao', '火山引擎TTS', '[{"key": "api_url","label": "API地址","type": "string"},{"key": "voice","label": "音色","type": "string"},{"key": "output_dir","label": "输出目录","type": "string"},{"key": "authorization","label": "授权","type": "string"},{"key": "appid","label": "应用ID","type": "string"},{"key": "access_token","label": "访问令牌","type": "string"},{"key": "cluster","label": "集群","type": "string"},{"key": "speed_ratio","label": "语速","type": "number"},{"key": "volume_ratio","label": "音量","type": "number"},{"key": "pitch_ratio","label": "音高","type": "number"}]', 2, 1, NOW(), 1, NOW()), +('SYSTEM_TTS_doubao', 'TTS', 'doubao', '火山引擎TTS', '[{"key":"api_url","label":"API地址","type":"string"},{"key":"voice","label":"音色","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"authorization","label":"授权","type":"string"},{"key":"appid","label":"应用ID","type":"string"},{"key":"access_token","label":"访问令牌","type":"string"},{"key":"cluster","label":"集群","type":"string"}]', 2, 1, NOW(), 1, NOW()), ('SYSTEM_TTS_siliconflow', 'TTS', 'siliconflow', '硅基流动TTS', '[{"key":"model","label":"模型","type":"string"},{"key":"voice","label":"音色","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"access_token","label":"访问令牌","type":"string"},{"key":"response_format","label":"响应格式","type":"string"}]', 3, 1, NOW(), 1, NOW()), ('SYSTEM_TTS_cozecn', 'TTS', 'cozecn', 'COZECN TTS', '[{"key":"voice","label":"音色","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"access_token","label":"访问令牌","type":"string"},{"key":"response_format","label":"响应格式","type":"string"}]', 4, 1, NOW(), 1, NOW()), ('SYSTEM_TTS_fishspeech', 'TTS', 'fishspeech', 'FishSpeech TTS', '[{"key":"output_dir","label":"输出目录","type":"string"},{"key":"response_format","label":"响应格式","type":"string"},{"key":"reference_id","label":"参考ID","type":"string"},{"key":"reference_audio","label":"参考音频","type":"dict","dict_name":"reference_audio"},{"key":"reference_text","label":"参考文本","type":"dict","dict_name":"reference_text"},{"key":"normalize","label":"是否标准化","type":"boolean"},{"key":"max_new_tokens","label":"最大新令牌数","type":"number"},{"key":"chunk_length","label":"块长度","type":"number"},{"key":"top_p","label":"top_p值","type":"number"},{"key":"repetition_penalty","label":"重复惩罚","type":"number"},{"key":"temperature","label":"温度","type":"number"},{"key":"streaming","label":"是否流式","type":"boolean"},{"key":"use_memory_cache","label":"是否使用内存缓存","type":"string"},{"key":"seed","label":"种子","type":"number"},{"key":"channels","label":"通道数","type":"number"},{"key":"rate","label":"采样率","type":"number"},{"key":"api_key","label":"API密钥","type":"string"},{"key":"api_url","label":"API地址","type":"string"}]', 5, 1, NOW(), 1, NOW()), diff --git a/main/manager-api/src/main/resources/db/changelog/202504301339.sql b/main/manager-api/src/main/resources/db/changelog/202504301339.sql new file mode 100644 index 00000000..e4393f6a --- /dev/null +++ b/main/manager-api/src/main/resources/db/changelog/202504301339.sql @@ -0,0 +1,3 @@ +update `ai_model_provider` set `fields` = +'[{"key": "api_url","label": "API地址","type": "string"},{"key": "voice","label": "音色","type": "string"},{"key": "output_dir","label": "输出目录","type": "string"},{"key": "authorization","label": "授权","type": "string"},{"key": "appid","label": "应用ID","type": "string"},{"key": "access_token","label": "访问令牌","type": "string"},{"key": "cluster","label": "集群","type": "string"},{"key": "speed_ratio","label": "语速","type": "number"},{"key": "volume_ratio","label": "音量","type": "number"},{"key": "pitch_ratio","label": "音高","type": "number"}]' +where `id` = 'SYSTEM_TTS_doubao'; diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml index 131aaf01..90735fe1 100755 --- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml @@ -85,4 +85,11 @@ databaseChangeLog: changes: - sqlFile: encoding: utf8 - path: classpath:db/changelog/202504291043.sql \ No newline at end of file + path: classpath:db/changelog/202504291043.sql + - changeSet: + id: 202504301339 + author: Goody + changes: + - sqlFile: + encoding: utf8 + path: classpath:db/changelog/202504301339.sql \ No newline at end of file diff --git a/main/xiaozhi-server/app.py b/main/xiaozhi-server/app.py index 63b40d71..7ed71ae5 100644 --- a/main/xiaozhi-server/app.py +++ b/main/xiaozhi-server/app.py @@ -1,7 +1,7 @@ import asyncio import sys import signal -from config.settings import load_config, check_config_file +from config.settings import load_config from core.websocket_server import WebSocketServer from core.ota_server import SimpleOtaServer from core.utils.util import check_ffmpeg_installed @@ -31,7 +31,6 @@ async def wait_for_exit(): async def main(): - check_config_file() check_ffmpeg_installed() config = load_config() diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml index db8dd5d5..3ce485ec 100644 --- a/main/xiaozhi-server/config.yaml +++ b/main/xiaozhi-server/config.yaml @@ -1,7 +1,7 @@ -# 如果您是一名开发者,建议阅读以下内容。如果不是开发者,可以忽略这部分内容。 -# 在开发中,在项目根目录创建data目录,将【config.yaml】复制一份,改成【.config.yaml】,放进data目录中 -# 系统会优先读取【data/.config.yaml】文件的配置。 -# 这样做,可以避免在提交代码的时候,错误地提交密钥信息,保护您的密钥安全。 +# 在开发中,请在项目根目录创建data目录,然后在data目录创建名称为【.config.yaml】的空文件 +# 然后你想修改覆盖修改什么配置,就修改【.config.yaml】文件,而不是修改【config.yaml】文件 +# 系统会优先读取【data/.config.yaml】文件的配置,如果【.config.yaml】文件里的配置不存在,系统会自动去读取【config.yaml】文件的配置。 +# 这样做,可以最简化配置,保护您的密钥安全。 # ##################################################################################### # #############################以下是服务器基本运行配置#################################### @@ -158,7 +158,7 @@ selected_module: # 不想开通意图识别,就设置成:nointent # 意图识别可使用intent_llm。优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间,这个意图识别暂时不支持控制音量大小等iot操作 # 意图识别可使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快,理论上能全部操作所有iot指令 - # 默认免费的ChatGLMLLM就已经支持function_call,但是如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028 + # 默认免费的ChatGLMLLM就已经支持function_call,但是如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-1-5-pro-32k-250115 Intent: function_call # 意图识别,是用于理解用户意图的模块,例如:播放音乐 diff --git a/main/xiaozhi-server/config/config_loader.py b/main/xiaozhi-server/config/config_loader.py index b892b437..35802a67 100644 --- a/main/xiaozhi-server/config/config_loader.py +++ b/main/xiaozhi-server/config/config_loader.py @@ -31,37 +31,19 @@ def load_config(): # 加载默认配置 default_config = read_config(default_config_path) + custom_config = read_config(custom_config_path) - # 加载用户自定义配置(如果存在) - if os.path.exists(custom_config_path): - custom_config = read_config(custom_config_path) + if custom_config.get("manager-api", {}).get("url"): + config = get_config_from_api(custom_config) + else: # 合并配置 config = merge_configs(default_config, custom_config) - else: - config = default_config - - 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 get_config_from_api(config): """从Java API获取配置""" # 初始化API客户端 @@ -128,23 +110,29 @@ def ensure_directories(config): def merge_configs(default_config, custom_config): """ 递归合并配置,custom_config优先级更高 - + Args: default_config: 默认配置 custom_config: 用户自定义配置 - + Returns: 合并后的配置 """ - if not isinstance(default_config, Mapping) or not isinstance(custom_config, Mapping): + if not isinstance(default_config, Mapping) or not isinstance( + custom_config, Mapping + ): return custom_config - + merged = dict(default_config) - + for key, value in custom_config.items(): - if key in merged and isinstance(merged[key], Mapping) and isinstance(value, Mapping): + if ( + key in merged + and isinstance(merged[key], Mapping) + and isinstance(value, Mapping) + ): merged[key] = merge_configs(merged[key], value) else: merged[key] = value - - return merged \ No newline at end of file + + return merged diff --git a/main/xiaozhi-server/config/logger.py b/main/xiaozhi-server/config/logger.py index 9eed8540..f20dc0ad 100644 --- a/main/xiaozhi-server/config/logger.py +++ b/main/xiaozhi-server/config/logger.py @@ -2,6 +2,7 @@ import os import sys from loguru import logger from config.config_loader import load_config +from config.settings import check_config_file SERVER_VERSION = "0.3.13" @@ -32,6 +33,7 @@ def formatter(record): def setup_logging(): + check_config_file() """从配置文件中读取日志配置,并设置日志输出格式和级别""" config = load_config() log_config = config["log"] diff --git a/main/xiaozhi-server/config/settings.py b/main/xiaozhi-server/config/settings.py index c12c70d8..fb8868b4 100644 --- a/main/xiaozhi-server/config/settings.py +++ b/main/xiaozhi-server/config/settings.py @@ -1,53 +1,33 @@ import os -from collections.abc import Mapping -from config import logger from config.config_loader import read_config, get_project_dir, load_config -TAG = __name__ -logger = logger.setup_logging() default_config_file = "config.yaml" - - -def find_missing_keys(new_config, old_config, parent_key=""): - """ - 递归查找缺失的配置项 - 返回格式:[缺失配置路径] - """ - missing_keys = [] - - if not isinstance(new_config, Mapping): - return missing_keys - - for key, value in new_config.items(): - # 构建当前配置路径 - full_path = f"{parent_key}.{key}" if parent_key else key - - # 检查键是否存在 - if key not in old_config: - missing_keys.append(full_path) - continue - - # 递归检查嵌套字典 - if isinstance(value, Mapping): - sub_missing = find_missing_keys( - value, old_config[key], parent_key=full_path - ) - missing_keys.extend(sub_missing) - return missing_keys +config_file_valid = False def check_config_file(): + global config_file_valid + if config_file_valid: + return """ 简化的配置检查,仅提示用户配置文件的使用情况 """ custom_config_file = get_project_dir() + "data/." + default_config_file if not os.path.exists(custom_config_file): - logger.bind(tag=TAG).info("提示: 使用默认配置文件。如需自定义配置,请创建 data/.config.yaml 文件") - else: - logger.bind(tag=TAG).info(f"提示: 使用自定义配置文件 data/.config.yaml,配置将覆盖默认值") - + raise FileNotFoundError( + "找不到data/.config.yaml文件,请按教程确认该配置文件是否存在" + ) + # 检查是否从API读取配置 config = load_config() if config.get("read_config_from_api", False): - logger.bind(tag=TAG).info("提示: 从API获取配置") + print("从API读取配置") + old_config_origin = read_config(custom_config_file) + if old_config_origin.get("selected_module") is not None: + error_msg = "您的配置文件好像既包含智控台的配置又包含本地配置:\n" + error_msg += "\n建议您:\n" + error_msg += "1、将根目录的config_from_api.yaml文件复制到data下,重命名为.config.yaml\n" + error_msg += "2、按教程配置好接口地址和密钥\n" + raise ValueError(error_msg) + config_file_valid = True diff --git a/main/xiaozhi-server/core/handle/textHandle.py b/main/xiaozhi-server/core/handle/textHandle.py index f0683d59..a26f5bd9 100644 --- a/main/xiaozhi-server/core/handle/textHandle.py +++ b/main/xiaozhi-server/core/handle/textHandle.py @@ -53,6 +53,8 @@ async def handleTextMessage(conn, message): # 如果是唤醒词,且关闭了唤醒词回复,就不用回答 await send_stt_message(conn, text) await send_tts_message(conn, "stop", None) + elif is_wakeup_words: + await startToChat(conn, "嘿,你好呀") else: # 否则需要LLM对文字内容进行答复 await startToChat(conn, text) diff --git a/main/xiaozhi-server/core/providers/tts/doubao.py b/main/xiaozhi-server/core/providers/tts/doubao.py index 1b6414c4..367a99c9 100644 --- a/main/xiaozhi-server/core/providers/tts/doubao.py +++ b/main/xiaozhi-server/core/providers/tts/doubao.py @@ -27,9 +27,14 @@ class TTSProvider(TTSProviderBase): else: self.voice = config.get("voice") - self.speed_ratio = float(config.get("speed_ratio", 0.1)) - self.volume_ratio = float(config.get("volume_ratio", 0.1)) - self.pitch_ratio = float(config.get("pitch_ratio", 0.1)) + # 处理空字符串的情况 + speed_ratio = config.get("speed_ratio", "1.0") + volume_ratio = config.get("volume_ratio", "1.0") + pitch_ratio = config.get("pitch_ratio", "1.0") + + self.speed_ratio = float(speed_ratio) if speed_ratio else 1.0 + self.volume_ratio = float(volume_ratio) if volume_ratio else 1.0 + self.pitch_ratio = float(pitch_ratio) if pitch_ratio else 1.0 self.api_url = config.get("api_url") self.authorization = config.get("authorization") diff --git a/main/xiaozhi-server/core/providers/tts/fishspeech.py b/main/xiaozhi-server/core/providers/tts/fishspeech.py index 30fee3c9..316dbed7 100644 --- a/main/xiaozhi-server/core/providers/tts/fishspeech.py +++ b/main/xiaozhi-server/core/providers/tts/fishspeech.py @@ -89,18 +89,35 @@ class TTSProvider(TTSProviderBase): self.reference_audio = parse_string_to_list(config.get("reference_audio")) self.reference_text = parse_string_to_list(config.get("reference_text")) self.format = config.get("format", "wav") - self.channels = int(config.get("channels", 1)) - self.rate = int(config.get("rate", 44100)) + self.api_key = config.get("api_key", "YOUR_API_KEY") have_key = check_model_key("FishSpeech TTS", self.api_key) if not have_key: return self.normalize = config.get("normalize", True) - self.max_new_tokens = int(config.get("max_new_tokens", 1024)) - self.chunk_length = int(config.get("chunk_length", 200)) - self.top_p = float(config.get("top_p", 0.7)) - self.repetition_penalty = float(config.get("repetition_penalty", 1.2)) - self.temperature = float(config.get("temperature", 0.7)) + + # 处理空字符串的情况 + channels = config.get("channels", "1") + rate = config.get("rate", "44100") + max_new_tokens = config.get("max_new_tokens", "1024") + chunk_length = config.get("chunk_length", "200") + + self.channels = int(channels) if channels else 1 + self.rate = int(rate) if rate else 44100 + self.max_new_tokens = int(max_new_tokens) if max_new_tokens else 1024 + self.chunk_length = int(chunk_length) if chunk_length else 200 + + # 处理空字符串的情况 + top_p = config.get("top_p", "0.7") + temperature = config.get("temperature", "0.7") + repetition_penalty = config.get("repetition_penalty", "1.2") + + self.top_p = float(top_p) if top_p else 0.7 + self.temperature = float(temperature) if temperature else 0.7 + self.repetition_penalty = ( + float(repetition_penalty) if repetition_penalty else 1.2 + ) + self.streaming = str(config.get("streaming", False)).lower() in ( "true", "1", diff --git a/main/xiaozhi-server/core/providers/tts/gpt_sovits_v2.py b/main/xiaozhi-server/core/providers/tts/gpt_sovits_v2.py index ec6c2053..b2aad88a 100644 --- a/main/xiaozhi-server/core/providers/tts/gpt_sovits_v2.py +++ b/main/xiaozhi-server/core/providers/tts/gpt_sovits_v2.py @@ -20,12 +20,29 @@ class TTSProvider(TTSProviderBase): self.ref_audio_path = config.get("ref_audio_path") self.prompt_text = config.get("prompt_text") self.prompt_lang = config.get("prompt_lang", "zh") - self.top_k = int(config.get("top_k", 5)) - self.top_p = float(config.get("top_p", 1)) - self.temperature = float(config.get("temperature", 1)) + + # 处理空字符串的情况 + top_k = config.get("top_k", "5") + top_p = config.get("top_p", "1") + temperature = config.get("temperature", "1") + batch_threshold = config.get("batch_threshold", "0.75") + batch_size = config.get("batch_size", "1") + speed_factor = config.get("speed_factor", "1.0") + seed = config.get("seed", "-1") + repetition_penalty = config.get("repetition_penalty", "1.35") + + self.top_k = int(top_k) if top_k else 5 + self.top_p = float(top_p) if top_p else 1 + self.temperature = float(temperature) if temperature else 1 + self.batch_threshold = float(batch_threshold) if batch_threshold else 0.75 + self.batch_size = int(batch_size) if batch_size else 1 + self.speed_factor = float(speed_factor) if speed_factor else 1.0 + self.seed = int(seed) if seed else -1 + self.repetition_penalty = ( + float(repetition_penalty) if repetition_penalty else 1.35 + ) + self.text_split_method = config.get("text_split_method", "cut0") - self.batch_size = int(config.get("batch_size", 1)) - self.batch_threshold = float(config.get("batch_threshold", 0.75)) self.split_bucket = str(config.get("split_bucket", True)).lower() in ( "true", @@ -37,19 +54,19 @@ class TTSProvider(TTSProviderBase): "1", "yes", ) - self.speed_factor = float(config.get("speed_factor", 1.0)) + self.streaming_mode = str(config.get("streaming_mode", False)).lower() in ( "true", "1", "yes", ) - self.seed = int(config.get("seed", -1)) + self.parallel_infer = str(config.get("parallel_infer", True)).lower() in ( "true", "1", "yes", ) - self.repetition_penalty = float(config.get("repetition_penalty", 1.35)) + self.aux_ref_audio_paths = parse_string_to_list( config.get("aux_ref_audio_paths") ) diff --git a/main/xiaozhi-server/core/providers/tts/gpt_sovits_v3.py b/main/xiaozhi-server/core/providers/tts/gpt_sovits_v3.py index b2746acf..488280f1 100644 --- a/main/xiaozhi-server/core/providers/tts/gpt_sovits_v3.py +++ b/main/xiaozhi-server/core/providers/tts/gpt_sovits_v3.py @@ -18,13 +18,22 @@ class TTSProvider(TTSProviderBase): self.prompt_text = config.get("prompt_text") self.prompt_language = config.get("prompt_language") self.text_language = config.get("text_language", "audo") - self.top_k = int(config.get("top_k", 15)) - self.top_p = float(config.get("top_p", 1.0)) - self.temperature = float(config.get("temperature", 1.0)) + + # 处理空字符串的情况 + top_k = config.get("top_k", "15") + top_p = config.get("top_p", "1.0") + temperature = config.get("temperature", "1.0") + sample_steps = config.get("sample_steps", "32") + speed = config.get("speed", "1.0") + + self.top_k = int(top_k) if top_k else 15 + self.top_p = float(top_p) if top_p else 1.0 + self.temperature = float(temperature) if temperature else 1.0 + self.sample_steps = int(sample_steps) if sample_steps else 32 + self.speed = float(speed) if speed else 1.0 + self.cut_punc = config.get("cut_punc", "") - self.speed = float(config.get("speed", 1.0)) self.inp_refs = parse_string_to_list(config.get("inp_refs")) - self.sample_steps = int(config.get("sample_steps", 32)) self.if_sr = str(config.get("if_sr", False)).lower() in ("true", "1", "yes") def generate_filename(self, extension=".wav"): diff --git a/main/xiaozhi-server/core/providers/tts/openai.py b/main/xiaozhi-server/core/providers/tts/openai.py index a5d48057..a7c4e30d 100644 --- a/main/xiaozhi-server/core/providers/tts/openai.py +++ b/main/xiaozhi-server/core/providers/tts/openai.py @@ -21,7 +21,11 @@ class TTSProvider(TTSProviderBase): else: self.voice = config.get("voice", "alloy") self.response_format = "wav" - self.speed = float(config.get("speed", 1.0)) + + # 处理空字符串的情况 + speed = config.get("speed", "1.0") + self.speed = float(speed) if speed else 1.0 + self.output_file = config.get("output_dir", "tmp/") check_model_key("TTS", self.api_key) diff --git a/main/xiaozhi-server/core/providers/vad/silero.py b/main/xiaozhi-server/core/providers/vad/silero.py index 3332d9ac..8ec1f6eb 100644 --- a/main/xiaozhi-server/core/providers/vad/silero.py +++ b/main/xiaozhi-server/core/providers/vad/silero.py @@ -21,8 +21,15 @@ class VADProvider(VADProviderBase): (get_speech_timestamps, _, _, _, _) = self.utils self.decoder = opuslib_next.Decoder(16000, 1) - self.vad_threshold = float(config.get("threshold", 0.5)) - self.silence_threshold_ms = int(config.get("min_silence_duration_ms", 1000)) + + # 处理空字符串的情况 + threshold = config.get("threshold", "0.5") + min_silence_duration_ms = config.get("min_silence_duration_ms", "1000") + + self.vad_threshold = float(threshold) if threshold else 0.5 + self.silence_threshold_ms = ( + int(min_silence_duration_ms) if min_silence_duration_ms else 1000 + ) def is_vad(self, conn, opus_packet): try: